From ee78216a286a7ba642012023a6c322d106334472 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Fri, 9 May 2025 14:54:08 -0700 Subject: [PATCH 01/52] Add GenerateRectilinearGrid --- .../geos/mesh/doctor/checks/generate_cube.py | 91 ++++++--- .../mesh/doctor/checks/generate_global_ids.py | 4 +- .../doctor/filters/GenerateRectilinearGrid.py | 185 ++++++++++++++++++ .../src/geos/mesh/doctor/filters/__init__.py | 0 geos-mesh/tests/test_generate_cube.py | 45 ++++- geos-mesh/tests/test_generate_global_ids.py | 4 +- 6 files changed, 298 insertions(+), 31 deletions(-) create mode 100644 geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py create mode 100644 geos-mesh/src/geos/mesh/doctor/filters/__init__.py diff --git a/geos-mesh/src/geos/mesh/doctor/checks/generate_cube.py b/geos-mesh/src/geos/mesh/doctor/checks/generate_cube.py index 4b4c71fbe..294973fe3 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/generate_cube.py +++ b/geos-mesh/src/geos/mesh/doctor/checks/generate_cube.py @@ -1,12 +1,13 @@ from dataclasses import dataclass import logging -import numpy +import numpy as np +import numpy.typing as npt 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.checks.generate_global_ids import build_global_ids from geos.mesh.vtk.io import VtkOutput, write_mesh @@ -38,9 +39,64 @@ class Options: @dataclass( frozen=True ) class XYZ: - x: numpy.ndarray - y: numpy.ndarray - z: numpy.ndarray + x: npt.NDArray + y: npt.NDArray + z: npt.NDArray + + +def build_coordinates( positions, num_elements ): + result = [] + it = zip( zip( positions, positions[ 1: ] ), num_elements ) + try: + coords, n = next( it ) + while True: + start, stop = coords + end_point = False + tmp = np.linspace( start=start, stop=stop, num=n + end_point, endpoint=end_point ) + coords, n = next( it ) + result.append( tmp ) + except StopIteration: + end_point = True + tmp = np.linspace( start=start, stop=stop, num=n + end_point, endpoint=end_point ) + result.append( tmp ) + return np.concatenate( result ) + + +def build_rectilinear_grid( x: npt.NDArray, y: npt.NDArray, z: npt.NDArray ) -> vtkUnstructuredGrid: + """ + Builds an unstructured vtk grid from the x,y,z coordinates. + :return: The unstructured mesh, even if it's topologically structured. + """ + rg = vtkRectilinearGrid() + rg.SetDimensions( len( x ), len( y ), len( z ) ) + rg.SetXCoordinates( numpy_to_vtk( x ) ) + rg.SetYCoordinates( numpy_to_vtk( y ) ) + rg.SetZCoordinates( numpy_to_vtk( z ) ) + + num_points = rg.GetNumberOfPoints() + num_cells = rg.GetNumberOfCells() + + points = vtkPoints() + points.Allocate( num_points ) + for i in range( rg.GetNumberOfPoints() ): + points.InsertNextPoint( rg.GetPoint( i ) ) + + cell_types = [ VTK_HEXAHEDRON ] * num_cells + cells = vtkCellArray() + cells.AllocateExact( num_cells, num_cells * 8 ) + + m = ( 0, 1, 3, 2, 4, 5, 7, 6 ) # VTK_VOXEL and VTK_HEXAHEDRON do not share the same ordering. + for i in range( rg.GetNumberOfCells() ): + c = rg.GetCell( i ) + new_cell = vtkHexahedron() + for j in range( 8 ): + new_cell.GetPointIds().SetId( j, c.GetPointId( m[ j ] ) ) + cells.InsertNextCell( new_cell ) + + mesh = vtkUnstructuredGrid() + mesh.SetPoints( points ) + mesh.SetCells( cell_types, cells ) + return mesh def build_rectilinear_blocks_mesh( xyzs: Iterable[ XYZ ] ) -> vtkUnstructuredGrid: @@ -89,7 +145,7 @@ def build_rectilinear_blocks_mesh( xyzs: Iterable[ XYZ ] ) -> vtkUnstructuredGri return mesh -def __add_fields( mesh: vtkUnstructuredGrid, fields: Iterable[ FieldInfo ] ) -> vtkUnstructuredGrid: +def add_fields( mesh: vtkUnstructuredGrid, fields: Iterable[ FieldInfo ] ) -> vtkUnstructuredGrid: for field_info in fields: if field_info.support == "CELLS": data = mesh.GetCellData() @@ -97,7 +153,7 @@ def __add_fields( mesh: vtkUnstructuredGrid, fields: Iterable[ FieldInfo ] ) -> elif field_info.support == "POINTS": data = mesh.GetPointData() n = mesh.GetNumberOfPoints() - array = numpy.ones( ( n, field_info.dimension ), dtype=float ) + array = np.ones( ( n, field_info.dimension ), dtype=float ) vtk_array = numpy_to_vtk( array ) vtk_array.SetName( field_info.name ) data.AddArray( vtk_array ) @@ -106,29 +162,12 @@ def __add_fields( mesh: vtkUnstructuredGrid, fields: Iterable[ FieldInfo ] ) -> def __build( options: Options ): - def build_coordinates( positions, num_elements ): - result = [] - it = zip( zip( positions, positions[ 1: ] ), num_elements ) - try: - coords, n = next( it ) - while True: - start, stop = coords - end_point = False - tmp = numpy.linspace( start=start, stop=stop, num=n + end_point, endpoint=end_point ) - coords, n = next( it ) - result.append( tmp ) - except StopIteration: - end_point = True - tmp = numpy.linspace( start=start, stop=stop, num=n + end_point, endpoint=end_point ) - result.append( tmp ) - return numpy.concatenate( result ) - x = build_coordinates( options.xs, options.nxs ) y = build_coordinates( options.ys, options.nys ) z = build_coordinates( options.zs, options.nzs ) cube = build_rectilinear_blocks_mesh( ( XYZ( x, y, z ), ) ) - cube = __add_fields( cube, options.fields ) - __build_global_ids( cube, options.generate_cells_global_ids, options.generate_points_global_ids ) + cube = add_fields( cube, options.fields ) + build_global_ids( cube, options.generate_cells_global_ids, options.generate_points_global_ids ) return cube diff --git a/geos-mesh/src/geos/mesh/doctor/checks/generate_global_ids.py b/geos-mesh/src/geos/mesh/doctor/checks/generate_global_ids.py index 6142ad7ca..7db29aa8e 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/generate_global_ids.py +++ b/geos-mesh/src/geos/mesh/doctor/checks/generate_global_ids.py @@ -16,7 +16,7 @@ class Result: info: str -def __build_global_ids( mesh, generate_cells_global_ids: bool, generate_points_global_ids: bool ) -> None: +def build_global_ids( mesh, generate_cells_global_ids: bool, generate_points_global_ids: bool ) -> None: """ Adds the global ids for cells and points in place into the mesh instance. :param mesh: @@ -46,7 +46,7 @@ def __build_global_ids( mesh, generate_cells_global_ids: bool, generate_points_g def __check( mesh, options: Options ) -> Result: - __build_global_ids( mesh, options.generate_cells_global_ids, options.generate_points_global_ids ) + 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}" ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py new file mode 100644 index 000000000..e846a0bf0 --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py @@ -0,0 +1,185 @@ +import numpy.typing as npt +from typing import Iterable, Sequence +from typing_extensions import Self +from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase +from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from geos.mesh.doctor.checks.generate_global_ids import build_global_ids +from geos.mesh.doctor.checks.generate_cube import FieldInfo, add_fields, build_coordinates, build_rectilinear_grid +from geos.mesh.vtk.io import VtkOutput, write_mesh +from geos.utils.Logger import Logger, getLogger + +__doc__ = """ +GenerateRectilinearGrid module is a vtk filter that allows to create a simple vtkUnstructuredGrid rectilinear grid. +GlobalIds for points and cells can be added. +You can create CellArray and PointArray of constant value = 1 and any dimension >= 1. + +No filter input and one filter output which is vtkUnstructuredGrid. + +To use the filter: + +.. code-block:: python + + from filters.GenerateRectilinearGrid import GenerateRectilinearGrid + + # instanciate the filter + generateRectilinearGridFilter: GenerateRectilinearGrid = GenerateRectilinearGrid() + + # set the coordinates of each block border for the X, Y and Z axis + generateRectilinearGridFilter.setCoordinates( [ 0.0, 5.0, 10.0 ], [ 0.0, 5.0, 10.0 ], [ 0.0, 10.0 ] ) + + # for each block defined, specify the number of cells that they should contain in the X, Y, Z axis + generateRectilinearGridFilter.setNumberElements( [ 5, 5 ], [ 5, 5 ], [ 10 ] ) + + # to add the GlobalIds for cells and points, set to True the generate global ids options + generateRectilinearGridFilter.setGenerateCellsGlobalIds( True ) + generateRectilinearGridFilter.setGeneratePointsGlobalIds( True ) + + # to create new arrays with a specific dimension, you can use the following commands + cells_dim1 = FieldInfo( "cell1", 1, "CELLS" ) # array "cell1" of shape ( number of cells, 1 ) + cells_dim3 = FieldInfo( "cell3", 3, "CELLS" ) # array "cell3" of shape ( number of cells, 3 ) + points_dim1 = FieldInfo( "point1", 1, "POINTS" ) # array "point1" of shape ( number of points, 1 ) + points_dim3 = FieldInfo( "point3", 3, "POINTS" ) # array "point3" of shape ( number of points, 3 ) + generateRectilinearGridFilter.setFields( [ cells_dim1, cells_dim3, points_dim1, points_dim3 ] ) + + # then, to obtain the constructed mesh out of all these operations, 2 solutions are available + + # solution1 + generateRectilinearGridFilter.Update() + mesh: vtkUnstructuredGrid = generateRectilinearGridFilter.GetOutputDataObject( 0 ) + + # solution2, which is a method calling the 2 instructions above + mesh: vtkUnstructuredGrid = generateRectilinearGridFilter.getRectilinearGrid() + + # finally, you can write the mesh at a specific destination with: + generateRectilinearGridFilter.writeGrid( "output/filepath/of/your/grid.vtu" ) +""" + + +class GenerateRectilinearGrid( VTKPythonAlgorithmBase ): + + def __init__( self: Self ) -> None: + """Vtk filter to generate a simple rectilinear grid. + + Output mesh is vtkUnstructuredGrid. + """ + super().__init__( nInputPorts=0, nOutputPorts=1, outputType='vtkUnstructuredGrid' ) + self.m_generateCellsGlobalIds: bool = False + self.m_generatePointsGlobalIds: bool = False + self.m_coordsX: Sequence[ float ] = None + self.m_coordsY: Sequence[ float ] = None + self.m_coordsZ: Sequence[ float ] = None + self.m_numberElementsX: Sequence[ int ] = None + self.m_numberElementsY: Sequence[ int ] = None + self.m_numberElementsZ: Sequence[ int ] = None + self.m_fields: Iterable[ FieldInfo ] = list() + self.m_logger: Logger = getLogger( "Generate Rectilinear Grid Filter" ) + + def RequestData( self: Self, request: vtkInformation, inInfo: vtkInformationVector, + outInfo: vtkInformationVector ) -> int: + opt = vtkUnstructuredGrid.GetData( outInfo ) + x: npt.NDArray = build_coordinates( self.m_coordsX, self.m_numberElementsX ) + y: npt.NDArray = build_coordinates( self.m_coordsY, self.m_numberElementsY ) + z: npt.NDArray = build_coordinates( self.m_coordsZ, self.m_numberElementsZ ) + output: vtkUnstructuredGrid = build_rectilinear_grid( x, y, z ) + output = add_fields( output, self.m_fields ) + build_global_ids( output, self.m_generateCellsGlobalIds, self.m_generatePointsGlobalIds ) + opt.ShallowCopy( output ) + return 1 + + def SetLogger( self: Self, logger: Logger ) -> None: + """Set the logger. + + Args: + logger (Logger): logger + """ + self.m_logger = logger + self.Modified() + + def getRectilinearGrid( self: Self ) -> vtkUnstructuredGrid: + """Returns a rectilinear grid as a vtkUnstructuredGrid. + + Args: + self (Self) + + Returns: + vtkUnstructuredGrid + """ + self.Update() # triggers RequestData + return self.GetOutputDataObject( 0 ) + + def setCoordinates( self: Self, coordsX: Sequence[ float ], coordsY: Sequence[ float ], + coordsZ: Sequence[ float ] ) -> None: + """Set the coordinates of the block you want to have in your grid by specifying the beginning and ending + coordinates along the X, Y and Z axis. + + Args: + self (Self) + coordsX (Sequence[ float ]) + coordsY (Sequence[ float ]) + coordsZ (Sequence[ float ]) + """ + self.m_coordsX = coordsX + self.m_coordsY = coordsY + self.m_coordsZ = coordsZ + self.Modified() + + def setGenerateCellsGlobalIds( self: Self, generate: bool ) -> None: + """Set the generation of global cells ids to be True or False. + + Args: + self (Self) + generate (bool) + """ + self.m_generateCellsGlobalIds = generate + self.Modified() + + def setGeneratePointsGlobalIds( self: Self, generate: bool ) -> None: + """Set the generation of global points ids to be True or False. + + Args: + self (Self) + generate (bool) + """ + self.m_generatePointsGlobalIds = generate + self.Modified() + + def setFields( self: Self, fields: Iterable[ FieldInfo ] ) -> None: + """Specify the cells or points array to be added to the grid. + + Args: + self (Self) + fields (Iterable[ FieldInfo ]) + """ + self.m_fields = fields + self.Modified() + + def setNumberElements( self: Self, numberElementsX: Sequence[ int ], numberElementsY: Sequence[ int ], + numberElementsZ: Sequence[ int ] ) -> None: + """For each block that was defined in setCoordinates, specify the number of cells that they should contain. + + Args: + self (Self) + numberElementsX (Sequence[ int ]) + numberElementsY (Sequence[ int ]) + numberElementsZ (Sequence[ int ]) + """ + self.m_numberElementsX = numberElementsX + self.m_numberElementsY = numberElementsY + self.m_numberElementsZ = numberElementsZ + self.Modified() + + def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: + """Writes a .vtu file of your rectilinear grid at the specified filepath. + + Args: + filepath (str): /path/to/your/file.vtu + is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. + canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. + Defaults to False. + """ + mesh: vtkUnstructuredGrid = self.getRectilinearGrid() + if mesh: + write_mesh( filepath, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) + else: + self.m_logger.error( f"No rectilinear grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/__init__.py b/geos-mesh/src/geos/mesh/doctor/filters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/geos-mesh/tests/test_generate_cube.py b/geos-mesh/tests/test_generate_cube.py index effa8aa83..cc6df3c62 100644 --- a/geos-mesh/tests/test_generate_cube.py +++ b/geos-mesh/tests/test_generate_cube.py @@ -1,4 +1,47 @@ -from geos.mesh.doctor.checks.generate_cube import __build, Options, FieldInfo +import pytest +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, vtkPointData, vtkCellData +from vtkmodules.vtkCommonCore import vtkDataArray +from geos.mesh.doctor.checks.generate_cube import FieldInfo, Options, __build +from geos.mesh.doctor.filters.GenerateRectilinearGrid import GenerateRectilinearGrid + + +@pytest.fixture +def generate_rectilinear_grid_filter() -> GenerateRectilinearGrid: + filter = GenerateRectilinearGrid() + filter.setCoordinates( [ 0.0, 5.0, 10.0 ], [ 0.0, 10.0, 20.0 ], [ 0.0, 50.0 ] ) + filter.setNumberElements( [ 5, 5 ], [ 5, 5 ], [ 10 ] ) # 10 cells along X, Y, Z axis + filter.setGenerateCellsGlobalIds( True ) + filter.setGeneratePointsGlobalIds( True ) + + cells_dim1 = FieldInfo( "cell1", 1, "CELLS" ) + cells_dim3 = FieldInfo( "cell3", 3, "CELLS" ) + points_dim1 = FieldInfo( "point1", 1, "POINTS" ) + points_dim3 = FieldInfo( "point3", 3, "POINTS" ) + filter.setFields( [ cells_dim1, cells_dim3, points_dim1, points_dim3 ] ) + + return filter + + +def test_generate_rectilinear_grid( generate_rectilinear_grid_filter: GenerateRectilinearGrid ) -> None: + generate_rectilinear_grid_filter.Update() + mesh = generate_rectilinear_grid_filter.GetOutputDataObject( 0 ) + + assert isinstance( mesh, vtkUnstructuredGrid ) + assert mesh.GetNumberOfCells() == 1000 + assert mesh.GetNumberOfPoints() == 1331 + assert mesh.GetBounds() == ( 0.0, 10.0, 0.0, 20.0, 0.0, 50.0 ) + + pointData: vtkPointData = mesh.GetPointData() + ptArray1: vtkDataArray = pointData.GetArray( "point1" ) + ptArray3: vtkDataArray = pointData.GetArray( "point3" ) + assert ptArray1.GetNumberOfComponents() == 1 + assert ptArray3.GetNumberOfComponents() == 3 + + cellData: vtkCellData = mesh.GetCellData() + cellArray1: vtkDataArray = cellData.GetArray( "cell1" ) + cellArray3: vtkDataArray = cellData.GetArray( "cell3" ) + assert cellArray1.GetNumberOfComponents() == 1 + assert cellArray3.GetNumberOfComponents() == 3 def test_generate_cube(): diff --git a/geos-mesh/tests/test_generate_global_ids.py b/geos-mesh/tests/test_generate_global_ids.py index 40c211799..127300f4b 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.checks.generate_global_ids import build_global_ids def test_generate_global_ids(): @@ -17,7 +17,7 @@ def test_generate_global_ids(): mesh.SetPoints( points ) mesh.SetCells( [ VTK_VERTEX ], vertices ) - __build_global_ids( mesh, True, True ) + build_global_ids( mesh, True, True ) global_cell_ids = mesh.GetCellData().GetGlobalIds() global_point_ids = mesh.GetPointData().GetGlobalIds() From cc8e2a533b2d63771b5d2fa09b3e6c167af6ac6d Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Fri, 9 May 2025 14:58:17 -0700 Subject: [PATCH 02/52] Add ElementVolumes filter --- .../mesh/doctor/filters/ElementVolumes.py | 179 ++++++++++++++ geos-mesh/tests/test_element_volumes.py | 229 +++++++++++++++++- 2 files changed, 404 insertions(+), 4 deletions(-) create mode 100644 geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py diff --git a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py new file mode 100644 index 000000000..417de0fc8 --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py @@ -0,0 +1,179 @@ +import numpy as np +import numpy.typing as npt +from typing_extensions import Self +from vtkmodules.util.numpy_support import vtk_to_numpy +from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase +from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from vtkmodules.vtkFiltersVerdict import vtkCellSizeFilter +from geos.mesh.vtk.io import VtkOutput, write_mesh +from geos.utils.Logger import Logger, getLogger + +__doc__ = """ +ElementVolumes module is a vtk filter that allows to calculate the volumes of every elements in a vtkUnstructuredGrid. + +One filter input is vtkUnstructuredGrid one filter output which is vtkUnstructuredGrid. + +To use the filter: + +.. code-block:: python + + from filters.ElementVolumes import ElementVolumes + + # instanciate the filter + elementVolumesFilter: ElementVolumes = ElementVolumes() + +""" + + +class ElementVolumes( VTKPythonAlgorithmBase ): + + def __init__( self: Self ) -> None: + """Vtk filter to calculate the volume of every element of a vtkUnstructuredGrid. + + Output mesh is vtkUnstructuredGrid. + """ + super().__init__( nInputPorts=1, nOutputPorts=1, inputType='vtkUnstructuredGrid', + outputType='vtkUnstructuredGrid' ) + self.m_returnNegativeZeroVolumes: bool = False + self.m_volumes: npt.NDArray = None + self.m_logger: Logger = getLogger( "Element Volumes Filter" ) + + def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestInformation. + + Args: + port (int): input port + info (vtkInformationVector): info + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + if port == 0: + info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) + return 1 + + def RequestInformation( + self: Self, + request: vtkInformation, # noqa: F841 + inInfoVec: list[ vtkInformationVector ], # noqa: F841 + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestInformation. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + executive = self.GetExecutive() # noqa: F841 + outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 + return 1 + + def RequestData( + self: Self, + request: vtkInformation, + inInfoVec: list[ vtkInformationVector ], + outInfo: vtkInformationVector + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestData. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) + output = vtkUnstructuredGrid.GetData( outInfo ) + + cellSize = vtkCellSizeFilter() + cellSize.ComputeAreaOff() + cellSize.ComputeLengthOff() + cellSize.ComputeSumOff() + cellSize.ComputeVertexCountOff() + cellSize.ComputeVolumeOn() + volume_array_name: str = "MESH_DOCTOR_VOLUME" + cellSize.SetVolumeArrayName( volume_array_name ) + + cellSize.SetInputData( input_mesh ) + cellSize.Update() + volumes: vtkDataArray = cellSize.GetOutput().GetCellData().GetArray( volume_array_name ) + self.m_volumes = volumes + + output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() + output_mesh.CopyStructure( input_mesh ) + output_mesh.CopyAttributes( input_mesh ) + output_mesh.GetCellData().AddArray( volumes ) + output.ShallowCopy( output_mesh ) + + if self.m_returnNegativeZeroVolumes: + self.m_logger.info( "The following table displays the indexes of the cells with a zero or negative volume" ) + self.m_logger.info( self.getNegativeZeroVolumes() ) + + return 1 + + def SetLogger( self: Self, logger: Logger ) -> None: + """Set the logger. + + Args: + logger (Logger): logger + """ + self.m_logger = logger + self.Modified() + + def getGrid( self: Self ) -> vtkUnstructuredGrid: + """Returns the vtkUnstructuredGrid with volumes. + + Args: + self (Self) + + Returns: + vtkUnstructuredGrid + """ + self.Update() # triggers RequestData + return self.GetOutputDataObject( 0 ) + + def getNegativeZeroVolumes( self: Self ) -> npt.NDArray: + """Returns a numpy array of all the negative and zero volumes of the input vtkUnstructuredGrid. + + Args: + self (Self) + + Returns: + npt.NDArray + """ + assert self.m_volumes is not None + volumes_np: npt.NDArray = vtk_to_numpy( self.m_volumes ) + indices = np.where( volumes_np <= 0 )[ 0 ] + return np.column_stack( ( indices, volumes_np[ indices ] ) ) + + def setReturnNegativeZeroVolumes( self: Self, returnNegativeZeroVolumes: bool ) -> None: + """Set the condition to return or not the negative and Zero volumes when calculating the volumes. + + Args: + self (Self) + returnNegativeZeroVolumes (bool) + """ + self.m_returnNegativeZeroVolumes = returnNegativeZeroVolumes + self.Modified() + + def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: + """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath with volumes. + + Args: + filepath (str): /path/to/your/file.vtu + is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. + canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. + Defaults to False. + """ + mesh: vtkUnstructuredGrid = self.getGrid() + if mesh: + write_mesh( filepath, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) + else: + self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) diff --git a/geos-mesh/tests/test_element_volumes.py b/geos-mesh/tests/test_element_volumes.py index 50635eb09..7c8e92986 100644 --- a/geos-mesh/tests/test_element_volumes.py +++ b/geos-mesh/tests/test_element_volumes.py @@ -1,7 +1,228 @@ -import numpy -from vtkmodules.vtkCommonCore import vtkPoints -from vtkmodules.vtkCommonDataModel import VTK_TETRA, vtkCellArray, vtkTetra, vtkUnstructuredGrid +import numpy as np +import numpy.typing as npt +import pytest +from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkHexahedron, vtkTetra, vtkUnstructuredGrid, VTK_TETRA +from vtkmodules.vtkCommonCore import vtkPoints, vtkIdList +from vtkmodules.util.numpy_support import vtk_to_numpy from geos.mesh.doctor.checks.element_volumes import Options, __check +from geos.mesh.doctor.filters.ElementVolumes import ElementVolumes + + +@pytest.fixture +def tetra_mesh() -> vtkUnstructuredGrid: + """Create a simple tetrahedron with known volume (1/6)""" + points = vtkPoints() + points.InsertNextPoint( 0, 0, 0 ) # Point 0 + points.InsertNextPoint( 1, 0, 0 ) # Point 1 + points.InsertNextPoint( 0, 1, 0 ) # Point 2 + points.InsertNextPoint( 0, 0, 1 ) # Point 3 + + tetra = vtkTetra() + tetra.GetPointIds().SetId( 0, 0 ) + tetra.GetPointIds().SetId( 1, 1 ) + tetra.GetPointIds().SetId( 2, 2 ) + tetra.GetPointIds().SetId( 3, 3 ) + + ug = vtkUnstructuredGrid() + ug.SetPoints( points ) + ug.InsertNextCell( tetra.GetCellType(), tetra.GetPointIds() ) + return ug + + +@pytest.fixture +def hexa_mesh() -> vtkUnstructuredGrid: + """Create a simple hexahedron with known volume (1.0)""" + points = vtkPoints() + points.InsertNextPoint( 0, 0, 0 ) # Point 0 + points.InsertNextPoint( 1, 0, 0 ) # Point 1 + points.InsertNextPoint( 1, 1, 0 ) # Point 2 + points.InsertNextPoint( 0, 1, 0 ) # Point 3 + points.InsertNextPoint( 0, 0, 1 ) # Point 4 + points.InsertNextPoint( 1, 0, 1 ) # Point 5 + points.InsertNextPoint( 1, 1, 1 ) # Point 6 + points.InsertNextPoint( 0, 1, 1 ) # Point 7 + + hexa = vtkHexahedron() + for i in range( 8 ): + hexa.GetPointIds().SetId( i, i ) + + ug = vtkUnstructuredGrid() + ug.SetPoints( points ) + ug.InsertNextCell( hexa.GetCellType(), hexa.GetPointIds() ) + return ug + + +@pytest.fixture +def negative_vol_mesh() -> vtkUnstructuredGrid: + """Create a tetrahedron with negative volume (wrong winding)""" + points = vtkPoints() + points.InsertNextPoint( 0, 0, 0 ) # Point 0 + points.InsertNextPoint( 1, 0, 0 ) # Point 1 + points.InsertNextPoint( 0, 1, 0 ) # Point 2 + points.InsertNextPoint( 0, 0, 1 ) # Point 3 + + tetra = vtkTetra() + # Switch two points to create negative volume + tetra.GetPointIds().SetId( 0, 0 ) + tetra.GetPointIds().SetId( 1, 2 ) # Swapped from normal order + tetra.GetPointIds().SetId( 2, 1 ) # Swapped from normal order + tetra.GetPointIds().SetId( 3, 3 ) + + ug = vtkUnstructuredGrid() + ug.SetPoints( points ) + ug.InsertNextCell( tetra.GetCellType(), tetra.GetPointIds() ) + return ug + + +@pytest.fixture +def zero_vol_mesh() -> vtkUnstructuredGrid: + """Create a tetrahedron with zero volume (coplanar points)""" + points = vtkPoints() + points.InsertNextPoint( 0, 0, 0 ) # Point 0 + points.InsertNextPoint( 1, 0, 0 ) # Point 1 + points.InsertNextPoint( 0, 1, 0 ) # Point 2 + points.InsertNextPoint( 1, 1, 0 ) # Point 3 (coplanar with others) + + tetra = vtkTetra() + tetra.GetPointIds().SetId( 0, 0 ) + tetra.GetPointIds().SetId( 1, 1 ) + tetra.GetPointIds().SetId( 2, 2 ) + tetra.GetPointIds().SetId( 3, 3 ) + + ug = vtkUnstructuredGrid() + ug.SetPoints( points ) + ug.InsertNextCell( tetra.GetCellType(), tetra.GetPointIds() ) + return ug + + +@pytest.fixture +def volume_filter() -> ElementVolumes: + """Create a fresh ElementVolumes filter for each test""" + return ElementVolumes() + + +def test_tetrahedron_volume( tetra_mesh: vtkUnstructuredGrid, volume_filter: ElementVolumes ) -> None: + """Test volume calculation for a regular tetrahedron""" + volume_filter.SetInputDataObject( 0, tetra_mesh ) + volume_filter.Update() + output: vtkUnstructuredGrid = volume_filter.getGrid() + + volumes: npt.NDArray = vtk_to_numpy( output.GetCellData().GetArray( "MESH_DOCTOR_VOLUME" ) ) + expected_volume: float = 1 / 6 # Tetrahedron volume + + assert len( volumes ) == 1 + assert volumes[ 0 ] == pytest.approx( expected_volume, abs=1e-6 ) + + +def test_hexahedron_volume( hexa_mesh: vtkUnstructuredGrid, volume_filter: ElementVolumes ) -> None: + """Test volume calculation for a regular hexahedron""" + volume_filter.SetInputDataObject( 0, hexa_mesh ) + volume_filter.Update() + output: vtkUnstructuredGrid = volume_filter.getGrid() + + volumes: npt.NDArray = vtk_to_numpy( output.GetCellData().GetArray( "MESH_DOCTOR_VOLUME" ) ) + expected_volume: float = 1.0 # Unit cube volume + + assert len( volumes ) == 1 + assert volumes[ 0 ] == pytest.approx( expected_volume, abs=1e-6 ) + + +def test_negative_volume_detection( negative_vol_mesh: vtkUnstructuredGrid, volume_filter: ElementVolumes ) -> None: + """Test detection of negative volumes""" + volume_filter.SetInputDataObject( 0, negative_vol_mesh ) + volume_filter.setReturnNegativeZeroVolumes( True ) + volume_filter.Update() + + output: vtkUnstructuredGrid = volume_filter.getGrid() + volumes: npt.NDArray = vtk_to_numpy( output.GetCellData().GetArray( "MESH_DOCTOR_VOLUME" ) ) + + assert len( volumes ) == 1 + assert volumes[ 0 ] < 0 + + # Test getNegativeZeroVolumes method + negative_zero_volumes: npt.NDArray = volume_filter.getNegativeZeroVolumes() + assert len( negative_zero_volumes ) == 1 + assert negative_zero_volumes[ 0, 0 ] == 0 # First cell index + assert negative_zero_volumes[ 0, 1 ] == volumes[ 0 ] # Volume value + + +def test_zero_volume_detection( zero_vol_mesh: vtkUnstructuredGrid, volume_filter: ElementVolumes ) -> None: + """Test detection of zero volumes""" + volume_filter.SetInputDataObject( 0, zero_vol_mesh ) + volume_filter.setReturnNegativeZeroVolumes( True ) + volume_filter.Update() + + output: vtkUnstructuredGrid = volume_filter.getGrid() + volumes: npt.NDArray = vtk_to_numpy( output.GetCellData().GetArray( "MESH_DOCTOR_VOLUME" ) ) + + assert len( volumes ) == 1 + assert volumes[ 0 ] == pytest.approx( 0.0, abs=1e-6 ) + + # Test getNegativeZeroVolumes method + negative_zero_volumes: npt.NDArray = volume_filter.getNegativeZeroVolumes() + assert len( negative_zero_volumes ) == 1 + assert negative_zero_volumes[ 0, 0 ] == 0 # First cell index + assert negative_zero_volumes[ 0, 1 ] == pytest.approx( 0.0, abs=1e-6 ) # Volume value + + +def test_return_negative_zero_volumes_flag( volume_filter: ElementVolumes ) -> None: + """Test setting and getting the returnNegativeZeroVolumes flag""" + # Default should be False + assert not volume_filter.m_returnNegativeZeroVolumes + + # Set to True and verify + volume_filter.setReturnNegativeZeroVolumes( True ) + assert volume_filter.m_returnNegativeZeroVolumes + + # Set to False and verify + volume_filter.setReturnNegativeZeroVolumes( False ) + assert not volume_filter.m_returnNegativeZeroVolumes + + +def test_mixed_mesh( tetra_mesh: vtkUnstructuredGrid, hexa_mesh: vtkUnstructuredGrid, + volume_filter: ElementVolumes ) -> None: + """Test with a combined mesh containing multiple element types""" + # Create a mixed mesh with both tet and hex + mixed_mesh = vtkUnstructuredGrid() + + # Copy points from tetra_mesh + tetra_points: vtkPoints = tetra_mesh.GetPoints() + points = vtkPoints() + for i in range( tetra_points.GetNumberOfPoints() ): + points.InsertNextPoint( tetra_points.GetPoint( i ) ) + + # Add points from hexa_mesh with offset + hexa_points: vtkPoints = hexa_mesh.GetPoints() + offset: int = points.GetNumberOfPoints() + for i in range( hexa_points.GetNumberOfPoints() ): + x, y, z = hexa_points.GetPoint( i ) + points.InsertNextPoint( x + 2, y, z ) # Offset in x-direction + + mixed_mesh.SetPoints( points ) + + # Add tetra cell + tetra_cell: vtkTetra = tetra_mesh.GetCell( 0 ) + ids: vtkIdList = tetra_cell.GetPointIds() + mixed_mesh.InsertNextCell( tetra_cell.GetCellType(), ids.GetNumberOfIds(), + [ ids.GetId( i ) for i in range( ids.GetNumberOfIds() ) ] ) + + # Add hexa cell with offset ids + hexa_cell: vtkHexahedron = hexa_mesh.GetCell( 0 ) + ids: vtkIdList = hexa_cell.GetPointIds() + hexa_ids: list[ int ] = [ ids.GetId( i ) + offset for i in range( ids.GetNumberOfIds() ) ] + mixed_mesh.InsertNextCell( hexa_cell.GetCellType(), len( hexa_ids ), hexa_ids ) + + # Apply filter + volume_filter.SetInputDataObject( 0, mixed_mesh ) + volume_filter.Update() + output: vtkUnstructuredGrid = volume_filter.getGrid() + + # Check volumes + volumes: npt.NDArray = vtk_to_numpy( output.GetCellData().GetArray( "MESH_DOCTOR_VOLUME" ) ) + + assert len( volumes ) == 2 + assert volumes[ 0 ] == pytest.approx( 1 / 6, abs=1e-6 ) # Tetrahedron volume + assert volumes[ 1 ] == pytest.approx( 1.0, abs=1e-6 ) # Hexahedron volume def test_simple_tet(): @@ -32,7 +253,7 @@ def test_simple_tet(): 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 + assert abs( result.element_volumes[ 0 ][ 1 ] - 1. / 6. ) < 10 * np.finfo( float ).eps result = __check( mesh, Options( min_volume=0. ) ) From b88bff72020e8c3ad063163bd3569863beb53e32 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Mon, 12 May 2025 10:01:43 -0700 Subject: [PATCH 03/52] Add GenerateFractures filter --- .../mesh/doctor/checks/generate_fractures.py | 6 +- .../mesh/doctor/filters/GenerateFractures.py | 212 ++++++++++++++++++ geos-mesh/tests/test_generate_fractures.py | 39 +++- 3 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py diff --git a/geos-mesh/src/geos/mesh/doctor/checks/generate_fractures.py b/geos-mesh/src/geos/mesh/doctor/checks/generate_fractures.py index bf6f961c9..5dce04a73 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/generate_fractures.py +++ b/geos-mesh/src/geos/mesh/doctor/checks/generate_fractures.py @@ -525,8 +525,8 @@ def __generate_fracture_mesh( old_mesh: vtkUnstructuredGrid, fracture_info: Frac return fracture_mesh -def __split_mesh_on_fractures( mesh: vtkUnstructuredGrid, - options: Options ) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: +def split_mesh_on_fractures( mesh: vtkUnstructuredGrid, + options: Options ) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: all_fracture_infos: list[ FractureInfo ] = list() for fracture_id in range( len( options.field_values_per_fracture ) ): fracture_info: FractureInfo = build_fracture_info( mesh, options, False, fracture_id ) @@ -545,7 +545,7 @@ def __split_mesh_on_fractures( mesh: vtkUnstructuredGrid, def __check( mesh, options: Options ) -> Result: - output_mesh, fracture_meshes = __split_mesh_on_fractures( mesh, options ) + 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 ): write_mesh( fracture_mesh, options.all_fractures_VtkOutput[ i ] ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py new file mode 100644 index 000000000..743918fc1 --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py @@ -0,0 +1,212 @@ +from typing_extensions import Self +from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase +from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from geos.mesh.doctor.checks.generate_fractures import Options, split_mesh_on_fractures +from geos.mesh.doctor.parsing.generate_fractures_parsing import convert, convert_to_fracture_policy +from geos.mesh.doctor.parsing.generate_fractures_parsing import ( __FIELD_NAME, __FIELD_VALUES, + __FRACTURES_DATA_MODE, __FRACTURES_OUTPUT_DIR, + __FRACTURES_DATA_MODE_VALUES, __POLICIES, __POLICY ) +from geos.mesh.vtk.io import VtkOutput, write_mesh +from geos.mesh.vtk.helpers import has_invalid_field +from geos.utils.Logger import Logger, getLogger + +__doc__ = """ +GenerateFractures module is a vtk filter that takes as input a vtkUnstructuredGrid that needs to be splited along +non embedded fractures. When saying "splited", it implies that if a fracture plane is defined between 2 cells, +the nodes of the face shared between both cells will be duplicated simple vtkUnstructuredGrid rectilinear grid. +GlobalIds for points and cells can be added. +You can create CellArray and PointArray of constant value = 1 and any dimension >= 1. + +No filter input and one output type which is vtkUnstructuredGrid. + +To use the filter: + +.. code-block:: python + +""" + + +FIELD_NAME = __FIELD_NAME +FIELD_VALUES = __FIELD_VALUES +FRACTURES_DATA_MODE = __FRACTURES_DATA_MODE +DATA_MODE = __FRACTURES_DATA_MODE_VALUES +FRACTURES_OUTPUT_DIR = __FRACTURES_OUTPUT_DIR +POLICIES = __POLICIES +POLICY = __POLICY + + +class GenerateFractures( VTKPythonAlgorithmBase ): + + def __init__( self: Self ) -> None: + """Vtk filter to generate a simple rectilinear grid. + + Output mesh is vtkUnstructuredGrid. + """ + super().__init__( nInputPorts=1, nOutputPorts=2, inputType='vtkUnstructuredGrid', + outputType='vtkUnstructuredGrid' ) + self.m_policy: str = POLICIES[ 1 ] + self.m_field_name: str = None + self.m_field_values: str = None + self.m_fractures_output_dir: str = None + self.m_output_modes_binary: str = { "mesh": DATA_MODE[ 0 ], "fractures": DATA_MODE[ 1 ] } + self.m_mesh_VtkOutput: VtkOutput = None + self.m_all_fractures_VtkOutput: list[ VtkOutput ] = None + self.m_logger: Logger = getLogger( "Generate Fractures Filter" ) + + def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestInformation. + + Args: + port (int): input port + info (vtkInformationVector): info + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + if port == 0: + info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) + return 1 + + def RequestInformation( + self: Self, + request: vtkInformation, # noqa: F841 + inInfoVec: list[ vtkInformationVector ], # noqa: F841 + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestInformation. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + executive = self.GetExecutive() # noqa: F841 + outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 + return 1 + + def RequestData( + self: Self, + request: vtkInformation, + inInfoVec: list[ vtkInformationVector ], + outInfo: list[ vtkInformationVector ] + ) -> int: + input_mesh = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) + if has_invalid_field( input_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." ) + self.m_logger.error( err_msg ) + return 0 + + parsed_options: dict[ str, str ] = self.getParsedOptions() + self.m_logger.critical( f"Parsed_options:\n{parsed_options}" ) + if len( parsed_options ) < 5: + self.m_logger.error( "You must set all variables before trying to create fractures." ) + return 0 + + options: Options = convert( parsed_options ) + self.m_all_fractures_VtkOutput = options.all_fractures_VtkOutput + output_mesh, fracture_meshes = split_mesh_on_fractures( input_mesh, options ) + opt = vtkUnstructuredGrid.GetData( outInfo, 0 ) + opt.ShallowCopy( output_mesh ) + + nbr_faults: int = len( fracture_meshes ) + self.SetNumberOfOutputPorts( 1 + nbr_faults ) # one output port for splitted mesh, the rest for every fault + for i in range( nbr_faults ): + opt_fault = vtkUnstructuredGrid.GetData( outInfo, i + 1 ) + opt_fault.ShallowCopy( fracture_meshes[ i ] ) + + return 1 + + def SetLogger( self: Self, logger: Logger ) -> None: + """Set the logger. + + Args: + logger (Logger): logger + """ + self.m_logger = logger + self.Modified() + + def getAllGrids( self: Self ) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: + """Returns the vtkUnstructuredGrid with volumes. + + Args: + self (Self) + + Returns: + vtkUnstructuredGrid + """ + self.Update() # triggers RequestData + splitted_grid: vtkUnstructuredGrid = self.GetOutputDataObject( 0 ) + nbrOutputPorts: int = self.GetNumberOfOutputPorts() + fracture_meshes: list[ vtkUnstructuredGrid ] = list() + for i in range( 1, nbrOutputPorts ): + fracture_meshes.append( self.GetOutputDataObject( i ) ) + return ( splitted_grid, fracture_meshes ) + + def getParsedOptions( self: Self ) -> dict[ str, str ]: + parsed_options: dict[ str, str ] = { "output": "./mesh.vtu", "data_mode": DATA_MODE[ 0 ] } + parsed_options[ POLICY ] = self.m_policy + parsed_options[ FRACTURES_DATA_MODE ] = self.m_output_modes_binary[ "fractures" ] + if self.m_field_name: + parsed_options[ FIELD_NAME ] = self.m_field_name + else: + self.m_logger.error( "No field name provided. Please use setFieldName." ) + if self.m_field_values: + parsed_options[ FIELD_VALUES ] = self.m_field_values + else: + self.m_logger.error( "No field values provided. Please use setFieldValues." ) + if self.m_fractures_output_dir: + parsed_options[ FRACTURES_OUTPUT_DIR ] = self.m_fractures_output_dir + else: + self.m_logger.error( "No fracture output directory provided. Please use setFracturesOutputDirectory." ) + return parsed_options + + def setFieldName( self: Self, field_name: str ) -> None: + self.m_field_name = field_name + self.Modified() + + def setFieldValues( self: Self, field_values: str ) -> None: + self.m_field_values = field_values + self.Modified() + + def setFracturesDataMode( self: Self, choice: int ) -> None: + if choice not in [ 0, 1 ]: + self.m_logger.error( f"setFracturesDataMode: Please choose either 0 for {DATA_MODE[ 0 ]} or 1 for", + f" {DATA_MODE[ 1 ]}, not '{choice}'." ) + else: + self.m_output_modes_binary[ "fractures" ] = DATA_MODE[ choice ] + self.Modified() + + def setFracturesOutputDirectory( self: Self, directory: str ) -> None: + self.m_fractures_output_dir = directory + self.Modified() + + def setOutputDataMode( self: Self, choice: int ) -> None: + if choice not in [ 0, 1 ]: + self.m_logger.error( f"setOutputDataMode: Please choose either 0 for {DATA_MODE[ 0 ]} or 1 for", + f" {DATA_MODE[ 1 ]}, not '{choice}'." ) + else: + self.m_output_modes_binary[ "mesh" ] = DATA_MODE[ choice ] + self.Modified() + + def setPolicy( self: Self, choice: int ) -> None: + if choice not in [ 0, 1 ]: + self.m_logger.error( f"setPolicy: Please choose either 0 for {POLICIES[ 0 ]} or 1 for {POLICIES[ 1 ]}," + f" not '{choice}'." ) + else: + self.m_policy = convert_to_fracture_policy( POLICIES[ choice ] ) + self.Modified() + + def writeMeshes( self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: + splitted_grid, fracture_meshes = self.getAllGrids() + if splitted_grid: + write_mesh( splitted_grid, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) + else: + self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) + + for i, fracture_mesh in enumerate( fracture_meshes ): + write_mesh( fracture_mesh, self.m_all_fractures_VtkOutput[ i ] ) diff --git a/geos-mesh/tests/test_generate_fractures.py b/geos-mesh/tests/test_generate_fractures.py index f97d4be99..8af7f0c82 100644 --- a/geos-mesh/tests/test_generate_fractures.py +++ b/geos-mesh/tests/test_generate_fractures.py @@ -6,8 +6,9 @@ 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, +from geos.mesh.doctor.checks.generate_fractures import ( split_mesh_on_fractures, Options, FracturePolicy, Coordinates3D, IDMapping ) +from geos.mesh.doctor.filters.GenerateFractures import GenerateFractures from geos.mesh.vtk.helpers import to_vtk_id_list FaceNodesCoords = tuple[ tuple[ float ] ] @@ -202,7 +203,7 @@ def __generate_test_data() -> Iterator[ TestCase ]: @pytest.mark.parametrize( "test_case", __generate_test_data() ) def test_generate_fracture( test_case: TestCase ): - main_mesh, fracture_meshes = __split_mesh_on_fractures( test_case.input_mesh, test_case.options ) + main_mesh, fracture_meshes = split_mesh_on_fractures( test_case.input_mesh, test_case.options ) fracture_mesh: vtkUnstructuredGrid = fracture_meshes[ 0 ] assert main_mesh.GetNumberOfPoints() == test_case.result.main_mesh_num_points assert main_mesh.GetNumberOfCells() == test_case.result.main_mesh_num_cells @@ -214,6 +215,32 @@ def test_generate_fracture( test_case: TestCase ): assert len( res ) == test_case.result.fracture_mesh_num_points +@pytest.mark.parametrize( "test_case_filter", __generate_test_data() ) +def test_GenerateFracture( test_case_filter: TestCase ): + genFracFilter = GenerateFractures() + genFracFilter.SetInputDataObject( 0, test_case_filter.input_mesh ) + genFracFilter.setFieldName( test_case_filter.options.field ) + field_values: str = ','.join( map( str, test_case_filter.options.field_values_combined ) ) + genFracFilter.setFieldValues( field_values ) + genFracFilter.setFracturesOutputDirectory( "." ) + if test_case_filter.options.policy == FracturePolicy.FIELD: + genFracFilter.setPolicy( 0 ) + else: + genFracFilter.setPolicy( 1 ) + genFracFilter.Update() + + main_mesh, fracture_meshes = genFracFilter.getAllGrids() + fracture_mesh: vtkUnstructuredGrid = fracture_meshes[ 0 ] + assert main_mesh.GetNumberOfPoints() == test_case_filter.result.main_mesh_num_points + assert main_mesh.GetNumberOfCells() == test_case_filter.result.main_mesh_num_cells + assert fracture_mesh.GetNumberOfPoints() == test_case_filter.result.fracture_mesh_num_points + assert fracture_mesh.GetNumberOfCells() == test_case_filter.result.fracture_mesh_num_cells + + res = format_collocated_nodes( fracture_mesh ) + assert res == test_case_filter.collocated_nodes + assert len( res ) == test_case_filter.result.fracture_mesh_num_points + + 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 where the goal is to add a cell data array with incrementing values. @@ -299,7 +326,7 @@ def add_quad( mesh: vtkUnstructuredGrid, face: FaceNodesCoords ): @pytest.mark.skip( "Test to be fixed" ) def test_copy_fields_when_splitting_mesh(): """This test is designed to check the __copy_fields method from generate_fractures, - that will be called when using __split_mesh_on_fractures method from generate_fractures. + that will be called when using split_mesh_on_fractures method from generate_fractures. """ # Generating the rectilinear grid and its quads on all borders x: numpy.array = numpy.array( [ 0, 1, 2 ] ) @@ -330,7 +357,7 @@ def test_copy_fields_when_splitting_mesh(): field_values_per_fracture=[ frozenset( map( int, [ "9" ] ) ) ], mesh_VtkOutput=None, all_fractures_VtkOutput=None ) - main_mesh, fracture_meshes = __split_mesh_on_fractures( mesh, options ) + main_mesh, fracture_meshes = split_mesh_on_fractures( mesh, options ) fracture_mesh: vtkUnstructuredGrid = fracture_meshes[ 0 ] assert main_mesh.GetCellData().GetNumberOfArrays() == 1 assert fracture_mesh.GetCellData().GetNumberOfArrays() == 1 @@ -344,7 +371,7 @@ def test_copy_fields_when_splitting_mesh(): # Test for invalid point field name add_simplified_field_for_cells( mesh, "GLOBAL_IDS_POINTS", 1 ) with pytest.raises( ValueError ) as pytest_wrapped_e: - main_mesh, fracture_meshes = __split_mesh_on_fractures( mesh, options ) + main_mesh, fracture_meshes = split_mesh_on_fractures( mesh, options ) assert pytest_wrapped_e.type == ValueError # Test for invalid cell field name mesh: vtkUnstructuredGrid = build_rectilinear_blocks_mesh( [ xyzs ] ) @@ -356,5 +383,5 @@ def test_copy_fields_when_splitting_mesh(): add_simplified_field_for_cells( mesh, "GLOBAL_IDS_CELLS", 1 ) assert mesh.GetCellData().GetNumberOfArrays() == 2 with pytest.raises( ValueError ) as pytest_wrapped_e: - main_mesh, fracture_meshes = __split_mesh_on_fractures( mesh, options ) + main_mesh, fracture_meshes = split_mesh_on_fractures( mesh, options ) assert pytest_wrapped_e.type == ValueError From d32ad33f265cf44a6bd62fde5d80984e8c7b5405 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 13 May 2025 16:57:36 -0700 Subject: [PATCH 04/52] Add CollocatedNodes filter --- .../mesh/doctor/checks/collocated_nodes.py | 39 ++-- .../mesh/doctor/filters/CollocatedNodes.py | 201 ++++++++++++++++++ geos-mesh/tests/test_collocated_nodes.py | 145 +++++++++++++ 3 files changed, 369 insertions(+), 16 deletions(-) create mode 100644 geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py diff --git a/geos-mesh/src/geos/mesh/doctor/checks/collocated_nodes.py b/geos-mesh/src/geos/mesh/doctor/checks/collocated_nodes.py index 91632b3ee..d64728ed2 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/collocated_nodes.py +++ b/geos-mesh/src/geos/mesh/doctor/checks/collocated_nodes.py @@ -2,9 +2,8 @@ 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 vtkmodules.vtkCommonDataModel import vtkIncrementalOctreePointLocator, vtkPointSet, vtkCell from geos.mesh.vtk.io import read_mesh @@ -15,23 +14,23 @@ class Options: @dataclass( frozen=True ) class Result: - nodes_buckets: Iterable[ Iterable[ int ] ] # Each bucket contains the duplicated node indices. - wrong_support_elements: Collection[ int ] # Element indices with support node indices appearing more than once. + nodes_buckets: list[ tuple[ int ] ] # Each bucket contains the duplicated node indices. + wrong_support_elements: list[ int ] # Element indices with support node indices appearing more than once. -def __check( mesh, options: Options ) -> Result: - points = mesh.GetPoints() +def find_collocated_nodes_buckets( mesh: vtkPointSet, tolerance: float ) -> list[ tuple[ int ] ]: + points: vtkPoints = mesh.GetPoints() locator = vtkIncrementalOctreePointLocator() - locator.SetTolerance( options.tolerance ) + locator.SetTolerance( tolerance ) output = vtkPoints() locator.InitPointInsertion( output, points.GetBounds() ) # original ids to/from filtered ids. filtered_to_original = numpy.ones( points.GetNumberOfPoints(), dtype=int ) * -1 - rejected_points = defaultdict( list ) - point_id = reference( 0 ) + rejected_points: dict[ int, list[ int ] ] = defaultdict( list ) + point_id: int = reference( 0 ) for i in range( points.GetNumberOfPoints() ): is_inserted = locator.InsertUniquePoint( points.GetPoint( i ), point_id ) if not is_inserted: @@ -48,21 +47,29 @@ def __check( mesh, options: Options ) -> Result: # original_to_filtered[i] = point_id.get() filtered_to_original[ point_id.get() ] = i - tmp = [] + collocated_nodes_buckets: list[ tuple[ int ] ] = list() for n, ns in rejected_points.items(): - tmp.append( ( n, *ns ) ) + collocated_nodes_buckets.append( ( n, *ns ) ) + return collocated_nodes_buckets + +def find_wrong_support_elements( mesh: vtkPointSet ) -> list[ int ]: # Checking that the support node indices appear only once per element. - wrong_support_elements = [] + wrong_support_elements: list[ int ] = list() for c in range( mesh.GetNumberOfCells() ): - cell = mesh.GetCell( c ) - num_points_per_cell = cell.GetNumberOfPoints() + cell: vtkCell = mesh.GetCell( c ) + num_points_per_cell: int = cell.GetNumberOfPoints() if len( { cell.GetPointId( i ) for i in range( num_points_per_cell ) } ) != num_points_per_cell: wrong_support_elements.append( c ) + return wrong_support_elements + - return Result( nodes_buckets=tmp, wrong_support_elements=wrong_support_elements ) +def __check( mesh: vtkPointSet, options: Options ) -> Result: + collocated_nodes_buckets = find_collocated_nodes_buckets( mesh, options.tolerance ) + wrong_support_elements = find_wrong_support_elements( mesh ) + return Result( nodes_buckets=collocated_nodes_buckets, wrong_support_elements=wrong_support_elements ) def check( vtk_input_file: str, options: Options ) -> Result: - mesh = read_mesh( vtk_input_file ) + mesh: vtkPointSet = read_mesh( vtk_input_file ) return __check( mesh, options ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py new file mode 100644 index 000000000..2719fc1a3 --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py @@ -0,0 +1,201 @@ +import numpy as np +import numpy.typing as npt +from typing_extensions import Self +from vtkmodules.util.numpy_support import numpy_to_vtk +from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase +from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from geos.mesh.doctor.checks.collocated_nodes import find_collocated_nodes_buckets, find_wrong_support_elements +from geos.mesh.vtk.io import VtkOutput, write_mesh +from geos.utils.Logger import Logger, getLogger + +__doc__ = """ +CollocatedNodes module is a vtk filter that allows to find the duplicated nodes of a vtkUnstructuredGrid. + +One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. + +To use the filter: + +.. code-block:: python + + from filters.CollocatedNodes import CollocatedNodes + + # instanciate the filter + collocatedNodesFilter: CollocatedNodes = CollocatedNodes() + +""" + + +class CollocatedNodes( VTKPythonAlgorithmBase ): + + def __init__( self: Self ) -> None: + """Vtk filter to find the duplicated nodes of a vtkUnstructuredGrid. + + Output mesh is vtkUnstructuredGrid. + """ + super().__init__( nInputPorts=1, nOutputPorts=1, inputType='vtkUnstructuredGrid', + outputType='vtkUnstructuredGrid' ) + self.m_collocatedNodesBuckets: list[ tuple[ int ] ] = list() + self.m_paintWrongSupportElements: int = 0 + self.m_tolerance: float = 0.0 + self.m_wrongSupportElements: list[ int ] = list() + self.m_logger: Logger = getLogger( "Element Volumes Filter" ) + + def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestInformation. + + Args: + port (int): input port + info (vtkInformationVector): info + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + if port == 0: + info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) + return 1 + + def RequestInformation( + self: Self, + request: vtkInformation, # noqa: F841 + inInfoVec: list[ vtkInformationVector ], # noqa: F841 + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestInformation. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + executive = self.GetExecutive() # noqa: F841 + outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 + return 1 + + def RequestData( + self: Self, + request: vtkInformation, + inInfoVec: list[ vtkInformationVector ], + outInfo: vtkInformationVector + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestData. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) + output = vtkUnstructuredGrid.GetData( outInfo ) + + self.m_collocatedNodesBuckets = find_collocated_nodes_buckets( input_mesh, self.m_tolerance ) + self.m_wrongSupportElements = find_wrong_support_elements( input_mesh ) + + self.m_logger.info( "The following list displays the nodes buckets that contains the duplicated node indices." ) + self.m_logger.info( self.getCollocatedNodeBuckets() ) + + self.m_logger.info( "The following list displays the indexes of the cells with support node indices " + + " appearing twice or more." ) + self.m_logger.info( self.getWrongSupportElements() ) + + output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() + output_mesh.CopyStructure( input_mesh ) + output_mesh.CopyAttributes( input_mesh ) + + if self.m_paintWrongSupportElements: + arrayWSP: npt.NDArray = np.zeros( ( output_mesh.GetNumberOfCells(), 1 ), dtype=int ) + arrayWSP[ self.m_wrongSupportElements ] = 1 + vtkArrayWSP: vtkDataArray = numpy_to_vtk( arrayWSP ) + vtkArrayWSP.SetName( "HasDuplicatedNodes" ) + output_mesh.GetCellData().AddArray( vtkArrayWSP ) + + output.ShallowCopy( output_mesh ) + + return 1 + + def SetLogger( self: Self, logger: Logger ) -> None: + """Set the logger. + + Args: + logger (Logger): logger + """ + self.m_logger = logger + self.Modified() + + def getGrid( self: Self ) -> vtkUnstructuredGrid: + """Returns the vtkUnstructuredGrid with volumes. + + Args: + self (Self) + + Returns: + vtkUnstructuredGrid + """ + self.Update() # triggers RequestData + return self.GetOutputDataObject( 0 ) + + def getCollocatedNodeBuckets( self: Self ) -> list[ tuple[ int ] ]: + """Returns the nodes buckets that contains the duplicated node indices. + + Args: + self (Self) + + Returns: + list[ tuple[ int ] ] + """ + return self.m_collocatedNodesBuckets + + def getWrongSupportElements( self: Self ) -> list[ int ]: + """Returns the element indices with support node indices appearing more than once. + + Args: + self (Self) + + Returns: + list[ int ] + """ + return self.m_wrongSupportElements + + def setPaintWrongSupportElements( self: Self, choice: int ) -> None: + """Set 0 or 1 to choose if you want to create a new "WrongSupportElements" array in your output data. + + Args: + self (Self) + choice (int): 0 or 1 + """ + if choice not in [ 0, 1 ]: + self.m_logger.error( f"setPaintWrongSupportElements: Please choose either 0 or 1 not '{choice}'." ) + else: + self.m_paintWrongSupportElements = choice + self.Modified() + + def setTolerance( self: Self, tolerance: float ) -> None: + """Set the tolerance parameter to define if two points are collocated or not. + + Args: + self (Self) + tolerance (float) + """ + self.m_tolerance = tolerance + self.Modified() + + def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: + """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath with volumes. + + Args: + filepath (str): /path/to/your/file.vtu + is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. + canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. + Defaults to False. + """ + mesh: vtkUnstructuredGrid = self.getGrid() + if mesh: + write_mesh( filepath, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) + else: + self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) diff --git a/geos-mesh/tests/test_collocated_nodes.py b/geos-mesh/tests/test_collocated_nodes.py index 2b74e30fe..cca423cfe 100644 --- a/geos-mesh/tests/test_collocated_nodes.py +++ b/geos-mesh/tests/test_collocated_nodes.py @@ -3,6 +3,7 @@ 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.filters.CollocatedNodes import CollocatedNodes def get_points() -> Iterator[ Tuple[ vtkPoints, int ] ]: @@ -64,3 +65,147 @@ def test_wrong_support_elements(): assert len( result.nodes_buckets ) == 0 assert len( result.wrong_support_elements ) == 1 assert result.wrong_support_elements[ 0 ] == 0 + + +# Create a test mesh with collocated nodes +@pytest.fixture +def sample_mesh() -> vtkUnstructuredGrid: + # Create a simple mesh with duplicate points + mesh = vtkUnstructuredGrid() + + # Create points + points = vtkPoints() + points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 0 + points.InsertNextPoint( 1.0, 0.0, 0.0 ) # Point 1 + points.InsertNextPoint( 0.0, 1.0, 0.0 ) # Point 2 + points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 3 - duplicate of Point 0 + points.InsertNextPoint( 2.0, 0.0, 0.0 ) # Point 4 + mesh.SetPoints( points ) + + # Create cells + cells = vtkCellArray() + # Create a triangular cell with normal connectivity + cells.InsertNextCell( 3 ) + cells.InsertCellPoint( 0 ) + cells.InsertCellPoint( 1 ) + cells.InsertCellPoint( 2 ) + + # Create a cell with duplicate point indices + cells.InsertNextCell( 3 ) + cells.InsertCellPoint( 3 ) # This is a duplicate of point 0 + cells.InsertCellPoint( 1 ) + cells.InsertCellPoint( 4 ) + mesh.SetCells( 5, cells ) # 5 is VTK_TRIANGLE + return mesh + + +@pytest.fixture +def collocated_nodes_filter() -> CollocatedNodes: + return CollocatedNodes() + + +def test_init( collocated_nodes_filter: CollocatedNodes ): + """Test initialization of the CollocatedNodes filter.""" + assert collocated_nodes_filter.m_collocatedNodesBuckets == list() + assert collocated_nodes_filter.m_paintWrongSupportElements == 0 + assert collocated_nodes_filter.m_tolerance == 0.0 + assert collocated_nodes_filter.m_wrongSupportElements == list() + + +def test_collocated_nodes_detection( sample_mesh: vtkUnstructuredGrid, collocated_nodes_filter: CollocatedNodes ): + """Test the filter's ability to detect collocated nodes.""" + # Set input mesh + collocated_nodes_filter.SetInputDataObject( sample_mesh ) + + # Set tolerance + collocated_nodes_filter.setTolerance( 1e-6 ) + + # Run filter + collocated_nodes_filter.Update() + + # Check results + buckets = collocated_nodes_filter.getCollocatedNodeBuckets() + assert len( buckets ) > 0 + + # We expect points 0 and 3 to be in the same bucket + bucket_with_duplicates = None + for bucket in buckets: + if 0 in bucket and 3 in bucket: + bucket_with_duplicates = bucket + break + + assert bucket_with_duplicates is not None, "Failed to detect collocated nodes 0 and 3" + + +def test_wrong_support_elements2( sample_mesh: vtkUnstructuredGrid, collocated_nodes_filter: CollocatedNodes ): + """Test the filter's ability to detect elements with wrong support.""" + # Set input mesh + collocated_nodes_filter.SetInputDataObject( sample_mesh ) + + # Run filter + collocated_nodes_filter.Update() + + # Check results + wrong_elements = collocated_nodes_filter.getWrongSupportElements() + + # In our test mesh, we don't have cells with duplicate point indices within the same cell + # So this should be empty + assert isinstance( wrong_elements, list ) + + +def test_paint_wrong_support_elements( sample_mesh: vtkUnstructuredGrid, collocated_nodes_filter: CollocatedNodes ): + """Test the painting of wrong support elements.""" + # Set input mesh + collocated_nodes_filter.SetInputDataObject( sample_mesh ) + + # Enable painting + collocated_nodes_filter.setPaintWrongSupportElements( 1 ) + + # Run filter + collocated_nodes_filter.Update() + + # Get output mesh + output_mesh = collocated_nodes_filter.getGrid() + + # Check if the array was added + cell_data = output_mesh.GetCellData() + has_array = cell_data.HasArray( "HasDuplicatedNodes" ) + assert has_array, "The HasDuplicatedNodes array wasn't added to cell data" + + +def test_set_paint_wrong_support_elements( collocated_nodes_filter: CollocatedNodes ): + """Test setPaintWrongSupportElements method.""" + # Valid input + collocated_nodes_filter.setPaintWrongSupportElements( 1 ) + assert collocated_nodes_filter.m_paintWrongSupportElements == 1 + + # Valid input + collocated_nodes_filter.setPaintWrongSupportElements( 0 ) + assert collocated_nodes_filter.m_paintWrongSupportElements == 0 + + # Invalid input + collocated_nodes_filter.setPaintWrongSupportElements( 2 ) + # Should remain unchanged + assert collocated_nodes_filter.m_paintWrongSupportElements == 0 + + +def test_set_tolerance( collocated_nodes_filter: CollocatedNodes ): + """Test setTolerance method.""" + collocated_nodes_filter.setTolerance( 0.001 ) + assert collocated_nodes_filter.m_tolerance == 0.001 + + +def test_get_collocated_node_buckets( collocated_nodes_filter: CollocatedNodes ): + """Test getCollocatedNodeBuckets method.""" + # Set a value manually + collocated_nodes_filter.m_collocatedNodesBuckets = [ ( 0, 1 ), ( 2, 3 ) ] + result = collocated_nodes_filter.getCollocatedNodeBuckets() + assert result == [ ( 0, 1 ), ( 2, 3 ) ] + + +def test_get_wrong_support_elements( collocated_nodes_filter: CollocatedNodes ): + """Test getWrongSupportElements method.""" + # Set a value manually + collocated_nodes_filter.m_wrongSupportElements = [ 0, 3, 5 ] + result = collocated_nodes_filter.getWrongSupportElements() + assert result == [ 0, 3, 5 ] From bac1a833ac6b1b95d1935aba801bd5fb90bba5a5 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Thu, 15 May 2025 15:24:33 -0700 Subject: [PATCH 05/52] Add SupportElements filter but without polyhedron checking due to lack of parallelism --- .../mesh/doctor/checks/supported_elements.py | 31 +- .../mesh/doctor/filters/SupportedElements.py | 210 ++++++++++++++ geos-mesh/tests/test_supported_elements.py | 274 +++++++++++++++++- 3 files changed, 502 insertions(+), 13 deletions(-) create mode 100644 geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py diff --git a/geos-mesh/src/geos/mesh/doctor/checks/supported_elements.py b/geos-mesh/src/geos/mesh/doctor/checks/supported_elements.py index affad3870..2e1a87643 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/supported_elements.py +++ b/geos-mesh/src/geos/mesh/doctor/checks/supported_elements.py @@ -30,6 +30,10 @@ class Result: # for multiprocessing, vtkUnstructuredGrid cannot be pickled. Let's use a global variable instead. MESH: Optional[ vtkUnstructuredGrid ] = None +supported_cell_types: set[ int ] = { + VTK_HEXAGONAL_PRISM, VTK_HEXAHEDRON, VTK_PENTAGONAL_PRISM, VTK_POLYHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_VOXEL, + VTK_WEDGE +} class IsPolyhedronConvertible: @@ -105,21 +109,19 @@ def __call__( self, ic: int ) -> int: return ic -def __check( mesh: vtkUnstructuredGrid, options: Options ) -> Result: +def find_unsupported_std_elements_types( mesh: vtkUnstructuredGrid ) -> set[ int ]: if hasattr( mesh, "GetDistinctCellTypesArray" ): # For more recent versions of vtk. - cell_types = set( vtk_to_numpy( mesh.GetDistinctCellTypesArray() ) ) + unique_cell_types = set( vtk_to_numpy( mesh.GetDistinctCellTypesArray() ) ) else: - cell_types = vtkCellTypes() - mesh.GetCellTypes( cell_types ) - cell_types = set( vtk_iter( cell_types ) ) - supported_cell_types = { - VTK_HEXAGONAL_PRISM, VTK_HEXAHEDRON, VTK_PENTAGONAL_PRISM, VTK_POLYHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_VOXEL, - VTK_WEDGE - } - unsupported_std_elements_types = cell_types - supported_cell_types + vtk_cell_types = vtkCellTypes() + mesh.GetCellTypes( vtk_cell_types ) + unique_cell_types = set( vtk_iter( vtk_cell_types ) ) + return unique_cell_types - supported_cell_types + +def find_unsupported_polyhedron_elements( mesh: vtkUnstructuredGrid, options: Options ) -> list[ int ]: # Dealing with polyhedron elements. - num_cells = mesh.GetNumberOfCells() + num_cells: int = 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 ), @@ -127,7 +129,12 @@ def __check( mesh: vtkUnstructuredGrid, options: Options ) -> Result: 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 ] + return [ i for i in result if i > -1 ] + + +def __check( mesh: vtkUnstructuredGrid, options: Options ) -> Result: + unsupported_std_elements_types: set[ int ] = find_unsupported_std_elements_types( mesh ) + unsupported_polyhedron_elements: list[ int ] = find_unsupported_polyhedron_elements( mesh, options ) return Result( unsupported_std_elements_types=frozenset( unsupported_std_elements_types ), unsupported_polyhedron_elements=frozenset( unsupported_polyhedron_elements ) ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py new file mode 100644 index 000000000..1bb13e26c --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py @@ -0,0 +1,210 @@ +import numpy as np +import numpy.typing as npt +from typing_extensions import Self +from vtkmodules.util.numpy_support import numpy_to_vtk +from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase +from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray, VTK_INT +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from geos.mesh.doctor.checks.supported_elements import ( Options, find_unsupported_std_elements_types, + find_unsupported_polyhedron_elements ) +from geos.mesh.vtk.io import VtkOutput, write_mesh +from geos.utils.Logger import Logger, getLogger + +__doc__ = """ +SupportedElements module is a vtk filter that allows ... a vtkUnstructuredGrid. + +One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. + +To use the filter: + +.. code-block:: python + + from filters.SupportedElements import SupportedElements + + # instanciate the filter + supportedElementsFilter: SupportedElements = SupportedElements() + +""" + + +class SupportedElements( VTKPythonAlgorithmBase ): + + def __init__( self: Self ) -> None: + """Vtk filter to ... a vtkUnstructuredGrid. + + Output mesh is vtkUnstructuredGrid. + """ + super().__init__( nInputPorts=1, + nOutputPorts=1, + inputType='vtkUnstructuredGrid', + outputType='vtkUnstructuredGrid' ) + self.m_paintUnsupportedElementTypes: int = 0 + # TODO Needs parallelism to work + # self.m_paintUnsupportedPolyhedrons: int = 0 + # self.m_chunk_size: int = 1 + # self.m_num_proc: int = 1 + self.m_logger: Logger = getLogger( "Element Volumes Filter" ) + + def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestInformation. + + Args: + port (int): input port + info (vtkInformationVector): info + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + if port == 0: + info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) + return 1 + + def RequestInformation( + self: Self, + request: vtkInformation, # noqa: F841 + inInfoVec: list[ vtkInformationVector ], # noqa: F841 + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestInformation. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + executive = self.GetExecutive() # noqa: F841 + outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 + return 1 + + def RequestData( + self: Self, + request: vtkInformation, + inInfoVec: list[ vtkInformationVector ], + outInfo: vtkInformationVector + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestData. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) + output = vtkUnstructuredGrid.GetData( outInfo ) + + output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() + output_mesh.CopyStructure( input_mesh ) + output_mesh.CopyAttributes( input_mesh ) + + unsupported_std_elt_types: set[ int ] = find_unsupported_std_elements_types( input_mesh ) + if len( unsupported_std_elt_types ) > 0: + self.m_logger.info( "The following vtk element types in your mesh are not supported by GEOS:" ) + self.m_logger.info( unsupported_std_elt_types ) + + if self.m_paintUnsupportedElementTypes: + nbr_cells: int = output_mesh.GetNumberOfCells() + arrayCellTypes: npt.NDArray = np.zeros( nbr_cells, dtype=int ) + for i in range( nbr_cells ): + arrayCellTypes[ i ] = output_mesh.GetCellType(i) + + arrayUET: npt.NDArray = np.zeros( nbr_cells, dtype=int ) + arrayUET[ np.isin( arrayCellTypes, list( unsupported_std_elt_types ) ) ] = 1 + vtkArrayWSP: vtkDataArray = numpy_to_vtk( arrayUET ) + vtkArrayWSP.SetName( "HasUnsupportedType" ) + output_mesh.GetCellData().AddArray( vtkArrayWSP ) + + # TODO Needs parallelism to work + # options = Options( self.m_num_proc, self.m_chunk_size ) + # unsupported_polyhedron_elts: list[ int ] = find_unsupported_polyhedron_elements( input_mesh, options ) + # if len( unsupported_polyhedron_elts ) > 0: + # self.m_logger.info( "The following vtk polyhedron cell indexes in your mesh are not supported by GEOS:" ) + # self.m_logger.info( unsupported_polyhedron_elts ) + + # if self.m_paintUnsupportedPolyhedrons: + # arrayUP: npt.NDArray = np.zeros( output_mesh.GetNumberOfCells(), dtype=int ) + # arrayUP[ unsupported_polyhedron_elts ] = 1 + # self.m_logger.info( f"arrayUP: {arrayUP}" ) + # vtkArrayWSP: vtkDataArray = numpy_to_vtk( arrayUP ) + # vtkArrayWSP.SetName( "IsUnsupportedPolyhedron" ) + # output_mesh.GetCellData().AddArray( vtkArrayWSP ) + + output.ShallowCopy( output_mesh ) + + return 1 + + def SetLogger( self: Self, logger: Logger ) -> None: + """Set the logger. + + Args: + logger (Logger): logger + """ + self.m_logger = logger + self.Modified() + + def getGrid( self: Self ) -> vtkUnstructuredGrid: + """Returns the vtkUnstructuredGrid with volumes. + + Args: + self (Self) + + Returns: + vtkUnstructuredGrid + """ + self.Update() # triggers RequestData + return self.GetOutputDataObject( 0 ) + + def setPaintUnsupportedElementTypes( self: Self, choice: int ) -> None: + """Set 0 or 1 to choose if you want to create a new "HasUnsupportedType" array in your output data. + + Args: + self (Self) + choice (int): 0 or 1 + """ + if choice not in [ 0, 1 ]: + self.m_logger.error( f"setPaintUnsupportedElementTypes: Please choose either 0 or 1 not '{choice}'." ) + else: + self.m_paintUnsupportedElementTypes = choice + self.Modified() + + # TODO Needs parallelism to work + # def setPaintUnsupportedPolyhedrons( self: Self, choice: int ) -> None: + # """Set 0 or 1 to choose if you want to create a new "IsUnsupportedPolyhedron" array in your output data. + + # Args: + # self (Self) + # choice (int): 0 or 1 + # """ + # if choice not in [ 0, 1 ]: + # self.m_logger.error( f"setPaintUnsupportedPolyhedrons: Please choose either 0 or 1 not '{choice}'." ) + # else: + # self.m_paintUnsupportedPolyhedrons = choice + # self.Modified() + + # def setChunkSize( self: Self, new_chunk_size: int ) -> None: + # self.m_chunk_size = new_chunk_size + # self.Modified() + + # def setNumProc( self: Self, new_num_proc: int ) -> None: + # self.m_num_proc = new_num_proc + # self.Modified() + + def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: + """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath with volumes. + + Args: + filepath (str): /path/to/your/file.vtu + is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. + canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. + Defaults to False. + """ + mesh: vtkUnstructuredGrid = self.getGrid() + if mesh: + write_mesh( filepath, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) + else: + self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) diff --git a/geos-mesh/tests/test_supported_elements.py b/geos-mesh/tests/test_supported_elements.py index 6126b8ea3..a7701d0bd 100644 --- a/geos-mesh/tests/test_supported_elements.py +++ b/geos-mesh/tests/test_supported_elements.py @@ -1,10 +1,13 @@ # import os import pytest +import numpy as np from typing import Tuple from vtkmodules.vtkCommonCore import vtkIdList, vtkPoints -from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, VTK_POLYHEDRON +from vtkmodules.vtkCommonDataModel import ( vtkUnstructuredGrid, vtkQuad, vtkTetra, vtkHexahedron, vtkPolyhedron, + vtkCellArray, VTK_POLYHEDRON, VTK_QUAD, VTK_TETRA, VTK_HEXAHEDRON ) # 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.filters.SupportedElements import SupportedElements from geos.mesh.vtk.helpers import to_vtk_id_list @@ -119,3 +122,272 @@ def test_parse_face_stream() -> None: face_stream = FaceStream.build_from_vtk_id_list( faces ) assert face_stream.num_faces == 12 assert face_stream.num_support_points == 20 + + +def create_simple_tetra_grid(): + """Create a simple tetrahedral grid for testing""" + # Create an unstructured grid + points_tetras: vtkPoints = vtkPoints() + points_tetras_coords: list[ tuple[ float ] ] = [ ( 1.0, 0.5, 0.0 ), # point0 + ( 1.0, 0.0, 1.0 ), + ( 1.0, 1.0, 1.0 ), + ( 0.0, 0.5, 0.5 ), + ( 2.0, 0.5, 0.5 ), + ( 1.0, 0.5, 2.0 ), # point5 + ( 0.0, 0.5, 1.5 ), + ( 2.0, 0.5, 1.5 ) ] + for point_tetra in points_tetras_coords: + points_tetras.InsertNextPoint( point_tetra ) + + tetra1: vtkTetra = vtkTetra() + tetra1.GetPointIds().SetId( 0, 0 ) + tetra1.GetPointIds().SetId( 1, 1 ) + tetra1.GetPointIds().SetId( 2, 2 ) + tetra1.GetPointIds().SetId( 3, 3 ) + + tetra2: vtkTetra = vtkTetra() + tetra2.GetPointIds().SetId( 0, 0 ) + tetra2.GetPointIds().SetId( 1, 2 ) + tetra2.GetPointIds().SetId( 2, 1 ) + tetra2.GetPointIds().SetId( 3, 4 ) + + tetra3: vtkTetra = vtkTetra() + tetra3.GetPointIds().SetId( 0, 1 ) + tetra3.GetPointIds().SetId( 1, 5 ) + tetra3.GetPointIds().SetId( 2, 2 ) + tetra3.GetPointIds().SetId( 3, 6 ) + + tetra4: vtkTetra = vtkTetra() + tetra4.GetPointIds().SetId( 0, 1 ) + tetra4.GetPointIds().SetId( 1, 2 ) + tetra4.GetPointIds().SetId( 2, 5 ) + tetra4.GetPointIds().SetId( 3, 7 ) + + tetras_cells: vtkCellArray = vtkCellArray() + tetras_cells.InsertNextCell( tetra1 ) + tetras_cells.InsertNextCell( tetra2 ) + tetras_cells.InsertNextCell( tetra3 ) + tetras_cells.InsertNextCell( tetra4 ) + + tetras_grid: vtkUnstructuredGrid = vtkUnstructuredGrid() + tetras_grid.SetPoints( points_tetras ) + tetras_grid.SetCells( VTK_TETRA, tetras_cells ) + return tetras_grid + + +def create_mixed_grid(): + """Create a grid with supported and unsupported cell types, 4 Hexahedrons with 2 quad fracs vertical""" + # Create an unstructured grid + four_hexs_points: vtkPoints = vtkPoints() + four_hexs_points_coords: list[ tuple[ float ] ] = [ ( 0.0, 0.0, 0.0 ), # point0 + ( 1.0, 0.0, 0.0 ), # point1 + ( 2.0, 0.0, 0.0 ), # point2 + ( 0.0, 1.0, 0.0 ), # point3 + ( 1.0, 1.0, 0.0 ), # point4 + ( 2.0, 1.0, 0.0 ), # point5 + ( 0.0, 0.0, 1.0 ), # point6 + ( 1.0, 0.0, 1.0 ), # point7 + ( 2.0, 0.0, 1.0 ), # point8 + ( 0.0, 1.0, 1.0 ), # point9 + ( 1.0, 1.0, 1.0 ), # point10 + ( 2.0, 1.0, 1.0 ), # point11 + ( 0.0, 0.0, 2.0 ), # point12 + ( 1.0, 0.0, 2.0 ), # point13 + ( 2.0, 0.0, 2.0 ), # point14 + ( 0.0, 1.0, 2.0 ), # point15 + ( 1.0, 1.0, 2.0 ), # point16 + ( 2.0, 1.0, 2.0 ) ] + for four_hexs_point in four_hexs_points_coords: + four_hexs_points.InsertNextPoint( four_hexs_point ) + + # hex1 + four_hex1: vtkHexahedron = vtkHexahedron() + four_hex1.GetPointIds().SetId( 0, 0 ) + four_hex1.GetPointIds().SetId( 1, 1 ) + four_hex1.GetPointIds().SetId( 2, 4 ) + four_hex1.GetPointIds().SetId( 3, 3 ) + four_hex1.GetPointIds().SetId( 4, 6 ) + four_hex1.GetPointIds().SetId( 5, 7 ) + four_hex1.GetPointIds().SetId( 6, 10 ) + four_hex1.GetPointIds().SetId( 7, 9 ) + + # hex2 + four_hex2: vtkHexahedron = vtkHexahedron() + four_hex2.GetPointIds().SetId( 0, 0 + 1 ) + four_hex2.GetPointIds().SetId( 1, 1 + 1 ) + four_hex2.GetPointIds().SetId( 2, 4 + 1 ) + four_hex2.GetPointIds().SetId( 3, 3 + 1 ) + four_hex2.GetPointIds().SetId( 4, 6 + 1 ) + four_hex2.GetPointIds().SetId( 5, 7 + 1 ) + four_hex2.GetPointIds().SetId( 6, 10 + 1 ) + four_hex2.GetPointIds().SetId( 7, 9 + 1 ) + + # hex3 + four_hex3: vtkHexahedron = vtkHexahedron() + four_hex3.GetPointIds().SetId( 0, 0 + 6 ) + four_hex3.GetPointIds().SetId( 1, 1 + 6 ) + four_hex3.GetPointIds().SetId( 2, 4 + 6 ) + four_hex3.GetPointIds().SetId( 3, 3 + 6 ) + four_hex3.GetPointIds().SetId( 4, 6 + 6 ) + four_hex3.GetPointIds().SetId( 5, 7 + 6 ) + four_hex3.GetPointIds().SetId( 6, 10 + 6 ) + four_hex3.GetPointIds().SetId( 7, 9 + 6 ) + + # hex4 + four_hex4: vtkHexahedron = vtkHexahedron() + four_hex4.GetPointIds().SetId( 0, 0 + 7 ) + four_hex4.GetPointIds().SetId( 1, 1 + 7 ) + four_hex4.GetPointIds().SetId( 2, 4 + 7 ) + four_hex4.GetPointIds().SetId( 3, 3 + 7 ) + four_hex4.GetPointIds().SetId( 4, 6 + 7 ) + four_hex4.GetPointIds().SetId( 5, 7 + 7 ) + four_hex4.GetPointIds().SetId( 6, 10 + 7 ) + four_hex4.GetPointIds().SetId( 7, 9 + 7 ) + + # quad1 + four_hex_quad1: vtkQuad = vtkQuad() + four_hex_quad1.GetPointIds().SetId( 0, 1 ) + four_hex_quad1.GetPointIds().SetId( 1, 4 ) + four_hex_quad1.GetPointIds().SetId( 2, 10 ) + four_hex_quad1.GetPointIds().SetId( 3, 7 ) + + # quad2 + four_hex_quad2: vtkQuad = vtkQuad() + four_hex_quad2.GetPointIds().SetId( 0, 1 + 6 ) + four_hex_quad2.GetPointIds().SetId( 1, 4 + 6 ) + four_hex_quad2.GetPointIds().SetId( 2, 10 + 6 ) + four_hex_quad2.GetPointIds().SetId( 3, 7 + 6 ) + + four_hex_grid_2_quads = vtkUnstructuredGrid() + four_hex_grid_2_quads.SetPoints( four_hexs_points ) + all_cell_types_four_hex_grid_2_quads = [ VTK_HEXAHEDRON ] * 4 + [ VTK_QUAD ] * 2 + all_cells_four_hex_grid_2_quads = [ four_hex1, four_hex2, four_hex3, four_hex4, four_hex_quad1, four_hex_quad2 ] + for cell_type, cell in zip( all_cell_types_four_hex_grid_2_quads, all_cells_four_hex_grid_2_quads ): + four_hex_grid_2_quads.InsertNextCell( cell_type, cell.GetPointIds() ) + return four_hex_grid_2_quads + + +def create_unsupported_polyhedron_grid(): + """Create a grid with an unsupported polyhedron (non-convex)""" + grid = vtkUnstructuredGrid() + # Create points for the grid + points = vtkPoints() # Need to import vtkPoints + # Create points for a non-convex polyhedron + point_coords = np.array( [ + [ 0.0, 0.0, 0.0 ], # 0 + [ 1.0, 0.0, 0.0 ], # 1 + [ 1.0, 1.0, 0.0 ], # 2 + [ 0.0, 1.0, 0.0 ], # 3 + [ 0.0, 0.0, 1.0 ], # 4 + [ 1.0, 0.0, 1.0 ], # 5 + [ 1.0, 1.0, 1.0 ], # 6 + [ 0.0, 1.0, 1.0 ], # 7 + [ 0.5, 0.5, -0.5 ] # 8 (point makes it non-convex) + ] ) + # Add points to the points array + for point in point_coords: + points.InsertNextPoint( point ) + # Set the points in the grid + grid.SetPoints( points ) + # Create a polyhedron + polyhedron = vtkPolyhedron() + # For simplicity, we'll create a polyhedron that would be recognized as unsupported + # This is a simplified example - you may need to adjust based on your actual implementation + polyhedron.GetPointIds().SetNumberOfIds( 9 ) + for i in range( 9 ): + polyhedron.GetPointIds().SetId( i, i ) + # Add the polyhedron to the grid + grid.InsertNextCell( polyhedron.GetCellType(), polyhedron.GetPointIds() ) + return grid + + +class TestSupportedElements: + + def test_only_supported_elements( self ): + """Test a grid with only supported element types""" + # Create grid with only supported elements (tetra) + grid = create_simple_tetra_grid() + # Apply the filter + filter = SupportedElements() + filter.SetInputDataObject( grid ) + filter.Update() + result = filter.getGrid() + assert result is not None + # Verify no arrays were added (since all elements are supported) + assert result.GetCellData().GetArray( "HasUnsupportedType" ) is None + assert result.GetCellData().GetArray( "IsUnsupportedPolyhedron" ) is None + + def test_unsupported_element_types( self ): + """Test a grid with unsupported element types""" + # Create grid with unsupported elements + grid = create_mixed_grid() + # Apply the filter with painting enabled + filter = SupportedElements() + filter.m_logger.critical( "test_unsupported_element_types" ) + filter.SetInputDataObject( grid ) + filter.setPaintUnsupportedElementTypes( 1 ) + filter.Update() + result = filter.getGrid() + assert result is not None + # Verify the array was added + unsupported_array = result.GetCellData().GetArray( "HasUnsupportedType" ) + assert unsupported_array is not None + for i in range( 0, 4 ): + assert unsupported_array.GetValue( i ) == 0 # Hexahedron should be supported + for j in range( 4, 6 ): + assert unsupported_array.GetValue( j ) == 1 # Quad should not be supported + + # TODO Needs parallelism to work + # def test_unsupported_polyhedron( self ): + # """Test a grid with unsupported polyhedron""" + # # Create grid with unsupported polyhedron + # grid = create_unsupported_polyhedron_grid() + # # Apply the filter with painting enabled + # filter = SupportedElements() + # filter.m_logger.critical( "test_unsupported_polyhedron" ) + # filter.SetInputDataObject( grid ) + # filter.setPaintUnsupportedPolyhedrons( 1 ) + # filter.Update() + # result = filter.getGrid() + # assert result is not None + # # Verify the array was added + # polyhedron_array = result.GetCellData().GetArray( "IsUnsupportedPolyhedron" ) + # assert polyhedron_array is None + # # Since we created an unsupported polyhedron, it should be marked + # assert polyhedron_array.GetValue( 0 ) == 1 + + def test_paint_flags( self ): + """Test setting invalid paint flags""" + filter = SupportedElements() + # Should log an error but not raise an exception + filter.setPaintUnsupportedElementTypes( 2 ) # Invalid value + filter.setPaintUnsupportedPolyhedrons( 2 ) # Invalid value + # Values should remain unchanged + assert filter.m_paintUnsupportedElementTypes == 0 + assert filter.m_paintUnsupportedPolyhedrons == 0 + + def test_set_chunk_size( self ): + """Test that setChunkSize properly updates the chunk size""" + # Create filter instance + filter = SupportedElements() + # Note the initial value + initial_chunk_size = filter.m_chunk_size + # Set a new chunk size + new_chunk_size = 100 + filter.setChunkSize( new_chunk_size ) + # Verify the chunk size was updated + assert filter.m_chunk_size == new_chunk_size + assert filter.m_chunk_size != initial_chunk_size + + def test_set_num_proc( self ): + """Test that setNumProc properly updates the number of processors""" + # Create filter instance + filter = SupportedElements() + # Note the initial value + initial_num_proc = filter.m_num_proc + # Set a new number of processors + new_num_proc = 4 + filter.setNumProc( new_num_proc ) + # Verify the number of processors was updated + assert filter.m_num_proc == new_num_proc + assert filter.m_num_proc != initial_num_proc From f180f8fb9118da38791245ecd2f91b747319f424 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Fri, 23 May 2025 10:01:06 -0700 Subject: [PATCH 06/52] To revert --- .../geos/mesh/doctor/checks/non_conformal.py | 330 +++++++++++------- .../geos/mesh/doctor/filters/NonConformal.py | 233 +++++++++++++ .../doctor/parsing/non_conformal_parsing.py | 24 +- 3 files changed, 434 insertions(+), 153 deletions(-) create mode 100644 geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py diff --git a/geos-mesh/src/geos/mesh/doctor/checks/non_conformal.py b/geos-mesh/src/geos/mesh/doctor/checks/non_conformal.py index 5d99b433d..3c2d4bf16 100644 --- a/geos-mesh/src/geos/mesh/doctor/checks/non_conformal.py +++ b/geos-mesh/src/geos/mesh/doctor/checks/non_conformal.py @@ -1,10 +1,11 @@ from dataclasses import dataclass import math -import numpy +import numpy as np +import numpy.typing as npt from tqdm import tqdm -from typing import List, Tuple, Any +from typing import Any, Sequence from vtk import reference as vtk_reference -from vtkmodules.vtkCommonCore import vtkIdList, vtkPoints +from vtkmodules.vtkCommonCore import vtkDataArray, vtkIdList, vtkPoints from vtkmodules.vtkCommonDataModel import ( vtkBoundingBox, vtkCell, vtkCellArray, vtkPointSet, vtkPolyData, vtkStaticCellLocator, vtkStaticPointLocator, vtkUnstructuredGrid, VTK_POLYHEDRON ) @@ -27,23 +28,22 @@ class Options: @dataclass( frozen=True ) class Result: - non_conformal_cells: List[ Tuple[ int, int ] ] + non_conformal_cells: list[ tuple[ int, int ] ] class BoundaryMesh: - """ - A BoundaryMesh is the envelope of the 3d mesh on which we want to perform the simulations. + """A BoundaryMesh is the envelope of the 3d mesh on which we want to perform the simulations. It is computed by vtk. But we want to be sure that the normals of the envelope are directed outwards. The `vtkDataSetSurfaceFilter` does not have the same behavior for standard vtk cells (like tets or hexs), and for polyhedron meshes, for which the result is a bit brittle. Therefore, we reorient the polyhedron cells ourselves, so we're sure that they point outwards. And then we compute the boundary meshes for both meshes, given that the computing options are not identical. """ - def __init__( self, mesh: vtkUnstructuredGrid ): - """ - Builds a boundary mesh. - :param mesh: The 3d mesh. + """Builds a boundary mesh. + + Args: + mesh (vtkUnstructuredGrid): The 3d mesh. """ # Building the boundary meshes boundary_mesh, __normals, self.__original_cells = BoundaryMesh.__build_boundary_mesh( mesh ) @@ -54,13 +54,13 @@ def __init__( self, mesh: vtkUnstructuredGrid ): self.re_boundary_mesh, re_normals, _ = BoundaryMesh.__build_boundary_mesh( reoriented_mesh, consistency=False ) num_cells = boundary_mesh.GetNumberOfCells() # Precomputing the underlying cell type - self.__is_underlying_cell_type_a_polyhedron = numpy.zeros( num_cells, dtype=bool ) + self.__is_underlying_cell_type_a_polyhedron = np.zeros( num_cells, dtype=bool ) for ic in range( num_cells ): self.__is_underlying_cell_type_a_polyhedron[ ic ] = mesh.GetCell( self.__original_cells.GetValue( ic ) ).GetCellType() == VTK_POLYHEDRON # Precomputing the normals - self.__normals: numpy.ndarray = numpy.empty( ( num_cells, 3 ), dtype=numpy.double, - order='C' ) # Do not modify the storage layout + self.__normals: np.ndarray = np.empty( ( num_cells, 3 ), dtype=np.double, + order='C' ) # Do not modify the storage layout for ic in range( num_cells ): if self.__is_underlying_cell_type_a_polyhedron[ ic ]: self.__normals[ ic, : ] = re_normals.GetTuple3( ic ) @@ -68,13 +68,16 @@ def __init__( self, mesh: vtkUnstructuredGrid ): self.__normals[ ic, : ] = __normals.GetTuple3( ic ) @staticmethod - def __build_boundary_mesh( mesh: vtkUnstructuredGrid, consistency=True ) -> Tuple[ vtkUnstructuredGrid, Any, Any ]: - """ - From a 3d mesh, build the envelope meshes. - :param mesh: The input 3d mesh. - :param consistency: The vtk option passed to the `vtkDataSetSurfaceFilter`. - :return: A tuple containing the boundary mesh, the normal vectors array, - an array that maps the id of the boundary element to the id of the 3d cell it touches. + def __build_boundary_mesh( mesh: vtkUnstructuredGrid, consistency=True ) -> tuple[ vtkUnstructuredGrid, Any, Any ]: + """From a 3d mesh, build the envelope meshes. + + Args: + mesh (vtkUnstructuredGrid): The input 3d mesh. + consistency (bool, optional): The vtk option passed to the `vtkDataSetSurfaceFilter`. Defaults to True. + + Returns: + tuple[ vtkUnstructuredGrid, Any, Any ]: A tuple containing the boundary mesh, the normal vectors array, + an array that maps the id of the boundary element to the id of the 3d cell it touches. """ f = vtkDataSetSurfaceFilter() f.PassThroughCellIdsOn() @@ -82,7 +85,7 @@ def __build_boundary_mesh( mesh: vtkUnstructuredGrid, consistency=True ) -> Tupl f.FastModeOff() # Note that we do not need the original points, but we could keep them as well if needed - original_cells_key = "ORIGINAL_CELLS" + original_cells_key: str = "ORIGINAL_CELLS" f.SetOriginalCellIdsName( original_cells_key ) boundary_mesh = vtkPolyData() @@ -95,7 +98,7 @@ def __build_boundary_mesh( mesh: vtkUnstructuredGrid, consistency=True ) -> Tupl n.ComputeCellNormalsOn() n.SetInputData( boundary_mesh ) n.Update() - normals = n.GetOutput().GetCellData().GetArray( "Normals" ) + normals: vtkDataArray = n.GetOutput().GetCellData().GetArray( "Normals" ) assert normals assert normals.GetNumberOfComponents() == 3 assert normals.GetNumberOfTuples() == boundary_mesh.GetNumberOfCells() @@ -104,74 +107,92 @@ def __build_boundary_mesh( mesh: vtkUnstructuredGrid, consistency=True ) -> Tupl return boundary_mesh, normals, original_cells def GetNumberOfCells( self ) -> int: - """ - The number of cells. - :return: An integer. + """The number of cells. + + Returns: + int """ return self.re_boundary_mesh.GetNumberOfCells() def GetNumberOfPoints( self ) -> int: - """ - The number of points. - :return: An integer. + """The number of points. + + Returns: + int """ return self.re_boundary_mesh.GetNumberOfPoints() - def bounds( self, i ) -> Tuple[ float, float, float, float, float, float ]: - """ - The boundrary box of cell `i`. - :param i: The boundary cell index. - :return: The vtk bounding box. + def bounds( self, i: int ) -> tuple[ float, float, float, float, float, float ]: + """The boundrary box of cell `i`. + + Args: + i (int): The boundary cell index. + + Returns: + tuple[ float, float, float, float, float, float ]: The vtk bounding box. """ return self.re_boundary_mesh.GetCell( i ).GetBounds() - def normals( self, i ) -> numpy.ndarray: - """ - The normal of cell `i`. This normal will be directed outwards - :param i: The boundary cell index. - :return: The normal as a length-3 numpy array. + def normals( self, i: int ) -> npt.NDArray: + """The normal of cell `i`. This normal will be directed outwards + + Args: + i (int): The boundary cell index. + + Returns: + npt.NDArray: The normal as a length-3 numpy array. """ return self.__normals[ i ] - def GetCell( self, i ) -> vtkCell: - """ - Cell i of the boundary mesh. This cell will have its normal directed outwards. - :param i: The boundary cell index. - :return: The cell instance. - :warning: This member function relies on the vtkUnstructuredGrid.GetCell member function which is not thread safe. + def GetCell( self, i: int ) -> vtkCell: + """Cell i of the boundary mesh. This cell will have its normal directed outwards. + This member function relies on the vtkUnstructuredGrid.GetCell member function which is not thread safe. + + Args: + i (int): The boundary cell index. + + Returns: + vtkCell: The cell instance. """ return self.re_boundary_mesh.GetCell( i ) - def GetPoint( self, i ) -> Tuple[ float, float, float ]: - """ - Point i of the boundary mesh. - :param i: The boundary point index. - :return: A length-3 tuple containing the coordinates of the point. - :warning: This member function relies on the vtkUnstructuredGrid.GetPoint member function which is not thread safe. + def GetPoint( self, i: int ) -> tuple[ float, float, float ]: + """Point i of the boundary mesh. + This member function relies on the vtkUnstructuredGrid.GetPoint member function which is not thread safe. + + Args: + i (int): The boundary point index. + + Returns: + tuple[ float, float, float ]: A length-3 tuple containing the coordinates of the point. """ return self.re_boundary_mesh.GetPoint( i ) @property - def original_cells( self ): - """ - Returns the 2d boundary cell to the 3d cell index of the original mesh. - :return: A 1d array. + def original_cells( self ) -> vtkDataArray: + """Returns the 2d boundary cell to the 3d cell index of the original mesh. + + Returns: + vtkDataArray: A 1d array. """ return self.__original_cells def build_poly_data_for_extrusion( i: int, boundary_mesh: BoundaryMesh ) -> vtkPolyData: - """ - Creates a vtkPolyData containing the unique cell `i` of the boundary mesh. + """Creates a vtkPolyData containing the unique cell `i` of the boundary mesh. This operation is needed to use the vtk extrusion filter. - :param i: The boundary cell index that will eventually be extruded. - :param boundary_mesh: - :return: The created vtkPolyData. + + Args: + i (int): The boundary cell index that will eventually be extruded. + boundary_mesh (BoundaryMesh) + + Returns: + vtkPolyData: The created vtkPolyData. """ cell = boundary_mesh.GetCell( i ) copied_cell = cell.NewInstance() copied_cell.DeepCopy( cell ) - points_ids_mapping = [] + points_ids_mapping: list[ int ] = list() for i in range( copied_cell.GetNumberOfPoints() ): copied_cell.GetPointIds().SetId( i, i ) points_ids_mapping.append( cell.GetPointId( i ) ) @@ -188,12 +209,15 @@ def build_poly_data_for_extrusion( i: int, boundary_mesh: BoundaryMesh ) -> vtkP def are_points_conformal( point_tolerance: float, cell_i: vtkCell, cell_j: vtkCell ) -> bool: - """ - Checks if points of cell `i` matches, one by one, the points of cell `j`. - :param point_tolerance: The point tolerance to consider that two points match. - :param cell_i: The first cell. - :param cell_j: The second cell. - :return: A boolean. + """Checks if points of cell `i` matches, one by one, the points of cell `j`. + + Args: + point_tolerance (float): The point tolerance to consider that two points match. + cell_i (vtkCell): The first cell. + cell_j (vtkCell): The second cell. + + Returns: + bool """ # In this last step, we check that the nodes are (or not) matching each other. if cell_i.GetNumberOfPoints() != cell_j.GetNumberOfPoints(): @@ -204,34 +228,33 @@ def are_points_conformal( point_tolerance: float, cell_i: vtkCell, cell_j: vtkCe points.SetPoints( cell_i.GetPoints() ) point_locator.SetDataSet( points ) point_locator.BuildLocator() - found_points = set() + found_points: set[ int ] = set() for ip in range( cell_j.GetNumberOfPoints() ): p = cell_j.GetPoints().GetPoint( ip ) squared_dist = vtk_reference( 0. ) # unused - found_point = point_locator.FindClosestPointWithinRadius( point_tolerance, p, squared_dist ) + found_point: int = point_locator.FindClosestPointWithinRadius( point_tolerance, p, squared_dist ) found_points.add( found_point ) return found_points == set( range( cell_i.GetNumberOfPoints() ) ) class Extruder: - """ - Computes and stores all the extrusions of the boundary faces. + """Computes and stores all the extrusions of the boundary faces. The main reason for this class is to be lazy and cache the extrusions. """ - def __init__( self, boundary_mesh: BoundaryMesh, face_tolerance: float ): - self.__extrusions: List[ vtkPolyData ] = [ - None, - ] * boundary_mesh.GetNumberOfCells() - self.__boundary_mesh = boundary_mesh - self.__face_tolerance = face_tolerance + self.__extrusions: list[ vtkPolyData ] = [ None ] * boundary_mesh.GetNumberOfCells() + self.__boundary_mesh: BoundaryMesh = boundary_mesh + self.__face_tolerance: float = face_tolerance - def __extrude( self, polygon_poly_data, normal ) -> vtkPolyData: - """ - Extrude the polygon data to create a volume that will be used for intersection. - :param polygon_poly_data: The data to extrude - :param normal: The (uniform) direction of the extrusion. - :return: The extrusion. + def __extrude( self, polygon_poly_data: vtkPolyData, normal: Sequence[ float ] ) -> vtkPolyData: + """Extrude the polygon data to create a volume that will be used for intersection. + + Args: + polygon_poly_data (_type_): The data to extrude + normal (_type_): The (uniform) direction of the extrusion. + + Returns: + vtkPolyData: The extrusion. """ extruder = vtkLinearExtrusionFilter() extruder.SetExtrusionTypeToVectorExtrusion() @@ -241,11 +264,14 @@ def __extrude( self, polygon_poly_data, normal ) -> vtkPolyData: extruder.Update() return extruder.GetOutput() - def __getitem__( self, i ) -> vtkPolyData: - """ - Returns the vtk extrusion for boundary element i. - :param i: The cell index. - :return: The vtk instance. + def __getitem__( self, i: int ) -> vtkPolyData: + """Returns the vtk extrusion for boundary element i. + + Args: + i (int): The cell index. + + Returns: + vtkPolyData: The vtk instance. """ extrusion = self.__extrusions[ i ] if extrusion: @@ -258,14 +284,17 @@ def __getitem__( self, i ) -> vtkPolyData: def are_faces_conformal_using_extrusions( extrusions: Extruder, i: int, j: int, boundary_mesh: vtkUnstructuredGrid, point_tolerance: float ) -> bool: - """ - Tests if two boundary faces are conformal, checking for intersection between their normal extruded volumes. - :param extrusions: The extrusions cache. - :param i: The cell index of the first cell. - :param j: The cell index of the second cell. - :param boundary_mesh: The boundary mesh. - :param point_tolerance: The point tolerance to consider that two points match. - :return: A boolean. + """Tests if two boundary faces are conformal, checking for intersection between their normal extruded volumes. + + Args: + extrusions (Extruder): The extrusions cache. + i (int): The cell index of the first cell. + j (int): The cell index of the second cell. + boundary_mesh (vtkUnstructuredGrid): The boundary mesh. + point_tolerance (float): The point tolerance to consider that two points match. + + Returns: + bool """ collision = vtkCollisionDetectionFilter() collision.SetCollisionModeToFirstContact() @@ -290,21 +319,24 @@ def are_faces_conformal_using_extrusions( extrusions: Extruder, i: int, j: int, def are_faces_conformal_using_distances( i: int, j: int, boundary_mesh: vtkUnstructuredGrid, face_tolerance: float, point_tolerance: float ) -> bool: - """ - Tests if two boundary faces are conformal, checking the minimal distance between triangulated surfaces. - :param i: The cell index of the first cell. - :param j: The cell index of the second cell. - :param boundary_mesh: The boundary mesh. - :param face_tolerance: The tolerance under which we should consider the two faces "touching" each other. - :param point_tolerance: The point tolerance to consider that two points match. - :return: A boolean. + """Tests if two boundary faces are conformal, checking the minimal distance between triangulated surfaces. + + Args: + i (int): The cell index of the first cell. + j (int): The cell index of the second cell. + boundary_mesh (vtkUnstructuredGrid): The boundary mesh. + face_tolerance (float): The tolerance under which we should consider the two faces "touching" each other. + point_tolerance (float): The point tolerance to consider that two points match. + + Returns: + bool """ cp_i = boundary_mesh.GetCell( i ).NewInstance() cp_i.DeepCopy( boundary_mesh.GetCell( i ) ) cp_j = boundary_mesh.GetCell( j ).NewInstance() cp_j.DeepCopy( boundary_mesh.GetCell( j ) ) - def triangulate( cell ): + def triangulate( cell: vtkCell ): assert cell.GetCellDimension() == 2 __points_ids = vtkIdList() __points = vtkPoints() @@ -323,13 +355,13 @@ def build_numpy_triangles( points_ids ): __t = [] for __pi in points_ids[ __i:__i + 3 ]: __t.append( boundary_mesh.GetPoint( __pi ) ) - __triangles.append( numpy.array( __t, dtype=float ) ) + __triangles.append( np.array( __t, dtype=float ) ) return __triangles triangles_i = build_numpy_triangles( points_ids_i ) triangles_j = build_numpy_triangles( points_ids_j ) - min_dist = numpy.inf + min_dist = np.inf for ti, tj in [ ( ti, tj ) for ti in triangles_i for tj in triangles_j ]: # Note that here, we compute the exact distance to compare with the threshold. # We could improve by exiting the iterative distance computation as soon as @@ -345,44 +377,57 @@ def build_numpy_triangles( points_ids ): return are_points_conformal( point_tolerance, cp_i, cp_j ) -def __check( 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 - :param options: The check options. - :return: The Result instance. - """ - boundary_mesh = BoundaryMesh( mesh ) - cos_theta = abs( math.cos( numpy.deg2rad( options.angle_tolerance ) ) ) - num_cells = boundary_mesh.GetNumberOfCells() +def compute_bounding_box( boundary_mesh: BoundaryMesh, face_tolerance: float ) -> npt.NDArray[ np.float64 ]: + # Precomputing the bounding boxes. + # The options are important to directly interact with memory in C++. + bounding_boxes = np.empty( ( boundary_mesh.GetNumberOfCells(), 6 ), dtype=np.double, order="C" ) + for i in range( boundary_mesh.GetNumberOfCells() ): + bb = vtkBoundingBox( boundary_mesh.bounds( i ) ) + bb.Inflate( 2 * face_tolerance ) + assert bounding_boxes[ + i, : ].data.contiguous # Do not modify the storage layout since vtk deals with raw memory here. + bb.GetBounds( bounding_boxes[ i, : ] ) + return bounding_boxes + +def compute_number_cells_per_node( boundary_mesh: BoundaryMesh ) -> npt.NDArray[ np.int64 ]: # Computing the exact number of cells per node - num_cells_per_node = numpy.zeros( boundary_mesh.GetNumberOfPoints(), dtype=int ) + num_cells_per_node = np.zeros( boundary_mesh.GetNumberOfPoints(), dtype=int ) for ic in range( boundary_mesh.GetNumberOfCells() ): c = boundary_mesh.GetCell( ic ) point_ids = c.GetPointIds() for point_id in vtk_iter( point_ids ): num_cells_per_node[ point_id ] += 1 + return num_cells_per_node + +def build_cell_locator( mesh: vtkUnstructuredGrid, numberMaxCellPerNode: int ) -> vtkStaticCellLocator: cell_locator = vtkStaticCellLocator() cell_locator.Initialize() - cell_locator.SetNumberOfCellsPerNode( num_cells_per_node.max() ) - cell_locator.SetDataSet( boundary_mesh.re_boundary_mesh ) + cell_locator.SetNumberOfCellsPerNode( numberMaxCellPerNode ) + cell_locator.SetDataSet( mesh ) cell_locator.BuildLocator() + return cell_locator - # Precomputing the bounding boxes. - # The options are important to directly interact with memory in C++. - bounding_boxes = numpy.empty( ( boundary_mesh.GetNumberOfCells(), 6 ), dtype=numpy.double, order="C" ) - for i in range( boundary_mesh.GetNumberOfCells() ): - bb = vtkBoundingBox( boundary_mesh.bounds( i ) ) - bb.Inflate( 2 * options.face_tolerance ) - assert bounding_boxes[ - i, : ].data.contiguous # Do not modify the storage layout since vtk deals with raw memory here. - bb.GetBounds( bounding_boxes[ i, : ] ) - non_conformal_cells = [] +def find_non_conformal_cells( mesh: vtkUnstructuredGrid, options: Options ) -> list[ tuple[ int, int ] ]: + # Extracts the outer surface of the 3D mesh. + # Ensures that face normals are consistently oriented outward. + boundary_mesh = BoundaryMesh( mesh ) + num_cells: int = boundary_mesh.GetNumberOfCells() + + # Used to filter out face pairs that are not facing each other. + cos_theta: float = abs( math.cos( np.deg2rad( options.angle_tolerance ) ) ) + + # Prepares extruded volumes of boundary faces for intersection testing. extrusions = Extruder( boundary_mesh, options.face_tolerance ) + + num_cells_per_node = compute_number_cells_per_node( boundary_mesh ) + bounding_boxes = compute_bounding_box( boundary_mesh, options.face_tolerance ) + cell_locator = build_cell_locator( boundary_mesh.re_boundary_mesh, num_cells_per_node.max() ) + close_cells = vtkIdList() + non_conformal_cells_boundary_id: list[ tuple[ int, int ] ] = list() # Looping on all the pairs of boundary cells. We'll hopefully discard most of the pairs. for i in tqdm( range( num_cells ), desc="Non conformal elements" ): cell_locator.FindCellsWithinBounds( bounding_boxes[ i ], close_cells ) @@ -391,17 +436,32 @@ def __check( mesh: vtkUnstructuredGrid, options: Options ) -> Result: continue # Discarding pairs that are not facing each others (with a threshold). normal_i, normal_j = boundary_mesh.normals( i ), boundary_mesh.normals( j ) - if numpy.dot( normal_i, normal_j ) > -cos_theta: # opposite directions only (can be facing or not) + if np.dot( normal_i, normal_j ) > - cos_theta: # opposite directions only (can be facing or not) continue # At this point, back-to-back and face-to-face pairs of elements are considered. if not are_faces_conformal_using_extrusions( extrusions, i, j, boundary_mesh, options.point_tolerance ): - non_conformal_cells.append( ( i, j ) ) + non_conformal_cells_boundary_id.append( ( i, j ) ) # Extracting the original 3d element index (and not the index of the boundary mesh). - tmp = [] - for i, j in non_conformal_cells: - tmp.append( ( boundary_mesh.original_cells.GetValue( i ), boundary_mesh.original_cells.GetValue( j ) ) ) + non_conformal_cells: list[ tuple[ int, int ] ] = list() + for i, j in non_conformal_cells_boundary_id: + non_conformal_cells.append( ( boundary_mesh.original_cells.GetValue( i ), + boundary_mesh.original_cells.GetValue( j ) ) ) + return non_conformal_cells + - return Result( non_conformal_cells=tmp ) +def __check( 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). + + Args: + mesh (vtkUnstructuredGrid): The vtk mesh + options (Options): The check options. + + Returns: + Result: The Result instance. + """ + non_conformal_cells = find_non_conformal_cells( mesh, options ) + return Result( non_conformal_cells=non_conformal_cells ) def check( vtk_input_file: str, options: Options ) -> Result: diff --git a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py new file mode 100644 index 000000000..a7c364b1c --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py @@ -0,0 +1,233 @@ +import numpy as np +import numpy.typing as npt +from typing_extensions import Self +from vtkmodules.util.numpy_support import numpy_to_vtk +from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase +from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from geos.mesh.doctor.checks.non_conformal import Options, find_non_conformal_cells +from geos.mesh.vtk.io import VtkOutput, write_mesh +from geos.utils.Logger import Logger, getLogger + +__doc__ = """ +NonConformal module is a vtk filter that ... of a vtkUnstructuredGrid. + +One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. + +To use the filter: + +.. code-block:: python + + from filters.NonConformal import NonConformal + + # instanciate the filter + nonConformalFilter: NonConformal = NonConformal() + +""" + + +class NonConformal( VTKPythonAlgorithmBase ): + + def __init__( self: Self ) -> None: + """Vtk filter to ... of a vtkUnstructuredGrid. + + Output mesh is vtkUnstructuredGrid. + """ + super().__init__( nInputPorts=1, nOutputPorts=1, inputType='vtkUnstructuredGrid', + outputType='vtkUnstructuredGrid' ) + self.m_angle_tolerance: float = 10.0 + self.m_face_tolerance: float = 0.0 + self.m_point_tolerance: float = 0.0 + + self.m_non_conformal_cells: list[ tuple[ int, int ] ] = list() + self.m_paintNonConformalCells: int = 0 + self.m_logger: Logger = getLogger( "Element Volumes Filter" ) + + def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestInformation. + + Args: + port (int): input port + info (vtkInformationVector): info + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + if port == 0: + info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) + return 1 + + def RequestInformation( + self: Self, + request: vtkInformation, # noqa: F841 + inInfoVec: list[ vtkInformationVector ], # noqa: F841 + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestInformation. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + executive = self.GetExecutive() # noqa: F841 + outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 + return 1 + + def RequestData( + self: Self, + request: vtkInformation, + inInfoVec: list[ vtkInformationVector ], + outInfo: vtkInformationVector + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestData. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) + output = vtkUnstructuredGrid.GetData( outInfo ) + + options = Options( self.m_angle_tolerance, self.m_point_tolerance, self.m_face_tolerance ) + non_conformal_cells = find_non_conformal_cells( input_mesh, options ) + self.m_non_conformal_cells = non_conformal_cells + + non_conformal_cells_extended = [ cell_id for pair in non_conformal_cells for cell_id in pair ] + unique_non_conformal_cells = frozenset( non_conformal_cells_extended ) + self.m_logger.info( f"You have {len( unique_non_conformal_cells )} non conformal cells.\n" + + f"{', '.join( map( str, sorted( non_conformal_cells_extended ) ) )}" ) + + output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() + output_mesh.CopyStructure( input_mesh ) + output_mesh.CopyAttributes( input_mesh ) + + if self.m_paintNonConformalCells: + arrayNC: npt.NDArray = np.zeros( ( output_mesh.GetNumberOfCells(), 1 ), dtype=int ) + arrayNC[ unique_non_conformal_cells ] = 1 + vtkArrayNC: vtkDataArray = numpy_to_vtk( arrayNC ) + vtkArrayNC.SetName( "IsNonConformal" ) + output_mesh.GetCellData().AddArray( vtkArrayNC ) + + output.ShallowCopy( output_mesh ) + + return 1 + + def SetLogger( self: Self, logger: Logger ) -> None: + """Set the logger. + + Args: + logger (Logger): logger + """ + self.m_logger = logger + self.Modified() + + def getGrid( self: Self ) -> vtkUnstructuredGrid: + """Returns the vtkUnstructuredGrid with volumes. + + Args: + self (Self) + + Returns: + vtkUnstructuredGrid + """ + self.Update() # triggers RequestData + return self.GetOutputDataObject( 0 ) + + def getAngleTolerance( self: Self ) -> float: + """Returns the angle tolerance. + + Args: + self (Self) + + Returns: + float + """ + return self.m_angle_tolerance + + def getfaceTolerance( self: Self ) -> float: + """Returns the face tolerance. + + Args: + self (Self) + + Returns: + float + """ + return self.m_face_tolerance + + def getPointTolerance( self: Self ) -> float: + """Returns the point tolerance. + + Args: + self (Self) + + Returns: + float + """ + return self.m_point_tolerance + + def setPaintNonConformalCells( self: Self, choice: int ) -> None: + """Set 0 or 1 to choose if you want to create a new "IsNonConformal" array in your output data. + + Args: + self (Self) + choice (int): 0 or 1 + """ + if choice not in [ 0, 1 ]: + self.m_logger.error( f"setPaintNonConformalCells: Please choose either 0 or 1 not '{choice}'." ) + else: + self.m_paintNonConformalCells = choice + self.Modified() + + def setAngleTolerance( self: Self, tolerance: float ) -> None: + """Set the angle tolerance parameter in degree. + + Args: + self (Self) + tolerance (float) + """ + self.m_angle_tolerance = tolerance + self.Modified() + + def setFaceTolerance( self: Self, tolerance: float ) -> None: + """Set the face tolerance parameter. + + Args: + self (Self) + tolerance (float) + """ + self.m_face_tolerance = tolerance + self.Modified() + + def setPointTolerance( self: Self, tolerance: float ) -> None: + """Set the point tolerance parameter. + + Args: + self (Self) + tolerance (float) + """ + self.m_point_tolerance = tolerance + self.Modified() + + def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: + """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath with volumes. + + Args: + filepath (str): /path/to/your/file.vtu + is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. + canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. + Defaults to False. + """ + mesh: vtkUnstructuredGrid = self.getGrid() + if mesh: + write_mesh( filepath, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) + else: + self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) 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 d4aeb46af..b0b0ca839 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,12 +1,5 @@ import logging - -from typing import ( - FrozenSet, - List, -) - from geos.mesh.doctor.checks.non_conformal import Options, Result - from . import NON_CONFORMAL __ANGLE_TOLERANCE = "angle_tolerance" @@ -15,8 +8,6 @@ __ANGLE_TOLERANCE_DEFAULT = 10. -__ALL_KEYWORDS = { __ANGLE_TOLERANCE, __POINT_TOLERANCE, __FACE_TOLERANCE } - def convert( parsed_options ) -> Options: return Options( angle_tolerance=parsed_options[ __ANGLE_TOLERANCE ], @@ -33,17 +24,14 @@ def fill_subparser( subparsers ) -> None: 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." ) + help="[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\"." ) + help="[float]: tolerance for two faces to be considered \"touching\"." ) def display_results( options: Options, result: Result ): - 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 ) - logging.error( - f"You have {len(non_conformal_cells)} non conformal cells.\n{', '.join(map(str, sorted(non_conformal_cells)))}" - ) + non_conformal_cells_extended = [ cell_id for pair in result.non_conformal_cells for cell_id in pair ] + unique_non_conformal_cells = frozenset( non_conformal_cells_extended ) + logging.error( f"You have {len( unique_non_conformal_cells )} non conformal cells.\n" + + f"{', '.join( map( str, sorted( non_conformal_cells_extended ) ) )}" ) From f14cc1bcb288e5c804e577d08140c7f8bc63db02 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 24 Jun 2025 15:58:34 -0700 Subject: [PATCH 07/52] Keep supported_elements like before merging --- .../mesh/doctor/actions/supported_elements.py | 129 ++++++---------- .../mesh/doctor/checks/supported_elements.py | 144 ------------------ 2 files changed, 48 insertions(+), 225 deletions(-) delete mode 100644 geos-mesh/src/geos/mesh/doctor/checks/supported_elements.py 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 8d9fd46aa..d667f1fcc 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py @@ -1,6 +1,7 @@ from dataclasses import dataclass import multiprocessing import networkx +from numpy import ones from tqdm import tqdm from typing import FrozenSet, Iterable, Mapping, Optional from vtkmodules.util.numpy_support import vtk_to_numpy @@ -29,40 +30,24 @@ class Result: # for multiprocessing, vtkUnstructuredGrid cannot be pickled. Let's use a global variable instead. MESH: Optional[ vtkUnstructuredGrid ] = None - - -def init_worker_mesh( input_file_for_worker: str ): - """Initializer for multiprocessing.Pool to set the global MESH variable in each worker process. - - Args: - input_file_for_worker (str): Filepath to vtk grid - """ - global MESH - 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: - 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 - # For now, the assert MESH is not None in __call__ will catch this. +supported_cell_types: set[ int ] = { + VTK_HEXAGONAL_PRISM, VTK_HEXAHEDRON, VTK_PENTAGONAL_PRISM, VTK_POLYHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_VOXEL, + VTK_WEDGE +} class IsPolyhedronConvertible: - def __init__( self ): + def __init__( self, mesh: vtkUnstructuredGrid ): + global MESH # for multiprocessing, vtkUnstructuredGrid cannot be pickled. Let's use a global variable instead. + MESH = mesh def build_prism_graph( n: int, name: str ) -> networkx.Graph: - """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. + """ + 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. """ tmp = networkx.cycle_graph( n ) for node in range( n ): @@ -90,34 +75,26 @@ 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. - - Args: - face_stream (_type_): The polyhedron. - - Returns: - str: The name of the supported type or an empty string. + :param face_stream: The polyhedron. + :return: 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. - - 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. + """ + 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. """ global MESH - 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." + assert MESH is not None if MESH.GetCellType( ic ) != VTK_POLYHEDRON: return -1 pt_ids = vtkIdList() @@ -128,50 +105,40 @@ def __call__( self, ic: int ) -> int: setup_logger.debug( f"Polyhedron cell {ic} can be converted into \"{converted_type_name}\"" ) return -1 else: - setup_logger.debug( - f"Polyhedron cell {ic} (in PID {multiprocessing.current_process().pid}) cannot be converted into any supported element." - ) + setup_logger.debug( f"Polyhedron cell {ic} cannot be converted into any supported element." ) return ic -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: - 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() ) - - if hasattr( mesh, "GetDistinctCellTypesArray" ): - cell_types_numpy = vtk_to_numpy( mesh.GetDistinctCellTypesArray() ) - cell_types = set( cell_types_numpy.tolist() ) +def find_unsupported_std_elements_types( mesh: vtkUnstructuredGrid ) -> set[ int ]: + if hasattr( mesh, "GetDistinctCellTypesArray" ): # For more recent versions of vtk. + unique_cell_types = set( vtk_to_numpy( mesh.GetDistinctCellTypesArray() ) ) else: - vtk_cell_types_obj = vtkCellTypes() - mesh.GetCellTypes( vtk_cell_types_obj ) - cell_types = set( vtk_iter( vtk_cell_types_obj ) ) + vtk_cell_types = vtkCellTypes() + mesh.GetCellTypes( vtk_cell_types ) + unique_cell_types = set( vtk_iter( vtk_cell_types ) ) + return unique_cell_types - supported_cell_types - supported_cell_types = { - VTK_HEXAGONAL_PRISM, VTK_HEXAHEDRON, VTK_PENTAGONAL_PRISM, VTK_POLYHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_VOXEL, - VTK_WEDGE - } - unsupported_std_elements_types = cell_types - supported_cell_types +def find_unsupported_polyhedron_elements( mesh: vtkUnstructuredGrid, options: Options ) -> list[ int ]: # Dealing with polyhedron elements. - num_cells = mesh.GetNumberOfCells() - 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 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 ) - + num_cells: int = mesh.GetNumberOfCells() + result = ones( num_cells, dtype=int ) * -1 + with multiprocessing.Pool( processes=options.num_proc ) as pool: + generator = pool.imap_unordered( IsPolyhedronConvertible( mesh ), + 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 + return [ i for i in result if i > -1 ] + + +def __action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: + unsupported_std_elements_types: set[ int ] = find_unsupported_std_elements_types( mesh ) + unsupported_polyhedron_elements: list[ int ] = find_unsupported_polyhedron_elements( mesh, options ) return Result( unsupported_std_elements_types=frozenset( unsupported_std_elements_types ), - unsupported_polyhedron_elements=frozenset( unsupported_polyhedron_indices ) ) + unsupported_polyhedron_elements=frozenset( unsupported_polyhedron_elements ) ) def action( vtk_input_file: str, options: Options ) -> Result: - return __action( vtk_input_file, options ) + mesh: vtkUnstructuredGrid = read_mesh( vtk_input_file ) + return __action( mesh, options ) diff --git a/geos-mesh/src/geos/mesh/doctor/checks/supported_elements.py b/geos-mesh/src/geos/mesh/doctor/checks/supported_elements.py deleted file mode 100644 index 2e1a87643..000000000 --- a/geos-mesh/src/geos/mesh/doctor/checks/supported_elements.py +++ /dev/null @@ -1,144 +0,0 @@ -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 -from vtkmodules.vtkCommonCore import vtkIdList -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.vtk.helpers import vtk_iter -from geos.mesh.vtk.io import read_mesh - - -@dataclass( frozen=True ) -class Options: - num_proc: 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 - - -# for multiprocessing, vtkUnstructuredGrid cannot be pickled. Let's use a global variable instead. -MESH: Optional[ vtkUnstructuredGrid ] = None -supported_cell_types: set[ int ] = { - VTK_HEXAGONAL_PRISM, VTK_HEXAHEDRON, VTK_PENTAGONAL_PRISM, VTK_POLYHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_VOXEL, - VTK_WEDGE -} - - -class IsPolyhedronConvertible: - - def __init__( self, mesh: vtkUnstructuredGrid ): - global MESH # for multiprocessing, vtkUnstructuredGrid cannot be pickled. Let's use a global variable instead. - MESH = mesh - - 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. - """ - tmp = networkx.cycle_graph( n ) - for node in range( n ): - tmp.add_edge( node, n ) - tmp.add_edge( node, n + 1 ) - tmp.name = name - return tmp - - # Building the reference graphs - tet_graph = networkx.complete_graph( 4 ) - tet_graph.name = "Tetrahedron" - pyr_graph = build_prism_graph( 4, "Pyramid" ) - pyr_graph.remove_node( 5 ) # Removing a node also removes its associated edges. - self.__reference_graphs: Mapping[ int, Iterable[ networkx.Graph ] ] = { - 4: ( tet_graph, ), - 5: ( pyr_graph, build_prism_graph( 3, "Wedge" ) ), - 6: ( build_prism_graph( 4, "Hexahedron" ), ), - 7: ( build_prism_graph( 5, "Prism5" ), ), - 8: ( build_prism_graph( 6, "Prism6" ), ), - 9: ( build_prism_graph( 7, "Prism7" ), ), - 10: ( build_prism_graph( 8, "Prism8" ), ), - 11: ( build_prism_graph( 9, "Prism9" ), ), - 12: ( build_prism_graph( 10, "Prism10" ), ), - 13: ( build_prism_graph( 11, "Prism11" ), ), - } - - def __is_polyhedron_supported( self, face_stream ) -> str: - """ - 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. - """ - cell_graph = build_face_to_face_connectivity_through_edges( face_stream, add_compatibility=True ) - 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. - """ - global MESH - assert MESH is not None - if MESH.GetCellType( ic ) != VTK_POLYHEDRON: - return -1 - pt_ids = vtkIdList() - MESH.GetFaceStream( ic, pt_ids ) - 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}\"" ) - return -1 - else: - logging.debug( f"Polyhedron cell {ic} cannot be converted into any supported element." ) - return ic - - -def find_unsupported_std_elements_types( mesh: vtkUnstructuredGrid ) -> set[ int ]: - if hasattr( mesh, "GetDistinctCellTypesArray" ): # For more recent versions of vtk. - unique_cell_types = set( vtk_to_numpy( mesh.GetDistinctCellTypesArray() ) ) - else: - vtk_cell_types = vtkCellTypes() - mesh.GetCellTypes( vtk_cell_types ) - unique_cell_types = set( vtk_iter( vtk_cell_types ) ) - return unique_cell_types - supported_cell_types - - -def find_unsupported_polyhedron_elements( mesh: vtkUnstructuredGrid, options: Options ) -> list[ int ]: - # Dealing with polyhedron elements. - num_cells: int = 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 ), - 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 - return [ i for i in result if i > -1 ] - - -def __check( mesh: vtkUnstructuredGrid, options: Options ) -> Result: - unsupported_std_elements_types: set[ int ] = find_unsupported_std_elements_types( mesh ) - unsupported_polyhedron_elements: list[ int ] = find_unsupported_polyhedron_elements( mesh, options ) - return Result( unsupported_std_elements_types=frozenset( unsupported_std_elements_types ), - unsupported_polyhedron_elements=frozenset( unsupported_polyhedron_elements ) ) - - -def check( vtk_input_file: str, options: Options ) -> Result: - mesh: vtkUnstructuredGrid = read_mesh( vtk_input_file ) - return __check( mesh, options ) From 2f367bb128051f74e531d5bdbce17ee3565c6d29 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 24 Jun 2025 16:46:12 -0700 Subject: [PATCH 08/52] Update already implemented filters to work after merge --- .../geos/mesh/doctor/actions/generate_cube.py | 2 +- .../mesh/doctor/filters/CollocatedNodes.py | 12 +- .../mesh/doctor/filters/ElementVolumes.py | 10 +- .../mesh/doctor/filters/GenerateFractures.py | 16 +- .../doctor/filters/GenerateRectilinearGrid.py | 14 +- .../geos/mesh/doctor/filters/NonConformal.py | 13 +- .../mesh/doctor/filters/SupportedElements.py | 422 +++++++++--------- geos-mesh/tests/test_generate_global_ids.py | 2 +- geos-mesh/tests/test_supported_elements.py | 143 +++--- 9 files changed, 318 insertions(+), 316 deletions(-) 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 0572e29cf..56df4f657 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py @@ -6,7 +6,7 @@ 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.doctor.parsing.cli_parsing import setup_logger from geos.mesh.io.vtkIO import VtkOutput, write_mesh diff --git a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py index 2719fc1a3..1a3b18350 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py @@ -5,9 +5,9 @@ from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid -from geos.mesh.doctor.checks.collocated_nodes import find_collocated_nodes_buckets, find_wrong_support_elements -from geos.mesh.vtk.io import VtkOutput, write_mesh -from geos.utils.Logger import Logger, getLogger +from geos.mesh.doctor.actions.collocated_nodes import find_collocated_nodes_buckets, find_wrong_support_elements +from geos.mesh.doctor.parsing.cli_parsing import setup_logger +from geos.mesh.io.vtkIO import VtkOutput, write_mesh __doc__ = """ CollocatedNodes module is a vtk filter that allows to find the duplicated nodes of a vtkUnstructuredGrid. @@ -39,7 +39,7 @@ def __init__( self: Self ) -> None: self.m_paintWrongSupportElements: int = 0 self.m_tolerance: float = 0.0 self.m_wrongSupportElements: list[ int ] = list() - self.m_logger: Logger = getLogger( "Element Volumes Filter" ) + self.m_logger = setup_logger def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: """Inherited from VTKPythonAlgorithmBase::RequestInformation. @@ -119,11 +119,11 @@ def RequestData( return 1 - def SetLogger( self: Self, logger: Logger ) -> None: + def SetLogger( self: Self, logger ) -> None: """Set the logger. Args: - logger (Logger): logger + logger """ self.m_logger = logger self.Modified() diff --git a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py index 417de0fc8..cbbe0c7d4 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py @@ -6,8 +6,8 @@ from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from vtkmodules.vtkFiltersVerdict import vtkCellSizeFilter -from geos.mesh.vtk.io import VtkOutput, write_mesh -from geos.utils.Logger import Logger, getLogger +from geos.mesh.doctor.parsing.cli_parsing import setup_logger +from geos.mesh.io.vtkIO import VtkOutput, write_mesh __doc__ = """ ElementVolumes module is a vtk filter that allows to calculate the volumes of every elements in a vtkUnstructuredGrid. @@ -37,7 +37,7 @@ def __init__( self: Self ) -> None: outputType='vtkUnstructuredGrid' ) self.m_returnNegativeZeroVolumes: bool = False self.m_volumes: npt.NDArray = None - self.m_logger: Logger = getLogger( "Element Volumes Filter" ) + self.m_logger = setup_logger def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: """Inherited from VTKPythonAlgorithmBase::RequestInformation. @@ -118,11 +118,11 @@ def RequestData( return 1 - def SetLogger( self: Self, logger: Logger ) -> None: + def SetLogger( self: Self, logger ) -> None: """Set the logger. Args: - logger (Logger): logger + logger """ self.m_logger = logger self.Modified() diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py index 743918fc1..d9b63f73c 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py @@ -2,14 +2,14 @@ from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid -from geos.mesh.doctor.checks.generate_fractures import Options, split_mesh_on_fractures +from geos.mesh.doctor.actions.generate_fractures import Options, split_mesh_on_fractures from geos.mesh.doctor.parsing.generate_fractures_parsing import convert, convert_to_fracture_policy from geos.mesh.doctor.parsing.generate_fractures_parsing import ( __FIELD_NAME, __FIELD_VALUES, __FRACTURES_DATA_MODE, __FRACTURES_OUTPUT_DIR, __FRACTURES_DATA_MODE_VALUES, __POLICIES, __POLICY ) -from geos.mesh.vtk.io import VtkOutput, write_mesh -from geos.mesh.vtk.helpers import has_invalid_field -from geos.utils.Logger import Logger, getLogger +from geos.mesh.doctor.parsing.cli_parsing import setup_logger +from geos.mesh.io.vtkIO import VtkOutput, write_mesh +from geos.mesh.utils.arrayHelpers import has_array __doc__ = """ GenerateFractures module is a vtk filter that takes as input a vtkUnstructuredGrid that needs to be splited along @@ -52,7 +52,7 @@ def __init__( self: Self ) -> None: self.m_output_modes_binary: str = { "mesh": DATA_MODE[ 0 ], "fractures": DATA_MODE[ 1 ] } self.m_mesh_VtkOutput: VtkOutput = None self.m_all_fractures_VtkOutput: list[ VtkOutput ] = None - self.m_logger: Logger = getLogger( "Generate Fractures Filter" ) + self.m_logger = setup_logger def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: """Inherited from VTKPythonAlgorithmBase::RequestInformation. @@ -95,7 +95,7 @@ def RequestData( outInfo: list[ vtkInformationVector ] ) -> int: input_mesh = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) - if has_invalid_field( input_mesh, [ "GLOBAL_IDS_POINTS", "GLOBAL_IDS_CELLS" ] ): + if has_array( input_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." ) self.m_logger.error( err_msg ) @@ -121,11 +121,11 @@ def RequestData( return 1 - def SetLogger( self: Self, logger: Logger ) -> None: + def SetLogger( self: Self, logger ) -> None: """Set the logger. Args: - logger (Logger): logger + logger """ self.m_logger = logger self.Modified() diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py index e846a0bf0..80df14491 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py @@ -4,10 +4,10 @@ from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid -from geos.mesh.doctor.checks.generate_global_ids import build_global_ids -from geos.mesh.doctor.checks.generate_cube import FieldInfo, add_fields, build_coordinates, build_rectilinear_grid -from geos.mesh.vtk.io import VtkOutput, write_mesh -from geos.utils.Logger import Logger, getLogger +from geos.mesh.doctor.actions.generate_global_ids import build_global_ids +from geos.mesh.doctor.actions.generate_cube import FieldInfo, add_fields, build_coordinates, build_rectilinear_grid +from geos.mesh.doctor.parsing.cli_parsing import setup_logger +from geos.mesh.io.vtkIO import VtkOutput, write_mesh __doc__ = """ GenerateRectilinearGrid module is a vtk filter that allows to create a simple vtkUnstructuredGrid rectilinear grid. @@ -73,7 +73,7 @@ def __init__( self: Self ) -> None: self.m_numberElementsY: Sequence[ int ] = None self.m_numberElementsZ: Sequence[ int ] = None self.m_fields: Iterable[ FieldInfo ] = list() - self.m_logger: Logger = getLogger( "Generate Rectilinear Grid Filter" ) + self.m_logger = setup_logger def RequestData( self: Self, request: vtkInformation, inInfo: vtkInformationVector, outInfo: vtkInformationVector ) -> int: @@ -87,11 +87,11 @@ def RequestData( self: Self, request: vtkInformation, inInfo: vtkInformationVect opt.ShallowCopy( output ) return 1 - def SetLogger( self: Self, logger: Logger ) -> None: + def SetLogger( self: Self, logger ) -> None: """Set the logger. Args: - logger (Logger): logger + logger """ self.m_logger = logger self.Modified() diff --git a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py index a7c364b1c..2f5ee1371 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py @@ -5,9 +5,9 @@ from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid -from geos.mesh.doctor.checks.non_conformal import Options, find_non_conformal_cells -from geos.mesh.vtk.io import VtkOutput, write_mesh -from geos.utils.Logger import Logger, getLogger +from geos.mesh.doctor.actions.non_conformal import Options, find_non_conformal_cells +from geos.mesh.doctor.parsing.cli_parsing import setup_logger +from geos.mesh.io.vtkIO import VtkOutput, write_mesh __doc__ = """ NonConformal module is a vtk filter that ... of a vtkUnstructuredGrid. @@ -38,10 +38,9 @@ def __init__( self: Self ) -> None: self.m_angle_tolerance: float = 10.0 self.m_face_tolerance: float = 0.0 self.m_point_tolerance: float = 0.0 - self.m_non_conformal_cells: list[ tuple[ int, int ] ] = list() self.m_paintNonConformalCells: int = 0 - self.m_logger: Logger = getLogger( "Element Volumes Filter" ) + self.m_logger = setup_logger def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: """Inherited from VTKPythonAlgorithmBase::RequestInformation. @@ -120,11 +119,11 @@ def RequestData( return 1 - def SetLogger( self: Self, logger: Logger ) -> None: + def SetLogger( self: Self, logger ) -> None: """Set the logger. Args: - logger (Logger): logger + logger """ self.m_logger = logger self.Modified() diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py index 1bb13e26c..3e7e870f5 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py @@ -1,210 +1,212 @@ -import numpy as np -import numpy.typing as npt -from typing_extensions import Self -from vtkmodules.util.numpy_support import numpy_to_vtk -from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase -from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray, VTK_INT -from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid -from geos.mesh.doctor.checks.supported_elements import ( Options, find_unsupported_std_elements_types, - find_unsupported_polyhedron_elements ) -from geos.mesh.vtk.io import VtkOutput, write_mesh -from geos.utils.Logger import Logger, getLogger - -__doc__ = """ -SupportedElements module is a vtk filter that allows ... a vtkUnstructuredGrid. - -One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. - -To use the filter: - -.. code-block:: python - - from filters.SupportedElements import SupportedElements - - # instanciate the filter - supportedElementsFilter: SupportedElements = SupportedElements() - -""" - - -class SupportedElements( VTKPythonAlgorithmBase ): - - def __init__( self: Self ) -> None: - """Vtk filter to ... a vtkUnstructuredGrid. - - Output mesh is vtkUnstructuredGrid. - """ - super().__init__( nInputPorts=1, - nOutputPorts=1, - inputType='vtkUnstructuredGrid', - outputType='vtkUnstructuredGrid' ) - self.m_paintUnsupportedElementTypes: int = 0 - # TODO Needs parallelism to work - # self.m_paintUnsupportedPolyhedrons: int = 0 - # self.m_chunk_size: int = 1 - # self.m_num_proc: int = 1 - self.m_logger: Logger = getLogger( "Element Volumes Filter" ) - - def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestInformation. - - Args: - port (int): input port - info (vtkInformationVector): info - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - if port == 0: - info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) - return 1 - - def RequestInformation( - self: Self, - request: vtkInformation, # noqa: F841 - inInfoVec: list[ vtkInformationVector ], # noqa: F841 - outInfoVec: vtkInformationVector, - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestInformation. - - Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - executive = self.GetExecutive() # noqa: F841 - outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 - return 1 - - def RequestData( - self: Self, - request: vtkInformation, - inInfoVec: list[ vtkInformationVector ], - outInfo: vtkInformationVector - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestData. - - Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) - output = vtkUnstructuredGrid.GetData( outInfo ) - - output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() - output_mesh.CopyStructure( input_mesh ) - output_mesh.CopyAttributes( input_mesh ) - - unsupported_std_elt_types: set[ int ] = find_unsupported_std_elements_types( input_mesh ) - if len( unsupported_std_elt_types ) > 0: - self.m_logger.info( "The following vtk element types in your mesh are not supported by GEOS:" ) - self.m_logger.info( unsupported_std_elt_types ) - - if self.m_paintUnsupportedElementTypes: - nbr_cells: int = output_mesh.GetNumberOfCells() - arrayCellTypes: npt.NDArray = np.zeros( nbr_cells, dtype=int ) - for i in range( nbr_cells ): - arrayCellTypes[ i ] = output_mesh.GetCellType(i) - - arrayUET: npt.NDArray = np.zeros( nbr_cells, dtype=int ) - arrayUET[ np.isin( arrayCellTypes, list( unsupported_std_elt_types ) ) ] = 1 - vtkArrayWSP: vtkDataArray = numpy_to_vtk( arrayUET ) - vtkArrayWSP.SetName( "HasUnsupportedType" ) - output_mesh.GetCellData().AddArray( vtkArrayWSP ) - - # TODO Needs parallelism to work - # options = Options( self.m_num_proc, self.m_chunk_size ) - # unsupported_polyhedron_elts: list[ int ] = find_unsupported_polyhedron_elements( input_mesh, options ) - # if len( unsupported_polyhedron_elts ) > 0: - # self.m_logger.info( "The following vtk polyhedron cell indexes in your mesh are not supported by GEOS:" ) - # self.m_logger.info( unsupported_polyhedron_elts ) - - # if self.m_paintUnsupportedPolyhedrons: - # arrayUP: npt.NDArray = np.zeros( output_mesh.GetNumberOfCells(), dtype=int ) - # arrayUP[ unsupported_polyhedron_elts ] = 1 - # self.m_logger.info( f"arrayUP: {arrayUP}" ) - # vtkArrayWSP: vtkDataArray = numpy_to_vtk( arrayUP ) - # vtkArrayWSP.SetName( "IsUnsupportedPolyhedron" ) - # output_mesh.GetCellData().AddArray( vtkArrayWSP ) - - output.ShallowCopy( output_mesh ) - - return 1 - - def SetLogger( self: Self, logger: Logger ) -> None: - """Set the logger. - - Args: - logger (Logger): logger - """ - self.m_logger = logger - self.Modified() - - def getGrid( self: Self ) -> vtkUnstructuredGrid: - """Returns the vtkUnstructuredGrid with volumes. - - Args: - self (Self) - - Returns: - vtkUnstructuredGrid - """ - self.Update() # triggers RequestData - return self.GetOutputDataObject( 0 ) - - def setPaintUnsupportedElementTypes( self: Self, choice: int ) -> None: - """Set 0 or 1 to choose if you want to create a new "HasUnsupportedType" array in your output data. - - Args: - self (Self) - choice (int): 0 or 1 - """ - if choice not in [ 0, 1 ]: - self.m_logger.error( f"setPaintUnsupportedElementTypes: Please choose either 0 or 1 not '{choice}'." ) - else: - self.m_paintUnsupportedElementTypes = choice - self.Modified() - - # TODO Needs parallelism to work - # def setPaintUnsupportedPolyhedrons( self: Self, choice: int ) -> None: - # """Set 0 or 1 to choose if you want to create a new "IsUnsupportedPolyhedron" array in your output data. - - # Args: - # self (Self) - # choice (int): 0 or 1 - # """ - # if choice not in [ 0, 1 ]: - # self.m_logger.error( f"setPaintUnsupportedPolyhedrons: Please choose either 0 or 1 not '{choice}'." ) - # else: - # self.m_paintUnsupportedPolyhedrons = choice - # self.Modified() - - # def setChunkSize( self: Self, new_chunk_size: int ) -> None: - # self.m_chunk_size = new_chunk_size - # self.Modified() - - # def setNumProc( self: Self, new_num_proc: int ) -> None: - # self.m_num_proc = new_num_proc - # self.Modified() - - def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: - """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath with volumes. - - Args: - filepath (str): /path/to/your/file.vtu - is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. - canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. - Defaults to False. - """ - mesh: vtkUnstructuredGrid = self.getGrid() - if mesh: - write_mesh( filepath, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) - else: - self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) +# TODO Find an implementation to keep multiprocessing while using vtkFilter + +# import numpy as np +# import numpy.typing as npt +# from typing_extensions import Self +# from vtkmodules.util.numpy_support import numpy_to_vtk +# from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase +# from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray, VTK_INT +# from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +# from geos.mesh.doctor.actions.supported_elements import ( Options, find_unsupported_std_elements_types, +# find_unsupported_polyhedron_elements ) +# from geos.mesh.io.vtkIO import VtkOutput, write_mesh +# from geos.utils.Logger import Logger, getLogger + +# __doc__ = """ +# SupportedElements module is a vtk filter that allows ... a vtkUnstructuredGrid. + +# One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. + +# To use the filter: + +# .. code-block:: python + +# from filters.SupportedElements import SupportedElements + +# # instanciate the filter +# supportedElementsFilter: SupportedElements = SupportedElements() + +# """ + + +# class SupportedElements( VTKPythonAlgorithmBase ): + +# def __init__( self: Self ) -> None: +# """Vtk filter to ... a vtkUnstructuredGrid. + +# Output mesh is vtkUnstructuredGrid. +# """ +# super().__init__( nInputPorts=1, +# nOutputPorts=1, +# inputType='vtkUnstructuredGrid', +# outputType='vtkUnstructuredGrid' ) +# self.m_paintUnsupportedElementTypes: int = 0 +# # TODO Needs parallelism to work +# # self.m_paintUnsupportedPolyhedrons: int = 0 +# # self.m_chunk_size: int = 1 +# # self.m_num_proc: int = 1 +# self.m_logger: Logger = getLogger( "Element Volumes Filter" ) + +# def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: +# """Inherited from VTKPythonAlgorithmBase::RequestInformation. + +# Args: +# port (int): input port +# info (vtkInformationVector): info + +# Returns: +# int: 1 if calculation successfully ended, 0 otherwise. +# """ +# if port == 0: +# info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) +# return 1 + +# def RequestInformation( +# self: Self, +# request: vtkInformation, # noqa: F841 +# inInfoVec: list[ vtkInformationVector ], # noqa: F841 +# outInfoVec: vtkInformationVector, +# ) -> int: +# """Inherited from VTKPythonAlgorithmBase::RequestInformation. + +# Args: +# request (vtkInformation): request +# inInfoVec (list[vtkInformationVector]): input objects +# outInfoVec (vtkInformationVector): output objects + +# Returns: +# int: 1 if calculation successfully ended, 0 otherwise. +# """ +# executive = self.GetExecutive() # noqa: F841 +# outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 +# return 1 + +# def RequestData( +# self: Self, +# request: vtkInformation, +# inInfoVec: list[ vtkInformationVector ], +# outInfo: vtkInformationVector +# ) -> int: +# """Inherited from VTKPythonAlgorithmBase::RequestData. + +# Args: +# request (vtkInformation): request +# inInfoVec (list[vtkInformationVector]): input objects +# outInfoVec (vtkInformationVector): output objects + +# Returns: +# int: 1 if calculation successfully ended, 0 otherwise. +# """ +# input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) +# output = vtkUnstructuredGrid.GetData( outInfo ) + +# output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() +# output_mesh.CopyStructure( input_mesh ) +# output_mesh.CopyAttributes( input_mesh ) + +# unsupported_std_elt_types: set[ int ] = find_unsupported_std_elements_types( input_mesh ) +# if len( unsupported_std_elt_types ) > 0: +# self.m_logger.info( "The following vtk element types in your mesh are not supported by GEOS:" ) +# self.m_logger.info( unsupported_std_elt_types ) + +# if self.m_paintUnsupportedElementTypes: +# nbr_cells: int = output_mesh.GetNumberOfCells() +# arrayCellTypes: npt.NDArray = np.zeros( nbr_cells, dtype=int ) +# for i in range( nbr_cells ): +# arrayCellTypes[ i ] = output_mesh.GetCellType(i) + +# arrayUET: npt.NDArray = np.zeros( nbr_cells, dtype=int ) +# arrayUET[ np.isin( arrayCellTypes, list( unsupported_std_elt_types ) ) ] = 1 +# vtkArrayWSP: vtkDataArray = numpy_to_vtk( arrayUET ) +# vtkArrayWSP.SetName( "HasUnsupportedType" ) +# output_mesh.GetCellData().AddArray( vtkArrayWSP ) + +# # TODO Needs parallelism to work +# # options = Options( self.m_num_proc, self.m_chunk_size ) +# # unsupported_polyhedron_elts: list[ int ] = find_unsupported_polyhedron_elements( input_mesh, options ) +# # if len( unsupported_polyhedron_elts ) > 0: +# # self.m_logger.info( "These vtk polyhedron cell indexes in your mesh are not supported by GEOS:" ) +# # self.m_logger.info( unsupported_polyhedron_elts ) + +# # if self.m_paintUnsupportedPolyhedrons: +# # arrayUP: npt.NDArray = np.zeros( output_mesh.GetNumberOfCells(), dtype=int ) +# # arrayUP[ unsupported_polyhedron_elts ] = 1 +# # self.m_logger.info( f"arrayUP: {arrayUP}" ) +# # vtkArrayWSP: vtkDataArray = numpy_to_vtk( arrayUP ) +# # vtkArrayWSP.SetName( "IsUnsupportedPolyhedron" ) +# # output_mesh.GetCellData().AddArray( vtkArrayWSP ) + +# output.ShallowCopy( output_mesh ) + +# return 1 + +# def SetLogger( self: Self, logger: Logger ) -> None: +# """Set the logger. + +# Args: +# logger (Logger): logger +# """ +# self.m_logger = logger +# self.Modified() + +# def getGrid( self: Self ) -> vtkUnstructuredGrid: +# """Returns the vtkUnstructuredGrid with volumes. + +# Args: +# self (Self) + +# Returns: +# vtkUnstructuredGrid +# """ +# self.Update() # triggers RequestData +# return self.GetOutputDataObject( 0 ) + +# def setPaintUnsupportedElementTypes( self: Self, choice: int ) -> None: +# """Set 0 or 1 to choose if you want to create a new "HasUnsupportedType" array in your output data. + +# Args: +# self (Self) +# choice (int): 0 or 1 +# """ +# if choice not in [ 0, 1 ]: +# self.m_logger.error( f"setPaintUnsupportedElementTypes: Please choose either 0 or 1 not '{choice}'." ) +# else: +# self.m_paintUnsupportedElementTypes = choice +# self.Modified() + +# # TODO Needs parallelism to work +# # def setPaintUnsupportedPolyhedrons( self: Self, choice: int ) -> None: +# # """Set 0 or 1 to choose if you want to create a new "IsUnsupportedPolyhedron" array in your output data. + +# # Args: +# # self (Self) +# # choice (int): 0 or 1 +# # """ +# # if choice not in [ 0, 1 ]: +# # self.m_logger.error( f"setPaintUnsupportedPolyhedrons: Please choose either 0 or 1 not '{choice}'." ) +# # else: +# # self.m_paintUnsupportedPolyhedrons = choice +# # self.Modified() + +# # def setChunkSize( self: Self, new_chunk_size: int ) -> None: +# # self.m_chunk_size = new_chunk_size +# # self.Modified() + +# # def setNumProc( self: Self, new_num_proc: int ) -> None: +# # self.m_num_proc = new_num_proc +# # self.Modified() + +# def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: +# """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath with volumes. + +# Args: +# filepath (str): /path/to/your/file.vtu +# is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. +# canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing +# file. Defaults to False. +# """ +# mesh: vtkUnstructuredGrid = self.getGrid() +# if mesh: +# write_mesh( filepath, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) +# else: +# self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) diff --git a/geos-mesh/tests/test_generate_global_ids.py b/geos-mesh/tests/test_generate_global_ids.py index 127300f4b..8f1ad25a6 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_supported_elements.py b/geos-mesh/tests/test_supported_elements.py index f1ef1a49b..abdfb31c3 100644 --- a/geos-mesh/tests/test_supported_elements.py +++ b/geos-mesh/tests/test_supported_elements.py @@ -7,7 +7,7 @@ vtkCellArray, VTK_POLYHEDRON, VTK_QUAD, VTK_TETRA, VTK_HEXAHEDRON ) # 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.doctor.filters.SupportedElements import SupportedElements +# from geos.mesh.doctor.filters.SupportedElements import SupportedElements from geos.mesh.utils.genericHelpers import to_vtk_id_list @@ -298,41 +298,42 @@ def create_unsupported_polyhedron_grid(): return grid -class TestSupportedElements: - - def test_only_supported_elements( self ): - """Test a grid with only supported element types""" - # Create grid with only supported elements (tetra) - grid = create_simple_tetra_grid() - # Apply the filter - filter = SupportedElements() - filter.SetInputDataObject( grid ) - filter.Update() - result = filter.getGrid() - assert result is not None - # Verify no arrays were added (since all elements are supported) - assert result.GetCellData().GetArray( "HasUnsupportedType" ) is None - assert result.GetCellData().GetArray( "IsUnsupportedPolyhedron" ) is None - - def test_unsupported_element_types( self ): - """Test a grid with unsupported element types""" - # Create grid with unsupported elements - grid = create_mixed_grid() - # Apply the filter with painting enabled - filter = SupportedElements() - filter.m_logger.critical( "test_unsupported_element_types" ) - filter.SetInputDataObject( grid ) - filter.setPaintUnsupportedElementTypes( 1 ) - filter.Update() - result = filter.getGrid() - assert result is not None - # Verify the array was added - unsupported_array = result.GetCellData().GetArray( "HasUnsupportedType" ) - assert unsupported_array is not None - for i in range( 0, 4 ): - assert unsupported_array.GetValue( i ) == 0 # Hexahedron should be supported - for j in range( 4, 6 ): - assert unsupported_array.GetValue( j ) == 1 # Quad should not be supported +# TODO reimplement once SupportedElements can handle multiprocessing +# class TestSupportedElements: + +# def test_only_supported_elements( self ): +# """Test a grid with only supported element types""" +# # Create grid with only supported elements (tetra) +# grid = create_simple_tetra_grid() +# # Apply the filter +# filter = SupportedElements() +# filter.SetInputDataObject( grid ) +# filter.Update() +# result = filter.getGrid() +# assert result is not None +# # Verify no arrays were added (since all elements are supported) +# assert result.GetCellData().GetArray( "HasUnsupportedType" ) is None +# assert result.GetCellData().GetArray( "IsUnsupportedPolyhedron" ) is None + +# def test_unsupported_element_types( self ): +# """Test a grid with unsupported element types""" +# # Create grid with unsupported elements +# grid = create_mixed_grid() +# # Apply the filter with painting enabled +# filter = SupportedElements() +# filter.m_logger.critical( "test_unsupported_element_types" ) +# filter.SetInputDataObject( grid ) +# filter.setPaintUnsupportedElementTypes( 1 ) +# filter.Update() +# result = filter.getGrid() +# assert result is not None +# # Verify the array was added +# unsupported_array = result.GetCellData().GetArray( "HasUnsupportedType" ) +# assert unsupported_array is not None +# for i in range( 0, 4 ): +# assert unsupported_array.GetValue( i ) == 0 # Hexahedron should be supported +# for j in range( 4, 6 ): +# assert unsupported_array.GetValue( j ) == 1 # Quad should not be supported # TODO Needs parallelism to work # def test_unsupported_polyhedron( self ): @@ -353,38 +354,38 @@ def test_unsupported_element_types( self ): # # Since we created an unsupported polyhedron, it should be marked # assert polyhedron_array.GetValue( 0 ) == 1 - def test_paint_flags( self ): - """Test setting invalid paint flags""" - filter = SupportedElements() - # Should log an error but not raise an exception - filter.setPaintUnsupportedElementTypes( 2 ) # Invalid value - filter.setPaintUnsupportedPolyhedrons( 2 ) # Invalid value - # Values should remain unchanged - assert filter.m_paintUnsupportedElementTypes == 0 - assert filter.m_paintUnsupportedPolyhedrons == 0 - - def test_set_chunk_size( self ): - """Test that setChunkSize properly updates the chunk size""" - # Create filter instance - filter = SupportedElements() - # Note the initial value - initial_chunk_size = filter.m_chunk_size - # Set a new chunk size - new_chunk_size = 100 - filter.setChunkSize( new_chunk_size ) - # Verify the chunk size was updated - assert filter.m_chunk_size == new_chunk_size - assert filter.m_chunk_size != initial_chunk_size - - def test_set_num_proc( self ): - """Test that setNumProc properly updates the number of processors""" - # Create filter instance - filter = SupportedElements() - # Note the initial value - initial_num_proc = filter.m_num_proc - # Set a new number of processors - new_num_proc = 4 - filter.setNumProc( new_num_proc ) - # Verify the number of processors was updated - assert filter.m_num_proc == new_num_proc - assert filter.m_num_proc != initial_num_proc + # def test_paint_flags( self ): + # """Test setting invalid paint flags""" + # filter = SupportedElements() + # # Should log an error but not raise an exception + # filter.setPaintUnsupportedElementTypes( 2 ) # Invalid value + # filter.setPaintUnsupportedPolyhedrons( 2 ) # Invalid value + # # Values should remain unchanged + # assert filter.m_paintUnsupportedElementTypes == 0 + # assert filter.m_paintUnsupportedPolyhedrons == 0 + + # def test_set_chunk_size( self ): + # """Test that setChunkSize properly updates the chunk size""" + # # Create filter instance + # filter = SupportedElements() + # # Note the initial value + # initial_chunk_size = filter.m_chunk_size + # # Set a new chunk size + # new_chunk_size = 100 + # filter.setChunkSize( new_chunk_size ) + # # Verify the chunk size was updated + # assert filter.m_chunk_size == new_chunk_size + # assert filter.m_chunk_size != initial_chunk_size + + # def test_set_num_proc( self ): + # """Test that setNumProc properly updates the number of processors""" + # # Create filter instance + # filter = SupportedElements() + # # Note the initial value + # initial_num_proc = filter.m_num_proc + # # Set a new number of processors + # new_num_proc = 4 + # filter.setNumProc( new_num_proc ) + # # Verify the number of processors was updated + # assert filter.m_num_proc == new_num_proc + # assert filter.m_num_proc != initial_num_proc From 0b4458450fb5972333da55194ccf00ef34f5b194 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Fri, 1 Aug 2025 15:01:44 -0700 Subject: [PATCH 09/52] Temporary commit --- .../geos/mesh/doctor/actions/all_checks.py | 7 +- .../actions/self_intersecting_elements.py | 110 +++++++++------ .../src/geos/mesh/doctor/filters/AllChecks.py | 132 ++++++++++++++++++ 3 files changed, 205 insertions(+), 44 deletions(-) create mode 100644 geos-mesh/src/geos/mesh/doctor/filters/AllChecks.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 index 253165d94..1219366b0 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py @@ -15,7 +15,7 @@ class Result: check_results: dict[ str, any ] -def action( vtk_input_file: str, options: Options ) -> list[ Result ]: +def get_check_results( vtk_input_file: str, options: Options ) -> dict[ str, any ]: check_results: dict[ str, any ] = dict() for check_name in options.checks_to_perform: check_action = __load_module_action( check_name ) @@ -23,4 +23,9 @@ def action( vtk_input_file: str, options: Options ) -> list[ Result ]: option = options.checks_options[ check_name ] check_result = check_action( vtk_input_file, option ) check_results[ check_name ] = check_result + return check_results + + +def action( vtk_input_file: str, options: Options ) -> Result: + check_results: dict[ str, any ] = get_check_results( vtk_input_file, options ) return Result( check_results=check_results ) 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 3b7d313ab..a4ab9d9e8 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 @@ -1,8 +1,9 @@ from dataclasses import dataclass -from typing import Collection, List +from typing import Collection from vtkmodules.util.numpy_support import vtk_to_numpy from vtkmodules.vtkFiltersGeneral import vtkCellValidator from vtkmodules.vtkCommonCore import vtkOutputWindow, vtkFileOutputWindow +from vtkmodules.vtkCommonDataModel import vtkPointSet from geos.mesh.io.vtkIO import read_mesh @@ -18,60 +19,83 @@ class Result: intersecting_faces_elements: Collection[ int ] non_contiguous_edges_elements: Collection[ int ] non_convex_elements: Collection[ int ] - faces_are_oriented_incorrectly_elements: Collection[ int ] + faces_oriented_incorrectly_elements: Collection[ int ] -def __action( mesh, options: Options ) -> Result: +def get_invalid_cell_ids( mesh: vtkPointSet, min_distance: float ) -> dict[ str, list[ int ] ]: + """For every cell element in a vtk mesh, check if the cell is invalid regarding 6 specific criteria: + "wrong_number_of_points", "intersecting_edges", "intersecting_faces", + "non_contiguous_edges","non_convex" and "faces_oriented_incorrectly". + + If any of this criteria was met, the cell index is added to a list corresponding to this specific criteria. + The dict with the complete list of cell indices by criteria is returned. + + Args: + mesh (vtkPointSet): A vtk grid. + min_distance (float): Minimum distance in the computation. + + Returns: + dict[ str, list[ int ] ]: + { + "wrong_number_of_points": [ 10, 34, ... ], + "intersecting_edges": [ ... ], + "intersecting_faces": [ ... ], + "non_contiguous_edges": [ ... ], + "non_convex": [ ... ], + "faces_oriented_incorrectly": [ ... ] + } + """ + # The goal of this first block is to silence the standard error output from VTK. The vtkCellValidator can be very + # verbose, printing a message for every cell it checks. We redirect the output to /dev/null to remove it. err_out = vtkFileOutputWindow() - err_out.SetFileName( "/dev/null" ) # vtkCellValidator outputs loads for each cell... + err_out.SetFileName( "/dev/null" ) vtk_std_err_out = vtkOutputWindow() vtk_std_err_out.SetInstance( err_out ) - valid = 0x0 - wrong_number_of_points = 0x01 - intersecting_edges = 0x02 - intersecting_faces = 0x04 - non_contiguous_edges = 0x08 - non_convex = 0x10 - faces_are_oriented_incorrectly = 0x20 - - wrong_number_of_points_elements: List[ int ] = [] - intersecting_edges_elements: List[ int ] = [] - intersecting_faces_elements: List[ int ] = [] - non_contiguous_edges_elements: List[ int ] = [] - non_convex_elements: List[ int ] = [] - faces_are_oriented_incorrectly_elements: List[ int ] = [] + # Different types of cell invalidity are defined as hexadecimal values, specific to vtkCellValidator + # Here NonPlanarFaces and DegenerateFaces can also be obtained. + error_masks: dict[ str, int ] = { + "wrong_number_of_points_elements": 0x01, # 0000 0001 + "intersecting_edges_elements": 0x02, # 0000 0010 + "intersecting_faces_elements": 0x04, # 0000 0100 + "non_contiguous_edges_elements": 0x08, # 0000 1000 + "non_convex_elements": 0x10, # 0001 0000 + "faces_oriented_incorrectly_elements": 0x20, # 0010 0000 + } - f = vtkCellValidator() - f.SetTolerance( options.min_distance ) + # The results can be stored in a dictionary where keys are the error names + # and values are the lists of cell indices with that error. + # We can initialize it directly from the keys of our error_masks dictionary. + invalid_cell_ids: dict[ str, list[ int ] ] = { error_name: list() for error_name in error_masks } - f.SetInputData( mesh ) - f.Update() + f = vtkCellValidator() + f.SetTolerance(min_distance) + f.SetInputData(mesh) + f.Update() # executes the filter output = f.GetOutput() - validity = output.GetCellData().GetArray( "ValidityState" ) # Could not change name using the vtk interface. + validity = output.GetCellData().GetArray( "ValidityState" ) assert validity is not None + # array of np.int16 that combines the flags using a bitwise OR operation for each cell index. validity = vtk_to_numpy( validity ) - for i, v in enumerate( validity ): - if not v & valid: - if v & wrong_number_of_points: - wrong_number_of_points_elements.append( i ) - if v & intersecting_edges: - intersecting_edges_elements.append( i ) - if v & intersecting_faces: - intersecting_faces_elements.append( i ) - if v & non_contiguous_edges: - non_contiguous_edges_elements.append( i ) - if v & non_convex: - non_convex_elements.append( i ) - if v & faces_are_oriented_incorrectly: - faces_are_oriented_incorrectly_elements.append( i ) - return Result( wrong_number_of_points_elements=wrong_number_of_points_elements, - intersecting_edges_elements=intersecting_edges_elements, - intersecting_faces_elements=intersecting_faces_elements, - non_contiguous_edges_elements=non_contiguous_edges_elements, - non_convex_elements=non_convex_elements, - faces_are_oriented_incorrectly_elements=faces_are_oriented_incorrectly_elements ) + for cell_index, validity_flag in enumerate( validity ): + if not validity_flag: # Skip valid cells ( validity_flag == 0 or 0000 0000 ) + continue + for error_name, error_mask in error_masks.items(): # Check only invalid cells against all possible errors. + if validity_flag & error_mask: + invalid_cell_ids[ error_name ].append( cell_index ) + + return invalid_cell_ids + + +def __action( mesh, options: Options ) -> Result: + invalid_cell_ids = get_invalid_cell_ids( mesh, options.min_distance ) + return Result( wrong_number_of_points_elements=invalid_cell_ids[ "wrong_number_of_points_elements" ], + intersecting_edges_elements=invalid_cell_ids[ "intersecting_edges_elements" ], + intersecting_faces_elements=invalid_cell_ids[ "intersecting_faces_elements" ], + non_contiguous_edges_elements=invalid_cell_ids[ "non_contiguous_edges_elements" ], + non_convex_elements=invalid_cell_ids[ "non_convex_elements" ], + faces_oriented_incorrectly_elements=invalid_cell_ids[ "faces_oriented_incorrectly_elements" ] ) def action( vtk_input_file: str, options: Options ) -> Result: diff --git a/geos-mesh/src/geos/mesh/doctor/filters/AllChecks.py b/geos-mesh/src/geos/mesh/doctor/filters/AllChecks.py new file mode 100644 index 000000000..a7be551fe --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/filters/AllChecks.py @@ -0,0 +1,132 @@ +from typing_extensions import Self +from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase +from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from geos.mesh.doctor.actions.all_checks import get_check_results +from geos.mesh.doctor.parsing.cli_parsing import setup_logger +from geos.mesh.io.vtkIO import VtkOutput, write_mesh + +__doc__ = """ +AllChecks module is a vtk filter that ... + +One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. + +To use the filter: + +.. code-block:: python + + from filters.AllChecks import AllChecks + + # instanciate the filter + allChecksFilter: AllChecks = AllChecks() + +""" + + +class AllChecks( VTKPythonAlgorithmBase ): + + def __init__( self: Self ) -> None: + """Vtk filter to ... of a vtkUnstructuredGrid. + + Output mesh is vtkUnstructuredGrid. + """ + super().__init__( nInputPorts=1, nOutputPorts=1, inputType='vtkUnstructuredGrid', + outputType='vtkUnstructuredGrid' ) + self.m_logger = setup_logger + + def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestInformation. + + Args: + port (int): input port + info (vtkInformationVector): info + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + if port == 0: + info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) + return 1 + + def RequestInformation( + self: Self, + request: vtkInformation, # noqa: F841 + inInfoVec: list[ vtkInformationVector ], # noqa: F841 + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestInformation. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + executive = self.GetExecutive() # noqa: F841 + outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 + return 1 + + def RequestData( + self: Self, + request: vtkInformation, + inInfoVec: list[ vtkInformationVector ], + outInfo: vtkInformationVector + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestData. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) + results: dict[ str, any ] = get_check_results( ... ) + output = vtkUnstructuredGrid.GetData( outInfo ) + + output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() + output_mesh.CopyStructure( input_mesh ) + output_mesh.CopyAttributes( input_mesh ) + output.ShallowCopy( output_mesh ) + + return 1 + + def SetLogger( self: Self, logger ) -> None: + """Set the logger. + + Args: + logger + """ + self.m_logger = logger + self.Modified() + + def getGrid( self: Self ) -> vtkUnstructuredGrid: + """Returns the vtkUnstructuredGrid with volumes. + + Args: + self (Self) + + Returns: + vtkUnstructuredGrid + """ + self.Update() # triggers RequestData + return self.GetOutputDataObject( 0 ) + + def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: + """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath with volumes. + + Args: + filepath (str): /path/to/your/file.vtu + is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. + canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. + Defaults to False. + """ + mesh: vtkUnstructuredGrid = self.getGrid() + if mesh: + write_mesh( filepath, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) + else: + self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) From f68b38d309fb478a3f1f8d499c005f63374eefa7 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Mon, 4 Aug 2025 16:03:10 -0700 Subject: [PATCH 10/52] Add base class for all mesh doctor VTK filters --- .../doctor/filters/BaseMeshDoctorFilter.py | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 geos-mesh/src/geos/mesh/doctor/filters/BaseMeshDoctorFilter.py diff --git a/geos-mesh/src/geos/mesh/doctor/filters/BaseMeshDoctorFilter.py b/geos-mesh/src/geos/mesh/doctor/filters/BaseMeshDoctorFilter.py new file mode 100644 index 000000000..6052f1b9e --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/filters/BaseMeshDoctorFilter.py @@ -0,0 +1,163 @@ +from typing_extensions import Self +from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase +from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from geos.mesh.doctor.parsing.cli_parsing import setup_logger +from geos.mesh.io.vtkIO import VtkOutput, write_mesh + +__doc__ = """Base class for all mesh doctor VTK filters.""" + + +class BaseMeshDoctorFilter( VTKPythonAlgorithmBase ): + """Base class for all mesh doctor VTK filters. + + This class provides common functionality shared across all mesh doctor filters, + including logger management, grid access, and file writing capabilities. + """ + + def __init__( + self: Self, + nInputPorts: int = 1, + nOutputPorts: int = 1, + inputType: str = 'vtkUnstructuredGrid', + outputType: str = 'vtkUnstructuredGrid' + ) -> None: + """Initialize the base mesh doctor filter. + + Args: + nInputPorts (int): Number of input ports. Defaults to 1. + nOutputPorts (int): Number of output ports. Defaults to 1. + inputType (str): Input data type. Defaults to 'vtkUnstructuredGrid'. + outputType (str): Output data type. Defaults to 'vtkUnstructuredGrid'. + """ + super().__init__( + nInputPorts=nInputPorts, + nOutputPorts=nOutputPorts, + inputType=inputType if nInputPorts > 0 else None, + outputType=outputType + ) + self.m_logger = setup_logger + + def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: + """Inherited from VTKPythonAlgorithmBase::FillInputPortInformation. + + Args: + port (int): input port + info (vtkInformationVector): info + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + if port == 0: + info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) + return 1 + + def RequestInformation( + self: Self, + request: vtkInformation, # noqa: F841 + inInfoVec: list[ vtkInformationVector ], # noqa: F841 + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestInformation. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + executive = self.GetExecutive() # noqa: F841 + outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 + return 1 + + def SetLogger( self: Self, logger ) -> None: + """Set the logger. + + Args: + logger: Logger instance to use + """ + self.m_logger = logger + self.Modified() + + def getGrid( self: Self ) -> vtkUnstructuredGrid: + """Returns the vtkUnstructuredGrid output. + + Args: + self (Self) + + Returns: + vtkUnstructuredGrid: The output grid + """ + self.Update() # triggers RequestData + return self.GetOutputDataObject( 0 ) + + def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: + """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath. + + Args: + filepath (str): /path/to/your/file.vtu + is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. + canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. + Defaults to False. + """ + mesh: vtkUnstructuredGrid = self.getGrid() + if mesh: + vtk_output = VtkOutput( filepath, is_data_mode_binary ) + write_mesh( mesh, vtk_output, canOverwrite ) + else: + self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) + + def copyInputToOutput( self: Self, input_mesh: vtkUnstructuredGrid ) -> vtkUnstructuredGrid: + """Helper method to copy input mesh structure and attributes to a new output mesh. + + Args: + input_mesh (vtkUnstructuredGrid): Input mesh to copy from + + Returns: + vtkUnstructuredGrid: New mesh with copied structure and attributes + """ + output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() + output_mesh.CopyStructure( input_mesh ) + output_mesh.CopyAttributes( input_mesh ) + return output_mesh + + +class BaseMeshDoctorGeneratorFilter( BaseMeshDoctorFilter ): + """Base class for mesh doctor generator filters (no input required). + + This class extends BaseMeshDoctorFilter for filters that generate meshes + from scratch without requiring input meshes. + """ + + def __init__( + self: Self, + nOutputPorts: int = 1, + outputType: str = 'vtkUnstructuredGrid' + ) -> None: + """Initialize the base mesh doctor generator filter. + + Args: + nOutputPorts (int): Number of output ports. Defaults to 1. + outputType (str): Output data type. Defaults to 'vtkUnstructuredGrid'. + """ + super().__init__( + nInputPorts=0, + nOutputPorts=nOutputPorts, + inputType=None, + outputType=outputType + ) + + def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: + """Generator filters don't have input ports. + + Args: + port (int): input port (not used) + info (vtkInformationVector): info (not used) + + Returns: + int: Always returns 1 + """ + # Generator filters don't have input ports, so this method is not used + return 1 From 9727b216763dd0c7534b68e9a99960e3f9d4c998 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Mon, 4 Aug 2025 16:13:36 -0700 Subject: [PATCH 11/52] Update existing filters with new base class --- .../src/geos/mesh/doctor/filters/AllChecks.py | 79 +------------------ .../mesh/doctor/filters/CollocatedNodes.py | 79 +------------------ .../mesh/doctor/filters/ElementVolumes.py | 77 +----------------- .../mesh/doctor/filters/GenerateFractures.py | 49 +----------- .../doctor/filters/GenerateRectilinearGrid.py | 43 +--------- .../geos/mesh/doctor/filters/NonConformal.py | 79 +------------------ 6 files changed, 15 insertions(+), 391 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/AllChecks.py b/geos-mesh/src/geos/mesh/doctor/filters/AllChecks.py index a7be551fe..be39729b4 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/AllChecks.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/AllChecks.py @@ -1,10 +1,8 @@ from typing_extensions import Self -from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.all_checks import get_check_results -from geos.mesh.doctor.parsing.cli_parsing import setup_logger -from geos.mesh.io.vtkIO import VtkOutput, write_mesh +from geos.mesh.doctor.filters.BaseMeshDoctorFilter import BaseMeshDoctorFilter __doc__ = """ AllChecks module is a vtk filter that ... @@ -23,7 +21,7 @@ """ -class AllChecks( VTKPythonAlgorithmBase ): +class AllChecks( BaseMeshDoctorFilter ): def __init__( self: Self ) -> None: """Vtk filter to ... of a vtkUnstructuredGrid. @@ -32,41 +30,6 @@ def __init__( self: Self ) -> None: """ super().__init__( nInputPorts=1, nOutputPorts=1, inputType='vtkUnstructuredGrid', outputType='vtkUnstructuredGrid' ) - self.m_logger = setup_logger - - def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestInformation. - - Args: - port (int): input port - info (vtkInformationVector): info - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - if port == 0: - info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) - return 1 - - def RequestInformation( - self: Self, - request: vtkInformation, # noqa: F841 - inInfoVec: list[ vtkInformationVector ], # noqa: F841 - outInfoVec: vtkInformationVector, - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestInformation. - - Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - executive = self.GetExecutive() # noqa: F841 - outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 - return 1 def RequestData( self: Self, @@ -93,40 +56,4 @@ def RequestData( output_mesh.CopyAttributes( input_mesh ) output.ShallowCopy( output_mesh ) - return 1 - - def SetLogger( self: Self, logger ) -> None: - """Set the logger. - - Args: - logger - """ - self.m_logger = logger - self.Modified() - - def getGrid( self: Self ) -> vtkUnstructuredGrid: - """Returns the vtkUnstructuredGrid with volumes. - - Args: - self (Self) - - Returns: - vtkUnstructuredGrid - """ - self.Update() # triggers RequestData - return self.GetOutputDataObject( 0 ) - - def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: - """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath with volumes. - - Args: - filepath (str): /path/to/your/file.vtu - is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. - canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. - Defaults to False. - """ - mesh: vtkUnstructuredGrid = self.getGrid() - if mesh: - write_mesh( filepath, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) - else: - self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) + return 1 \ No newline at end of file diff --git a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py index 1a3b18350..710f04c71 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py @@ -2,12 +2,10 @@ import numpy.typing as npt from typing_extensions import Self from vtkmodules.util.numpy_support import numpy_to_vtk -from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.collocated_nodes import find_collocated_nodes_buckets, find_wrong_support_elements -from geos.mesh.doctor.parsing.cli_parsing import setup_logger -from geos.mesh.io.vtkIO import VtkOutput, write_mesh +from geos.mesh.doctor.filters.BaseMeshDoctorFilter import BaseMeshDoctorFilter __doc__ = """ CollocatedNodes module is a vtk filter that allows to find the duplicated nodes of a vtkUnstructuredGrid. @@ -26,7 +24,7 @@ """ -class CollocatedNodes( VTKPythonAlgorithmBase ): +class CollocatedNodes( BaseMeshDoctorFilter ): def __init__( self: Self ) -> None: """Vtk filter to find the duplicated nodes of a vtkUnstructuredGrid. @@ -39,41 +37,6 @@ def __init__( self: Self ) -> None: self.m_paintWrongSupportElements: int = 0 self.m_tolerance: float = 0.0 self.m_wrongSupportElements: list[ int ] = list() - self.m_logger = setup_logger - - def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestInformation. - - Args: - port (int): input port - info (vtkInformationVector): info - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - if port == 0: - info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) - return 1 - - def RequestInformation( - self: Self, - request: vtkInformation, # noqa: F841 - inInfoVec: list[ vtkInformationVector ], # noqa: F841 - outInfoVec: vtkInformationVector, - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestInformation. - - Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - executive = self.GetExecutive() # noqa: F841 - outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 - return 1 def RequestData( self: Self, @@ -100,7 +63,7 @@ def RequestData( self.m_logger.info( "The following list displays the nodes buckets that contains the duplicated node indices." ) self.m_logger.info( self.getCollocatedNodeBuckets() ) - self.m_logger.info( "The following list displays the indexes of the cells with support node indices " + + self.m_logger.info( "The following list displays the indexes of the cells with support node indices " " appearing twice or more." ) self.m_logger.info( self.getWrongSupportElements() ) @@ -119,27 +82,6 @@ def RequestData( return 1 - def SetLogger( self: Self, logger ) -> None: - """Set the logger. - - Args: - logger - """ - self.m_logger = logger - self.Modified() - - def getGrid( self: Self ) -> vtkUnstructuredGrid: - """Returns the vtkUnstructuredGrid with volumes. - - Args: - self (Self) - - Returns: - vtkUnstructuredGrid - """ - self.Update() # triggers RequestData - return self.GetOutputDataObject( 0 ) - def getCollocatedNodeBuckets( self: Self ) -> list[ tuple[ int ] ]: """Returns the nodes buckets that contains the duplicated node indices. @@ -184,18 +126,3 @@ def setTolerance( self: Self, tolerance: float ) -> None: """ self.m_tolerance = tolerance self.Modified() - - def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: - """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath with volumes. - - Args: - filepath (str): /path/to/your/file.vtu - is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. - canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. - Defaults to False. - """ - mesh: vtkUnstructuredGrid = self.getGrid() - if mesh: - write_mesh( filepath, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) - else: - self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py index cbbe0c7d4..21bbd2433 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py @@ -2,12 +2,10 @@ import numpy.typing as npt from typing_extensions import Self from vtkmodules.util.numpy_support import vtk_to_numpy -from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from vtkmodules.vtkFiltersVerdict import vtkCellSizeFilter -from geos.mesh.doctor.parsing.cli_parsing import setup_logger -from geos.mesh.io.vtkIO import VtkOutput, write_mesh +from geos.mesh.doctor.filters.BaseMeshDoctorFilter import BaseMeshDoctorFilter __doc__ = """ ElementVolumes module is a vtk filter that allows to calculate the volumes of every elements in a vtkUnstructuredGrid. @@ -26,7 +24,7 @@ """ -class ElementVolumes( VTKPythonAlgorithmBase ): +class ElementVolumes( BaseMeshDoctorFilter ): def __init__( self: Self ) -> None: """Vtk filter to calculate the volume of every element of a vtkUnstructuredGrid. @@ -37,41 +35,6 @@ def __init__( self: Self ) -> None: outputType='vtkUnstructuredGrid' ) self.m_returnNegativeZeroVolumes: bool = False self.m_volumes: npt.NDArray = None - self.m_logger = setup_logger - - def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestInformation. - - Args: - port (int): input port - info (vtkInformationVector): info - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - if port == 0: - info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) - return 1 - - def RequestInformation( - self: Self, - request: vtkInformation, # noqa: F841 - inInfoVec: list[ vtkInformationVector ], # noqa: F841 - outInfoVec: vtkInformationVector, - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestInformation. - - Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - executive = self.GetExecutive() # noqa: F841 - outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 - return 1 def RequestData( self: Self, @@ -118,27 +81,6 @@ def RequestData( return 1 - def SetLogger( self: Self, logger ) -> None: - """Set the logger. - - Args: - logger - """ - self.m_logger = logger - self.Modified() - - def getGrid( self: Self ) -> vtkUnstructuredGrid: - """Returns the vtkUnstructuredGrid with volumes. - - Args: - self (Self) - - Returns: - vtkUnstructuredGrid - """ - self.Update() # triggers RequestData - return self.GetOutputDataObject( 0 ) - def getNegativeZeroVolumes( self: Self ) -> npt.NDArray: """Returns a numpy array of all the negative and zero volumes of the input vtkUnstructuredGrid. @@ -162,18 +104,3 @@ def setReturnNegativeZeroVolumes( self: Self, returnNegativeZeroVolumes: bool ) """ self.m_returnNegativeZeroVolumes = returnNegativeZeroVolumes self.Modified() - - def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: - """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath with volumes. - - Args: - filepath (str): /path/to/your/file.vtu - is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. - canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. - Defaults to False. - """ - mesh: vtkUnstructuredGrid = self.getGrid() - if mesh: - write_mesh( filepath, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) - else: - self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py index d9b63f73c..1a75bdcf1 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py @@ -1,13 +1,12 @@ from typing_extensions import Self -from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.generate_fractures import Options, split_mesh_on_fractures +from geos.mesh.doctor.filters.BaseMeshDoctorFilter import BaseMeshDoctorFilter from geos.mesh.doctor.parsing.generate_fractures_parsing import convert, convert_to_fracture_policy from geos.mesh.doctor.parsing.generate_fractures_parsing import ( __FIELD_NAME, __FIELD_VALUES, __FRACTURES_DATA_MODE, __FRACTURES_OUTPUT_DIR, __FRACTURES_DATA_MODE_VALUES, __POLICIES, __POLICY ) -from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.io.vtkIO import VtkOutput, write_mesh from geos.mesh.utils.arrayHelpers import has_array @@ -36,7 +35,7 @@ POLICY = __POLICY -class GenerateFractures( VTKPythonAlgorithmBase ): +class GenerateFractures( BaseMeshDoctorFilter ): def __init__( self: Self ) -> None: """Vtk filter to generate a simple rectilinear grid. @@ -52,41 +51,6 @@ def __init__( self: Self ) -> None: self.m_output_modes_binary: str = { "mesh": DATA_MODE[ 0 ], "fractures": DATA_MODE[ 1 ] } self.m_mesh_VtkOutput: VtkOutput = None self.m_all_fractures_VtkOutput: list[ VtkOutput ] = None - self.m_logger = setup_logger - - def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestInformation. - - Args: - port (int): input port - info (vtkInformationVector): info - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - if port == 0: - info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) - return 1 - - def RequestInformation( - self: Self, - request: vtkInformation, # noqa: F841 - inInfoVec: list[ vtkInformationVector ], # noqa: F841 - outInfoVec: vtkInformationVector, - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestInformation. - - Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - executive = self.GetExecutive() # noqa: F841 - outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 - return 1 def RequestData( self: Self, @@ -121,15 +85,6 @@ def RequestData( return 1 - def SetLogger( self: Self, logger ) -> None: - """Set the logger. - - Args: - logger - """ - self.m_logger = logger - self.Modified() - def getAllGrids( self: Self ) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: """Returns the vtkUnstructuredGrid with volumes. diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py index 80df14491..d58968a87 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py @@ -1,13 +1,11 @@ import numpy.typing as npt from typing import Iterable, Sequence from typing_extensions import Self -from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.generate_global_ids import build_global_ids from geos.mesh.doctor.actions.generate_cube import FieldInfo, add_fields, build_coordinates, build_rectilinear_grid -from geos.mesh.doctor.parsing.cli_parsing import setup_logger -from geos.mesh.io.vtkIO import VtkOutput, write_mesh +from geos.mesh.doctor.filters.BaseMeshDoctorFilter import BaseMeshDoctorGeneratorFilter __doc__ = """ GenerateRectilinearGrid module is a vtk filter that allows to create a simple vtkUnstructuredGrid rectilinear grid. @@ -56,7 +54,7 @@ """ -class GenerateRectilinearGrid( VTKPythonAlgorithmBase ): +class GenerateRectilinearGrid( BaseMeshDoctorGeneratorFilter ): def __init__( self: Self ) -> None: """Vtk filter to generate a simple rectilinear grid. @@ -73,7 +71,6 @@ def __init__( self: Self ) -> None: self.m_numberElementsY: Sequence[ int ] = None self.m_numberElementsZ: Sequence[ int ] = None self.m_fields: Iterable[ FieldInfo ] = list() - self.m_logger = setup_logger def RequestData( self: Self, request: vtkInformation, inInfo: vtkInformationVector, outInfo: vtkInformationVector ) -> int: @@ -87,27 +84,6 @@ def RequestData( self: Self, request: vtkInformation, inInfo: vtkInformationVect opt.ShallowCopy( output ) return 1 - def SetLogger( self: Self, logger ) -> None: - """Set the logger. - - Args: - logger - """ - self.m_logger = logger - self.Modified() - - def getRectilinearGrid( self: Self ) -> vtkUnstructuredGrid: - """Returns a rectilinear grid as a vtkUnstructuredGrid. - - Args: - self (Self) - - Returns: - vtkUnstructuredGrid - """ - self.Update() # triggers RequestData - return self.GetOutputDataObject( 0 ) - def setCoordinates( self: Self, coordsX: Sequence[ float ], coordsY: Sequence[ float ], coordsZ: Sequence[ float ] ) -> None: """Set the coordinates of the block you want to have in your grid by specifying the beginning and ending @@ -168,18 +144,3 @@ def setNumberElements( self: Self, numberElementsX: Sequence[ int ], numberEleme self.m_numberElementsY = numberElementsY self.m_numberElementsZ = numberElementsZ self.Modified() - - def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: - """Writes a .vtu file of your rectilinear grid at the specified filepath. - - Args: - filepath (str): /path/to/your/file.vtu - is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. - canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. - Defaults to False. - """ - mesh: vtkUnstructuredGrid = self.getRectilinearGrid() - if mesh: - write_mesh( filepath, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) - else: - self.m_logger.error( f"No rectilinear grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py index 2f5ee1371..d3ddd412a 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py @@ -2,12 +2,10 @@ import numpy.typing as npt from typing_extensions import Self from vtkmodules.util.numpy_support import numpy_to_vtk -from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.non_conformal import Options, find_non_conformal_cells -from geos.mesh.doctor.parsing.cli_parsing import setup_logger -from geos.mesh.io.vtkIO import VtkOutput, write_mesh +from geos.mesh.doctor.filters.BaseMeshDoctorFilter import BaseMeshDoctorFilter __doc__ = """ NonConformal module is a vtk filter that ... of a vtkUnstructuredGrid. @@ -26,7 +24,7 @@ """ -class NonConformal( VTKPythonAlgorithmBase ): +class NonConformal( BaseMeshDoctorFilter ): def __init__( self: Self ) -> None: """Vtk filter to ... of a vtkUnstructuredGrid. @@ -40,41 +38,6 @@ def __init__( self: Self ) -> None: self.m_point_tolerance: float = 0.0 self.m_non_conformal_cells: list[ tuple[ int, int ] ] = list() self.m_paintNonConformalCells: int = 0 - self.m_logger = setup_logger - - def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestInformation. - - Args: - port (int): input port - info (vtkInformationVector): info - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - if port == 0: - info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) - return 1 - - def RequestInformation( - self: Self, - request: vtkInformation, # noqa: F841 - inInfoVec: list[ vtkInformationVector ], # noqa: F841 - outInfoVec: vtkInformationVector, - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestInformation. - - Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - executive = self.GetExecutive() # noqa: F841 - outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 - return 1 def RequestData( self: Self, @@ -101,7 +64,7 @@ def RequestData( non_conformal_cells_extended = [ cell_id for pair in non_conformal_cells for cell_id in pair ] unique_non_conformal_cells = frozenset( non_conformal_cells_extended ) - self.m_logger.info( f"You have {len( unique_non_conformal_cells )} non conformal cells.\n" + + self.m_logger.info( f"You have {len( unique_non_conformal_cells )} non conformal cells.\n" f"{', '.join( map( str, sorted( non_conformal_cells_extended ) ) )}" ) output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() @@ -119,27 +82,6 @@ def RequestData( return 1 - def SetLogger( self: Self, logger ) -> None: - """Set the logger. - - Args: - logger - """ - self.m_logger = logger - self.Modified() - - def getGrid( self: Self ) -> vtkUnstructuredGrid: - """Returns the vtkUnstructuredGrid with volumes. - - Args: - self (Self) - - Returns: - vtkUnstructuredGrid - """ - self.Update() # triggers RequestData - return self.GetOutputDataObject( 0 ) - def getAngleTolerance( self: Self ) -> float: """Returns the angle tolerance. @@ -215,18 +157,3 @@ def setPointTolerance( self: Self, tolerance: float ) -> None: """ self.m_point_tolerance = tolerance self.Modified() - - def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: - """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath with volumes. - - Args: - filepath (str): /path/to/your/file.vtu - is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. - canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. - Defaults to False. - """ - mesh: vtkUnstructuredGrid = self.getGrid() - if mesh: - write_mesh( filepath, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) - else: - self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) From 9516825a37244096b01e7997274c437b84738b9c Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 5 Aug 2025 13:31:20 -0700 Subject: [PATCH 12/52] Fix init for GenerateRectilinearGrid --- .../src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py index d58968a87..9b9eceab8 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py @@ -61,7 +61,7 @@ def __init__( self: Self ) -> None: Output mesh is vtkUnstructuredGrid. """ - super().__init__( nInputPorts=0, nOutputPorts=1, outputType='vtkUnstructuredGrid' ) + super().__init__() self.m_generateCellsGlobalIds: bool = False self.m_generatePointsGlobalIds: bool = False self.m_coordsX: Sequence[ float ] = None From b9d9a78587c94f033a360ecbc27b6036e1447071 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 5 Aug 2025 13:33:53 -0700 Subject: [PATCH 13/52] Add SelfIntersectingElements --- .../filters/SelfIntersectingElements.py | 197 ++++++++++++++++++ .../tests/test_self_intersecting_elements.py | 139 ++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py new file mode 100644 index 000000000..a2c814cb6 --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py @@ -0,0 +1,197 @@ +import numpy as np +import numpy.typing as npt +from typing_extensions import Self +from vtkmodules.util.numpy_support import numpy_to_vtk +from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from geos.mesh.doctor.actions.self_intersecting_elements import get_invalid_cell_ids +from geos.mesh.doctor.filters.BaseMeshDoctorFilter import BaseMeshDoctorFilter + + +__doc__ = """ +SelfIntersectingElements module is a vtk filter that allows to find invalid elements in a vtkUnstructuredGrid. + +One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. + +To use the filter: + +.. code-block:: python + + from filters.SelfIntersectingElements import SelfIntersectingElements + + # instanciate the filter + selfIntersectingElementsFilter: SelfIntersectingElements = SelfIntersectingElements() + +""" + + +class SelfIntersectingElements( BaseMeshDoctorFilter ): + + def __init__( self: Self ) -> None: + """Vtk filter to find invalid elements of a vtkUnstructuredGrid. + + Output mesh is vtkUnstructuredGrid. + """ + super().__init__( nInputPorts=1, nOutputPorts=1, inputType='vtkUnstructuredGrid', + outputType='vtkUnstructuredGrid' ) + self.m_min_distance: float = 0.0 + self.m_wrong_number_of_points_elements: list[ int ] = list() + self.m_intersecting_edges_elements: list[ int ] = list() + self.m_intersecting_faces_elements: list[ int ] = list() + self.m_non_contiguous_edges_elements: list[ int ] = list() + self.m_non_convex_elements: list[ int ] = list() + self.m_faces_oriented_incorrectly_elements: list[ int ] = list() + self.m_paintInvalidElements: int = 0 + + def RequestData( + self: Self, + request: vtkInformation, + inInfoVec: list[ vtkInformationVector ], + outInfo: vtkInformationVector + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestData. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) + output = vtkUnstructuredGrid.GetData( outInfo ) + + invalid_cells = get_invalid_cell_ids( input_mesh, self.m_min_distance ) + + self.m_wrong_number_of_points_elements = invalid_cells.get( "wrong_number_of_points_elements", [] ) + self.m_intersecting_edges_elements = invalid_cells.get( "intersecting_edges_elements", [] ) + self.m_intersecting_faces_elements = invalid_cells.get( "intersecting_faces_elements", [] ) + self.m_non_contiguous_edges_elements = invalid_cells.get( "non_contiguous_edges_elements", [] ) + self.m_non_convex_elements = invalid_cells.get( "non_convex_elements", [] ) + self.m_faces_oriented_incorrectly_elements = invalid_cells.get( "faces_oriented_incorrectly_elements", [] ) + + # Log the results + total_invalid = sum( len( invalid_list ) for invalid_list in invalid_cells.values() ) + self.m_logger.info( f"Found {total_invalid} invalid elements:" ) + for criterion, cell_list in invalid_cells.items(): + if cell_list: + self.m_logger.info( f" {criterion}: {len( cell_list )} elements - {cell_list}" ) + + output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() + output_mesh.CopyStructure( input_mesh ) + output_mesh.CopyAttributes( input_mesh ) + + if self.m_paintInvalidElements: + # Create arrays to mark invalid elements + for criterion, cell_list in invalid_cells.items(): + if cell_list: + array: npt.NDArray = np.zeros( ( output_mesh.GetNumberOfCells(), 1 ), dtype=int ) + array[ cell_list ] = 1 + vtkArray: vtkDataArray = numpy_to_vtk( array ) + vtkArray.SetName( f"Is{criterion.replace('_', '').title()}" ) + output_mesh.GetCellData().AddArray( vtkArray ) + + output.ShallowCopy( output_mesh ) + + return 1 + + def getMinDistance( self: Self ) -> float: + """Returns the minimum distance. + + Args: + self (Self) + + Returns: + float + """ + return self.m_min_distance + + def getWrongNumberOfPointsElements( self: Self ) -> list[ int ]: + """Returns elements with wrong number of points. + + Args: + self (Self) + + Returns: + list[int] + """ + return self.m_wrong_number_of_points_elements + + def getIntersectingEdgesElements( self: Self ) -> list[ int ]: + """Returns elements with intersecting edges. + + Args: + self (Self) + + Returns: + list[int] + """ + return self.m_intersecting_edges_elements + + def getIntersectingFacesElements( self: Self ) -> list[ int ]: + """Returns elements with intersecting faces. + + Args: + self (Self) + + Returns: + list[int] + """ + return self.m_intersecting_faces_elements + + def getNonContiguousEdgesElements( self: Self ) -> list[ int ]: + """Returns elements with non-contiguous edges. + + Args: + self (Self) + + Returns: + list[int] + """ + return self.m_non_contiguous_edges_elements + + def getNonConvexElements( self: Self ) -> list[ int ]: + """Returns non-convex elements. + + Args: + self (Self) + + Returns: + list[int] + """ + return self.m_non_convex_elements + + def getFacesOrientedIncorrectlyElements( self: Self ) -> list[ int ]: + """Returns elements with incorrectly oriented faces. + + Args: + self (Self) + + Returns: + list[int] + """ + return self.m_faces_oriented_incorrectly_elements + + def setPaintInvalidElements( self: Self, choice: int ) -> None: + """Set 0 or 1 to choose if you want to create arrays marking invalid elements in your output data. + + Args: + self (Self) + choice (int): 0 or 1 + """ + if choice not in [ 0, 1 ]: + self.m_logger.error( f"setPaintInvalidElements: Please choose either 0 or 1 not '{choice}'." ) + else: + self.m_paintInvalidElements = choice + self.Modified() + + def setMinDistance( self: Self, distance: float ) -> None: + """Set the minimum distance parameter. + + Args: + self (Self) + distance (float) + """ + self.m_min_distance = distance + self.Modified() diff --git a/geos-mesh/tests/test_self_intersecting_elements.py b/geos-mesh/tests/test_self_intersecting_elements.py index 45216f013..27fddb8bf 100644 --- a/geos-mesh/tests/test_self_intersecting_elements.py +++ b/geos-mesh/tests/test_self_intersecting_elements.py @@ -1,6 +1,8 @@ from vtkmodules.vtkCommonCore import vtkPoints from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkHexahedron, vtkUnstructuredGrid, VTK_HEXAHEDRON from geos.mesh.doctor.actions.self_intersecting_elements import Options, __action +from geos.mesh.doctor.filters.SelfIntersectingElements import SelfIntersectingElements +import pytest def test_jumbled_hex(): @@ -39,3 +41,140 @@ def test_jumbled_hex(): assert len( result.intersecting_faces_elements ) == 1 assert result.intersecting_faces_elements[ 0 ] == 0 + + +@pytest.fixture +def jumbled_hex_mesh(): + """Create a hexahedron with intentionally swapped nodes to create self-intersecting faces.""" + points = vtkPoints() + points.SetNumberOfPoints( 8 ) + points.SetPoint( 0, ( 0, 0, 0 ) ) + points.SetPoint( 1, ( 1, 0, 0 ) ) + points.SetPoint( 2, ( 1, 1, 0 ) ) + points.SetPoint( 3, ( 0, 1, 0 ) ) + points.SetPoint( 4, ( 0, 0, 1 ) ) + points.SetPoint( 5, ( 1, 0, 1 ) ) + points.SetPoint( 6, ( 1, 1, 1 ) ) + points.SetPoint( 7, ( 0, 1, 1 ) ) + + cell_types = [ VTK_HEXAHEDRON ] + cells = vtkCellArray() + cells.AllocateExact( 1, 8 ) + + hex = vtkHexahedron() + hex.GetPointIds().SetId( 0, 0 ) + hex.GetPointIds().SetId( 1, 1 ) + hex.GetPointIds().SetId( 2, 3 ) # Intentionally wrong + hex.GetPointIds().SetId( 3, 2 ) # Intentionally wrong + hex.GetPointIds().SetId( 4, 4 ) + hex.GetPointIds().SetId( 5, 5 ) + hex.GetPointIds().SetId( 6, 6 ) + hex.GetPointIds().SetId( 7, 7 ) + cells.InsertNextCell( hex ) + + mesh = vtkUnstructuredGrid() + mesh.SetPoints( points ) + mesh.SetCells( cell_types, cells ) + return mesh + + +@pytest.fixture +def valid_hex_mesh(): + """Create a properly ordered hexahedron with no self-intersecting faces.""" + points = vtkPoints() + points.SetNumberOfPoints( 8 ) + points.SetPoint( 0, ( 0, 0, 0 ) ) + points.SetPoint( 1, ( 1, 0, 0 ) ) + points.SetPoint( 2, ( 1, 1, 0 ) ) + points.SetPoint( 3, ( 0, 1, 0 ) ) + points.SetPoint( 4, ( 0, 0, 1 ) ) + points.SetPoint( 5, ( 1, 0, 1 ) ) + points.SetPoint( 6, ( 1, 1, 1 ) ) + points.SetPoint( 7, ( 0, 1, 1 ) ) + + cell_types = [ VTK_HEXAHEDRON ] + cells = vtkCellArray() + cells.AllocateExact( 1, 8 ) + + hex = vtkHexahedron() + for i in range( 8 ): + hex.GetPointIds().SetId( i, i ) + cells.InsertNextCell( hex ) + + mesh = vtkUnstructuredGrid() + mesh.SetPoints( points ) + mesh.SetCells( cell_types, cells ) + return mesh + + +def test_self_intersecting_elements_filter_detects_invalid_elements( jumbled_hex_mesh ): + """Test that the SelfIntersectingElements filter correctly detects invalid elements.""" + filter = SelfIntersectingElements() + filter.setMinDistance( 0.0 ) + filter.SetInputDataObject( 0, jumbled_hex_mesh ) + filter.Update() + + output = filter.getGrid() + # Check that the filter detected the invalid element + intersecting_faces = filter.getIntersectingFacesElements() + assert len( intersecting_faces ) == 1 + assert intersecting_faces[ 0 ] == 0 + + # Check that output mesh has same structure + assert output.GetNumberOfCells() == 1 + assert output.GetNumberOfPoints() == 8 + + +def test_self_intersecting_elements_filter_valid_mesh( valid_hex_mesh ): + """Test that the SelfIntersectingElements filter finds no issues in a valid mesh.""" + filter = SelfIntersectingElements() + filter.setMinDistance( 1e-12 ) # Use small tolerance instead of 0.0 + filter.SetInputDataObject( 0, valid_hex_mesh ) + filter.Update() + + output = filter.getGrid() + # Check that no invalid elements were found + assert len( filter.getIntersectingFacesElements() ) == 0 + assert len( filter.getWrongNumberOfPointsElements() ) == 0 + assert len( filter.getIntersectingEdgesElements() ) == 0 + assert len( filter.getNonContiguousEdgesElements() ) == 0 + assert len( filter.getNonConvexElements() ) == 0 + assert len( filter.getFacesOrientedIncorrectlyElements() ) == 0 + + # Check that output mesh has same structure + assert output.GetNumberOfCells() == 1 + assert output.GetNumberOfPoints() == 8 + + +def test_self_intersecting_elements_filter_paint_invalid_elements( jumbled_hex_mesh ): + """Test that the SelfIntersectingElements filter can paint invalid elements.""" + filter = SelfIntersectingElements() + filter.setMinDistance( 0.0 ) + filter.setPaintInvalidElements( 1 ) # Enable painting + filter.SetInputDataObject( 0, jumbled_hex_mesh ) + filter.Update() + + output = filter.getGrid() + # Check that painting arrays were added to the output + cell_data = output.GetCellData() + + # Should have arrays marking the invalid elements + # The exact array names depend on the implementation + assert cell_data.GetNumberOfArrays() > 0 + + # Check that invalid elements were detected + intersecting_faces = filter.getIntersectingFacesElements() + assert len( intersecting_faces ) == 1 + + +def test_self_intersecting_elements_filter_getters_setters(): + """Test getter and setter methods of the SelfIntersectingElements filter.""" + filter = SelfIntersectingElements() + + # Test min distance getter/setter + filter.setMinDistance( 0.5 ) + assert filter.getMinDistance() == 0.5 + + # Test paint invalid elements setter (no getter available) + filter.setPaintInvalidElements( 1 ) + # No exception should be raised From 4838fcee4aad34d16132c5439fa3bce5e0f642de Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 5 Aug 2025 16:15:03 -0700 Subject: [PATCH 14/52] Change __action to mesh_action function for better clarity --- .../src/geos/mesh/doctor/actions/all_checks.py | 14 +++++++++++--- .../geos/mesh/doctor/actions/check_fractures.py | 4 ++-- .../geos/mesh/doctor/actions/collocated_nodes.py | 4 ++-- .../geos/mesh/doctor/actions/element_volumes.py | 4 ++-- .../mesh/doctor/actions/fix_elements_orderings.py | 4 ++-- .../src/geos/mesh/doctor/actions/generate_cube.py | 4 ++-- .../geos/mesh/doctor/actions/generate_fractures.py | 4 ++-- .../mesh/doctor/actions/generate_global_ids.py | 4 ++-- .../src/geos/mesh/doctor/actions/non_conformal.py | 4 ++-- .../doctor/actions/self_intersecting_elements.py | 4 ++-- .../geos/mesh/doctor/actions/supported_elements.py | 4 ++-- geos-mesh/tests/test_collocated_nodes.py | 6 +++--- geos-mesh/tests/test_element_volumes.py | 6 +++--- geos-mesh/tests/test_non_conformal.py | 12 ++++++------ geos-mesh/tests/test_self_intersecting_elements.py | 4 ++-- 15 files changed, 45 insertions(+), 37 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 1219366b0..8f91e6703 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from vtkmodules.vtkCommonDataModel import vtkPointSet from geos.mesh.doctor.register import __load_module_action from geos.mesh.doctor.parsing.cli_parsing import setup_logger @@ -15,13 +16,20 @@ class Result: check_results: dict[ str, any ] -def get_check_results( vtk_input_file: str, options: Options ) -> dict[ str, any ]: +def get_check_results( vtk_input: str | vtkPointSet, options: Options ) -> dict[ str, any ]: + isFilepath: bool = isinstance( vtk_input, str ) + isVtkUnstructuredGrid: bool = isinstance( vtk_input, vtkPointSet ) + assert isFilepath | isVtkUnstructuredGrid, "Invalid input type, should either be a filepath to .vtu file" \ + " or a vtkPointSet object" check_results: dict[ str, any ] = dict() for check_name in options.checks_to_perform: - check_action = __load_module_action( check_name ) + if isVtkUnstructuredGrid: # we need to call the mesh_action function that takes a vtkPointSet as input + check_action = __load_module_action( check_name, "mesh_action" ) + else: # because its a filepath, we can call the regular action function + check_action = __load_module_action( 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_result = check_action( vtk_input, option ) check_results[ check_name ] = check_result return check_results 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 17d3f8931..5e054d2a5 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/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" {found}) for collocated nodes {cns}." ) -def __action( vtk_input_file: str, options: Options ) -> Result: +def mesh_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() @@ -150,7 +150,7 @@ def __action( vtk_input_file: str, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: try: - return __action( vtk_input_file, options ) + return mesh_action( vtk_input_file, options ) except BaseException as 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 0e5768e3f..e39685f13 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py @@ -63,7 +63,7 @@ def find_wrong_support_elements( mesh: vtkPointSet ) -> list[ int ]: return wrong_support_elements -def __action( mesh: vtkPointSet, options: Options ) -> Result: +def mesh_action( mesh: vtkPointSet, options: Options ) -> Result: collocated_nodes_buckets = find_collocated_nodes_buckets( mesh, options.tolerance ) wrong_support_elements = find_wrong_support_elements( mesh ) return Result( nodes_buckets=collocated_nodes_buckets, wrong_support_elements=wrong_support_elements ) @@ -71,4 +71,4 @@ def __action( mesh: vtkPointSet, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: mesh: vtkPointSet = read_mesh( vtk_input_file ) - return __action( mesh, options ) + return mesh_action( mesh, options ) 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 e5380c3c0..97496894e 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/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 __action( mesh, options: Options ) -> Result: +def mesh_action( mesh, options: Options ) -> Result: cs = vtkCellSizeFilter() cs.ComputeAreaOff() @@ -68,4 +68,4 @@ def __action( mesh, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: mesh = read_mesh( vtk_input_file ) - return __action( mesh, options ) + return mesh_action( mesh, options ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/fix_elements_orderings.py b/geos-mesh/src/geos/mesh/doctor/actions/fix_elements_orderings.py index 3e00cf52e..1b76f8bd6 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/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 __action( mesh, options: Options ) -> Result: +def mesh_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 @@ -50,4 +50,4 @@ def __action( mesh, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: mesh = read_mesh( vtk_input_file ) - return __action( mesh, options ) + return mesh_action( mesh, options ) 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 56df4f657..32396cbb3 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py @@ -171,7 +171,7 @@ def __build( options: Options ): return cube -def __action( options: Options ) -> Result: +def mesh_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}" ) @@ -179,7 +179,7 @@ def __action( options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: try: - return __action( options ) + return mesh_action( options ) except BaseException as 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 c99e4b459..f010b7d04 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/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 __action( mesh, options: Options ) -> Result: +def mesh_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 ): @@ -564,7 +564,7 @@ def action( vtk_input_file: str, options: Options ) -> Result: " is to split the mesh and then generate global ids for new split meshes." ) setup_logger.error( err_msg ) raise ValueError( err_msg ) - return __action( mesh, options ) + return mesh_action( mesh, options ) except BaseException as 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 72ee5820c..2be2c5bdf 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 @@ -45,7 +45,7 @@ def build_global_ids( mesh, generate_cells_global_ids: bool, generate_points_glo mesh.GetCellData().SetGlobalIds( cells_global_ids ) -def __action( mesh, options: Options ) -> Result: +def mesh_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}" ) @@ -54,7 +54,7 @@ def __action( mesh, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: try: mesh = read_mesh( vtk_input_file ) - return __action( mesh, options ) + return mesh_action( mesh, options ) except BaseException as e: setup_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 00a88f592..94390e099 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py @@ -448,7 +448,7 @@ def find_non_conformal_cells( mesh: vtkUnstructuredGrid, options: Options ) -> l return non_conformal_cells -def __action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: +def mesh_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). @@ -465,4 +465,4 @@ def __action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: mesh = read_mesh( vtk_input_file ) - return __action( mesh, options ) + return mesh_action( mesh, options ) 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 a4ab9d9e8..6af7c272a 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 @@ -88,7 +88,7 @@ def get_invalid_cell_ids( mesh: vtkPointSet, min_distance: float ) -> dict[ str, return invalid_cell_ids -def __action( mesh, options: Options ) -> Result: +def mesh_action( mesh, options: Options ) -> Result: invalid_cell_ids = get_invalid_cell_ids( mesh, options.min_distance ) return Result( wrong_number_of_points_elements=invalid_cell_ids[ "wrong_number_of_points_elements" ], intersecting_edges_elements=invalid_cell_ids[ "intersecting_edges_elements" ], @@ -100,4 +100,4 @@ def __action( mesh, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: mesh = read_mesh( vtk_input_file ) - return __action( mesh, options ) + return mesh_action( mesh, options ) 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 d667f1fcc..39331fde3 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py @@ -132,7 +132,7 @@ def find_unsupported_polyhedron_elements( mesh: vtkUnstructuredGrid, options: Op return [ i for i in result if i > -1 ] -def __action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: +def mesh_action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: unsupported_std_elements_types: set[ int ] = find_unsupported_std_elements_types( mesh ) unsupported_polyhedron_elements: list[ int ] = find_unsupported_polyhedron_elements( mesh, options ) return Result( unsupported_std_elements_types=frozenset( unsupported_std_elements_types ), @@ -141,4 +141,4 @@ def __action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: mesh: vtkUnstructuredGrid = read_mesh( vtk_input_file ) - return __action( mesh, options ) + return mesh_action( mesh, options ) diff --git a/geos-mesh/tests/test_collocated_nodes.py b/geos-mesh/tests/test_collocated_nodes.py index 1cbbafd55..c4de479b0 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.actions.collocated_nodes import Options, __action +from geos.mesh.doctor.actions.collocated_nodes import Options, mesh_action from geos.mesh.doctor.filters.CollocatedNodes import CollocatedNodes @@ -28,7 +28,7 @@ def test_simple_collocated_points( data: Tuple[ vtkPoints, int ] ): mesh = vtkUnstructuredGrid() mesh.SetPoints( points ) - result = __action( mesh, Options( tolerance=1.e-12 ) ) + result = mesh_action( mesh, Options( tolerance=1.e-12 ) ) assert len( result.wrong_support_elements ) == 0 assert len( result.nodes_buckets ) == num_nodes_bucket @@ -59,7 +59,7 @@ def test_wrong_support_elements(): mesh.SetPoints( points ) mesh.SetCells( cell_types, cells ) - result = __action( mesh, Options( tolerance=1.e-12 ) ) + result = mesh_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 e29fcc443..64b903bd2 100644 --- a/geos-mesh/tests/test_element_volumes.py +++ b/geos-mesh/tests/test_element_volumes.py @@ -4,7 +4,7 @@ from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkHexahedron, vtkTetra, vtkUnstructuredGrid, VTK_TETRA from vtkmodules.vtkCommonCore import vtkPoints, vtkIdList from vtkmodules.util.numpy_support import vtk_to_numpy -from geos.mesh.doctor.actions.element_volumes import Options, __action +from geos.mesh.doctor.actions.element_volumes import Options, mesh_action from geos.mesh.doctor.filters.ElementVolumes import ElementVolumes @@ -249,12 +249,12 @@ def test_simple_tet(): mesh.SetPoints( points ) mesh.SetCells( cell_types, cells ) - result = __action( mesh, Options( min_volume=1. ) ) + result = mesh_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 * np.finfo( float ).eps - result = __action( mesh, Options( min_volume=0. ) ) + result = mesh_action( mesh, Options( min_volume=0. ) ) assert len( result.element_volumes ) == 0 diff --git a/geos-mesh/tests/test_non_conformal.py b/geos-mesh/tests/test_non_conformal.py index 9f6da41a8..e95c46972 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.actions.generate_cube import build_rectilinear_blocks_mesh, XYZ -from geos.mesh.doctor.actions.non_conformal import Options, __action +from geos.mesh.doctor.actions.non_conformal import Options, mesh_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 = __action( mesh, options ) + results = mesh_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 = __action( mesh, options ) + results = mesh_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 = __action( mesh, options ) + results = mesh_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 = __action( mesh, options ) + results = mesh_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 = __action( mesh, options ) + results = mesh_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_self_intersecting_elements.py b/geos-mesh/tests/test_self_intersecting_elements.py index 27fddb8bf..992992223 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.actions.self_intersecting_elements import Options, __action +from geos.mesh.doctor.actions.self_intersecting_elements import Options, mesh_action from geos.mesh.doctor.filters.SelfIntersectingElements import SelfIntersectingElements import pytest @@ -37,7 +37,7 @@ def test_jumbled_hex(): mesh.SetPoints( points ) mesh.SetCells( cell_types, cells ) - result = __action( mesh, Options( min_distance=0. ) ) + result = mesh_action( mesh, Options( min_distance=0. ) ) assert len( result.intersecting_faces_elements ) == 1 assert result.intersecting_faces_elements[ 0 ] == 0 From e7a41347cd3e18e50b2b53aeb132f4e2bfaba116 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 6 Aug 2025 15:35:50 -0700 Subject: [PATCH 15/52] Change base class names + add MeshDoctorChecks base --- .../mesh/doctor/filters/CollocatedNodes.py | 4 +- .../mesh/doctor/filters/ElementVolumes.py | 4 +- .../mesh/doctor/filters/GenerateFractures.py | 4 +- .../doctor/filters/GenerateRectilinearGrid.py | 4 +- ...eMeshDoctorFilter.py => MeshDoctorBase.py} | 136 +++++++++++++++++- .../geos/mesh/doctor/filters/NonConformal.py | 4 +- .../filters/SelfIntersectingElements.py | 4 +- 7 files changed, 145 insertions(+), 15 deletions(-) rename geos-mesh/src/geos/mesh/doctor/filters/{BaseMeshDoctorFilter.py => MeshDoctorBase.py} (50%) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py index 710f04c71..6262aa56f 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py @@ -5,7 +5,7 @@ from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.collocated_nodes import find_collocated_nodes_buckets, find_wrong_support_elements -from geos.mesh.doctor.filters.BaseMeshDoctorFilter import BaseMeshDoctorFilter +from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase __doc__ = """ CollocatedNodes module is a vtk filter that allows to find the duplicated nodes of a vtkUnstructuredGrid. @@ -24,7 +24,7 @@ """ -class CollocatedNodes( BaseMeshDoctorFilter ): +class CollocatedNodes( MeshDoctorBase ): def __init__( self: Self ) -> None: """Vtk filter to find the duplicated nodes of a vtkUnstructuredGrid. diff --git a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py index 21bbd2433..30270f5be 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py @@ -5,7 +5,7 @@ from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from vtkmodules.vtkFiltersVerdict import vtkCellSizeFilter -from geos.mesh.doctor.filters.BaseMeshDoctorFilter import BaseMeshDoctorFilter +from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase __doc__ = """ ElementVolumes module is a vtk filter that allows to calculate the volumes of every elements in a vtkUnstructuredGrid. @@ -24,7 +24,7 @@ """ -class ElementVolumes( BaseMeshDoctorFilter ): +class ElementVolumes( MeshDoctorBase ): def __init__( self: Self ) -> None: """Vtk filter to calculate the volume of every element of a vtkUnstructuredGrid. diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py index 1a75bdcf1..0a497ed46 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py @@ -2,7 +2,7 @@ from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.generate_fractures import Options, split_mesh_on_fractures -from geos.mesh.doctor.filters.BaseMeshDoctorFilter import BaseMeshDoctorFilter +from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase from geos.mesh.doctor.parsing.generate_fractures_parsing import convert, convert_to_fracture_policy from geos.mesh.doctor.parsing.generate_fractures_parsing import ( __FIELD_NAME, __FIELD_VALUES, __FRACTURES_DATA_MODE, __FRACTURES_OUTPUT_DIR, @@ -35,7 +35,7 @@ POLICY = __POLICY -class GenerateFractures( BaseMeshDoctorFilter ): +class GenerateFractures( MeshDoctorBase ): def __init__( self: Self ) -> None: """Vtk filter to generate a simple rectilinear grid. diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py index 9b9eceab8..26378aa02 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py @@ -5,7 +5,7 @@ from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.generate_global_ids import build_global_ids from geos.mesh.doctor.actions.generate_cube import FieldInfo, add_fields, build_coordinates, build_rectilinear_grid -from geos.mesh.doctor.filters.BaseMeshDoctorFilter import BaseMeshDoctorGeneratorFilter +from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorGenerator __doc__ = """ GenerateRectilinearGrid module is a vtk filter that allows to create a simple vtkUnstructuredGrid rectilinear grid. @@ -54,7 +54,7 @@ """ -class GenerateRectilinearGrid( BaseMeshDoctorGeneratorFilter ): +class GenerateRectilinearGrid( MeshDoctorGenerator ): def __init__( self: Self ) -> None: """Vtk filter to generate a simple rectilinear grid. diff --git a/geos-mesh/src/geos/mesh/doctor/filters/BaseMeshDoctorFilter.py b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py similarity index 50% rename from geos-mesh/src/geos/mesh/doctor/filters/BaseMeshDoctorFilter.py rename to geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py index 6052f1b9e..cf3930471 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/BaseMeshDoctorFilter.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py @@ -2,13 +2,15 @@ from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from geos.mesh.doctor.actions.all_checks import Options, Result +from geos.mesh.doctor.parsing._shared_checks_parsing_logic import CheckFeature from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.io.vtkIO import VtkOutput, write_mesh __doc__ = """Base class for all mesh doctor VTK filters.""" -class BaseMeshDoctorFilter( VTKPythonAlgorithmBase ): +class MeshDoctorBase( VTKPythonAlgorithmBase ): """Base class for all mesh doctor VTK filters. This class provides common functionality shared across all mesh doctor filters, @@ -124,10 +126,10 @@ def copyInputToOutput( self: Self, input_mesh: vtkUnstructuredGrid ) -> vtkUnstr return output_mesh -class BaseMeshDoctorGeneratorFilter( BaseMeshDoctorFilter ): +class MeshDoctorGenerator( MeshDoctorBase ): """Base class for mesh doctor generator filters (no input required). - This class extends BaseMeshDoctorFilter for filters that generate meshes + This class extends MeshDoctorBase for filters that generate meshes from scratch without requiring input meshes. """ @@ -161,3 +163,131 @@ def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> i """ # Generator filters don't have input ports, so this method is not used return 1 + + +class MeshDoctorChecks( MeshDoctorBase ): + + def __init__( + self: Self, + checks_to_perform: list[ str ], + check_features_config: dict[ str, CheckFeature ], + ordered_check_names: list[ str ] + ) -> None: + super().__init__() + self.m_checks_to_perform: list[ str ] = checks_to_perform + self.m_check_parameters: dict[ str, dict[ str, any ] ] = dict() # Custom parameters override + self.m_check_results: dict[ str, any ] = dict() + self.m_CHECK_FEATURES_CONFIG: dict[ str, CheckFeature ] = check_features_config + self.m_ORDERED_CHECK_NAMES: list[ str ] = ordered_check_names + + def _buildOptions( self: Self ) -> Options: + """Build Options object using the same logic as the parsing system. + + Returns: + Options: Properly configured options for all checks + """ + # Start with default parameters for all configured checks + default_params: dict[ str, dict[ str, any ] ] = { + name: feature.default_params.copy() for name, feature in self.m_CHECK_FEATURES_CONFIG.items() + } + final_check_params: dict[ str, dict[ str, any ] ] = { + name: default_params[ name ] for name in self.m_checks_to_perform + } + + # Apply any custom parameter overrides + for check_name in self.m_checks_to_perform: + if check_name in self.m_check_parameters: + final_check_params[check_name].update(self.m_check_parameters[check_name]) + + # 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 self.m_checks_to_perform: + if check_name not in self.m_CHECK_FEATURES_CONFIG: + self.m_logger.warning(f"Check '{check_name}' is not available. Skipping.") + continue + + params = final_check_params[ check_name ] + feature_config = self.m_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: + self.m_logger.error( f"Failed to create options for check '{check_name}': {e}. " + f"This check will be skipped." ) + + return Options( checks_to_perform=list(individual_check_options.keys()), + checks_options=individual_check_options, + check_displays=individual_check_display ) + + def getAvailableChecks( self: Self ) -> list[str]: + """Returns the list of available check names. + + Returns: + list[str]: List of available check names + """ + return self.m_ORDERED_CHECK_NAMES + + def getCheckResults( self: Self ) -> dict[ str, any ]: + """Returns the results of all performed checks. + + Args: + self (Self) + + Returns: + dict[str, any]: Dictionary mapping check names to their results + """ + return self.m_check_results + + def getDefaultParameters( self: Self, check_name: str ) -> dict[str, any]: + """Get the default parameters for a specific check. + + Args: + check_name (str): Name of the check + + Returns: + dict[str, any]: Dictionary of default parameters + """ + if check_name in self.m_CHECK_FEATURES_CONFIG: + return self.m_CHECK_FEATURES_CONFIG[check_name].default_params + return {} + + def setChecksToPerform( self: Self, checks_to_perform: list[str] ) -> None: + """Set which checks to perform. + + Args: + self (Self) + checks_to_perform (list[str]): List of check names to perform. + """ + self.m_checks_to_perform = checks_to_perform + self.Modified() + + def setCheckParameter( self: Self, check_name: str, parameter_name: str, value: any ) -> None: + """Set a parameter for a specific check. + + Args: + self (Self) + check_name (str): Name of the check (e.g., "collocated_nodes") + parameter_name (str): Name of the parameter (e.g., "tolerance") + value (any): Value to set for the parameter + """ + if check_name not in self.m_check_parameters: + self.m_check_parameters[check_name] = {} + self.m_check_parameters[check_name][parameter_name] = value + self.Modified() + + def setAllChecksParameter( self: Self, parameter_name: str, value: any ) -> None: + """Set a parameter for all checks that support it. + + Args: + self (Self) + parameter_name (str): Name of the parameter (e.g., "tolerance") + value (any): Value to set for the parameter + """ + for check_name in self.m_checks_to_perform: + if check_name in self.m_CHECK_FEATURES_CONFIG: + default_params = self.m_CHECK_FEATURES_CONFIG[check_name].default_params + if parameter_name in default_params: + self.setCheckParameter(check_name, parameter_name, value) + self.Modified() diff --git a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py index d3ddd412a..a2ead4ffd 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py @@ -5,7 +5,7 @@ from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.non_conformal import Options, find_non_conformal_cells -from geos.mesh.doctor.filters.BaseMeshDoctorFilter import BaseMeshDoctorFilter +from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase __doc__ = """ NonConformal module is a vtk filter that ... of a vtkUnstructuredGrid. @@ -24,7 +24,7 @@ """ -class NonConformal( BaseMeshDoctorFilter ): +class NonConformal( MeshDoctorBase ): def __init__( self: Self ) -> None: """Vtk filter to ... of a vtkUnstructuredGrid. diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py index a2c814cb6..57d4f180f 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py @@ -5,7 +5,7 @@ from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.self_intersecting_elements import get_invalid_cell_ids -from geos.mesh.doctor.filters.BaseMeshDoctorFilter import BaseMeshDoctorFilter +from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase __doc__ = """ @@ -25,7 +25,7 @@ """ -class SelfIntersectingElements( BaseMeshDoctorFilter ): +class SelfIntersectingElements( MeshDoctorBase ): def __init__( self: Self ) -> None: """Vtk filter to find invalid elements of a vtkUnstructuredGrid. From 41ce1a13d7851e757fce517cd7809dd19132ba9a Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 6 Aug 2025 16:49:12 -0700 Subject: [PATCH 16/52] Remove MeshDoctorChecks class --- .../mesh/doctor/filters/MeshDoctorBase.py | 130 ------------------ 1 file changed, 130 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py index cf3930471..14277892a 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py @@ -2,8 +2,6 @@ from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid -from geos.mesh.doctor.actions.all_checks import Options, Result -from geos.mesh.doctor.parsing._shared_checks_parsing_logic import CheckFeature from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.io.vtkIO import VtkOutput, write_mesh @@ -163,131 +161,3 @@ def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> i """ # Generator filters don't have input ports, so this method is not used return 1 - - -class MeshDoctorChecks( MeshDoctorBase ): - - def __init__( - self: Self, - checks_to_perform: list[ str ], - check_features_config: dict[ str, CheckFeature ], - ordered_check_names: list[ str ] - ) -> None: - super().__init__() - self.m_checks_to_perform: list[ str ] = checks_to_perform - self.m_check_parameters: dict[ str, dict[ str, any ] ] = dict() # Custom parameters override - self.m_check_results: dict[ str, any ] = dict() - self.m_CHECK_FEATURES_CONFIG: dict[ str, CheckFeature ] = check_features_config - self.m_ORDERED_CHECK_NAMES: list[ str ] = ordered_check_names - - def _buildOptions( self: Self ) -> Options: - """Build Options object using the same logic as the parsing system. - - Returns: - Options: Properly configured options for all checks - """ - # Start with default parameters for all configured checks - default_params: dict[ str, dict[ str, any ] ] = { - name: feature.default_params.copy() for name, feature in self.m_CHECK_FEATURES_CONFIG.items() - } - final_check_params: dict[ str, dict[ str, any ] ] = { - name: default_params[ name ] for name in self.m_checks_to_perform - } - - # Apply any custom parameter overrides - for check_name in self.m_checks_to_perform: - if check_name in self.m_check_parameters: - final_check_params[check_name].update(self.m_check_parameters[check_name]) - - # 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 self.m_checks_to_perform: - if check_name not in self.m_CHECK_FEATURES_CONFIG: - self.m_logger.warning(f"Check '{check_name}' is not available. Skipping.") - continue - - params = final_check_params[ check_name ] - feature_config = self.m_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: - self.m_logger.error( f"Failed to create options for check '{check_name}': {e}. " - f"This check will be skipped." ) - - return Options( checks_to_perform=list(individual_check_options.keys()), - checks_options=individual_check_options, - check_displays=individual_check_display ) - - def getAvailableChecks( self: Self ) -> list[str]: - """Returns the list of available check names. - - Returns: - list[str]: List of available check names - """ - return self.m_ORDERED_CHECK_NAMES - - def getCheckResults( self: Self ) -> dict[ str, any ]: - """Returns the results of all performed checks. - - Args: - self (Self) - - Returns: - dict[str, any]: Dictionary mapping check names to their results - """ - return self.m_check_results - - def getDefaultParameters( self: Self, check_name: str ) -> dict[str, any]: - """Get the default parameters for a specific check. - - Args: - check_name (str): Name of the check - - Returns: - dict[str, any]: Dictionary of default parameters - """ - if check_name in self.m_CHECK_FEATURES_CONFIG: - return self.m_CHECK_FEATURES_CONFIG[check_name].default_params - return {} - - def setChecksToPerform( self: Self, checks_to_perform: list[str] ) -> None: - """Set which checks to perform. - - Args: - self (Self) - checks_to_perform (list[str]): List of check names to perform. - """ - self.m_checks_to_perform = checks_to_perform - self.Modified() - - def setCheckParameter( self: Self, check_name: str, parameter_name: str, value: any ) -> None: - """Set a parameter for a specific check. - - Args: - self (Self) - check_name (str): Name of the check (e.g., "collocated_nodes") - parameter_name (str): Name of the parameter (e.g., "tolerance") - value (any): Value to set for the parameter - """ - if check_name not in self.m_check_parameters: - self.m_check_parameters[check_name] = {} - self.m_check_parameters[check_name][parameter_name] = value - self.Modified() - - def setAllChecksParameter( self: Self, parameter_name: str, value: any ) -> None: - """Set a parameter for all checks that support it. - - Args: - self (Self) - parameter_name (str): Name of the parameter (e.g., "tolerance") - value (any): Value to set for the parameter - """ - for check_name in self.m_checks_to_perform: - if check_name in self.m_CHECK_FEATURES_CONFIG: - default_params = self.m_CHECK_FEATURES_CONFIG[check_name].default_params - if parameter_name in default_params: - self.setCheckParameter(check_name, parameter_name, value) - self.Modified() From 353abc5b475ed2b2205024442030f9c816783fe1 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 12 Aug 2025 13:41:49 -0700 Subject: [PATCH 17/52] Fix global variable assignation for MESH in supported_elements --- .../mesh/doctor/actions/supported_elements.py | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) 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 39331fde3..7731cc144 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py @@ -29,7 +29,16 @@ class Result: # for multiprocessing, vtkUnstructuredGrid cannot be pickled. Let's use a global variable instead. +# Global variable to be set in each worker process MESH: Optional[ vtkUnstructuredGrid ] = None + + +def init_worker( mesh_to_init: vtkUnstructuredGrid ) -> None: + """Initializer for each worker process to set the global mesh.""" + global MESH + MESH = mesh_to_init + + supported_cell_types: set[ int ] = { VTK_HEXAGONAL_PRISM, VTK_HEXAHEDRON, VTK_PENTAGONAL_PRISM, VTK_POLYHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_VOXEL, VTK_WEDGE @@ -38,9 +47,7 @@ class Result: class IsPolyhedronConvertible: - def __init__( self, mesh: vtkUnstructuredGrid ): - global MESH # for multiprocessing, vtkUnstructuredGrid cannot be pickled. Let's use a global variable instead. - MESH = mesh + def __init__( self ): def build_prism_graph( n: int, name: str ) -> networkx.Graph: """ @@ -123,12 +130,15 @@ def find_unsupported_polyhedron_elements( mesh: vtkUnstructuredGrid, options: Op # Dealing with polyhedron elements. num_cells: int = mesh.GetNumberOfCells() result = ones( num_cells, dtype=int ) * -1 - with multiprocessing.Pool( processes=options.num_proc ) as pool: - generator = pool.imap_unordered( IsPolyhedronConvertible( mesh ), - range( num_cells ), - chunksize=options.chunk_size ) + # Use the initializer to set up each worker process + # Pass the mesh to the initializer + with multiprocessing.Pool( processes=options.nproc, initializer=init_worker, initargs=( mesh, ) ) as pool: + # Pass a mesh-free instance of the class to the workers. + # The MESH global will already be set in each worker. + generator = pool.imap_unordered( IsPolyhedronConvertible(), 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 + return [ i for i in result if i > -1 ] From 0009e5158872f0dcb9f5989e396874bf64126b90 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 12 Aug 2025 13:43:52 -0700 Subject: [PATCH 18/52] Add AllChecks and MainChecks filters --- .../src/geos/mesh/doctor/filters/AllChecks.py | 59 --- .../src/geos/mesh/doctor/filters/Checks.py | 214 ++++++++++ geos-mesh/tests/test_Checks.py | 370 ++++++++++++++++++ 3 files changed, 584 insertions(+), 59 deletions(-) delete mode 100644 geos-mesh/src/geos/mesh/doctor/filters/AllChecks.py create mode 100644 geos-mesh/src/geos/mesh/doctor/filters/Checks.py create mode 100644 geos-mesh/tests/test_Checks.py diff --git a/geos-mesh/src/geos/mesh/doctor/filters/AllChecks.py b/geos-mesh/src/geos/mesh/doctor/filters/AllChecks.py deleted file mode 100644 index be39729b4..000000000 --- a/geos-mesh/src/geos/mesh/doctor/filters/AllChecks.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing_extensions import Self -from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector -from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid -from geos.mesh.doctor.actions.all_checks import get_check_results -from geos.mesh.doctor.filters.BaseMeshDoctorFilter import BaseMeshDoctorFilter - -__doc__ = """ -AllChecks module is a vtk filter that ... - -One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. - -To use the filter: - -.. code-block:: python - - from filters.AllChecks import AllChecks - - # instanciate the filter - allChecksFilter: AllChecks = AllChecks() - -""" - - -class AllChecks( BaseMeshDoctorFilter ): - - def __init__( self: Self ) -> None: - """Vtk filter to ... of a vtkUnstructuredGrid. - - Output mesh is vtkUnstructuredGrid. - """ - super().__init__( nInputPorts=1, nOutputPorts=1, inputType='vtkUnstructuredGrid', - outputType='vtkUnstructuredGrid' ) - - def RequestData( - self: Self, - request: vtkInformation, - inInfoVec: list[ vtkInformationVector ], - outInfo: vtkInformationVector - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestData. - - Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) - results: dict[ str, any ] = get_check_results( ... ) - output = vtkUnstructuredGrid.GetData( outInfo ) - - output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() - output_mesh.CopyStructure( input_mesh ) - output_mesh.CopyAttributes( input_mesh ) - output.ShallowCopy( output_mesh ) - - return 1 \ No newline at end of file diff --git a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py new file mode 100644 index 000000000..b4ed18539 --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py @@ -0,0 +1,214 @@ +from types import SimpleNamespace +from typing_extensions import Self +from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from geos.mesh.doctor.actions.all_checks import Options, get_check_results +from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase +from geos.mesh.doctor.parsing._shared_checks_parsing_logic import CheckFeature, display_results +from geos.mesh.doctor.parsing.all_checks_parsing import ( CHECK_FEATURES_CONFIG as cfc_all_checks, + ORDERED_CHECK_NAMES as ocn_all_checks ) +from geos.mesh.doctor.parsing.main_checks_parsing import ( CHECK_FEATURES_CONFIG as cfc_main_checks, + ORDERED_CHECK_NAMES as ocn_main_checks ) + +__doc__ = """ +AllChecks module is a vtk filter that ... + +One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. + +To use the filter: + +.. code-block:: python + + from filters.AllChecks import AllChecks + + # instanciate the filter + allChecksFilter: AllChecks = AllChecks() + +""" + + +class MeshDoctorChecks( MeshDoctorBase ): + + def __init__( + self: Self, + checks_to_perform: list[ str ], + check_features_config: dict[ str, CheckFeature ], + ordered_check_names: list[ str ] + ) -> None: + super().__init__() + self.m_checks_to_perform: list[ str ] = checks_to_perform + self.m_check_parameters: dict[ str, dict[ str, any ] ] = dict() # Custom parameters override + self.m_check_results: dict[ str, any ] = dict() + self.m_CHECK_FEATURES_CONFIG: dict[ str, CheckFeature ] = check_features_config + self.m_ORDERED_CHECK_NAMES: list[ str ] = ordered_check_names + + def RequestData( + self: Self, + request: vtkInformation, + inInfoVec: list[ vtkInformationVector ], + outInfo: vtkInformationVector + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestData. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) + output = vtkUnstructuredGrid.GetData( outInfo ) + + # Build the options using the parsing logic structure + options = self._buildOptions() + self.m_check_results = get_check_results( input_mesh, options ) + + results_wrapper = SimpleNamespace( check_results=self.m_check_results ) + display_results( options, results_wrapper ) + + output_mesh: vtkUnstructuredGrid = self.copyInputToOutput( input_mesh ) + output.ShallowCopy( output_mesh ) + + return 1 + + def _buildOptions( self: Self ) -> Options: + """Build Options object using the same logic as the parsing system. + + Returns: + Options: Properly configured options for all checks + """ + # Start with default parameters for all configured checks + default_params: dict[ str, dict[ str, any ] ] = { + name: feature.default_params.copy() for name, feature in self.m_CHECK_FEATURES_CONFIG.items() + } + final_check_params: dict[ str, dict[ str, any ] ] = { + name: default_params[ name ] for name in self.m_checks_to_perform + } + + # Apply any custom parameter overrides + for check_name in self.m_checks_to_perform: + if check_name in self.m_check_parameters: + final_check_params[ check_name ].update( self.m_check_parameters[ check_name ] ) + + # 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 self.m_checks_to_perform: + if check_name not in self.m_CHECK_FEATURES_CONFIG: + self.m_logger.warning(f"Check '{check_name}' is not available. Skipping.") + continue + + params = final_check_params[ check_name ] + feature_config = self.m_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: + self.m_logger.error( f"Failed to create options for check '{check_name}': {e}. " + f"This check will be skipped." ) + + return Options( checks_to_perform=list( individual_check_options.keys() ), + checks_options=individual_check_options, + check_displays=individual_check_display ) + + def getAvailableChecks( self: Self ) -> list[ str ]: + """Returns the list of available check names. + + Returns: + list[str]: List of available check names + """ + return self.m_ORDERED_CHECK_NAMES + + def getCheckResults( self: Self ) -> dict[ str, any ]: + """Returns the results of all performed checks. + + Args: + self (Self) + + Returns: + dict[str, any]: Dictionary mapping check names to their results + """ + return self.m_check_results + + def getDefaultParameters( self: Self, check_name: str ) -> dict[ str, any ]: + """Get the default parameters for a specific check. + + Args: + check_name (str): Name of the check + + Returns: + dict[str, any]: Dictionary of default parameters + """ + if check_name in self.m_CHECK_FEATURES_CONFIG: + return self.m_CHECK_FEATURES_CONFIG[ check_name ].default_params + return {} + + def setChecksToPerform( self: Self, checks_to_perform: list[ str ] ) -> None: + """Set which checks to perform. + + Args: + self (Self) + checks_to_perform (list[str]): List of check names to perform. + """ + self.m_checks_to_perform = checks_to_perform + self.Modified() + + def setCheckParameter( self: Self, check_name: str, parameter_name: str, value: any ) -> None: + """Set a parameter for a specific check. + + Args: + self (Self) + check_name (str): Name of the check (e.g., "collocated_nodes") + parameter_name (str): Name of the parameter (e.g., "tolerance") + value (any): Value to set for the parameter + """ + if check_name not in self.m_check_parameters: + self.m_check_parameters[ check_name ] = {} + self.m_check_parameters[ check_name ][ parameter_name ] = value + self.Modified() + + def setAllChecksParameter( self: Self, parameter_name: str, value: any ) -> None: + """Set a parameter for all checks that support it. + + Args: + self (Self) + parameter_name (str): Name of the parameter (e.g., "tolerance") + value (any): Value to set for the parameter + """ + for check_name in self.m_checks_to_perform: + if check_name in self.m_CHECK_FEATURES_CONFIG: + default_params = self.m_CHECK_FEATURES_CONFIG[ check_name ].default_params + if parameter_name in default_params: + self.setCheckParameter( check_name, parameter_name, value ) + self.Modified() + + +class AllChecks( MeshDoctorChecks ): + + def __init__( self: Self ) -> None: + """Vtk filter to ... of a vtkUnstructuredGrid. + + Output mesh is vtkUnstructuredGrid. + """ + super().__init__( + checks_to_perform=ocn_all_checks, + check_features_config=cfc_all_checks, + ordered_check_names=ocn_all_checks + ) + + +class MainChecks( MeshDoctorChecks ): + + def __init__( self: Self ) -> None: + """Vtk filter to ... of a vtkUnstructuredGrid. + + Output mesh is vtkUnstructuredGrid. + """ + super().__init__( + checks_to_perform=ocn_main_checks, + check_features_config=cfc_main_checks, + ordered_check_names=ocn_main_checks + ) diff --git a/geos-mesh/tests/test_Checks.py b/geos-mesh/tests/test_Checks.py new file mode 100644 index 000000000..540939f6d --- /dev/null +++ b/geos-mesh/tests/test_Checks.py @@ -0,0 +1,370 @@ +import pytest +from vtkmodules.vtkCommonCore import vtkPoints +from vtkmodules.vtkCommonDataModel import ( + vtkCellArray, vtkTetra, vtkUnstructuredGrid, + VTK_TETRA +) +from geos.mesh.doctor.filters.Checks import AllChecks, MainChecks + + +@pytest.fixture +def simple_mesh_with_issues() -> vtkUnstructuredGrid: + """Create a simple test mesh with known issues for testing checks. + + This mesh includes: + - Collocated nodes (points 0 and 3 are at the same location) + - A very small volume element + - Wrong support elements (duplicate node indices in cells) + + Returns: + vtkUnstructuredGrid: Test mesh with various issues + """ + mesh = vtkUnstructuredGrid() + + # Create points with some collocated nodes + points = vtkPoints() + points.InsertNextPoint(0.0, 0.0, 0.0) # Point 0 + points.InsertNextPoint(1.0, 0.0, 0.0) # Point 1 + points.InsertNextPoint(0.0, 1.0, 0.0) # Point 2 + points.InsertNextPoint(0.0, 0.0, 0.0) # Point 3 - duplicate of Point 0 + points.InsertNextPoint(0.0, 0.0, 1.0) # Point 4 + points.InsertNextPoint(2.0, 0.0, 0.0) # Point 5 + points.InsertNextPoint(2.01, 0.0, 0.0) # Point 6 - very close to Point 5 (small volume) + points.InsertNextPoint(2.0, 0.01, 0.0) # Point 7 - creates tiny element + points.InsertNextPoint(2.0, 0.0, 0.01) # Point 8 - creates tiny element + mesh.SetPoints(points) + + # Create cells + cells = vtkCellArray() + cell_types = [] + + # Normal tetrahedron + tet1 = vtkTetra() + tet1.GetPointIds().SetId(0, 0) + tet1.GetPointIds().SetId(1, 1) + tet1.GetPointIds().SetId(2, 2) + tet1.GetPointIds().SetId(3, 4) + cells.InsertNextCell(tet1) + cell_types.append(VTK_TETRA) + + # Tetrahedron with duplicate node indices (wrong support) + tet2 = vtkTetra() + tet2.GetPointIds().SetId(0, 3) # This is collocated with point 0 + tet2.GetPointIds().SetId(1, 1) + tet2.GetPointIds().SetId(2, 2) + tet2.GetPointIds().SetId(3, 0) # Duplicate reference to same location + cells.InsertNextCell(tet2) + cell_types.append(VTK_TETRA) + + # Very small volume tetrahedron + tet3 = vtkTetra() + tet3.GetPointIds().SetId(0, 5) + tet3.GetPointIds().SetId(1, 6) + tet3.GetPointIds().SetId(2, 7) + tet3.GetPointIds().SetId(3, 8) + cells.InsertNextCell(tet3) + cell_types.append(VTK_TETRA) + + mesh.SetCells(cell_types, cells) + return mesh + + +@pytest.fixture +def clean_mesh() -> vtkUnstructuredGrid: + """Create a clean test mesh without issues. + + Returns: + vtkUnstructuredGrid: Clean test mesh + """ + mesh = vtkUnstructuredGrid() + + # Create well-separated points + points = vtkPoints() + points.InsertNextPoint(0.0, 0.0, 0.0) # Point 0 + points.InsertNextPoint(1.0, 0.0, 0.0) # Point 1 + points.InsertNextPoint(0.0, 1.0, 0.0) # Point 2 + points.InsertNextPoint(0.0, 0.0, 1.0) # Point 3 + mesh.SetPoints(points) + + # Create a single clean tetrahedron + cells = vtkCellArray() + cell_types = [] + + tet = vtkTetra() + tet.GetPointIds().SetId(0, 0) + tet.GetPointIds().SetId(1, 1) + tet.GetPointIds().SetId(2, 2) + tet.GetPointIds().SetId(3, 3) + cells.InsertNextCell(tet) + cell_types.append(VTK_TETRA) + + mesh.SetCells(cell_types, cells) + return mesh + + +@pytest.fixture +def all_checks_filter() -> AllChecks: + """Create a fresh AllChecks filter for each test.""" + return AllChecks() + + +@pytest.fixture +def main_checks_filter() -> MainChecks: + """Create a fresh MainChecks filter for each test.""" + return MainChecks() + + +class TestAllChecks: + """Test class for AllChecks filter functionality.""" + + def test_filter_creation(self, all_checks_filter: AllChecks): + """Test that AllChecks filter can be created successfully.""" + assert all_checks_filter is not None + assert hasattr(all_checks_filter, 'getAvailableChecks') + assert hasattr(all_checks_filter, 'setChecksToPerform') + assert hasattr(all_checks_filter, 'setCheckParameter') + + def test_available_checks(self, all_checks_filter: AllChecks): + """Test that all expected checks are available.""" + available_checks = all_checks_filter.getAvailableChecks() + + # Check that we have the expected checks for AllChecks + expected_checks = [ + 'collocated_nodes', + 'element_volumes', + 'non_conformal', + 'self_intersecting_elements', + 'supported_elements' + ] + + for check in expected_checks: + assert check in available_checks, f"Check '{check}' should be available" + + def test_default_parameters(self, all_checks_filter: AllChecks): + """Test that default parameters are correctly retrieved.""" + available_checks = all_checks_filter.getAvailableChecks() + + for check_name in available_checks: + defaults = all_checks_filter.getDefaultParameters(check_name) + assert isinstance(defaults, dict), f"Default parameters for '{check_name}' should be a dict" + + # Test specific known defaults + collocated_defaults = all_checks_filter.getDefaultParameters('collocated_nodes') + assert 'tolerance' in collocated_defaults + + volume_defaults = all_checks_filter.getDefaultParameters('element_volumes') + assert 'min_volume' in volume_defaults + + def test_set_checks_to_perform(self, all_checks_filter: AllChecks): + """Test setting specific checks to perform.""" + # Set specific checks + checks_to_perform = ['collocated_nodes', 'element_volumes'] + all_checks_filter.setChecksToPerform(checks_to_perform) + + # Verify by checking if the filter state changed + assert hasattr(all_checks_filter, 'm_checks_to_perform') + assert all_checks_filter.m_checks_to_perform == checks_to_perform + + def test_set_check_parameter(self, all_checks_filter: AllChecks): + """Test setting parameters for specific checks.""" + # Set a tolerance parameter for collocated nodes + all_checks_filter.setCheckParameter('collocated_nodes', 'tolerance', 1e-6) + + # Set minimum volume for element volumes + all_checks_filter.setCheckParameter('element_volumes', 'min_volume', 0.001) + + # Verify parameters are stored + assert 'collocated_nodes' in all_checks_filter.m_check_parameters + assert all_checks_filter.m_check_parameters['collocated_nodes']['tolerance'] == 1e-6 + assert all_checks_filter.m_check_parameters['element_volumes']['min_volume'] == 0.001 + + def test_set_all_checks_parameter(self, all_checks_filter: AllChecks): + """Test setting a parameter that applies to all compatible checks.""" + # Set tolerance for all checks that support it + all_checks_filter.setAllChecksParameter('tolerance', 1e-8) + + # Check that tolerance was set for checks that support it + if 'collocated_nodes' in all_checks_filter.m_check_parameters: + assert all_checks_filter.m_check_parameters['collocated_nodes']['tolerance'] == 1e-8 + + def test_process_mesh_with_issues(self, all_checks_filter: AllChecks, simple_mesh_with_issues: vtkUnstructuredGrid): + """Test processing a mesh with known issues.""" + # Configure for specific checks + all_checks_filter.setChecksToPerform(['collocated_nodes', 'element_volumes']) + all_checks_filter.setCheckParameter('collocated_nodes', 'tolerance', 1e-12) + all_checks_filter.setCheckParameter('element_volumes', 'min_volume', 1e-3) + + # Process the mesh + all_checks_filter.SetInputDataObject(0, simple_mesh_with_issues) + all_checks_filter.Update() + + # Check results + results = all_checks_filter.getCheckResults() + + assert 'collocated_nodes' in results + assert 'element_volumes' in results + + # Check that collocated nodes were found + collocated_result = results['collocated_nodes'] + assert hasattr(collocated_result, 'nodes_buckets') + # We expect to find collocated nodes (points 0 and 3) + assert len(collocated_result.nodes_buckets) > 0 + + # Check that volume issues were detected + volume_result = results['element_volumes'] + assert hasattr(volume_result, 'element_volumes') + + def test_process_clean_mesh(self, all_checks_filter: AllChecks, clean_mesh: vtkUnstructuredGrid): + """Test processing a clean mesh without issues.""" + # Configure checks + all_checks_filter.setChecksToPerform(['collocated_nodes', 'element_volumes']) + all_checks_filter.setCheckParameter('collocated_nodes', 'tolerance', 1e-12) + all_checks_filter.setCheckParameter('element_volumes', 'min_volume', 1e-6) + + # Process the mesh + all_checks_filter.SetInputDataObject(0, clean_mesh) + all_checks_filter.Update() + + # Check results + results = all_checks_filter.getCheckResults() + + assert 'collocated_nodes' in results + assert 'element_volumes' in results + + # Check that no issues were found + collocated_result = results['collocated_nodes'] + assert len(collocated_result.nodes_buckets) == 0 + + volume_result = results['element_volumes'] + assert len(volume_result.element_volumes) == 0 + + def test_output_mesh_unchanged(self, all_checks_filter: AllChecks, clean_mesh: vtkUnstructuredGrid): + """Test that the output mesh is unchanged from the input (checks don't modify geometry).""" + original_num_points = clean_mesh.GetNumberOfPoints() + original_num_cells = clean_mesh.GetNumberOfCells() + + # Process the mesh + all_checks_filter.SetInputDataObject(0, clean_mesh) + all_checks_filter.Update() + + # Get output mesh + output_mesh = all_checks_filter.getGrid() + + # Verify structure is unchanged + assert output_mesh.GetNumberOfPoints() == original_num_points + assert output_mesh.GetNumberOfCells() == original_num_cells + + # Verify points are the same + for i in range(original_num_points): + original_point = clean_mesh.GetPoint(i) + output_point = output_mesh.GetPoint(i) + assert original_point == output_point + + +class TestMainChecks: + """Test class for MainChecks filter functionality.""" + + def test_filter_creation(self, main_checks_filter: MainChecks): + """Test that MainChecks filter can be created successfully.""" + assert main_checks_filter is not None + assert hasattr(main_checks_filter, 'getAvailableChecks') + assert hasattr(main_checks_filter, 'setChecksToPerform') + assert hasattr(main_checks_filter, 'setCheckParameter') + + def test_available_checks(self, main_checks_filter: MainChecks): + """Test that main checks are available (subset of all checks).""" + available_checks = main_checks_filter.getAvailableChecks() + + # MainChecks should have a subset of checks + expected_main_checks = [ + 'collocated_nodes', + 'element_volumes', + 'self_intersecting_elements' + ] + + for check in expected_main_checks: + assert check in available_checks, f"Main check '{check}' should be available" + + def test_process_mesh(self, main_checks_filter: MainChecks, simple_mesh_with_issues: vtkUnstructuredGrid): + """Test processing a mesh with MainChecks.""" + # Process the mesh with default configuration + main_checks_filter.SetInputDataObject(0, simple_mesh_with_issues) + main_checks_filter.Update() + + # Check that results are obtained + results = main_checks_filter.getCheckResults() + assert isinstance(results, dict) + assert len(results) > 0 + + # Check that main checks were performed + available_checks = main_checks_filter.getAvailableChecks() + for check_name in available_checks: + if check_name in results: + result = results[check_name] + assert result is not None + + +class TestFilterComparison: + """Test class for comparing AllChecks and MainChecks filters.""" + + def test_all_checks_vs_main_checks_availability(self, all_checks_filter: AllChecks, main_checks_filter: MainChecks): + """Test that MainChecks is a subset of AllChecks.""" + all_checks = set(all_checks_filter.getAvailableChecks()) + main_checks = set(main_checks_filter.getAvailableChecks()) + + # MainChecks should be a subset of AllChecks + assert main_checks.issubset(all_checks), "MainChecks should be a subset of AllChecks" + + # AllChecks should have more checks than MainChecks + assert len(all_checks) >= len(main_checks), "AllChecks should have at least as many checks as MainChecks" + + def test_parameter_consistency(self, all_checks_filter: AllChecks, main_checks_filter: MainChecks): + """Test that parameter handling is consistent between filters.""" + # Get common checks + all_checks = set(all_checks_filter.getAvailableChecks()) + main_checks = set(main_checks_filter.getAvailableChecks()) + common_checks = all_checks.intersection(main_checks) + + # Test that default parameters are the same for common checks + for check_name in common_checks: + all_defaults = all_checks_filter.getDefaultParameters(check_name) + main_defaults = main_checks_filter.getDefaultParameters(check_name) + assert all_defaults == main_defaults, f"Default parameters should be the same for '{check_name}'" + + +class TestErrorHandling: + """Test class for error handling and edge cases.""" + + def test_invalid_check_name(self, all_checks_filter: AllChecks): + """Test handling of invalid check names.""" + # Try to set an invalid check + invalid_checks = ['nonexistent_check'] + all_checks_filter.setChecksToPerform(invalid_checks) + + # The filter should handle this gracefully + # (The actual behavior depends on implementation - it might warn or ignore) + assert all_checks_filter.m_checks_to_perform == invalid_checks + + def test_invalid_parameter_name(self, all_checks_filter: AllChecks): + """Test handling of invalid parameter names.""" + # Try to set an invalid parameter + all_checks_filter.setCheckParameter('collocated_nodes', 'invalid_param', 123) + + # This should not crash the filter + assert 'collocated_nodes' in all_checks_filter.m_check_parameters + assert 'invalid_param' in all_checks_filter.m_check_parameters['collocated_nodes'] + + def test_empty_mesh(self, all_checks_filter: AllChecks): + """Test handling of empty mesh.""" + # Create an empty mesh + empty_mesh = vtkUnstructuredGrid() + empty_mesh.SetPoints(vtkPoints()) + + # Process the empty mesh + all_checks_filter.setChecksToPerform(['collocated_nodes']) + all_checks_filter.SetInputDataObject(0, empty_mesh) + all_checks_filter.Update() + + # Should complete without error + results = all_checks_filter.getCheckResults() + assert isinstance(results, dict) From a67d3cb76f10bcf38437a4a0ef52a472339d7e04 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 12 Aug 2025 13:45:13 -0700 Subject: [PATCH 19/52] yapf on mesh-doctor --- .../geos/mesh/doctor/actions/non_conformal.py | 8 ++-- .../actions/self_intersecting_elements.py | 14 +++--- .../src/geos/mesh/doctor/filters/Checks.py | 48 ++++++++----------- .../mesh/doctor/filters/CollocatedNodes.py | 12 ++--- .../mesh/doctor/filters/ElementVolumes.py | 12 ++--- .../mesh/doctor/filters/GenerateFractures.py | 19 ++++---- .../mesh/doctor/filters/MeshDoctorBase.py | 35 +++++--------- .../geos/mesh/doctor/filters/NonConformal.py | 12 ++--- .../filters/SelfIntersectingElements.py | 13 ++--- .../mesh/doctor/filters/SupportedElements.py | 1 - 10 files changed, 70 insertions(+), 104 deletions(-) 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 94390e099..a0c9000b7 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py @@ -38,6 +38,7 @@ class BoundaryMesh: Therefore, we reorient the polyhedron cells ourselves, so we're sure that they point outwards. And then we compute the boundary meshes for both meshes, given that the computing options are not identical. """ + def __init__( self, mesh: vtkUnstructuredGrid ): """Builds a boundary mesh. @@ -240,6 +241,7 @@ class Extruder: """Computes and stores all the extrusions of the boundary faces. The main reason for this class is to be lazy and cache the extrusions. """ + def __init__( self, boundary_mesh: BoundaryMesh, face_tolerance: float ): self.__extrusions: list[ vtkPolyData ] = [ None ] * boundary_mesh.GetNumberOfCells() self.__boundary_mesh: BoundaryMesh = boundary_mesh @@ -435,7 +437,7 @@ def find_non_conformal_cells( mesh: vtkUnstructuredGrid, options: Options ) -> l continue # Discarding pairs that are not facing each others (with a threshold). normal_i, normal_j = boundary_mesh.normals( i ), boundary_mesh.normals( j ) - if np.dot( normal_i, normal_j ) > - cos_theta: # opposite directions only (can be facing or not) + if np.dot( normal_i, normal_j ) > -cos_theta: # opposite directions only (can be facing or not) continue # At this point, back-to-back and face-to-face pairs of elements are considered. if not are_faces_conformal_using_extrusions( extrusions, i, j, boundary_mesh, options.point_tolerance ): @@ -443,8 +445,8 @@ def find_non_conformal_cells( mesh: vtkUnstructuredGrid, options: Options ) -> l # Extracting the original 3d element index (and not the index of the boundary mesh). non_conformal_cells: list[ tuple[ int, int ] ] = list() for i, j in non_conformal_cells_boundary_id: - non_conformal_cells.append( ( boundary_mesh.original_cells.GetValue( i ), - boundary_mesh.original_cells.GetValue( j ) ) ) + non_conformal_cells.append( + ( boundary_mesh.original_cells.GetValue( i ), boundary_mesh.original_cells.GetValue( j ) ) ) return non_conformal_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 6af7c272a..5cb1cefd0 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 @@ -55,11 +55,11 @@ def get_invalid_cell_ids( mesh: vtkPointSet, min_distance: float ) -> dict[ str, # Different types of cell invalidity are defined as hexadecimal values, specific to vtkCellValidator # Here NonPlanarFaces and DegenerateFaces can also be obtained. error_masks: dict[ str, int ] = { - "wrong_number_of_points_elements": 0x01, # 0000 0001 - "intersecting_edges_elements": 0x02, # 0000 0010 - "intersecting_faces_elements": 0x04, # 0000 0100 - "non_contiguous_edges_elements": 0x08, # 0000 1000 - "non_convex_elements": 0x10, # 0001 0000 + "wrong_number_of_points_elements": 0x01, # 0000 0001 + "intersecting_edges_elements": 0x02, # 0000 0010 + "intersecting_faces_elements": 0x04, # 0000 0100 + "non_contiguous_edges_elements": 0x08, # 0000 1000 + "non_convex_elements": 0x10, # 0001 0000 "faces_oriented_incorrectly_elements": 0x20, # 0010 0000 } @@ -69,8 +69,8 @@ def get_invalid_cell_ids( mesh: vtkPointSet, min_distance: float ) -> dict[ str, invalid_cell_ids: dict[ str, list[ int ] ] = { error_name: list() for error_name in error_masks } f = vtkCellValidator() - f.SetTolerance(min_distance) - f.SetInputData(mesh) + f.SetTolerance( min_distance ) + f.SetInputData( mesh ) f.Update() # executes the filter output = f.GetOutput() diff --git a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py index b4ed18539..2db01f21b 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py @@ -5,10 +5,10 @@ from geos.mesh.doctor.actions.all_checks import Options, get_check_results from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase from geos.mesh.doctor.parsing._shared_checks_parsing_logic import CheckFeature, display_results -from geos.mesh.doctor.parsing.all_checks_parsing import ( CHECK_FEATURES_CONFIG as cfc_all_checks, - ORDERED_CHECK_NAMES as ocn_all_checks ) -from geos.mesh.doctor.parsing.main_checks_parsing import ( CHECK_FEATURES_CONFIG as cfc_main_checks, - ORDERED_CHECK_NAMES as ocn_main_checks ) +from geos.mesh.doctor.parsing.all_checks_parsing import ( CHECK_FEATURES_CONFIG as cfc_all_checks, ORDERED_CHECK_NAMES + as ocn_all_checks ) +from geos.mesh.doctor.parsing.main_checks_parsing import ( CHECK_FEATURES_CONFIG as cfc_main_checks, ORDERED_CHECK_NAMES + as ocn_main_checks ) __doc__ = """ AllChecks module is a vtk filter that ... @@ -29,12 +29,8 @@ class MeshDoctorChecks( MeshDoctorBase ): - def __init__( - self: Self, - checks_to_perform: list[ str ], - check_features_config: dict[ str, CheckFeature ], - ordered_check_names: list[ str ] - ) -> None: + def __init__( self: Self, checks_to_perform: list[ str ], check_features_config: dict[ str, CheckFeature ], + ordered_check_names: list[ str ] ) -> None: super().__init__() self.m_checks_to_perform: list[ str ] = checks_to_perform self.m_check_parameters: dict[ str, dict[ str, any ] ] = dict() # Custom parameters override @@ -42,12 +38,8 @@ def __init__( self.m_CHECK_FEATURES_CONFIG: dict[ str, CheckFeature ] = check_features_config self.m_ORDERED_CHECK_NAMES: list[ str ] = ordered_check_names - def RequestData( - self: Self, - request: vtkInformation, - inInfoVec: list[ vtkInformationVector ], - outInfo: vtkInformationVector - ) -> int: + def RequestData( self: Self, request: vtkInformation, inInfoVec: list[ vtkInformationVector ], + outInfo: vtkInformationVector ) -> int: """Inherited from VTKPythonAlgorithmBase::RequestData. Args: @@ -81,10 +73,12 @@ def _buildOptions( self: Self ) -> Options: """ # Start with default parameters for all configured checks default_params: dict[ str, dict[ str, any ] ] = { - name: feature.default_params.copy() for name, feature in self.m_CHECK_FEATURES_CONFIG.items() + name: feature.default_params.copy() + for name, feature in self.m_CHECK_FEATURES_CONFIG.items() } final_check_params: dict[ str, dict[ str, any ] ] = { - name: default_params[ name ] for name in self.m_checks_to_perform + name: default_params[ name ] + for name in self.m_checks_to_perform } # Apply any custom parameter overrides @@ -98,7 +92,7 @@ def _buildOptions( self: Self ) -> Options: for check_name in self.m_checks_to_perform: if check_name not in self.m_CHECK_FEATURES_CONFIG: - self.m_logger.warning(f"Check '{check_name}' is not available. Skipping.") + self.m_logger.warning( f"Check '{check_name}' is not available. Skipping." ) continue params = final_check_params[ check_name ] @@ -193,11 +187,9 @@ def __init__( self: Self ) -> None: Output mesh is vtkUnstructuredGrid. """ - super().__init__( - checks_to_perform=ocn_all_checks, - check_features_config=cfc_all_checks, - ordered_check_names=ocn_all_checks - ) + super().__init__( checks_to_perform=ocn_all_checks, + check_features_config=cfc_all_checks, + ordered_check_names=ocn_all_checks ) class MainChecks( MeshDoctorChecks ): @@ -207,8 +199,6 @@ def __init__( self: Self ) -> None: Output mesh is vtkUnstructuredGrid. """ - super().__init__( - checks_to_perform=ocn_main_checks, - check_features_config=cfc_main_checks, - ordered_check_names=ocn_main_checks - ) + super().__init__( checks_to_perform=ocn_main_checks, + check_features_config=cfc_main_checks, + ordered_check_names=ocn_main_checks ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py index 6262aa56f..49273bfdc 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py @@ -31,19 +31,17 @@ def __init__( self: Self ) -> None: Output mesh is vtkUnstructuredGrid. """ - super().__init__( nInputPorts=1, nOutputPorts=1, inputType='vtkUnstructuredGrid', + super().__init__( nInputPorts=1, + nOutputPorts=1, + inputType='vtkUnstructuredGrid', outputType='vtkUnstructuredGrid' ) self.m_collocatedNodesBuckets: list[ tuple[ int ] ] = list() self.m_paintWrongSupportElements: int = 0 self.m_tolerance: float = 0.0 self.m_wrongSupportElements: list[ int ] = list() - def RequestData( - self: Self, - request: vtkInformation, - inInfoVec: list[ vtkInformationVector ], - outInfo: vtkInformationVector - ) -> int: + def RequestData( self: Self, request: vtkInformation, inInfoVec: list[ vtkInformationVector ], + outInfo: vtkInformationVector ) -> int: """Inherited from VTKPythonAlgorithmBase::RequestData. Args: diff --git a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py index 30270f5be..9d29e18f0 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py @@ -31,17 +31,15 @@ def __init__( self: Self ) -> None: Output mesh is vtkUnstructuredGrid. """ - super().__init__( nInputPorts=1, nOutputPorts=1, inputType='vtkUnstructuredGrid', + super().__init__( nInputPorts=1, + nOutputPorts=1, + inputType='vtkUnstructuredGrid', outputType='vtkUnstructuredGrid' ) self.m_returnNegativeZeroVolumes: bool = False self.m_volumes: npt.NDArray = None - def RequestData( - self: Self, - request: vtkInformation, - inInfoVec: list[ vtkInformationVector ], - outInfo: vtkInformationVector - ) -> int: + def RequestData( self: Self, request: vtkInformation, inInfoVec: list[ vtkInformationVector ], + outInfo: vtkInformationVector ) -> int: """Inherited from VTKPythonAlgorithmBase::RequestData. Args: diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py index 0a497ed46..0031c3466 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py @@ -4,9 +4,9 @@ from geos.mesh.doctor.actions.generate_fractures import Options, split_mesh_on_fractures from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase from geos.mesh.doctor.parsing.generate_fractures_parsing import convert, convert_to_fracture_policy -from geos.mesh.doctor.parsing.generate_fractures_parsing import ( __FIELD_NAME, __FIELD_VALUES, - __FRACTURES_DATA_MODE, __FRACTURES_OUTPUT_DIR, - __FRACTURES_DATA_MODE_VALUES, __POLICIES, __POLICY ) +from geos.mesh.doctor.parsing.generate_fractures_parsing import ( __FIELD_NAME, __FIELD_VALUES, __FRACTURES_DATA_MODE, + __FRACTURES_OUTPUT_DIR, __FRACTURES_DATA_MODE_VALUES, + __POLICIES, __POLICY ) from geos.mesh.io.vtkIO import VtkOutput, write_mesh from geos.mesh.utils.arrayHelpers import has_array @@ -25,7 +25,6 @@ """ - FIELD_NAME = __FIELD_NAME FIELD_VALUES = __FIELD_VALUES FRACTURES_DATA_MODE = __FRACTURES_DATA_MODE @@ -42,7 +41,9 @@ def __init__( self: Self ) -> None: Output mesh is vtkUnstructuredGrid. """ - super().__init__( nInputPorts=1, nOutputPorts=2, inputType='vtkUnstructuredGrid', + super().__init__( nInputPorts=1, + nOutputPorts=2, + inputType='vtkUnstructuredGrid', outputType='vtkUnstructuredGrid' ) self.m_policy: str = POLICIES[ 1 ] self.m_field_name: str = None @@ -52,12 +53,8 @@ def __init__( self: Self ) -> None: self.m_mesh_VtkOutput: VtkOutput = None self.m_all_fractures_VtkOutput: list[ VtkOutput ] = None - def RequestData( - self: Self, - request: vtkInformation, - inInfoVec: list[ vtkInformationVector ], - outInfo: list[ vtkInformationVector ] - ) -> int: + def RequestData( self: Self, request: vtkInformation, inInfoVec: list[ vtkInformationVector ], + outInfo: list[ vtkInformationVector ] ) -> int: input_mesh = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) if has_array( input_mesh, [ "GLOBAL_IDS_POINTS", "GLOBAL_IDS_CELLS" ] ): err_msg: str = ( "The mesh cannot contain global ids for neither cells nor points. The correct procedure " + diff --git a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py index 14277892a..c631767fb 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py @@ -15,13 +15,11 @@ class MeshDoctorBase( VTKPythonAlgorithmBase ): including logger management, grid access, and file writing capabilities. """ - def __init__( - self: Self, - nInputPorts: int = 1, - nOutputPorts: int = 1, - inputType: str = 'vtkUnstructuredGrid', - outputType: str = 'vtkUnstructuredGrid' - ) -> None: + def __init__( self: Self, + nInputPorts: int = 1, + nOutputPorts: int = 1, + inputType: str = 'vtkUnstructuredGrid', + outputType: str = 'vtkUnstructuredGrid' ) -> None: """Initialize the base mesh doctor filter. Args: @@ -30,12 +28,10 @@ def __init__( inputType (str): Input data type. Defaults to 'vtkUnstructuredGrid'. outputType (str): Output data type. Defaults to 'vtkUnstructuredGrid'. """ - super().__init__( - nInputPorts=nInputPorts, - nOutputPorts=nOutputPorts, - inputType=inputType if nInputPorts > 0 else None, - outputType=outputType - ) + super().__init__( nInputPorts=nInputPorts, + nOutputPorts=nOutputPorts, + inputType=inputType if nInputPorts > 0 else None, + outputType=outputType ) self.m_logger = setup_logger def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: @@ -131,23 +127,14 @@ class MeshDoctorGenerator( MeshDoctorBase ): from scratch without requiring input meshes. """ - def __init__( - self: Self, - nOutputPorts: int = 1, - outputType: str = 'vtkUnstructuredGrid' - ) -> None: + def __init__( self: Self, nOutputPorts: int = 1, outputType: str = 'vtkUnstructuredGrid' ) -> None: """Initialize the base mesh doctor generator filter. Args: nOutputPorts (int): Number of output ports. Defaults to 1. outputType (str): Output data type. Defaults to 'vtkUnstructuredGrid'. """ - super().__init__( - nInputPorts=0, - nOutputPorts=nOutputPorts, - inputType=None, - outputType=outputType - ) + super().__init__( nInputPorts=0, nOutputPorts=nOutputPorts, inputType=None, outputType=outputType ) def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: """Generator filters don't have input ports. diff --git a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py index a2ead4ffd..b749c339d 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py @@ -31,7 +31,9 @@ def __init__( self: Self ) -> None: Output mesh is vtkUnstructuredGrid. """ - super().__init__( nInputPorts=1, nOutputPorts=1, inputType='vtkUnstructuredGrid', + super().__init__( nInputPorts=1, + nOutputPorts=1, + inputType='vtkUnstructuredGrid', outputType='vtkUnstructuredGrid' ) self.m_angle_tolerance: float = 10.0 self.m_face_tolerance: float = 0.0 @@ -39,12 +41,8 @@ def __init__( self: Self ) -> None: self.m_non_conformal_cells: list[ tuple[ int, int ] ] = list() self.m_paintNonConformalCells: int = 0 - def RequestData( - self: Self, - request: vtkInformation, - inInfoVec: list[ vtkInformationVector ], - outInfo: vtkInformationVector - ) -> int: + def RequestData( self: Self, request: vtkInformation, inInfoVec: list[ vtkInformationVector ], + outInfo: vtkInformationVector ) -> int: """Inherited from VTKPythonAlgorithmBase::RequestData. Args: diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py index 57d4f180f..67a030cfb 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py @@ -7,7 +7,6 @@ from geos.mesh.doctor.actions.self_intersecting_elements import get_invalid_cell_ids from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase - __doc__ = """ SelfIntersectingElements module is a vtk filter that allows to find invalid elements in a vtkUnstructuredGrid. @@ -32,7 +31,9 @@ def __init__( self: Self ) -> None: Output mesh is vtkUnstructuredGrid. """ - super().__init__( nInputPorts=1, nOutputPorts=1, inputType='vtkUnstructuredGrid', + super().__init__( nInputPorts=1, + nOutputPorts=1, + inputType='vtkUnstructuredGrid', outputType='vtkUnstructuredGrid' ) self.m_min_distance: float = 0.0 self.m_wrong_number_of_points_elements: list[ int ] = list() @@ -43,12 +44,8 @@ def __init__( self: Self ) -> None: self.m_faces_oriented_incorrectly_elements: list[ int ] = list() self.m_paintInvalidElements: int = 0 - def RequestData( - self: Self, - request: vtkInformation, - inInfoVec: list[ vtkInformationVector ], - outInfo: vtkInformationVector - ) -> int: + def RequestData( self: Self, request: vtkInformation, inInfoVec: list[ vtkInformationVector ], + outInfo: vtkInformationVector ) -> int: """Inherited from VTKPythonAlgorithmBase::RequestData. Args: diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py index 3e7e870f5..f398f0199 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py @@ -28,7 +28,6 @@ # """ - # class SupportedElements( VTKPythonAlgorithmBase ): # def __init__( self: Self ) -> None: From 686ff6885b70481b52df8f5f2acabf13213289cb Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 12 Aug 2025 14:17:25 -0700 Subject: [PATCH 20/52] Update __doc__ for each filter --- .../src/geos/mesh/doctor/filters/Checks.py | 35 ++++++++++++++-- .../mesh/doctor/filters/CollocatedNodes.py | 24 ++++++++++- .../mesh/doctor/filters/ElementVolumes.py | 24 +++++++++-- .../mesh/doctor/filters/GenerateFractures.py | 42 ++++++++++++++++--- .../doctor/filters/GenerateRectilinearGrid.py | 6 +-- .../mesh/doctor/filters/MeshDoctorBase.py | 42 ++++++++++++++++++- .../geos/mesh/doctor/filters/NonConformal.py | 26 +++++++++++- .../filters/SelfIntersectingElements.py | 28 ++++++++++++- .../mesh/doctor/filters/SupportedElements.py | 34 +++++++++++---- 9 files changed, 230 insertions(+), 31 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py index 2db01f21b..e972aa77a 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py @@ -11,19 +11,46 @@ as ocn_main_checks ) __doc__ = """ -AllChecks module is a vtk filter that ... +Checks module is a vtk filter that performs comprehensive mesh validation checks on a vtkUnstructuredGrid. +This module contains AllChecks and MainChecks filters that run various quality checks including element validation, +node validation, topology checks, and geometric integrity verification. One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. -To use the filter: +To use the AllChecks filter: .. code-block:: python - from filters.AllChecks import AllChecks + from filters.Checks import AllChecks - # instanciate the filter + # instantiate the filter for all available checks allChecksFilter: AllChecks = AllChecks() + # set input mesh + allChecksFilter.SetInputData(mesh) + + # optionally customize check parameters + allChecksFilter.setCheckParameter("collocated_nodes", "tolerance", 1e-6) + allChecksFilter.setGlobalParameter("tolerance", 1e-6) # applies to all checks with tolerance parameter + + # execute the checks + output_mesh: vtkUnstructuredGrid = allChecksFilter.getGrid() + + # get check results + check_results = allChecksFilter.getCheckResults() + +To use the MainChecks filter (subset of most important checks): + +.. code-block:: python + + from filters.Checks import MainChecks + + # instantiate the filter for main checks only + mainChecksFilter: MainChecks = MainChecks() + + # set input mesh and run checks + mainChecksFilter.SetInputData(mesh) + output_mesh: vtkUnstructuredGrid = mainChecksFilter.getGrid() """ diff --git a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py index 49273bfdc..a9c30dab1 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py @@ -8,7 +8,9 @@ from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase __doc__ = """ -CollocatedNodes module is a vtk filter that allows to find the duplicated nodes of a vtkUnstructuredGrid. +CollocatedNodes module is a vtk filter that identifies and handles duplicated/collocated nodes in a vtkUnstructuredGrid. +The filter can detect nodes that are within a specified tolerance distance and optionally identify elements +that have support nodes appearing more than once (wrong support elements). One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. @@ -18,9 +20,27 @@ from filters.CollocatedNodes import CollocatedNodes - # instanciate the filter + # instantiate the filter collocatedNodesFilter: CollocatedNodes = CollocatedNodes() + # set the tolerance for detecting collocated nodes + collocatedNodesFilter.setTolerance(1e-6) + + # optionally enable painting of wrong support elements + collocatedNodesFilter.setPaintWrongSupportElements(1) # 1 to enable, 0 to disable + + # set input mesh + collocatedNodesFilter.SetInputData(mesh) + + # execute the filter + output_mesh: vtkUnstructuredGrid = collocatedNodesFilter.getGrid() + + # get results + collocated_buckets = collocatedNodesFilter.getCollocatedNodeBuckets() # list of tuples with collocated node indices + wrong_support_elements = collocatedNodesFilter.getWrongSupportElements() # list of problematic element indices + + # write the output mesh + collocatedNodesFilter.writeGrid("output/mesh_with_collocated_info.vtu") """ diff --git a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py index 9d29e18f0..abb6fbe0b 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py @@ -8,9 +8,11 @@ from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase __doc__ = """ -ElementVolumes module is a vtk filter that allows to calculate the volumes of every elements in a vtkUnstructuredGrid. +ElementVolumes module is a vtk filter that calculates the volumes of all elements in a vtkUnstructuredGrid. +The filter can identify elements with negative or zero volumes, which typically indicate mesh quality issues +such as inverted elements or degenerate cells. -One filter input is vtkUnstructuredGrid one filter output which is vtkUnstructuredGrid. +One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. To use the filter: @@ -18,9 +20,25 @@ from filters.ElementVolumes import ElementVolumes - # instanciate the filter + # instantiate the filter elementVolumesFilter: ElementVolumes = ElementVolumes() + # optionally enable detection of negative/zero volume elements + elementVolumesFilter.setReturnNegativeZeroVolumes(True) + + # set input mesh + elementVolumesFilter.SetInputData(mesh) + + # execute the filter + output_mesh: vtkUnstructuredGrid = elementVolumesFilter.getGrid() + + # get problematic elements (if enabled) + if elementVolumesFilter.m_returnNegativeZeroVolumes: + negative_zero_volumes = elementVolumesFilter.getNegativeZeroVolumes() + # returns numpy array with shape (n, 2) where first column is element index, second is volume + + # write the output mesh with volume information + elementVolumesFilter.writeGrid("output/mesh_with_volumes.vtu") """ diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py index 0031c3466..be0246625 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py @@ -11,18 +11,48 @@ from geos.mesh.utils.arrayHelpers import has_array __doc__ = """ -GenerateFractures module is a vtk filter that takes as input a vtkUnstructuredGrid that needs to be splited along -non embedded fractures. When saying "splited", it implies that if a fracture plane is defined between 2 cells, -the nodes of the face shared between both cells will be duplicated simple vtkUnstructuredGrid rectilinear grid. -GlobalIds for points and cells can be added. -You can create CellArray and PointArray of constant value = 1 and any dimension >= 1. +GenerateFractures module is a vtk filter that splits a vtkUnstructuredGrid along non-embedded fractures. +When a fracture plane is defined between two cells, the nodes of the shared face will be duplicated +to create a discontinuity. The filter generates both the split main mesh and separate fracture meshes. -No filter input and one output type which is vtkUnstructuredGrid. +One filter input is vtkUnstructuredGrid, two filter outputs which are vtkUnstructuredGrid. To use the filter: .. code-block:: python + from filters.GenerateFractures import GenerateFractures + + # instantiate the filter + generateFracturesFilter: GenerateFractures = GenerateFractures() + + # set the field name that defines fracture regions + generateFracturesFilter.setFieldName("fracture_field") + + # set the field values that identify fracture boundaries + generateFracturesFilter.setFieldValues("1,2") # comma-separated values + + # set fracture policy (0 for internal fractures, 1 for boundary fractures) + generateFracturesFilter.setPolicy(1) + + # set output directory for fracture meshes + generateFracturesFilter.setFracturesOutputDirectory("./fractures/") + + # optionally set data mode (0 for ASCII, 1 for binary) + generateFracturesFilter.setOutputDataMode(1) + generateFracturesFilter.setFracturesDataMode(1) + + # set input mesh + generateFracturesFilter.SetInputData(mesh) + + # execute the filter + generateFracturesFilter.Update() + + # get the split mesh and fracture meshes + split_mesh, fracture_meshes = generateFracturesFilter.getAllGrids() + + # write all meshes + generateFracturesFilter.writeMeshes("output/split_mesh.vtu", is_data_mode_binary=True) """ FIELD_NAME = __FIELD_NAME diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py index 26378aa02..56bfe2996 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py @@ -43,12 +43,12 @@ # then, to obtain the constructed mesh out of all these operations, 2 solutions are available # solution1 + mesh: vtkUnstructuredGrid = generateRectilinearGridFilter.getGrid() + + # solution2, which calls the same method as above generateRectilinearGridFilter.Update() mesh: vtkUnstructuredGrid = generateRectilinearGridFilter.GetOutputDataObject( 0 ) - # solution2, which is a method calling the 2 instructions above - mesh: vtkUnstructuredGrid = generateRectilinearGridFilter.getRectilinearGrid() - # finally, you can write the mesh at a specific destination with: generateRectilinearGridFilter.writeGrid( "output/filepath/of/your/grid.vtu" ) """ diff --git a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py index c631767fb..d85213abe 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py @@ -5,7 +5,47 @@ from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.io.vtkIO import VtkOutput, write_mesh -__doc__ = """Base class for all mesh doctor VTK filters.""" +__doc__ = """ +MeshDoctorBase module provides base classes for all mesh doctor VTK filters. + +MeshDoctorBase serves as the foundation class for filters that process existing meshes, +while MeshDoctorGenerator is for filters that generate new meshes from scratch. + +These base classes provide common functionality including: +- Logger management and setup +- Grid access and manipulation methods +- File I/O operations for writing VTK unstructured grids +- Standard VTK filter interface implementation + +All mesh doctor filters should inherit from one of these base classes to ensure +consistent behavior and interface across the mesh doctor toolkit. + +Example usage patterns: + +.. code-block:: python + + # For filters that process existing meshes + from filters.MeshDoctorBase import MeshDoctorBase + + class MyProcessingFilter(MeshDoctorBase): + def __init__(self): + super().__init__(nInputPorts=1, nOutputPorts=1) + + def RequestData(self, request, inInfoVec, outInfo): + # Process input mesh and create output + pass + + # For filters that generate meshes from scratch + from filters.MeshDoctorBase import MeshDoctorGenerator + + class MyGeneratorFilter(MeshDoctorGenerator): + def __init__(self): + super().__init__(nOutputPorts=1) + + def RequestData(self, request, inInfo, outInfo): + # Generate new mesh + pass +""" class MeshDoctorBase( VTKPythonAlgorithmBase ): diff --git a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py index b749c339d..22d927cad 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py @@ -8,7 +8,9 @@ from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase __doc__ = """ -NonConformal module is a vtk filter that ... of a vtkUnstructuredGrid. +NonConformal module is a vtk filter that detects non-conformal mesh interfaces in a vtkUnstructuredGrid. +Non-conformal interfaces occur when adjacent cells do not share nodes or faces properly, which can indicate +mesh quality issues or intentional non-matching grid interfaces that require special handling. One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. @@ -18,9 +20,29 @@ from filters.NonConformal import NonConformal - # instanciate the filter + # instantiate the filter nonConformalFilter: NonConformal = NonConformal() + # set tolerance parameters + nonConformalFilter.setPointTolerance(1e-6) # tolerance for point matching + nonConformalFilter.setFaceTolerance(1e-6) # tolerance for face matching + nonConformalFilter.setAngleTolerance(10.0) # angle tolerance in degrees + + # optionally enable painting of non-conformal cells + nonConformalFilter.setPaintNonConformalCells(1) # 1 to enable, 0 to disable + + # set input mesh + nonConformalFilter.SetInputData(mesh) + + # execute the filter + output_mesh: vtkUnstructuredGrid = nonConformalFilter.getGrid() + + # get non-conformal cell pairs + non_conformal_cells = nonConformalFilter.getNonConformalCells() + # returns list of tuples with (cell1_id, cell2_id) for non-conformal interfaces + + # write the output mesh + nonConformalFilter.writeGrid("output/mesh_with_nonconformal_info.vtu") """ diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py index 67a030cfb..5db9dc2be 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py @@ -8,7 +8,9 @@ from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase __doc__ = """ -SelfIntersectingElements module is a vtk filter that allows to find invalid elements in a vtkUnstructuredGrid. +SelfIntersectingElements module is a vtk filter that identifies various types of invalid or problematic elements +in a vtkUnstructuredGrid. It detects elements with intersecting edges, intersecting faces, non-contiguous edges, +non-convex shapes, incorrectly oriented faces, and wrong number of points. One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. @@ -18,9 +20,31 @@ from filters.SelfIntersectingElements import SelfIntersectingElements - # instanciate the filter + # instantiate the filter selfIntersectingElementsFilter: SelfIntersectingElements = SelfIntersectingElements() + # set minimum distance parameter for intersection detection + selfIntersectingElementsFilter.setMinDistance(1e-6) + + # optionally enable painting of invalid elements + selfIntersectingElementsFilter.setPaintInvalidElements(1) # 1 to enable, 0 to disable + + # set input mesh + selfIntersectingElementsFilter.SetInputData(mesh) + + # execute the filter + output_mesh: vtkUnstructuredGrid = selfIntersectingElementsFilter.getGrid() + + # get different types of problematic elements + wrong_points_elements = selfIntersectingElementsFilter.getWrongNumberOfPointsElements() + intersecting_edges_elements = selfIntersectingElementsFilter.getIntersectingEdgesElements() + intersecting_faces_elements = selfIntersectingElementsFilter.getIntersectingFacesElements() + non_contiguous_edges_elements = selfIntersectingElementsFilter.getNonContiguousEdgesElements() + non_convex_elements = selfIntersectingElementsFilter.getNonConvexElements() + wrong_oriented_faces_elements = selfIntersectingElementsFilter.getFacesOrientedIncorrectlyElements() + + # write the output mesh + selfIntersectingElementsFilter.writeGrid("output/mesh_with_invalid_elements.vtu") """ diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py index f398f0199..4a3e26ea8 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py @@ -13,19 +13,37 @@ # from geos.utils.Logger import Logger, getLogger # __doc__ = """ -# SupportedElements module is a vtk filter that allows ... a vtkUnstructuredGrid. - +# SupportedElements module is a vtk filter that identifies unsupported element types and problematic polyhedron +# elements in a vtkUnstructuredGrid. It checks for element types that are not supported by GEOS and validates +# polyhedron elements for geometric correctness. +# # One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. - +# # To use the filter: - +# # .. code-block:: python - +# # from filters.SupportedElements import SupportedElements - -# # instanciate the filter +# +# # instantiate the filter # supportedElementsFilter: SupportedElements = SupportedElements() - +# +# # optionally enable painting of unsupported element types +# supportedElementsFilter.setPaintUnsupportedElementTypes(1) # 1 to enable, 0 to disable +# +# # set input mesh +# supportedElementsFilter.SetInputData(mesh) +# +# # execute the filter +# output_mesh: vtkUnstructuredGrid = supportedElementsFilter.getGrid() +# +# # get unsupported elements +# unsupported_elements = supportedElementsFilter.getUnsupportedElements() +# +# # write the output mesh +# supportedElementsFilter.writeGrid("output/mesh_with_support_info.vtu") +# +# Note: This filter is currently disabled due to multiprocessing requirements. # """ # class SupportedElements( VTKPythonAlgorithmBase ): From 8a0776144f71fa010fb3a7e6409b78702d888fff Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 12 Aug 2025 15:55:10 -0700 Subject: [PATCH 21/52] Update .rst documentations --- docs/geos-mesh.rst | 2 + docs/geos_mesh_docs/doctor.rst | 4 +- docs/geos_mesh_docs/doctor_filters.rst | 19 ++ docs/geos_mesh_docs/filters/AllChecks.rst | 88 ++++++ .../filters/CollocatedNodes.rst | 131 ++++++++ .../geos_mesh_docs/filters/ElementVolumes.rst | 160 ++++++++++ .../filters/GenerateFractures.rst | 220 +++++++++++++ .../filters/GenerateRectilinearGrid.rst | 218 +++++++++++++ docs/geos_mesh_docs/filters/MainChecks.rst | 98 ++++++ docs/geos_mesh_docs/filters/NonConformal.rst | 250 +++++++++++++++ .../filters/SelfIntersectingElements.rst | 293 ++++++++++++++++++ .../filters/SupportedElements.rst | 228 ++++++++++++++ docs/geos_mesh_docs/filters/index.rst | 192 ++++++++++++ 13 files changed, 1901 insertions(+), 2 deletions(-) create mode 100644 docs/geos_mesh_docs/doctor_filters.rst create mode 100644 docs/geos_mesh_docs/filters/AllChecks.rst create mode 100644 docs/geos_mesh_docs/filters/CollocatedNodes.rst create mode 100644 docs/geos_mesh_docs/filters/ElementVolumes.rst create mode 100644 docs/geos_mesh_docs/filters/GenerateFractures.rst create mode 100644 docs/geos_mesh_docs/filters/GenerateRectilinearGrid.rst create mode 100644 docs/geos_mesh_docs/filters/MainChecks.rst create mode 100644 docs/geos_mesh_docs/filters/NonConformal.rst create mode 100644 docs/geos_mesh_docs/filters/SelfIntersectingElements.rst create mode 100644 docs/geos_mesh_docs/filters/SupportedElements.rst create mode 100644 docs/geos_mesh_docs/filters/index.rst diff --git a/docs/geos-mesh.rst b/docs/geos-mesh.rst index 061f596db..2221cc943 100644 --- a/docs/geos-mesh.rst +++ b/docs/geos-mesh.rst @@ -9,6 +9,8 @@ GEOS Mesh tools ./geos_mesh_docs/doctor + ./geos_mesh_docs/doctor_filters + ./geos_mesh_docs/converter ./geos_mesh_docs/io diff --git a/docs/geos_mesh_docs/doctor.rst b/docs/geos_mesh_docs/doctor.rst index 0e66d84fd..612b06680 100644 --- a/docs/geos_mesh_docs/doctor.rst +++ b/docs/geos_mesh_docs/doctor.rst @@ -310,8 +310,8 @@ It will also verify that the ``VTK_POLYHEDRON`` cells can effectively get conver .. code-block:: $ mesh-doctor supported_elements --help - usage: mesh_doctor.py supported_elements [-h] [--chunck_size 1] [--nproc 8] + usage: mesh_doctor.py supported_elements [-h] [--chunk_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 + --chunk_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/docs/geos_mesh_docs/doctor_filters.rst b/docs/geos_mesh_docs/doctor_filters.rst new file mode 100644 index 000000000..119ee508e --- /dev/null +++ b/docs/geos_mesh_docs/doctor_filters.rst @@ -0,0 +1,19 @@ +VTK Filters +=========== + +In addition to the command-line interface, mesh-doctor functionality is also available as VTK filters for programmatic use in Python. These filters provide the same mesh validation and processing capabilities but can be integrated directly into Python workflows and visualization pipelines. + +The VTK filters offer several advantages: + +* **Integration**: Easy integration with existing VTK/ParaView workflows +* **Scripting**: Programmatic access for batch processing and automation +* **Visualization**: Direct output suitable for ParaView visualization +* **Flexibility**: Configurable parameters and output options +* **Chaining**: Ability to combine multiple filters in processing pipelines + +For detailed documentation on available filters, their parameters, and usage examples, see: + +.. toctree:: + :maxdepth: 1 + + filters/index \ No newline at end of file diff --git a/docs/geos_mesh_docs/filters/AllChecks.rst b/docs/geos_mesh_docs/filters/AllChecks.rst new file mode 100644 index 000000000..d7dd6fd52 --- /dev/null +++ b/docs/geos_mesh_docs/filters/AllChecks.rst @@ -0,0 +1,88 @@ +AllChecks Filter +================ + +.. autoclass:: geos.mesh.doctor.filters.Checks.AllChecks + :members: + :undoc-members: + :show-inheritance: + +Overview +-------- + +The AllChecks filter performs comprehensive mesh validation by running all available quality checks on a vtkUnstructuredGrid. This filter is part of the mesh doctor toolkit and provides detailed analysis of mesh quality, topology, and geometric integrity. + +Features +-------- + +* Comprehensive mesh validation with all available quality checks +* Configurable check parameters for customized validation +* Detailed reporting of found issues +* Integration with mesh doctor parsing system +* Support for both individual check parameter customization and global parameter setting + +Usage Example +------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.Checks import AllChecks + + # Instantiate the filter for all available checks + allChecksFilter = AllChecks() + + # Set input mesh + allChecksFilter.SetInputData(mesh) + + # Optionally customize check parameters + allChecksFilter.setCheckParameter("collocated_nodes", "tolerance", 1e-6) + allChecksFilter.setGlobalParameter("tolerance", 1e-6) # applies to all checks with tolerance parameter + + # Execute the checks and get output + output_mesh = allChecksFilter.getGrid() + + # Get check results + check_results = allChecksFilter.getCheckResults() + + # Write the output mesh + allChecksFilter.writeGrid("output/mesh_with_check_results.vtu") + +Parameters +---------- + +The AllChecks filter supports parameter customization for individual checks: + +* **setCheckParameter(check_name, parameter_name, value)**: Set specific parameter for a named check +* **setGlobalParameter(parameter_name, value)**: Apply parameter to all checks that support it + +Common parameters include: + +* **tolerance**: Distance tolerance for geometric checks (e.g., collocated nodes, non-conformal interfaces) +* **angle_tolerance**: Angular tolerance for orientation checks +* **min_volume**: Minimum acceptable element volume + +Available Checks +---------------- + +The AllChecks filter includes all checks available in the mesh doctor system: + +* Collocated nodes detection +* Element volume validation +* Self-intersecting elements detection +* Non-conformal interface detection +* Supported element type validation +* And many more quality checks + +Output +------ + +* **Input**: vtkUnstructuredGrid +* **Output**: vtkUnstructuredGrid (copy of input with potential additional arrays marking issues) +* **Check Results**: Detailed dictionary with results from all performed checks + +See Also +-------- + +* :doc:`MainChecks ` - Subset of most important checks +* :doc:`CollocatedNodes ` - Individual collocated nodes check +* :doc:`ElementVolumes ` - Individual element volume check +* :doc:`SelfIntersectingElements ` - Individual self-intersection check diff --git a/docs/geos_mesh_docs/filters/CollocatedNodes.rst b/docs/geos_mesh_docs/filters/CollocatedNodes.rst new file mode 100644 index 000000000..96edae1a7 --- /dev/null +++ b/docs/geos_mesh_docs/filters/CollocatedNodes.rst @@ -0,0 +1,131 @@ +CollocatedNodes Filter +====================== + +.. automodule:: geos.mesh.doctor.filters.CollocatedNodes + :members: + :undoc-members: + :show-inheritance: + +Overview +-------- + +The CollocatedNodes filter identifies and handles duplicated/collocated nodes in a vtkUnstructuredGrid. Collocated nodes are nodes that are positioned very close to each other (within a specified tolerance), which can indicate mesh quality issues or modeling problems. + +Features +-------- + +* Detection of collocated/duplicated nodes within specified tolerance +* Identification of elements with wrong support (nodes appearing multiple times) +* Optional marking of problematic elements in output mesh +* Configurable tolerance for distance-based node comparison +* Detailed reporting of found collocated node groups + +Usage Example +------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.CollocatedNodes import CollocatedNodes + + # Instantiate the filter + collocatedNodesFilter = CollocatedNodes() + + # Set the tolerance for detecting collocated nodes + collocatedNodesFilter.setTolerance(1e-6) + + # Optionally enable painting of wrong support elements + collocatedNodesFilter.setPaintWrongSupportElements(1) # 1 to enable, 0 to disable + + # Set input mesh + collocatedNodesFilter.SetInputData(mesh) + + # Execute the filter and get output + output_mesh = collocatedNodesFilter.getGrid() + + # Get results + collocated_buckets = collocatedNodesFilter.getCollocatedNodeBuckets() # list of tuples with collocated node indices + wrong_support_elements = collocatedNodesFilter.getWrongSupportElements() # list of problematic element indices + + # Write the output mesh + collocatedNodesFilter.writeGrid("output/mesh_with_collocated_info.vtu") + +Parameters +---------- + +setTolerance(tolerance) + Set the distance tolerance for determining if two nodes are collocated. + + * **tolerance** (float): Distance threshold below which nodes are considered collocated + * **Default**: 0.0 + +setPaintWrongSupportElements(choice) + Enable/disable creation of array marking elements with wrong support nodes. + + * **choice** (int): 1 to enable marking, 0 to disable + * **Default**: 0 + +Results Access +-------------- + +getCollocatedNodeBuckets() + Returns groups of collocated node indices. + + * **Returns**: list[tuple[int]] - Each tuple contains indices of nodes that are collocated + +getWrongSupportElements() + Returns element indices that have support nodes appearing more than once. + + * **Returns**: list[int] - Element indices with problematic support nodes + +Understanding the Results +------------------------- + +**Collocated Node Buckets** + +Each bucket is a tuple containing node indices that are within the specified tolerance of each other: + +.. code-block:: python + + # Example result: [(0, 15, 23), (7, 42), (100, 101, 102, 103)] + # This means: + # - Nodes 0, 15, and 23 are collocated + # - Nodes 7 and 42 are collocated + # - Nodes 100, 101, 102, and 103 are collocated + +**Wrong Support Elements** + +Elements where the same node appears multiple times in the element's connectivity. This usually indicates: + +* Degenerate elements +* Mesh generation errors +* Topology problems + +Common Use Cases +---------------- + +* **Mesh Quality Assessment**: Identify potential mesh issues before simulation +* **Mesh Preprocessing**: Clean up meshes by detecting node duplicates +* **Debugging**: Understand why meshes might have connectivity problems +* **Validation**: Ensure mesh meets quality standards for specific applications + +Output +------ + +* **Input**: vtkUnstructuredGrid +* **Output**: vtkUnstructuredGrid with optional arrays marking problematic elements +* **Additional Data**: When painting is enabled, adds "WrongSupportElements" array to cell data + +Best Practices +-------------- + +* Set tolerance based on mesh scale and precision requirements +* Use smaller tolerances for high-precision meshes +* Enable painting to visualize problematic areas in ParaView +* Check both collocated buckets and wrong support elements for comprehensive analysis + +See Also +-------- + +* :doc:`AllChecks ` - Includes collocated nodes check among others +* :doc:`MainChecks ` - Includes collocated nodes check in main set +* :doc:`SelfIntersectingElements ` - Related geometric validation diff --git a/docs/geos_mesh_docs/filters/ElementVolumes.rst b/docs/geos_mesh_docs/filters/ElementVolumes.rst new file mode 100644 index 000000000..2a9910a65 --- /dev/null +++ b/docs/geos_mesh_docs/filters/ElementVolumes.rst @@ -0,0 +1,160 @@ +ElementVolumes Filter +===================== + +.. automodule:: geos.mesh.doctor.filters.ElementVolumes + :members: + :undoc-members: + :show-inheritance: + +Overview +-------- + +The ElementVolumes filter calculates the volumes of all elements in a vtkUnstructuredGrid and can identify elements with negative or zero volumes. Such elements typically indicate serious mesh quality issues including inverted elements, degenerate cells, or incorrect node ordering. + +Features +-------- + +* Volume calculation for all element types +* Detection of negative and zero volume elements +* Detailed reporting of problematic elements with their volumes +* Integration with VTK's cell size computation +* Optional filtering to return only problematic elements + +Usage Example +------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.ElementVolumes import ElementVolumes + + # Instantiate the filter + elementVolumesFilter = ElementVolumes() + + # Optionally enable detection of negative/zero volume elements + elementVolumesFilter.setReturnNegativeZeroVolumes(True) + + # Set input mesh + elementVolumesFilter.SetInputData(mesh) + + # Execute the filter and get output + output_mesh = elementVolumesFilter.getGrid() + + # Get problematic elements (if enabled) + if elementVolumesFilter.m_returnNegativeZeroVolumes: + negative_zero_volumes = elementVolumesFilter.getNegativeZeroVolumes() + # Returns numpy array with shape (n, 2) where first column is element index, second is volume + + # Write the output mesh with volume information + elementVolumesFilter.writeGrid("output/mesh_with_volumes.vtu") + +Parameters +---------- + +setReturnNegativeZeroVolumes(returnNegativeZeroVolumes) + Enable/disable specific detection and return of elements with negative or zero volumes. + + * **returnNegativeZeroVolumes** (bool): True to enable detection, False to disable + * **Default**: False + +Results Access +-------------- + +getNegativeZeroVolumes() + Returns detailed information about elements with negative or zero volumes. + + * **Returns**: numpy.ndarray with shape (n, 2) + + * Column 0: Element indices with problematic volumes + * Column 1: Corresponding volume values (≤ 0) + + * **Note**: Only available when returnNegativeZeroVolumes is enabled + +Understanding Volume Issues +--------------------------- + +**Negative Volumes** + +Indicate elements with inverted geometry: + +* **Tetrahedra**: Nodes ordered incorrectly (clockwise instead of counter-clockwise) +* **Hexahedra**: Faces oriented inward instead of outward +* **Other elements**: Similar orientation/ordering issues + +**Zero Volumes** + +Indicate degenerate elements: + +* **Collapsed elements**: All nodes lie in the same plane (3D) or line (2D) +* **Duplicate nodes**: Multiple nodes at the same location within the element +* **Extreme aspect ratios**: Elements stretched to near-zero thickness + +**Impact on Simulations** + +Elements with non-positive volumes can cause: + +* Numerical instabilities +* Convergence problems +* Incorrect physical results +* Solver failures + +Common Fixes +------------ + +**For Negative Volumes:** + +1. **Reorder nodes**: Correct the connectivity order +2. **Flip elements**: Use mesh repair tools +3. **Regenerate mesh**: If issues are widespread + +**For Zero Volumes:** + +1. **Remove degenerate elements**: Delete problematic cells +2. **Merge collocated nodes**: Fix duplicate node issues +3. **Improve mesh quality**: Regenerate with better settings + +Output +------ + +* **Input**: vtkUnstructuredGrid +* **Output**: vtkUnstructuredGrid with volume information added as cell data +* **Volume Array**: "Volume" array added to cell data containing computed volumes +* **Additional Data**: When returnNegativeZeroVolumes is enabled, provides detailed problematic element information + +Integration with Other Filters +------------------------------ + +The ElementVolumes filter works well in combination with: + +* **CollocatedNodes**: Fix node duplication that can cause zero volumes +* **SelfIntersectingElements**: Detect other geometric problems +* **AllChecks/MainChecks**: Comprehensive mesh validation including volume checks + +Example Workflow +---------------- + +.. code-block:: python + + # Complete volume analysis workflow + volumeFilter = ElementVolumes() + volumeFilter.setReturnNegativeZeroVolumes(True) + volumeFilter.SetInputData(mesh) + + output_mesh = volumeFilter.getGrid() + + # Analyze results + problematic = volumeFilter.getNegativeZeroVolumes() + + if len(problematic) > 0: + print(f"Found {len(problematic)} elements with non-positive volumes:") + for idx, volume in problematic: + print(f" Element {idx}: volume = {volume}") + else: + print("All elements have positive volumes - mesh is good!") + +See Also +-------- + +* :doc:`AllChecks ` - Includes element volume check among others +* :doc:`MainChecks ` - Includes element volume check in main set +* :doc:`CollocatedNodes ` - Can help fix zero volume issues +* :doc:`SelfIntersectingElements ` - Related geometric validation diff --git a/docs/geos_mesh_docs/filters/GenerateFractures.rst b/docs/geos_mesh_docs/filters/GenerateFractures.rst new file mode 100644 index 000000000..78c6feab7 --- /dev/null +++ b/docs/geos_mesh_docs/filters/GenerateFractures.rst @@ -0,0 +1,220 @@ +GenerateFractures Filter +======================== + +.. automodule:: geos.mesh.doctor.filters.GenerateFractures + :members: + :undoc-members: + :show-inheritance: + +Overview +-------- + +The GenerateFractures filter splits a vtkUnstructuredGrid along non-embedded fractures. When a fracture plane is defined between two cells, the nodes of the shared face are duplicated to create a discontinuity. The filter generates both the split main mesh and separate fracture meshes. + +Features +-------- + +* Mesh splitting along fracture planes with node duplication +* Multiple fracture policy support (internal vs boundary fractures) +* Configurable fracture identification via field values +* Generation of separate fracture mesh outputs +* Flexible output data modes (ASCII/binary) +* Automatic fracture mesh file management + +Usage Example +------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.GenerateFractures import GenerateFractures + + # Instantiate the filter + generateFracturesFilter = GenerateFractures() + + # Set the field name that defines fracture regions + generateFracturesFilter.setFieldName("fracture_field") + + # Set the field values that identify fracture boundaries + generateFracturesFilter.setFieldValues("1,2") # comma-separated values + + # Set fracture policy (0 for internal fractures, 1 for boundary fractures) + generateFracturesFilter.setPolicy(1) + + # Set output directory for fracture meshes + generateFracturesFilter.setFracturesOutputDirectory("./fractures/") + + # Optionally set data mode (0 for ASCII, 1 for binary) + generateFracturesFilter.setOutputDataMode(1) + generateFracturesFilter.setFracturesDataMode(1) + + # Set input mesh + generateFracturesFilter.SetInputData(mesh) + + # Execute the filter + generateFracturesFilter.Update() + + # Get the split mesh and fracture meshes + split_mesh, fracture_meshes = generateFracturesFilter.getAllGrids() + + # Write all meshes + generateFracturesFilter.writeMeshes("output/split_mesh.vtu", is_data_mode_binary=True) + +Parameters +---------- + +setFieldName(field_name) + Set the name of the cell data field that defines fracture regions. + + * **field_name** (str): Name of the field in cell data + +setFieldValues(field_values) + Set the field values that identify fracture boundaries. + + * **field_values** (str): Comma-separated list of values (e.g., "1,2,3") + +setPolicy(choice) + Set the fracture generation policy. + + * **choice** (int): + + * 0: Internal fractures policy + * 1: Boundary fractures policy + +setFracturesOutputDirectory(directory) + Set the output directory for individual fracture mesh files. + + * **directory** (str): Path to output directory + +setOutputDataMode(choice) + Set the data format for the main output mesh. + + * **choice** (int): + + * 0: ASCII format + * 1: Binary format + +setFracturesDataMode(choice) + Set the data format for fracture mesh files. + + * **choice** (int): + + * 0: ASCII format + * 1: Binary format + +Fracture Policies +----------------- + +**Internal Fractures Policy (0)** + +* Creates fractures within the mesh interior +* Duplicates nodes at internal interfaces +* Suitable for modeling embedded fracture networks + +**Boundary Fractures Policy (1)** + +* Creates fractures at mesh boundaries +* Handles fractures that extend to domain edges +* Suitable for modeling fault systems extending beyond the domain + +Results Access +-------------- + +getAllGrids() + Returns both the split mesh and individual fracture meshes. + + * **Returns**: tuple (split_mesh, fracture_meshes) + + * **split_mesh**: vtkUnstructuredGrid - Main mesh with fractures applied + * **fracture_meshes**: list[vtkUnstructuredGrid] - Individual fracture surfaces + +writeMeshes(filepath, is_data_mode_binary, canOverwrite) + Write all generated meshes to files. + + * **filepath** (str): Path for main split mesh + * **is_data_mode_binary** (bool): Use binary format + * **canOverwrite** (bool): Allow overwriting existing files + +Understanding Fracture Generation +--------------------------------- + +**Input Requirements** + +1. **Fracture Field**: Cell data array identifying regions separated by fractures +2. **Field Values**: Specific values indicating fracture boundaries +3. **Policy**: How to handle fracture creation + +**Process** + +1. **Identification**: Find interfaces between cells with different field values +2. **Node Duplication**: Create separate nodes for each side of the fracture +3. **Mesh Splitting**: Update connectivity to use duplicated nodes +4. **Fracture Extraction**: Generate separate meshes for fracture surfaces + +**Output Structure** + +* **Split Mesh**: Original mesh with fractures as discontinuities +* **Fracture Meshes**: Individual surface meshes for each fracture + +Common Use Cases +---------------- + +* **Geomechanics**: Modeling fault systems in geological domains +* **Fluid Flow**: Creating discrete fracture networks +* **Contact Mechanics**: Preparing meshes for contact simulations +* **Multi-physics**: Coupling different physics across fracture interfaces + +Example Workflow +---------------- + +.. code-block:: python + + # Complete fracture generation workflow + fracture_filter = GenerateFractures() + + # Configure fracture detection + fracture_filter.setFieldName("material_id") + fracture_filter.setFieldValues("1,2") # Fracture between materials 1 and 2 + fracture_filter.setPolicy(1) # Boundary fractures + + # Configure output + fracture_filter.setFracturesOutputDirectory("./fractures/") + fracture_filter.setOutputDataMode(1) # Binary for efficiency + fracture_filter.setFracturesDataMode(1) + + # Process mesh + fracture_filter.SetInputData(original_mesh) + fracture_filter.Update() + + # Get results + split_mesh, fracture_surfaces = fracture_filter.getAllGrids() + + print(f"Generated {len(fracture_surfaces)} fracture surfaces") + + # Write all outputs + fracture_filter.writeMeshes("output/domain_with_fractures.vtu") + +Output +------ + +* **Input**: vtkUnstructuredGrid with fracture identification field +* **Outputs**: + + * Split mesh with fractures as discontinuities + * Individual fracture surface meshes +* **File Output**: Automatic writing of fracture meshes to specified directory + +Best Practices +-------------- + +* Ensure fracture field values are properly assigned to cells +* Use appropriate policy based on fracture geometry +* Check that fractures form continuous surfaces +* Verify mesh quality after fracture generation +* Use binary format for large meshes to improve I/O performance + +See Also +-------- + +* :doc:`GenerateRectilinearGrid ` - Basic mesh generation +* :doc:`CollocatedNodes ` - May be needed after fracture generation +* :doc:`ElementVolumes ` - Quality check after splitting diff --git a/docs/geos_mesh_docs/filters/GenerateRectilinearGrid.rst b/docs/geos_mesh_docs/filters/GenerateRectilinearGrid.rst new file mode 100644 index 000000000..a29540ee2 --- /dev/null +++ b/docs/geos_mesh_docs/filters/GenerateRectilinearGrid.rst @@ -0,0 +1,218 @@ +GenerateRectilinearGrid Filter +============================== + +.. automodule:: geos.mesh.doctor.filters.GenerateRectilinearGrid + :members: + :undoc-members: + :show-inheritance: + +Overview +-------- + +The GenerateRectilinearGrid filter creates simple rectilinear (structured) grids as vtkUnstructuredGrid objects. This filter is useful for generating regular meshes for testing, validation, or as starting points for more complex mesh generation workflows. + +Features +-------- + +* Generation of 3D rectilinear grids with customizable dimensions +* Flexible block-based coordinate specification +* Variable element density per block +* Optional global ID generation for points and cells +* Customizable field generation with arbitrary dimensions +* Direct mesh generation without input requirements + +Usage Example +------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.GenerateRectilinearGrid import GenerateRectilinearGrid + from geos.mesh.doctor.actions.generate_cube import FieldInfo + + # Instantiate the filter + generateRectilinearGridFilter = GenerateRectilinearGrid() + + # Set the coordinates of each block border for the X, Y and Z axis + generateRectilinearGridFilter.setCoordinates([0.0, 5.0, 10.0], [0.0, 5.0, 10.0], [0.0, 10.0]) + + # For each block defined, specify the number of cells that they should contain in the X, Y, Z axis + generateRectilinearGridFilter.setNumberElements([5, 5], [5, 5], [10]) + + # To add the GlobalIds for cells and points, set to True the generate global ids options + generateRectilinearGridFilter.setGenerateCellsGlobalIds(True) + generateRectilinearGridFilter.setGeneratePointsGlobalIds(True) + + # To create new arrays with a specific dimension, you can use the following commands + cells_dim1 = FieldInfo("cell1", 1, "CELLS") # array "cell1" of shape (number of cells, 1) + cells_dim3 = FieldInfo("cell3", 3, "CELLS") # array "cell3" of shape (number of cells, 3) + points_dim1 = FieldInfo("point1", 1, "POINTS") # array "point1" of shape (number of points, 1) + points_dim3 = FieldInfo("point3", 3, "POINTS") # array "point3" of shape (number of points, 3) + generateRectilinearGridFilter.setFields([cells_dim1, cells_dim3, points_dim1, points_dim3]) + + # Then, to obtain the constructed mesh out of all these operations, 2 solutions are available + + # Solution1 (recommended) + mesh = generateRectilinearGridFilter.getGrid() + + # Solution2 (manual) + generateRectilinearGridFilter.Update() + mesh = generateRectilinearGridFilter.GetOutputDataObject(0) + + # Finally, you can write the mesh at a specific destination with: + generateRectilinearGridFilter.writeGrid("output/filepath/of/your/grid.vtu") + +Parameters +---------- + +setCoordinates(coordsX, coordsY, coordsZ) + Set the coordinates that define block boundaries along each axis. + + * **coordsX** (Sequence[float]): X-coordinates of block boundaries + * **coordsY** (Sequence[float]): Y-coordinates of block boundaries + * **coordsZ** (Sequence[float]): Z-coordinates of block boundaries + +setNumberElements(numberElementsX, numberElementsY, numberElementsZ) + Set the number of elements in each block along each axis. + + * **numberElementsX** (Sequence[int]): Number of elements in each X-block + * **numberElementsY** (Sequence[int]): Number of elements in each Y-block + * **numberElementsZ** (Sequence[int]): Number of elements in each Z-block + +setGenerateCellsGlobalIds(generate) + Enable/disable generation of global cell IDs. + + * **generate** (bool): True to generate global cell IDs + +setGeneratePointsGlobalIds(generate) + Enable/disable generation of global point IDs. + + * **generate** (bool): True to generate global point IDs + +setFields(fields) + Specify additional cell or point arrays to be added to the grid. + + * **fields** (Iterable[FieldInfo]): List of field specifications + +Field Specification +------------------- + +Fields are specified using FieldInfo objects: + +.. code-block:: python + + from geos.mesh.doctor.actions.generate_cube import FieldInfo + + # Create a field specification + field = FieldInfo(name, dimension, location) + +**Parameters:** + +* **name** (str): Name of the array +* **dimension** (int): Number of components (1 for scalars, 3 for vectors, etc.) +* **location** (str): "CELLS" for cell data, "POINTS" for point data + +**Examples:** + +.. code-block:: python + + # Scalar cell field + pressure = FieldInfo("pressure", 1, "CELLS") + + # Vector point field + velocity = FieldInfo("velocity", 3, "POINTS") + + # Tensor cell field + stress = FieldInfo("stress", 6, "CELLS") # 6 components for symmetric tensor + +Grid Construction Logic +----------------------- + +**Coordinate Specification** + +Coordinates define block boundaries. For example: + +.. code-block:: python + + coordsX = [0.0, 5.0, 10.0] # Creates 2 blocks in X: [0,5] and [5,10] + numberElementsX = [5, 10] # 5 elements in first block, 10 in second + +**Element Distribution** + +Each block can have different element densities: + +* Block 1: 5 elements distributed uniformly in [0.0, 5.0] +* Block 2: 10 elements distributed uniformly in [5.0, 10.0] + +**Total Grid Size** + +* Total elements: numberElementsX[0] × numberElementsY[0] × numberElementsZ[0] + ... (for each block combination) +* Total points: (sum(numberElementsX) + len(numberElementsX)) × (sum(numberElementsY) + len(numberElementsY)) × (sum(numberElementsZ) + len(numberElementsZ)) + +Example: Multi-Block Grid +------------------------- + +.. code-block:: python + + # Create a grid with 3 blocks in X, 2 in Y, 1 in Z + filter = GenerateRectilinearGrid() + + # Define block boundaries + filter.setCoordinates( + [0.0, 1.0, 3.0, 5.0], # 3 blocks: [0,1], [1,3], [3,5] + [0.0, 2.0, 4.0], # 2 blocks: [0,2], [2,4] + [0.0, 1.0] # 1 block: [0,1] + ) + + # Define element counts per block + filter.setNumberElements( + [10, 20, 10], # 10, 20, 10 elements in X blocks + [15, 15], # 15, 15 elements in Y blocks + [5] # 5 elements in Z block + ) + + # Add global IDs and custom fields + filter.setGenerateCellsGlobalIds(True) + filter.setGeneratePointsGlobalIds(True) + + material_id = FieldInfo("material", 1, "CELLS") + coordinates = FieldInfo("coords", 3, "POINTS") + filter.setFields([material_id, coordinates]) + + # Generate the grid + mesh = filter.getGrid() + +Output +------ + +* **Input**: None (generator filter) +* **Output**: vtkUnstructuredGrid with hexahedral elements +* **Additional Arrays**: + + * Global IDs (if enabled) + * Custom fields (if specified) + * All arrays filled with constant value 1.0 + +Use Cases +--------- + +* **Testing**: Create simple grids for algorithm testing +* **Validation**: Generate known geometries for code validation +* **Prototyping**: Quick mesh generation for initial simulations +* **Benchmarking**: Standard grids for performance testing +* **Education**: Simple examples for learning mesh-based methods + +Best Practices +-------------- + +* Start with simple single-block grids before using multi-block configurations +* Ensure coordinate sequences are monotonically increasing +* Match the length of numberElements arrays with coordinate block counts +* Use meaningful field names for better mesh organization +* Enable global IDs when mesh will be used in parallel computations + +See Also +-------- + +* :doc:`GenerateFractures ` - Advanced mesh modification +* :doc:`ElementVolumes ` - Quality assessment for generated meshes +* :doc:`AllChecks ` - Comprehensive validation of generated meshes diff --git a/docs/geos_mesh_docs/filters/MainChecks.rst b/docs/geos_mesh_docs/filters/MainChecks.rst new file mode 100644 index 000000000..722e98188 --- /dev/null +++ b/docs/geos_mesh_docs/filters/MainChecks.rst @@ -0,0 +1,98 @@ +MainChecks Filter +================= + +.. autoclass:: geos.mesh.doctor.filters.Checks.MainChecks + :members: + :undoc-members: + :show-inheritance: + +Overview +-------- + +The MainChecks filter performs essential mesh validation by running the most important quality checks on a vtkUnstructuredGrid. This filter provides a streamlined subset of checks that are most critical for mesh quality assessment. + +Features +-------- + +* Essential mesh validation with the most important quality checks +* Faster execution compared to AllChecks +* Configurable check parameters +* Focused on critical mesh quality issues +* Same interface as AllChecks for consistency + +Usage Example +------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.Checks import MainChecks + + # Instantiate the filter for main checks only + mainChecksFilter = MainChecks() + + # Set input mesh + mainChecksFilter.SetInputData(mesh) + + # Optionally customize check parameters + mainChecksFilter.setCheckParameter("collocated_nodes", "tolerance", 1e-6) + + # Execute the checks and get output + output_mesh = mainChecksFilter.getGrid() + + # Get check results + check_results = mainChecksFilter.getCheckResults() + + # Write the output mesh + mainChecksFilter.writeGrid("output/mesh_main_checks.vtu") + +Main Checks Included +-------------------- + +The MainChecks filter includes a curated subset of the most important checks: + +* **Collocated nodes**: Detect duplicate/overlapping nodes +* **Element volumes**: Identify negative or zero volume elements +* **Self-intersecting elements**: Find geometrically invalid elements +* **Basic topology validation**: Ensure mesh connectivity is valid + +These checks cover the most common and critical mesh quality issues that can affect simulation stability and accuracy. + +When to Use MainChecks vs AllChecks +----------------------------------- + +**Use MainChecks when:** + +* You need quick mesh validation +* You're doing routine quality checks +* Performance is important +* You want to focus on critical issues only + +**Use AllChecks when:** + +* You need comprehensive mesh analysis +* You're debugging complex mesh issues +* You have time for thorough validation +* You need detailed reporting on all aspects + +Parameters +---------- + +Same parameter interface as AllChecks: + +* **setCheckParameter(check_name, parameter_name, value)**: Set specific parameter for a named check +* **setGlobalParameter(parameter_name, value)**: Apply parameter to all checks that support it + +Output +------ + +* **Input**: vtkUnstructuredGrid +* **Output**: vtkUnstructuredGrid (copy of input with potential additional arrays marking issues) +* **Check Results**: Dictionary with results from performed main checks + +See Also +-------- + +* :doc:`AllChecks ` - Comprehensive mesh validation with all checks +* :doc:`CollocatedNodes ` - Individual collocated nodes check +* :doc:`ElementVolumes ` - Individual element volume check +* :doc:`SelfIntersectingElements ` - Individual self-intersection check diff --git a/docs/geos_mesh_docs/filters/NonConformal.rst b/docs/geos_mesh_docs/filters/NonConformal.rst new file mode 100644 index 000000000..44991af86 --- /dev/null +++ b/docs/geos_mesh_docs/filters/NonConformal.rst @@ -0,0 +1,250 @@ +NonConformal Filter +=================== + +.. automodule:: geos.mesh.doctor.filters.NonConformal + :members: + :undoc-members: + :show-inheritance: + +Overview +-------- + +The NonConformal filter detects non-conformal mesh interfaces in a vtkUnstructuredGrid. Non-conformal interfaces occur when adjacent cells do not share nodes or faces properly, which can indicate mesh quality issues or intentional non-matching grid interfaces that require special handling in simulations. + +Features +-------- + +* Detection of non-conformal interfaces between mesh elements +* Configurable tolerance parameters for different geometric aspects +* Optional marking of non-conformal cells in output mesh +* Support for point, face, and angle tolerance specifications +* Detailed reporting of non-conformal cell pairs + +Usage Example +------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.NonConformal import NonConformal + + # Instantiate the filter + nonConformalFilter = NonConformal() + + # Set tolerance parameters + nonConformalFilter.setPointTolerance(1e-6) # tolerance for point matching + nonConformalFilter.setFaceTolerance(1e-6) # tolerance for face matching + nonConformalFilter.setAngleTolerance(10.0) # angle tolerance in degrees + + # Optionally enable painting of non-conformal cells + nonConformalFilter.setPaintNonConformalCells(1) # 1 to enable, 0 to disable + + # Set input mesh + nonConformalFilter.SetInputData(mesh) + + # Execute the filter and get output + output_mesh = nonConformalFilter.getGrid() + + # Get non-conformal cell pairs + non_conformal_cells = nonConformalFilter.getNonConformalCells() + # Returns list of tuples with (cell1_id, cell2_id) for non-conformal interfaces + + # Write the output mesh + nonConformalFilter.writeGrid("output/mesh_with_nonconformal_info.vtu") + +Parameters +---------- + +setPointTolerance(tolerance) + Set the tolerance for point position matching. + + * **tolerance** (float): Distance below which points are considered coincident + * **Default**: 0.0 + +setFaceTolerance(tolerance) + Set the tolerance for face geometry matching. + + * **tolerance** (float): Distance tolerance for face-to-face matching + * **Default**: 0.0 + +setAngleTolerance(tolerance) + Set the tolerance for face normal angle differences. + + * **tolerance** (float): Maximum angle difference in degrees + * **Default**: 10.0 + +setPaintNonConformalCells(choice) + Enable/disable creation of array marking non-conformal cells. + + * **choice** (int): 1 to enable marking, 0 to disable + * **Default**: 0 + +Results Access +-------------- + +getNonConformalCells() + Returns pairs of cell indices that have non-conformal interfaces. + + * **Returns**: list[tuple[int, int]] - Each tuple contains (cell1_id, cell2_id) + +getAngleTolerance() + Get the current angle tolerance setting. + + * **Returns**: float - Current angle tolerance in degrees + +getFaceTolerance() + Get the current face tolerance setting. + + * **Returns**: float - Current face tolerance + +getPointTolerance() + Get the current point tolerance setting. + + * **Returns**: float - Current point tolerance + +Understanding Non-Conformal Interfaces +--------------------------------------- + +**Conformal vs Non-Conformal** + +**Conformal Interface**: Adjacent cells share exact nodes and faces + +.. code-block:: + + Cell A: nodes [1, 2, 3, 4] + Cell B: nodes [3, 4, 5, 6] # Shares nodes 3,4 with Cell A + → CONFORMAL + +**Non-Conformal Interface**: Adjacent cells do not share nodes/faces exactly + +.. code-block:: + + Cell A: nodes [1, 2, 3, 4] + Cell B: nodes [5, 6, 7, 8] # No shared nodes with Cell A but geometrically adjacent + → NON-CONFORMAL + +**Types of Non-Conformity** + +1. **Hanging Nodes**: T-junctions where one element edge intersects another element face +2. **Mismatched Boundaries**: Interfaces where element boundaries don't align +3. **Gap Interfaces**: Small gaps between elements that should be connected +4. **Overlapping Interfaces**: Elements that overlap slightly due to meshing errors + +Tolerance Parameters Explained +------------------------------ + +**Point Tolerance** + +Controls how close points must be to be considered the same: + +* **Too small**: May miss near-coincident points that should match +* **Too large**: May incorrectly group distinct points +* **Typical values**: 1e-6 to 1e-12 depending on mesh scale + +**Face Tolerance** + +Controls how closely face geometries must match: + +* **Distance-based**: Maximum separation between face centroids or boundaries +* **Affects**: Detection of faces that should be conformal but have small gaps +* **Typical values**: 1e-6 to 1e-10 + +**Angle Tolerance** + +Controls how closely face normals must align: + +* **In degrees**: Maximum angle between face normal vectors +* **Affects**: Detection of faces that should be coplanar but have slight orientation differences +* **Typical values**: 0.1 to 10.0 degrees + +Common Causes of Non-Conformity +------------------------------- + +1. **Mesh Generation Issues**: + + * Different mesh densities in adjacent regions + * Boundary misalignment during mesh merging + * Floating-point precision errors + +2. **Intentional Design**: + + * Adaptive mesh refinement interfaces + * Multi-scale coupling boundaries + * Domain decomposition interfaces + +3. **Mesh Processing Errors**: + + * Node merging tolerances too strict + * Coordinate transformation errors + * File format conversion issues + +Impact on Simulations +--------------------- + +**Potential Problems**: + +* **Gaps**: Can cause fluid/heat leakage in flow simulations +* **Overlaps**: May create artificial constraints or stress concentrations +* **Inconsistent Physics**: Different discretizations across interfaces + +**When Non-Conformity is Acceptable**: + +* **Mortar Methods**: Designed to handle non-matching grids +* **Penalty Methods**: Use constraints to enforce continuity +* **Adaptive Refinement**: Temporary non-conformity during adaptation + +Example Analysis Workflow +------------------------- + +.. code-block:: python + + # Comprehensive non-conformity analysis + nc_filter = NonConformal() + + # Configure for sensitive detection + nc_filter.setPointTolerance(1e-8) + nc_filter.setFaceTolerance(1e-8) + nc_filter.setAngleTolerance(1.0) # 1 degree tolerance + + # Enable visualization + nc_filter.setPaintNonConformalCells(1) + + # Process mesh + nc_filter.SetInputData(mesh) + output_mesh = nc_filter.getGrid() + + # Analyze results + non_conformal_pairs = nc_filter.getNonConformalCells() + + if len(non_conformal_pairs) == 0: + print("Mesh is fully conformal - all interfaces match properly") + else: + print(f"Found {len(non_conformal_pairs)} non-conformal interfaces:") + for cell1, cell2 in non_conformal_pairs[:10]: # Show first 10 + print(f" Cells {cell1} and {cell2} have non-conformal interface") + + # Write mesh with marking for visualization + nc_filter.writeGrid("output/mesh_nonconformal_marked.vtu") + +Output +------ + +* **Input**: vtkUnstructuredGrid +* **Output**: vtkUnstructuredGrid with optional marking arrays +* **Marking Array**: When painting is enabled, adds "IsNonConformal" array to cell data +* **Cell Pairs**: List of non-conformal cell index pairs + +Best Practices +-------------- + +* **Set appropriate tolerances** based on mesh precision and simulation requirements +* **Use painting** to visualize non-conformal regions in ParaView +* **Consider physics requirements** when deciding if non-conformity is acceptable +* **Combine with other checks** for comprehensive mesh validation +* **Document intentional non-conformity** for future reference + +See Also +-------- + +* :doc:`AllChecks ` - Includes non-conformal check among others +* :doc:`CollocatedNodes ` - Related to point matching issues +* :doc:`SelfIntersectingElements ` - Related geometric validation diff --git a/docs/geos_mesh_docs/filters/SelfIntersectingElements.rst b/docs/geos_mesh_docs/filters/SelfIntersectingElements.rst new file mode 100644 index 000000000..059f7e568 --- /dev/null +++ b/docs/geos_mesh_docs/filters/SelfIntersectingElements.rst @@ -0,0 +1,293 @@ +SelfIntersectingElements Filter +=============================== + +.. automodule:: geos.mesh.doctor.filters.SelfIntersectingElements + :members: + :undoc-members: + :show-inheritance: + +Overview +-------- + +The SelfIntersectingElements filter identifies various types of invalid or problematic elements in a vtkUnstructuredGrid. It performs comprehensive geometric validation to detect elements with intersecting edges, intersecting faces, non-contiguous edges, non-convex shapes, incorrectly oriented faces, and wrong number of points. + +Features +-------- + +* Detection of multiple types of geometric element problems +* Configurable minimum distance parameter for intersection detection +* Optional marking of invalid elements in output mesh +* Detailed classification of different problem types +* Comprehensive reporting of all detected issues + +Usage Example +------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.SelfIntersectingElements import SelfIntersectingElements + + # Instantiate the filter + selfIntersectingElementsFilter = SelfIntersectingElements() + + # Set minimum distance parameter for intersection detection + selfIntersectingElementsFilter.setMinDistance(1e-6) + + # Optionally enable painting of invalid elements + selfIntersectingElementsFilter.setPaintInvalidElements(1) # 1 to enable, 0 to disable + + # Set input mesh + selfIntersectingElementsFilter.SetInputData(mesh) + + # Execute the filter and get output + output_mesh = selfIntersectingElementsFilter.getGrid() + + # Get different types of problematic elements + wrong_points_elements = selfIntersectingElementsFilter.getWrongNumberOfPointsElements() + intersecting_edges_elements = selfIntersectingElementsFilter.getIntersectingEdgesElements() + intersecting_faces_elements = selfIntersectingElementsFilter.getIntersectingFacesElements() + non_contiguous_edges_elements = selfIntersectingElementsFilter.getNonContiguousEdgesElements() + non_convex_elements = selfIntersectingElementsFilter.getNonConvexElements() + wrong_oriented_faces_elements = selfIntersectingElementsFilter.getFacesOrientedIncorrectlyElements() + + # Write the output mesh + selfIntersectingElementsFilter.writeGrid("output/mesh_with_invalid_elements.vtu") + +Parameters +---------- + +setMinDistance(distance) + Set the minimum distance parameter for intersection detection. + + * **distance** (float): Minimum distance threshold for geometric calculations + * **Default**: 0.0 + * **Usage**: Smaller values detect more subtle problems, larger values ignore minor issues + +setPaintInvalidElements(choice) + Enable/disable creation of arrays marking invalid elements. + + * **choice** (int): 1 to enable marking, 0 to disable + * **Default**: 0 + * **Effect**: When enabled, adds arrays to cell data identifying different problem types + +Types of Problems Detected +-------------------------- + +getWrongNumberOfPointsElements() + Elements with incorrect number of points for their cell type. + + * **Returns**: list[int] - Element indices with wrong point counts + * **Examples**: Triangle with 4 points, hexahedron with 7 points + +getIntersectingEdgesElements() + Elements where edges intersect themselves. + + * **Returns**: list[int] - Element indices with self-intersecting edges + * **Examples**: Twisted quadrilaterals, folded triangles + +getIntersectingFacesElements() + Elements where faces intersect each other. + + * **Returns**: list[int] - Element indices with self-intersecting faces + * **Examples**: Inverted tetrahedra, twisted hexahedra + +getNonContiguousEdgesElements() + Elements where edges are not properly connected. + + * **Returns**: list[int] - Element indices with connectivity issues + * **Examples**: Disconnected edge loops, gaps in element boundaries + +getNonConvexElements() + Elements that are not convex as required. + + * **Returns**: list[int] - Element indices that are non-convex + * **Examples**: Concave quadrilaterals, non-convex polygons + +getFacesOrientedIncorrectlyElements() + Elements with incorrectly oriented faces. + + * **Returns**: list[int] - Element indices with orientation problems + * **Examples**: Inward-pointing face normals, inconsistent winding + +Understanding Element Problems +------------------------------ + +**Wrong Number of Points** + +Each VTK cell type has a specific number of points: + +* Triangle: 3 points +* Quadrilateral: 4 points +* Tetrahedron: 4 points +* Hexahedron: 8 points +* etc. + +**Self-Intersecting Edges** + +Edges that cross over themselves: + +.. code-block:: + + Valid triangle: Invalid triangle (bow-tie): + A A + / \ /|\ + / \ / | \ + B-----C B--+--C + \|/ + D + +**Self-Intersecting Faces** + +3D elements where faces intersect: + +.. code-block:: + + Valid tetrahedron Invalid tetrahedron (inverted) + D D + /|\ /|\ + / | \ / | \ + A--+--C C--+--A (face ABC flipped) + \ | / \ | / + \|/ \|/ + B B + +**Non-Contiguous Edges** + +Element boundaries that don't form continuous loops: + +* Missing edges between consecutive points +* Duplicate edges +* Gaps in the boundary + +**Non-Convex Elements** + +Elements that have internal angles > 180 degrees: + +* Can cause numerical issues in finite element calculations +* May indicate mesh generation problems +* Some solvers require strictly convex elements + +**Incorrectly Oriented Faces** + +Faces with normals pointing in wrong directions: + +* Outward normals pointing inward +* Inconsistent winding order +* Can affect normal-based calculations + +Common Causes and Solutions +--------------------------- + +**Wrong Number of Points** + +* **Cause**: Mesh file corruption, wrong cell type specification +* **Solution**: Fix cell type definitions or regenerate mesh + +**Self-Intersecting Edges/Faces** + +* **Cause**: Node coordinate errors, mesh deformation, bad mesh generation +* **Solution**: Fix node coordinates, improve mesh quality settings + +**Non-Contiguous Edges** + +* **Cause**: Missing connectivity information, duplicate nodes +* **Solution**: Fix element connectivity, merge duplicate nodes + +**Non-Convex Elements** + +* **Cause**: Poor mesh quality, extreme deformation +* **Solution**: Improve mesh generation parameters, element quality checks + +**Wrong Face Orientation** + +* **Cause**: Inconsistent node ordering, mesh processing errors +* **Solution**: Fix element node ordering, use mesh repair tools + +Example Comprehensive Analysis +------------------------------ + +.. code-block:: python + + # Detailed element validation workflow + validator = SelfIntersectingElements() + validator.setMinDistance(1e-8) # Very sensitive detection + validator.setPaintInvalidElements(1) # Enable visualization + + validator.SetInputData(mesh) + output_mesh = validator.getGrid() + + # Collect all problems + problems = { + 'Wrong points': validator.getWrongNumberOfPointsElements(), + 'Intersecting edges': validator.getIntersectingEdgesElements(), + 'Intersecting faces': validator.getIntersectingFacesElements(), + 'Non-contiguous edges': validator.getNonContiguousEdgesElements(), + 'Non-convex': validator.getNonConvexElements(), + 'Wrong orientation': validator.getFacesOrientedIncorrectlyElements() + } + + # Report results + total_problems = sum(len(elements) for elements in problems.values()) + + if total_problems == 0: + print("✓ All elements are geometrically valid!") + else: + print(f"⚠ Found {total_problems} problematic elements:") + for problem_type, elements in problems.items(): + if elements: + print(f" {problem_type}: {len(elements)} elements") + print(f" Examples: {elements[:5]}") # Show first 5 + + # Save results for visualization + validator.writeGrid("output/mesh_validation_results.vtu") + +Impact on Simulations +--------------------- + +**Numerical Issues** + +* Poor convergence +* Solver instabilities +* Incorrect results +* Simulation crashes + +**Physical Accuracy** + +* Wrong material volumes +* Incorrect flow paths +* Bad stress/strain calculations +* Energy conservation violations + +**Performance Impact** + +* Slower convergence +* Need for smaller time steps +* Additional stabilization methods +* Increased computational cost + +Output +------ + +* **Input**: vtkUnstructuredGrid +* **Output**: vtkUnstructuredGrid with optional marking arrays +* **Problem Lists**: Separate lists for each type of geometric problem +* **Marking Arrays**: When painting is enabled, adds arrays identifying problem types + +Best Practices +-------------- + +* **Set appropriate minimum distance** based on mesh precision +* **Enable painting** to visualize problems in ParaView +* **Check all problem types** for comprehensive validation +* **Fix problems before simulation** to avoid numerical issues +* **Use with other validators** for complete mesh assessment +* **Document any intentionally invalid elements** if they serve a purpose + +See Also +-------- + +* :doc:`AllChecks ` - Includes self-intersection check among others +* :doc:`MainChecks ` - Includes self-intersection check in main set +* :doc:`ElementVolumes ` - Related to geometric validity +* :doc:`CollocatedNodes ` - Can help fix some geometric issues +* :doc:`NonConformal ` - Related interface validation diff --git a/docs/geos_mesh_docs/filters/SupportedElements.rst b/docs/geos_mesh_docs/filters/SupportedElements.rst new file mode 100644 index 000000000..a2fb59e69 --- /dev/null +++ b/docs/geos_mesh_docs/filters/SupportedElements.rst @@ -0,0 +1,228 @@ +SupportedElements Filter +======================== + +.. automodule:: geos.mesh.doctor.filters.SupportedElements + :members: + :undoc-members: + :show-inheritance: + +Overview +-------- + +The SupportedElements filter identifies unsupported element types and problematic polyhedron elements in a vtkUnstructuredGrid. It validates that all elements in the mesh are supported by GEOS and checks polyhedron elements for geometric correctness. + +.. note:: + This filter is currently disabled due to multiprocessing requirements that are incompatible with the VTK filter framework. The implementation exists but is commented out in the source code. + +Features (When Available) +------------------------- + +* Detection of unsupported VTK element types +* Validation of polyhedron element geometry +* Optional marking of unsupported elements in output mesh +* Integration with parallel processing for large meshes +* Detailed reporting of element type compatibility + +Intended Usage Example +---------------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.SupportedElements import SupportedElements + + # Instantiate the filter + supportedElementsFilter = SupportedElements() + + # Optionally enable painting of unsupported element types + supportedElementsFilter.setPaintUnsupportedElementTypes(1) # 1 to enable, 0 to disable + + # Set input mesh + supportedElementsFilter.SetInputData(mesh) + + # Execute the filter and get output + output_mesh = supportedElementsFilter.getGrid() + + # Get unsupported elements + unsupported_elements = supportedElementsFilter.getUnsupportedElements() + + # Write the output mesh + supportedElementsFilter.writeGrid("output/mesh_with_support_info.vtu") + +GEOS Supported Element Types +---------------------------- + +GEOS supports the following VTK element types: + +**Standard Elements** + +* **VTK_VERTEX** (1): Point elements +* **VTK_LINE** (3): Line segments +* **VTK_TRIANGLE** (5): Triangular elements +* **VTK_QUAD** (9): Quadrilateral elements +* **VTK_TETRA** (10): Tetrahedral elements +* **VTK_HEXAHEDRON** (12): Hexahedral (brick) elements +* **VTK_WEDGE** (13): Wedge/prism elements +* **VTK_PYRAMID** (14): Pyramid elements + +**Higher-Order Elements** + +* **VTK_QUADRATIC_TRIANGLE** (22): 6-node triangles +* **VTK_QUADRATIC_QUAD** (23): 8-node quadrilaterals +* **VTK_QUADRATIC_TETRA** (24): 10-node tetrahedra +* **VTK_QUADRATIC_HEXAHEDRON** (25): 20-node hexahedra + +**Special Elements** + +* **VTK_POLYHEDRON** (42): General polyhedra (with validation) + +Unsupported Element Types +------------------------- + +Elements not supported by GEOS include: + +* **VTK_PIXEL** (8): Axis-aligned rectangles +* **VTK_VOXEL** (11): Axis-aligned boxes +* **VTK_PENTAGONAL_PRISM** (15): 5-sided prisms +* **VTK_HEXAGONAL_PRISM** (16): 6-sided prisms +* Various specialized or experimental VTK cell types + +Polyhedron Validation +--------------------- + +For polyhedron elements (VTK_POLYHEDRON), additional checks are performed: + +**Geometric Validation** + +* Face planarity +* Edge connectivity +* Volume calculation +* Normal consistency + +**Topological Validation** + +* Manifold surface verification +* Closed surface check +* Face orientation consistency + +**Quality Checks** + +* Aspect ratio limits +* Volume positivity +* Face area positivity + +Common Issues and Solutions +--------------------------- + +**Unsupported Standard Elements** + +* **Problem**: Mesh contains VTK_PIXEL or VTK_VOXEL elements +* **Solution**: Convert to VTK_QUAD or VTK_HEXAHEDRON respectively +* **Tools**: VTK conversion filters or mesh processing software + +**Invalid Polyhedra** + +* **Problem**: Non-manifold or self-intersecting polyhedra +* **Solution**: Use mesh repair tools or regenerate with better quality settings +* **Prevention**: Validate polyhedra during mesh generation + +**Mixed Element Types** + +* **Problem**: Mesh contains both supported and unsupported elements +* **Solution**: Selective element type conversion or mesh region separation + +Current Status and Alternatives +------------------------------- + +**Why Disabled** + +The SupportedElements filter requires multiprocessing capabilities for efficient polyhedron validation on large meshes. However, the VTK Python filter framework doesn't integrate well with multiprocessing, leading to: + +* Process spawning issues +* Memory management problems +* Inconsistent results across platforms + +**Alternative Approaches** + +1. **Command-Line Tool**: Use the ``mesh-doctor supported_elements`` command instead +2. **Direct Function Calls**: Import and use the underlying functions directly +3. **Manual Validation**: Check element types programmatically + +**Command-Line Alternative** + +.. code-block:: bash + + # Use mesh-doctor command line tool instead + mesh-doctor -i input_mesh.vtu supported_elements --help + +**Direct Function Usage** + +.. code-block:: python + + from geos.mesh.doctor.actions.supported_elements import ( + find_unsupported_std_elements_types, + find_unsupported_polyhedron_elements + ) + + # Direct function usage (not as VTK filter) + unsupported_std = find_unsupported_std_elements_types(mesh) + # Note: polyhedron validation requires multiprocessing setup + +Future Development +------------------ + +**Planned Improvements** + +* Integration with VTK's parallel processing capabilities +* Alternative implementation without multiprocessing dependency +* Better error handling and reporting +* Performance optimizations for large meshes + +**Workaround Implementation** + +Until the filter is re-enabled, users can: + +1. Use the command-line interface +2. Implement custom validation loops +3. Use external mesh validation tools +4. Perform validation in separate processes + +Example Manual Validation +------------------------- + +.. code-block:: python + + import vtk + + def check_supported_elements(mesh): + """Manual check for supported element types.""" + supported_types = { + vtk.VTK_VERTEX, vtk.VTK_LINE, vtk.VTK_TRIANGLE, vtk.VTK_QUAD, + vtk.VTK_TETRA, vtk.VTK_HEXAHEDRON, vtk.VTK_WEDGE, vtk.VTK_PYRAMID, + vtk.VTK_QUADRATIC_TRIANGLE, vtk.VTK_QUADRATIC_QUAD, + vtk.VTK_QUADRATIC_TETRA, vtk.VTK_QUADRATIC_HEXAHEDRON, + vtk.VTK_POLYHEDRON + } + + unsupported = [] + for i in range(mesh.GetNumberOfCells()): + cell_type = mesh.GetCellType(i) + if cell_type not in supported_types: + unsupported.append((i, cell_type)) + + return unsupported + + # Usage + unsupported_elements = check_supported_elements(mesh) + if unsupported_elements: + print(f"Found {len(unsupported_elements)} unsupported elements") + for cell_id, cell_type in unsupported_elements[:5]: + print(f" Cell {cell_id}: type {cell_type}") + +See Also +-------- + +* :doc:`AllChecks ` - Would include supported elements check when available +* :doc:`SelfIntersectingElements ` - Related geometric validation +* :doc:`ElementVolumes ` - Basic element validation +* GEOS documentation on supported element types +* VTK documentation on cell types diff --git a/docs/geos_mesh_docs/filters/index.rst b/docs/geos_mesh_docs/filters/index.rst new file mode 100644 index 000000000..456dd2e85 --- /dev/null +++ b/docs/geos_mesh_docs/filters/index.rst @@ -0,0 +1,192 @@ +Mesh Doctor Filters +=================== + +The mesh doctor filters provide VTK-based tools for mesh quality assessment, validation, and processing. All filters work with vtkUnstructuredGrid data and follow a consistent interface pattern. + +Quality Assessment Filters +-------------------------- + +These filters analyze existing meshes for various quality issues and geometric problems. + +.. toctree:: + :maxdepth: 1 + + AllChecks + MainChecks + CollocatedNodes + ElementVolumes + SelfIntersectingElements + NonConformal + +Mesh Generation Filters +----------------------- + +These filters create new meshes from scratch or modify existing meshes. + +.. toctree:: + :maxdepth: 1 + + GenerateRectilinearGrid + GenerateFractures + +Processing Filters +------------------ + +These filters perform specialized processing and validation tasks. + +.. toctree:: + :maxdepth: 1 + + SupportedElements + +Common Usage Pattern +==================== + +All mesh doctor filters follow a consistent usage pattern: + +.. code-block:: python + + from geos.mesh.doctor.filters.FilterName import FilterName + + # Instantiate the filter + filter = FilterName() + + # Configure filter parameters + filter.setParameter(value) + + # Set input data (for processing filters, not needed for generators) + filter.SetInputData(mesh) + + # Execute the filter and get output + output_mesh = filter.getGrid() + + # Access specific results (filter-dependent) + results = filter.getSpecificResults() + + # Write results to file + filter.writeGrid("output/result.vtu") + +Filter Categories Explained +=========================== + +Quality Assessment +------------------ + +**Purpose**: Identify mesh quality issues, geometric problems, and topology errors + +**When to use**: +- Before running simulations +- After mesh generation or modification +- During mesh debugging +- For mesh quality reporting + +**Key filters**: +- **AllChecks/MainChecks**: Comprehensive validation suites +- **CollocatedNodes**: Duplicate node detection +- **ElementVolumes**: Volume validation +- **SelfIntersectingElements**: Geometric integrity +- **NonConformal**: Interface validation + +Mesh Generation +--------------- + +**Purpose**: Create new meshes or modify existing ones + +**When to use**: +- Creating test meshes +- Generating simple geometries +- Adding fractures to existing meshes +- Prototyping mesh-based algorithms + +**Key filters**: +- **GenerateRectilinearGrid**: Simple structured grids +- **GenerateFractures**: Fracture network generation + +Processing +---------- + +**Purpose**: Specialized mesh processing and validation + +**When to use**: +- Validating element type compatibility +- Preparing meshes for specific solvers +- Advanced geometric analysis + +**Key filters**: +- **SupportedElements**: GEOS compatibility validation + +Quick Reference +=============== + +Filter Selection Guide +---------------------- + +**For routine mesh validation**: + Use :doc:`MainChecks ` for fast, essential checks + +**For comprehensive analysis**: + Use :doc:`AllChecks ` for detailed validation + +**For specific problems**: + - Duplicate nodes → :doc:`CollocatedNodes ` + - Negative volumes → :doc:`ElementVolumes ` + - Invalid geometry → :doc:`SelfIntersectingElements ` + - Interface issues → :doc:`NonConformal ` + +**For mesh generation**: + - Simple grids → :doc:`GenerateRectilinearGrid ` + - Fracture networks → :doc:`GenerateFractures ` + +**For compatibility checking**: + - GEOS support → :doc:`SupportedElements ` + +Parameter Guidelines +-------------------- + +**Tolerance Parameters**: + - High precision meshes: 1e-12 to 1e-8 + - Standard meshes: 1e-8 to 1e-6 + - Coarse meshes: 1e-6 to 1e-4 + +**Painting Options**: + - Enable (1) for visualization in ParaView + - Disable (0) for performance in batch processing + +**Output Modes**: + - Binary for large meshes and performance + - ASCII for debugging and text processing + +Best Practices +============== + +Workflow Integration +-------------------- + +1. **Start with quality assessment** using MainChecks or AllChecks +2. **Address specific issues** with targeted filters +3. **Validate fixes** by re-running quality checks +4. **Document mesh quality** for simulation reference + +Performance Considerations +-------------------------- + +- Use appropriate tolerances (not unnecessarily strict) +- Enable painting only when needed for visualization +- Use binary output for large meshes +- Run comprehensive checks during development, lighter checks in production + +Error Handling +-------------- + +- Check filter results before proceeding with simulations +- Save problematic meshes for debugging +- Document known issues and their acceptable thresholds +- Use multiple validation approaches for critical applications + +See Also +======== + +- **GEOS Documentation**: Supported element types and mesh requirements +- **VTK Documentation**: VTK data formats and cell types +- **ParaView**: Visualization of mesh quality results +- **Mesh Generation Tools**: Creating high-quality input meshes From d81951ee522c1d43fcd38747eeeb9690b838601c Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 12 Aug 2025 16:48:54 -0700 Subject: [PATCH 22/52] Improve displayed results for supported_elements --- .../mesh/doctor/actions/supported_elements.py | 18 +++++++++-------- .../src/geos/mesh/utils/genericHelpers.py | 20 +++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) 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 7731cc144..782415108 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py @@ -3,7 +3,7 @@ import networkx from numpy import ones from tqdm import tqdm -from typing import FrozenSet, Iterable, Mapping, Optional +from typing import Iterable, Mapping, Optional from vtkmodules.util.numpy_support import vtk_to_numpy from vtkmodules.vtkCommonCore import vtkIdList from vtkmodules.vtkCommonDataModel import ( vtkCellTypes, vtkUnstructuredGrid, VTK_HEXAGONAL_PRISM, VTK_HEXAHEDRON, @@ -12,7 +12,7 @@ from geos.mesh.doctor.actions.vtk_polyhedron import build_face_to_face_connectivity_through_edges, FaceStream from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.io.vtkIO import read_mesh -from geos.mesh.utils.genericHelpers import vtk_iter +from geos.mesh.utils.genericHelpers import get_vtk_constant_str, vtk_iter @dataclass( frozen=True ) @@ -23,8 +23,8 @@ class Options: @dataclass( frozen=True ) class Result: - unsupported_std_elements_types: FrozenSet[ int ] # list of unsupported types - unsupported_polyhedron_elements: FrozenSet[ + unsupported_std_elements_types: list[ str ] # list of unsupported types + unsupported_polyhedron_elements: frozenset[ int ] # list of polyhedron elements that could not be converted to supported std elements @@ -116,14 +116,16 @@ def __call__( self, ic: int ) -> int: return ic -def find_unsupported_std_elements_types( mesh: vtkUnstructuredGrid ) -> set[ int ]: +def find_unsupported_std_elements_types( mesh: vtkUnstructuredGrid ) -> list[ str ]: if hasattr( mesh, "GetDistinctCellTypesArray" ): # For more recent versions of vtk. unique_cell_types = set( vtk_to_numpy( mesh.GetDistinctCellTypesArray() ) ) else: vtk_cell_types = vtkCellTypes() mesh.GetCellTypes( vtk_cell_types ) unique_cell_types = set( vtk_iter( vtk_cell_types ) ) - return unique_cell_types - supported_cell_types + result_values: set[ int ] = unique_cell_types - supported_cell_types + results = [ f"{get_vtk_constant_str( i )}" for i in frozenset( result_values ) ] + return results def find_unsupported_polyhedron_elements( mesh: vtkUnstructuredGrid, options: Options ) -> list[ int ]: @@ -143,9 +145,9 @@ def find_unsupported_polyhedron_elements( mesh: vtkUnstructuredGrid, options: Op def mesh_action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: - unsupported_std_elements_types: set[ int ] = find_unsupported_std_elements_types( mesh ) + unsupported_std_elements_types: list[ str ] = find_unsupported_std_elements_types( mesh ) unsupported_polyhedron_elements: list[ int ] = find_unsupported_polyhedron_elements( mesh, options ) - return Result( unsupported_std_elements_types=frozenset( unsupported_std_elements_types ), + return Result( unsupported_std_elements_types=unsupported_std_elements_types, unsupported_polyhedron_elements=frozenset( unsupported_polyhedron_elements ) ) diff --git a/geos-mesh/src/geos/mesh/utils/genericHelpers.py b/geos-mesh/src/geos/mesh/utils/genericHelpers.py index de0624fd9..e45fbdb80 100644 --- a/geos-mesh/src/geos/mesh/utils/genericHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/genericHelpers.py @@ -6,6 +6,7 @@ from typing import Iterator, List, Sequence, Any, Union from vtkmodules.util.numpy_support import numpy_to_vtk from vtkmodules.vtkCommonCore import vtkIdList, vtkPoints, reference +import vtkmodules.vtkCommonDataModel as vtk_dm from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, vtkMultiBlockDataSet, vtkPolyData, vtkDataSet, vtkDataObject, vtkPlane, vtkCellTypes, vtkIncrementalOctreePointLocator from vtkmodules.vtkFiltersCore import vtk3DLinearGridPlaneCutter from geos.mesh.utils.multiblockHelpers import ( getBlockElementIndexesFlatten, getBlockFromFlatIndex ) @@ -20,6 +21,25 @@ """ +def get_vtk_constant_str( vtk_int_value: int ) -> str: + """ + Finds the string name of a VTK constant from its integer value. + + Args: + vtk_int_value: The integer value of the constant (e.g., 12). + + Returns: + A string like "12: VTK_HEXAHEDRON" or "12: ". + """ + # Search through the vtkCommonDataModel module + for name in dir( vtk_dm ): + # We only want variables that start with "VTK_" + if name.startswith( "VTK_" ): + if getattr( vtk_dm, name ) == vtk_int_value: + return f"{vtk_int_value}: {name}" + return f"{vtk_int_value}: " + + def to_vtk_id_list( data: List[ int ] ) -> vtkIdList: """Utility function transforming a list of ids into a vtkIdList. From dfe85b27883238c14300309d00c6abdc76e56637 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 12 Aug 2025 17:51:25 -0700 Subject: [PATCH 23/52] yapf --- geos-mesh/tests/test_Checks.py | 379 ++++++++++----------- geos-mesh/tests/test_generate_fractures.py | 1 - geos-mesh/tests/test_supported_elements.py | 164 ++++----- 3 files changed, 269 insertions(+), 275 deletions(-) diff --git a/geos-mesh/tests/test_Checks.py b/geos-mesh/tests/test_Checks.py index 540939f6d..5661c0689 100644 --- a/geos-mesh/tests/test_Checks.py +++ b/geos-mesh/tests/test_Checks.py @@ -1,9 +1,6 @@ import pytest from vtkmodules.vtkCommonCore import vtkPoints -from vtkmodules.vtkCommonDataModel import ( - vtkCellArray, vtkTetra, vtkUnstructuredGrid, - VTK_TETRA -) +from vtkmodules.vtkCommonDataModel import ( vtkCellArray, vtkTetra, vtkUnstructuredGrid, VTK_TETRA ) from geos.mesh.doctor.filters.Checks import AllChecks, MainChecks @@ -20,52 +17,52 @@ def simple_mesh_with_issues() -> vtkUnstructuredGrid: vtkUnstructuredGrid: Test mesh with various issues """ mesh = vtkUnstructuredGrid() - + # Create points with some collocated nodes points = vtkPoints() - points.InsertNextPoint(0.0, 0.0, 0.0) # Point 0 - points.InsertNextPoint(1.0, 0.0, 0.0) # Point 1 - points.InsertNextPoint(0.0, 1.0, 0.0) # Point 2 - points.InsertNextPoint(0.0, 0.0, 0.0) # Point 3 - duplicate of Point 0 - points.InsertNextPoint(0.0, 0.0, 1.0) # Point 4 - points.InsertNextPoint(2.0, 0.0, 0.0) # Point 5 - points.InsertNextPoint(2.01, 0.0, 0.0) # Point 6 - very close to Point 5 (small volume) - points.InsertNextPoint(2.0, 0.01, 0.0) # Point 7 - creates tiny element - points.InsertNextPoint(2.0, 0.0, 0.01) # Point 8 - creates tiny element - mesh.SetPoints(points) - + points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 0 + points.InsertNextPoint( 1.0, 0.0, 0.0 ) # Point 1 + points.InsertNextPoint( 0.0, 1.0, 0.0 ) # Point 2 + points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 3 - duplicate of Point 0 + points.InsertNextPoint( 0.0, 0.0, 1.0 ) # Point 4 + points.InsertNextPoint( 2.0, 0.0, 0.0 ) # Point 5 + points.InsertNextPoint( 2.01, 0.0, 0.0 ) # Point 6 - very close to Point 5 (small volume) + points.InsertNextPoint( 2.0, 0.01, 0.0 ) # Point 7 - creates tiny element + points.InsertNextPoint( 2.0, 0.0, 0.01 ) # Point 8 - creates tiny element + mesh.SetPoints( points ) + # Create cells cells = vtkCellArray() cell_types = [] - + # Normal tetrahedron tet1 = vtkTetra() - tet1.GetPointIds().SetId(0, 0) - tet1.GetPointIds().SetId(1, 1) - tet1.GetPointIds().SetId(2, 2) - tet1.GetPointIds().SetId(3, 4) - cells.InsertNextCell(tet1) - cell_types.append(VTK_TETRA) - + tet1.GetPointIds().SetId( 0, 0 ) + tet1.GetPointIds().SetId( 1, 1 ) + tet1.GetPointIds().SetId( 2, 2 ) + tet1.GetPointIds().SetId( 3, 4 ) + cells.InsertNextCell( tet1 ) + cell_types.append( VTK_TETRA ) + # Tetrahedron with duplicate node indices (wrong support) tet2 = vtkTetra() - tet2.GetPointIds().SetId(0, 3) # This is collocated with point 0 - tet2.GetPointIds().SetId(1, 1) - tet2.GetPointIds().SetId(2, 2) - tet2.GetPointIds().SetId(3, 0) # Duplicate reference to same location - cells.InsertNextCell(tet2) - cell_types.append(VTK_TETRA) - + tet2.GetPointIds().SetId( 0, 3 ) # This is collocated with point 0 + tet2.GetPointIds().SetId( 1, 1 ) + tet2.GetPointIds().SetId( 2, 2 ) + tet2.GetPointIds().SetId( 3, 0 ) # Duplicate reference to same location + cells.InsertNextCell( tet2 ) + cell_types.append( VTK_TETRA ) + # Very small volume tetrahedron tet3 = vtkTetra() - tet3.GetPointIds().SetId(0, 5) - tet3.GetPointIds().SetId(1, 6) - tet3.GetPointIds().SetId(2, 7) - tet3.GetPointIds().SetId(3, 8) - cells.InsertNextCell(tet3) - cell_types.append(VTK_TETRA) - - mesh.SetCells(cell_types, cells) + tet3.GetPointIds().SetId( 0, 5 ) + tet3.GetPointIds().SetId( 1, 6 ) + tet3.GetPointIds().SetId( 2, 7 ) + tet3.GetPointIds().SetId( 3, 8 ) + cells.InsertNextCell( tet3 ) + cell_types.append( VTK_TETRA ) + + mesh.SetCells( cell_types, cells ) return mesh @@ -77,28 +74,28 @@ def clean_mesh() -> vtkUnstructuredGrid: vtkUnstructuredGrid: Clean test mesh """ mesh = vtkUnstructuredGrid() - + # Create well-separated points points = vtkPoints() - points.InsertNextPoint(0.0, 0.0, 0.0) # Point 0 - points.InsertNextPoint(1.0, 0.0, 0.0) # Point 1 - points.InsertNextPoint(0.0, 1.0, 0.0) # Point 2 - points.InsertNextPoint(0.0, 0.0, 1.0) # Point 3 - mesh.SetPoints(points) - + points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 0 + points.InsertNextPoint( 1.0, 0.0, 0.0 ) # Point 1 + points.InsertNextPoint( 0.0, 1.0, 0.0 ) # Point 2 + points.InsertNextPoint( 0.0, 0.0, 1.0 ) # Point 3 + mesh.SetPoints( points ) + # Create a single clean tetrahedron cells = vtkCellArray() cell_types = [] - + tet = vtkTetra() - tet.GetPointIds().SetId(0, 0) - tet.GetPointIds().SetId(1, 1) - tet.GetPointIds().SetId(2, 2) - tet.GetPointIds().SetId(3, 3) - cells.InsertNextCell(tet) - cell_types.append(VTK_TETRA) - - mesh.SetCells(cell_types, cells) + tet.GetPointIds().SetId( 0, 0 ) + tet.GetPointIds().SetId( 1, 1 ) + tet.GetPointIds().SetId( 2, 2 ) + tet.GetPointIds().SetId( 3, 3 ) + cells.InsertNextCell( tet ) + cell_types.append( VTK_TETRA ) + + mesh.SetCells( cell_types, cells ) return mesh @@ -116,255 +113,249 @@ def main_checks_filter() -> MainChecks: class TestAllChecks: """Test class for AllChecks filter functionality.""" - - def test_filter_creation(self, all_checks_filter: AllChecks): + + def test_filter_creation( self, all_checks_filter: AllChecks ): """Test that AllChecks filter can be created successfully.""" assert all_checks_filter is not None - assert hasattr(all_checks_filter, 'getAvailableChecks') - assert hasattr(all_checks_filter, 'setChecksToPerform') - assert hasattr(all_checks_filter, 'setCheckParameter') - - def test_available_checks(self, all_checks_filter: AllChecks): + assert hasattr( all_checks_filter, 'getAvailableChecks' ) + assert hasattr( all_checks_filter, 'setChecksToPerform' ) + assert hasattr( all_checks_filter, 'setCheckParameter' ) + + def test_available_checks( self, all_checks_filter: AllChecks ): """Test that all expected checks are available.""" available_checks = all_checks_filter.getAvailableChecks() - + # Check that we have the expected checks for AllChecks expected_checks = [ - 'collocated_nodes', - 'element_volumes', - 'non_conformal', - 'self_intersecting_elements', - 'supported_elements' + 'collocated_nodes', 'element_volumes', 'non_conformal', 'self_intersecting_elements', 'supported_elements' ] - + for check in expected_checks: assert check in available_checks, f"Check '{check}' should be available" - - def test_default_parameters(self, all_checks_filter: AllChecks): + + def test_default_parameters( self, all_checks_filter: AllChecks ): """Test that default parameters are correctly retrieved.""" available_checks = all_checks_filter.getAvailableChecks() - + for check_name in available_checks: - defaults = all_checks_filter.getDefaultParameters(check_name) - assert isinstance(defaults, dict), f"Default parameters for '{check_name}' should be a dict" - + defaults = all_checks_filter.getDefaultParameters( check_name ) + assert isinstance( defaults, dict ), f"Default parameters for '{check_name}' should be a dict" + # Test specific known defaults - collocated_defaults = all_checks_filter.getDefaultParameters('collocated_nodes') + collocated_defaults = all_checks_filter.getDefaultParameters( 'collocated_nodes' ) assert 'tolerance' in collocated_defaults - - volume_defaults = all_checks_filter.getDefaultParameters('element_volumes') + + volume_defaults = all_checks_filter.getDefaultParameters( 'element_volumes' ) assert 'min_volume' in volume_defaults - - def test_set_checks_to_perform(self, all_checks_filter: AllChecks): + + def test_set_checks_to_perform( self, all_checks_filter: AllChecks ): """Test setting specific checks to perform.""" # Set specific checks - checks_to_perform = ['collocated_nodes', 'element_volumes'] - all_checks_filter.setChecksToPerform(checks_to_perform) - + checks_to_perform = [ 'collocated_nodes', 'element_volumes' ] + all_checks_filter.setChecksToPerform( checks_to_perform ) + # Verify by checking if the filter state changed - assert hasattr(all_checks_filter, 'm_checks_to_perform') + assert hasattr( all_checks_filter, 'm_checks_to_perform' ) assert all_checks_filter.m_checks_to_perform == checks_to_perform - - def test_set_check_parameter(self, all_checks_filter: AllChecks): + + def test_set_check_parameter( self, all_checks_filter: AllChecks ): """Test setting parameters for specific checks.""" # Set a tolerance parameter for collocated nodes - all_checks_filter.setCheckParameter('collocated_nodes', 'tolerance', 1e-6) - + all_checks_filter.setCheckParameter( 'collocated_nodes', 'tolerance', 1e-6 ) + # Set minimum volume for element volumes - all_checks_filter.setCheckParameter('element_volumes', 'min_volume', 0.001) - + all_checks_filter.setCheckParameter( 'element_volumes', 'min_volume', 0.001 ) + # Verify parameters are stored assert 'collocated_nodes' in all_checks_filter.m_check_parameters - assert all_checks_filter.m_check_parameters['collocated_nodes']['tolerance'] == 1e-6 - assert all_checks_filter.m_check_parameters['element_volumes']['min_volume'] == 0.001 - - def test_set_all_checks_parameter(self, all_checks_filter: AllChecks): + assert all_checks_filter.m_check_parameters[ 'collocated_nodes' ][ 'tolerance' ] == 1e-6 + assert all_checks_filter.m_check_parameters[ 'element_volumes' ][ 'min_volume' ] == 0.001 + + def test_set_all_checks_parameter( self, all_checks_filter: AllChecks ): """Test setting a parameter that applies to all compatible checks.""" # Set tolerance for all checks that support it - all_checks_filter.setAllChecksParameter('tolerance', 1e-8) - + all_checks_filter.setAllChecksParameter( 'tolerance', 1e-8 ) + # Check that tolerance was set for checks that support it if 'collocated_nodes' in all_checks_filter.m_check_parameters: - assert all_checks_filter.m_check_parameters['collocated_nodes']['tolerance'] == 1e-8 - - def test_process_mesh_with_issues(self, all_checks_filter: AllChecks, simple_mesh_with_issues: vtkUnstructuredGrid): + assert all_checks_filter.m_check_parameters[ 'collocated_nodes' ][ 'tolerance' ] == 1e-8 + + def test_process_mesh_with_issues( self, all_checks_filter: AllChecks, + simple_mesh_with_issues: vtkUnstructuredGrid ): """Test processing a mesh with known issues.""" # Configure for specific checks - all_checks_filter.setChecksToPerform(['collocated_nodes', 'element_volumes']) - all_checks_filter.setCheckParameter('collocated_nodes', 'tolerance', 1e-12) - all_checks_filter.setCheckParameter('element_volumes', 'min_volume', 1e-3) - + all_checks_filter.setChecksToPerform( [ 'collocated_nodes', 'element_volumes' ] ) + all_checks_filter.setCheckParameter( 'collocated_nodes', 'tolerance', 1e-12 ) + all_checks_filter.setCheckParameter( 'element_volumes', 'min_volume', 1e-3 ) + # Process the mesh - all_checks_filter.SetInputDataObject(0, simple_mesh_with_issues) + all_checks_filter.SetInputDataObject( 0, simple_mesh_with_issues ) all_checks_filter.Update() - + # Check results results = all_checks_filter.getCheckResults() - + assert 'collocated_nodes' in results assert 'element_volumes' in results - + # Check that collocated nodes were found - collocated_result = results['collocated_nodes'] - assert hasattr(collocated_result, 'nodes_buckets') + collocated_result = results[ 'collocated_nodes' ] + assert hasattr( collocated_result, 'nodes_buckets' ) # We expect to find collocated nodes (points 0 and 3) - assert len(collocated_result.nodes_buckets) > 0 - + assert len( collocated_result.nodes_buckets ) > 0 + # Check that volume issues were detected - volume_result = results['element_volumes'] - assert hasattr(volume_result, 'element_volumes') - - def test_process_clean_mesh(self, all_checks_filter: AllChecks, clean_mesh: vtkUnstructuredGrid): + volume_result = results[ 'element_volumes' ] + assert hasattr( volume_result, 'element_volumes' ) + + def test_process_clean_mesh( self, all_checks_filter: AllChecks, clean_mesh: vtkUnstructuredGrid ): """Test processing a clean mesh without issues.""" # Configure checks - all_checks_filter.setChecksToPerform(['collocated_nodes', 'element_volumes']) - all_checks_filter.setCheckParameter('collocated_nodes', 'tolerance', 1e-12) - all_checks_filter.setCheckParameter('element_volumes', 'min_volume', 1e-6) - + all_checks_filter.setChecksToPerform( [ 'collocated_nodes', 'element_volumes' ] ) + all_checks_filter.setCheckParameter( 'collocated_nodes', 'tolerance', 1e-12 ) + all_checks_filter.setCheckParameter( 'element_volumes', 'min_volume', 1e-6 ) + # Process the mesh - all_checks_filter.SetInputDataObject(0, clean_mesh) + all_checks_filter.SetInputDataObject( 0, clean_mesh ) all_checks_filter.Update() - + # Check results results = all_checks_filter.getCheckResults() - + assert 'collocated_nodes' in results assert 'element_volumes' in results - + # Check that no issues were found - collocated_result = results['collocated_nodes'] - assert len(collocated_result.nodes_buckets) == 0 - - volume_result = results['element_volumes'] - assert len(volume_result.element_volumes) == 0 - - def test_output_mesh_unchanged(self, all_checks_filter: AllChecks, clean_mesh: vtkUnstructuredGrid): + collocated_result = results[ 'collocated_nodes' ] + assert len( collocated_result.nodes_buckets ) == 0 + + volume_result = results[ 'element_volumes' ] + assert len( volume_result.element_volumes ) == 0 + + def test_output_mesh_unchanged( self, all_checks_filter: AllChecks, clean_mesh: vtkUnstructuredGrid ): """Test that the output mesh is unchanged from the input (checks don't modify geometry).""" original_num_points = clean_mesh.GetNumberOfPoints() original_num_cells = clean_mesh.GetNumberOfCells() - + # Process the mesh - all_checks_filter.SetInputDataObject(0, clean_mesh) + all_checks_filter.SetInputDataObject( 0, clean_mesh ) all_checks_filter.Update() - + # Get output mesh output_mesh = all_checks_filter.getGrid() - + # Verify structure is unchanged assert output_mesh.GetNumberOfPoints() == original_num_points assert output_mesh.GetNumberOfCells() == original_num_cells - + # Verify points are the same - for i in range(original_num_points): - original_point = clean_mesh.GetPoint(i) - output_point = output_mesh.GetPoint(i) + for i in range( original_num_points ): + original_point = clean_mesh.GetPoint( i ) + output_point = output_mesh.GetPoint( i ) assert original_point == output_point class TestMainChecks: """Test class for MainChecks filter functionality.""" - - def test_filter_creation(self, main_checks_filter: MainChecks): + + def test_filter_creation( self, main_checks_filter: MainChecks ): """Test that MainChecks filter can be created successfully.""" assert main_checks_filter is not None - assert hasattr(main_checks_filter, 'getAvailableChecks') - assert hasattr(main_checks_filter, 'setChecksToPerform') - assert hasattr(main_checks_filter, 'setCheckParameter') - - def test_available_checks(self, main_checks_filter: MainChecks): + assert hasattr( main_checks_filter, 'getAvailableChecks' ) + assert hasattr( main_checks_filter, 'setChecksToPerform' ) + assert hasattr( main_checks_filter, 'setCheckParameter' ) + + def test_available_checks( self, main_checks_filter: MainChecks ): """Test that main checks are available (subset of all checks).""" available_checks = main_checks_filter.getAvailableChecks() - + # MainChecks should have a subset of checks - expected_main_checks = [ - 'collocated_nodes', - 'element_volumes', - 'self_intersecting_elements' - ] - + expected_main_checks = [ 'collocated_nodes', 'element_volumes', 'self_intersecting_elements' ] + for check in expected_main_checks: assert check in available_checks, f"Main check '{check}' should be available" - - def test_process_mesh(self, main_checks_filter: MainChecks, simple_mesh_with_issues: vtkUnstructuredGrid): + + def test_process_mesh( self, main_checks_filter: MainChecks, simple_mesh_with_issues: vtkUnstructuredGrid ): """Test processing a mesh with MainChecks.""" # Process the mesh with default configuration - main_checks_filter.SetInputDataObject(0, simple_mesh_with_issues) + main_checks_filter.SetInputDataObject( 0, simple_mesh_with_issues ) main_checks_filter.Update() - + # Check that results are obtained results = main_checks_filter.getCheckResults() - assert isinstance(results, dict) - assert len(results) > 0 - + assert isinstance( results, dict ) + assert len( results ) > 0 + # Check that main checks were performed available_checks = main_checks_filter.getAvailableChecks() for check_name in available_checks: if check_name in results: - result = results[check_name] + result = results[ check_name ] assert result is not None class TestFilterComparison: """Test class for comparing AllChecks and MainChecks filters.""" - - def test_all_checks_vs_main_checks_availability(self, all_checks_filter: AllChecks, main_checks_filter: MainChecks): + + def test_all_checks_vs_main_checks_availability( self, all_checks_filter: AllChecks, + main_checks_filter: MainChecks ): """Test that MainChecks is a subset of AllChecks.""" - all_checks = set(all_checks_filter.getAvailableChecks()) - main_checks = set(main_checks_filter.getAvailableChecks()) - + all_checks = set( all_checks_filter.getAvailableChecks() ) + main_checks = set( main_checks_filter.getAvailableChecks() ) + # MainChecks should be a subset of AllChecks - assert main_checks.issubset(all_checks), "MainChecks should be a subset of AllChecks" - + assert main_checks.issubset( all_checks ), "MainChecks should be a subset of AllChecks" + # AllChecks should have more checks than MainChecks - assert len(all_checks) >= len(main_checks), "AllChecks should have at least as many checks as MainChecks" - - def test_parameter_consistency(self, all_checks_filter: AllChecks, main_checks_filter: MainChecks): + assert len( all_checks ) >= len( main_checks ), "AllChecks should have at least as many checks as MainChecks" + + def test_parameter_consistency( self, all_checks_filter: AllChecks, main_checks_filter: MainChecks ): """Test that parameter handling is consistent between filters.""" # Get common checks - all_checks = set(all_checks_filter.getAvailableChecks()) - main_checks = set(main_checks_filter.getAvailableChecks()) - common_checks = all_checks.intersection(main_checks) - + all_checks = set( all_checks_filter.getAvailableChecks() ) + main_checks = set( main_checks_filter.getAvailableChecks() ) + common_checks = all_checks.intersection( main_checks ) + # Test that default parameters are the same for common checks for check_name in common_checks: - all_defaults = all_checks_filter.getDefaultParameters(check_name) - main_defaults = main_checks_filter.getDefaultParameters(check_name) + all_defaults = all_checks_filter.getDefaultParameters( check_name ) + main_defaults = main_checks_filter.getDefaultParameters( check_name ) assert all_defaults == main_defaults, f"Default parameters should be the same for '{check_name}'" class TestErrorHandling: """Test class for error handling and edge cases.""" - - def test_invalid_check_name(self, all_checks_filter: AllChecks): + + def test_invalid_check_name( self, all_checks_filter: AllChecks ): """Test handling of invalid check names.""" # Try to set an invalid check - invalid_checks = ['nonexistent_check'] - all_checks_filter.setChecksToPerform(invalid_checks) - + invalid_checks = [ 'nonexistent_check' ] + all_checks_filter.setChecksToPerform( invalid_checks ) + # The filter should handle this gracefully # (The actual behavior depends on implementation - it might warn or ignore) assert all_checks_filter.m_checks_to_perform == invalid_checks - - def test_invalid_parameter_name(self, all_checks_filter: AllChecks): + + def test_invalid_parameter_name( self, all_checks_filter: AllChecks ): """Test handling of invalid parameter names.""" # Try to set an invalid parameter - all_checks_filter.setCheckParameter('collocated_nodes', 'invalid_param', 123) - + all_checks_filter.setCheckParameter( 'collocated_nodes', 'invalid_param', 123 ) + # This should not crash the filter assert 'collocated_nodes' in all_checks_filter.m_check_parameters - assert 'invalid_param' in all_checks_filter.m_check_parameters['collocated_nodes'] - - def test_empty_mesh(self, all_checks_filter: AllChecks): + assert 'invalid_param' in all_checks_filter.m_check_parameters[ 'collocated_nodes' ] + + def test_empty_mesh( self, all_checks_filter: AllChecks ): """Test handling of empty mesh.""" # Create an empty mesh empty_mesh = vtkUnstructuredGrid() - empty_mesh.SetPoints(vtkPoints()) - + empty_mesh.SetPoints( vtkPoints() ) + # Process the empty mesh - all_checks_filter.setChecksToPerform(['collocated_nodes']) - all_checks_filter.SetInputDataObject(0, empty_mesh) + all_checks_filter.setChecksToPerform( [ 'collocated_nodes' ] ) + all_checks_filter.SetInputDataObject( 0, empty_mesh ) all_checks_filter.Update() - + # Should complete without error results = all_checks_filter.getCheckResults() - assert isinstance(results, dict) + assert isinstance( results, dict ) diff --git a/geos-mesh/tests/test_generate_fractures.py b/geos-mesh/tests/test_generate_fractures.py index b22c89a89..fb6fd978f 100644 --- a/geos-mesh/tests/test_generate_fractures.py +++ b/geos-mesh/tests/test_generate_fractures.py @@ -11,7 +11,6 @@ from geos.mesh.doctor.filters.GenerateFractures import GenerateFractures from geos.mesh.utils.genericHelpers import to_vtk_id_list - FaceNodesCoords = tuple[ tuple[ float ] ] IDMatrix = Sequence[ Sequence[ int ] ] diff --git a/geos-mesh/tests/test_supported_elements.py b/geos-mesh/tests/test_supported_elements.py index abdfb31c3..297c3899b 100644 --- a/geos-mesh/tests/test_supported_elements.py +++ b/geos-mesh/tests/test_supported_elements.py @@ -125,14 +125,16 @@ def create_simple_tetra_grid(): """Create a simple tetrahedral grid for testing""" # Create an unstructured grid points_tetras: vtkPoints = vtkPoints() - points_tetras_coords: list[ tuple[ float ] ] = [ ( 1.0, 0.5, 0.0 ), # point0 - ( 1.0, 0.0, 1.0 ), - ( 1.0, 1.0, 1.0 ), - ( 0.0, 0.5, 0.5 ), - ( 2.0, 0.5, 0.5 ), - ( 1.0, 0.5, 2.0 ), # point5 - ( 0.0, 0.5, 1.5 ), - ( 2.0, 0.5, 1.5 ) ] + points_tetras_coords: list[ tuple[ float ] ] = [ + ( 1.0, 0.5, 0.0 ), # point0 + ( 1.0, 0.0, 1.0 ), + ( 1.0, 1.0, 1.0 ), + ( 0.0, 0.5, 0.5 ), + ( 2.0, 0.5, 0.5 ), + ( 1.0, 0.5, 2.0 ), # point5 + ( 0.0, 0.5, 1.5 ), + ( 2.0, 0.5, 1.5 ) + ] for point_tetra in points_tetras_coords: points_tetras.InsertNextPoint( point_tetra ) @@ -176,24 +178,26 @@ def create_mixed_grid(): """Create a grid with supported and unsupported cell types, 4 Hexahedrons with 2 quad fracs vertical""" # Create an unstructured grid four_hexs_points: vtkPoints = vtkPoints() - four_hexs_points_coords: list[ tuple[ float ] ] = [ ( 0.0, 0.0, 0.0 ), # point0 - ( 1.0, 0.0, 0.0 ), # point1 - ( 2.0, 0.0, 0.0 ), # point2 - ( 0.0, 1.0, 0.0 ), # point3 - ( 1.0, 1.0, 0.0 ), # point4 - ( 2.0, 1.0, 0.0 ), # point5 - ( 0.0, 0.0, 1.0 ), # point6 - ( 1.0, 0.0, 1.0 ), # point7 - ( 2.0, 0.0, 1.0 ), # point8 - ( 0.0, 1.0, 1.0 ), # point9 - ( 1.0, 1.0, 1.0 ), # point10 - ( 2.0, 1.0, 1.0 ), # point11 - ( 0.0, 0.0, 2.0 ), # point12 - ( 1.0, 0.0, 2.0 ), # point13 - ( 2.0, 0.0, 2.0 ), # point14 - ( 0.0, 1.0, 2.0 ), # point15 - ( 1.0, 1.0, 2.0 ), # point16 - ( 2.0, 1.0, 2.0 ) ] + four_hexs_points_coords: list[ tuple[ float ] ] = [ + ( 0.0, 0.0, 0.0 ), # point0 + ( 1.0, 0.0, 0.0 ), # point1 + ( 2.0, 0.0, 0.0 ), # point2 + ( 0.0, 1.0, 0.0 ), # point3 + ( 1.0, 1.0, 0.0 ), # point4 + ( 2.0, 1.0, 0.0 ), # point5 + ( 0.0, 0.0, 1.0 ), # point6 + ( 1.0, 0.0, 1.0 ), # point7 + ( 2.0, 0.0, 1.0 ), # point8 + ( 0.0, 1.0, 1.0 ), # point9 + ( 1.0, 1.0, 1.0 ), # point10 + ( 2.0, 1.0, 1.0 ), # point11 + ( 0.0, 0.0, 2.0 ), # point12 + ( 1.0, 0.0, 2.0 ), # point13 + ( 2.0, 0.0, 2.0 ), # point14 + ( 0.0, 1.0, 2.0 ), # point15 + ( 1.0, 1.0, 2.0 ), # point16 + ( 2.0, 1.0, 2.0 ) + ] for four_hexs_point in four_hexs_points_coords: four_hexs_points.InsertNextPoint( four_hexs_point ) @@ -335,57 +339,57 @@ def create_unsupported_polyhedron_grid(): # for j in range( 4, 6 ): # assert unsupported_array.GetValue( j ) == 1 # Quad should not be supported - # TODO Needs parallelism to work - # def test_unsupported_polyhedron( self ): - # """Test a grid with unsupported polyhedron""" - # # Create grid with unsupported polyhedron - # grid = create_unsupported_polyhedron_grid() - # # Apply the filter with painting enabled - # filter = SupportedElements() - # filter.m_logger.critical( "test_unsupported_polyhedron" ) - # filter.SetInputDataObject( grid ) - # filter.setPaintUnsupportedPolyhedrons( 1 ) - # filter.Update() - # result = filter.getGrid() - # assert result is not None - # # Verify the array was added - # polyhedron_array = result.GetCellData().GetArray( "IsUnsupportedPolyhedron" ) - # assert polyhedron_array is None - # # Since we created an unsupported polyhedron, it should be marked - # assert polyhedron_array.GetValue( 0 ) == 1 - - # def test_paint_flags( self ): - # """Test setting invalid paint flags""" - # filter = SupportedElements() - # # Should log an error but not raise an exception - # filter.setPaintUnsupportedElementTypes( 2 ) # Invalid value - # filter.setPaintUnsupportedPolyhedrons( 2 ) # Invalid value - # # Values should remain unchanged - # assert filter.m_paintUnsupportedElementTypes == 0 - # assert filter.m_paintUnsupportedPolyhedrons == 0 - - # def test_set_chunk_size( self ): - # """Test that setChunkSize properly updates the chunk size""" - # # Create filter instance - # filter = SupportedElements() - # # Note the initial value - # initial_chunk_size = filter.m_chunk_size - # # Set a new chunk size - # new_chunk_size = 100 - # filter.setChunkSize( new_chunk_size ) - # # Verify the chunk size was updated - # assert filter.m_chunk_size == new_chunk_size - # assert filter.m_chunk_size != initial_chunk_size - - # def test_set_num_proc( self ): - # """Test that setNumProc properly updates the number of processors""" - # # Create filter instance - # filter = SupportedElements() - # # Note the initial value - # initial_num_proc = filter.m_num_proc - # # Set a new number of processors - # new_num_proc = 4 - # filter.setNumProc( new_num_proc ) - # # Verify the number of processors was updated - # assert filter.m_num_proc == new_num_proc - # assert filter.m_num_proc != initial_num_proc +# TODO Needs parallelism to work +# def test_unsupported_polyhedron( self ): +# """Test a grid with unsupported polyhedron""" +# # Create grid with unsupported polyhedron +# grid = create_unsupported_polyhedron_grid() +# # Apply the filter with painting enabled +# filter = SupportedElements() +# filter.m_logger.critical( "test_unsupported_polyhedron" ) +# filter.SetInputDataObject( grid ) +# filter.setPaintUnsupportedPolyhedrons( 1 ) +# filter.Update() +# result = filter.getGrid() +# assert result is not None +# # Verify the array was added +# polyhedron_array = result.GetCellData().GetArray( "IsUnsupportedPolyhedron" ) +# assert polyhedron_array is None +# # Since we created an unsupported polyhedron, it should be marked +# assert polyhedron_array.GetValue( 0 ) == 1 + +# def test_paint_flags( self ): +# """Test setting invalid paint flags""" +# filter = SupportedElements() +# # Should log an error but not raise an exception +# filter.setPaintUnsupportedElementTypes( 2 ) # Invalid value +# filter.setPaintUnsupportedPolyhedrons( 2 ) # Invalid value +# # Values should remain unchanged +# assert filter.m_paintUnsupportedElementTypes == 0 +# assert filter.m_paintUnsupportedPolyhedrons == 0 + +# def test_set_chunk_size( self ): +# """Test that setChunkSize properly updates the chunk size""" +# # Create filter instance +# filter = SupportedElements() +# # Note the initial value +# initial_chunk_size = filter.m_chunk_size +# # Set a new chunk size +# new_chunk_size = 100 +# filter.setChunkSize( new_chunk_size ) +# # Verify the chunk size was updated +# assert filter.m_chunk_size == new_chunk_size +# assert filter.m_chunk_size != initial_chunk_size + +# def test_set_num_proc( self ): +# """Test that setNumProc properly updates the number of processors""" +# # Create filter instance +# filter = SupportedElements() +# # Note the initial value +# initial_num_proc = filter.m_num_proc +# # Set a new number of processors +# new_num_proc = 4 +# filter.setNumProc( new_num_proc ) +# # Verify the number of processors was updated +# assert filter.m_num_proc == new_num_proc +# assert filter.m_num_proc != initial_num_proc From ce1ca679b52c871b2922d6ab74c2a9f04c809e9f Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 13 Aug 2025 15:11:14 -0700 Subject: [PATCH 24/52] Update vtkIO to be more robust, reduce duplication --- geos-mesh/src/geos/mesh/io/vtkIO.py | 313 +++++++++++++++------------- 1 file changed, 172 insertions(+), 141 deletions(-) diff --git a/geos-mesh/src/geos/mesh/io/vtkIO.py b/geos-mesh/src/geos/mesh/io/vtkIO.py index 1b93648a2..16543a8fa 100644 --- a/geos-mesh/src/geos/mesh/io/vtkIO.py +++ b/geos-mesh/src/geos/mesh/io/vtkIO.py @@ -1,16 +1,17 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. # SPDX-FileContributor: Alexandre Benedicto - -import os.path -import logging from dataclasses import dataclass +from enum import Enum +import os.path from typing import Optional -from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, vtkStructuredGrid, vtkPointSet -from vtkmodules.vtkIOLegacy import vtkUnstructuredGridWriter, vtkUnstructuredGridReader -from vtkmodules.vtkIOXML import ( vtkXMLUnstructuredGridReader, vtkXMLUnstructuredGridWriter, - vtkXMLStructuredGridReader, vtkXMLPUnstructuredGridReader, +from vtkmodules.vtkCommonDataModel import vtkPointSet, vtkUnstructuredGrid +from vtkmodules.vtkIOCore import vtkWriter +from vtkmodules.vtkIOLegacy import vtkDataReader, vtkUnstructuredGridWriter, vtkUnstructuredGridReader +from vtkmodules.vtkIOXML import ( vtkXMLDataReader, vtkXMLUnstructuredGridReader, vtkXMLUnstructuredGridWriter, + vtkXMLWriter, vtkXMLStructuredGridReader, vtkXMLPUnstructuredGridReader, vtkXMLPStructuredGridReader, vtkXMLStructuredGridWriter ) +from geos.utils.Logger import getLogger __doc__ = """ Input and Ouput methods for VTK meshes: @@ -18,175 +19,205 @@ - VTK, VTS, VTU writers """ +io_logger = getLogger( "IO for geos-mesh" ) +io_logger.propagate = False + + +class VtkFormat( Enum ): + """Enumeration for supported VTK file formats and their extensions.""" + VTK = ".vtk" + VTS = ".vts" + VTU = ".vtu" + PVTU = ".pvtu" + PVTS = ".pvts" + + +# Centralized mapping of formats to their corresponding reader classes +READER_MAP: dict[ VtkFormat, vtkDataReader | vtkXMLDataReader ] = { + VtkFormat.VTK: vtkUnstructuredGridReader, + VtkFormat.VTS: vtkXMLStructuredGridReader, + VtkFormat.VTU: vtkXMLUnstructuredGridReader, + VtkFormat.PVTU: vtkXMLPUnstructuredGridReader, + VtkFormat.PVTS: vtkXMLPStructuredGridReader +} + +# Centralized mapping of formats to their corresponding writer classes +WRITER_MAP: dict[ VtkFormat, vtkWriter | vtkXMLWriter ] = { + VtkFormat.VTK: vtkUnstructuredGridWriter, + VtkFormat.VTS: vtkXMLStructuredGridWriter, + VtkFormat.VTU: vtkXMLUnstructuredGridWriter, +} + @dataclass( frozen=True ) class VtkOutput: - output: str - is_data_mode_binary: bool + """Configuration for writing a VTK file.""" + output_path: str + is_binary: bool = True -def __read_vtk( vtk_input_file: str ) -> Optional[ vtkUnstructuredGrid ]: - reader = vtkUnstructuredGridReader() - logging.info( f"Testing file format \"{vtk_input_file}\" using legacy format reader..." ) - reader.SetFileName( vtk_input_file ) - if reader.IsFileUnstructuredGrid(): - logging.info( f"Reader matches. Reading file \"{vtk_input_file}\" using legacy format reader." ) - reader.Update() - return reader.GetOutput() - else: - logging.info( "Reader did not match the input file format." ) - return None +def _read_data( filepath: str, reader_class: vtkDataReader | vtkXMLDataReader ) -> Optional[ vtkPointSet ]: + """Generic helper to read a VTK file using a specific reader class.""" + reader: vtkDataReader | vtkXMLDataReader = reader_class() + io_logger.info( f"Attempting to read '{filepath}' with {reader_class.__name__}..." ) + # VTK readers have different methods to check file compatibility + can_read: bool = False + if hasattr( reader, 'CanReadFile' ): + can_read = reader.CanReadFile( filepath ) + elif hasattr( reader, 'IsFileUnstructuredGrid' ): # Legacy reader + can_read = reader.IsFileUnstructuredGrid() -def __read_vts( vtk_input_file: str ) -> Optional[ vtkStructuredGrid ]: - reader = vtkXMLStructuredGridReader() - logging.info( f"Testing file format \"{vtk_input_file}\" using XML format reader..." ) - if reader.CanReadFile( vtk_input_file ): - reader.SetFileName( vtk_input_file ) - logging.info( f"Reader matches. Reading file \"{vtk_input_file}\" using XML format reader." ) + if can_read: + reader.SetFileName( filepath ) reader.Update() + io_logger.info( "Read successful." ) return reader.GetOutput() - else: - logging.info( "Reader did not match the input file format." ) - return None + io_logger.info( "Reader did not match the file format." ) + return None -def __read_vtu( vtk_input_file: str ) -> Optional[ vtkUnstructuredGrid ]: - reader = vtkXMLUnstructuredGridReader() - logging.info( f"Testing file format \"{vtk_input_file}\" using XML format reader..." ) - if reader.CanReadFile( vtk_input_file ): - reader.SetFileName( vtk_input_file ) - logging.info( f"Reader matches. Reading file \"{vtk_input_file}\" using XML format reader." ) - reader.Update() - return reader.GetOutput() - else: - logging.info( "Reader did not match the input file format." ) - return None +def _write_data( mesh: vtkPointSet, writer_class: vtkWriter | vtkXMLWriter, output_path: str, is_binary: bool ) -> int: + """Generic helper to write a VTK file using a specific writer class.""" + io_logger.info( f"Writing mesh to '{output_path}' using {writer_class.__name__}..." ) + writer: vtkWriter | vtkXMLWriter = writer_class() + writer.SetFileName( output_path ) + writer.SetInputData( mesh ) -def __read_pvts( vtk_input_file: str ) -> Optional[ vtkStructuredGrid ]: - reader = vtkXMLPStructuredGridReader() - logging.info( f"Testing file format \"{vtk_input_file}\" using XML format reader..." ) - if reader.CanReadFile( vtk_input_file ): - reader.SetFileName( vtk_input_file ) - logging.info( f"Reader matches. Reading file \"{vtk_input_file}\" using XML format reader." ) - reader.Update() - return reader.GetOutput() - else: - logging.info( "Reader did not match the input file format." ) - return None + # Set data mode only for XML writers that support it + if hasattr( writer, 'SetDataModeToBinary' ): + if is_binary: + writer.SetDataModeToBinary() + io_logger.info( "Data mode set to Binary." ) + else: + writer.SetDataModeToAscii() + io_logger.info( "Data mode set to ASCII." ) + return writer.Write() -def __read_pvtu( vtk_input_file: str ) -> Optional[ vtkUnstructuredGrid ]: - reader = vtkXMLPUnstructuredGridReader() - logging.info( f"Testing file format \"{vtk_input_file}\" using XML format reader..." ) - if reader.CanReadFile( vtk_input_file ): - reader.SetFileName( vtk_input_file ) - logging.info( f"Reader matches. Reading file \"{vtk_input_file}\" using XML format reader." ) - reader.Update() - return reader.GetOutput() - else: - logging.info( "Reader did not match the input file format." ) - return None +def read_mesh( filepath: str ) -> vtkPointSet: + """ + Reads a VTK file, automatically detecting the format. -def read_mesh( vtk_input_file: str ) -> vtkPointSet: - """Read vtk file and build either an unstructured grid or a structured grid from it. + It first tries the reader associated with the file extension, then falls + back to trying all available readers if the first attempt fails. Args: - vtk_input_file (str): The file name. Extension will be used to guess file format\ - If first guess fails, other available readers will be tried. + filepath (str): The path to the VTK file. Raises: - ValueError: Invalid file path error - ValueError: No appropriate reader available for the file format + FileNotFoundError: If the input file does not exist. + ValueError: If no suitable reader can be found for the file. Returns: - vtkPointSet: Mesh read + vtkPointSet: The resulting mesh data. """ - if not os.path.exists( vtk_input_file ): - err_msg: str = f"Invalid file path. Could not read \"{vtk_input_file}\"." - logging.error( err_msg ) - raise ValueError( err_msg ) - file_extension = os.path.splitext( vtk_input_file )[ -1 ] - extension_to_reader = { - ".vtk": __read_vtk, - ".vts": __read_vts, - ".vtu": __read_vtu, - ".pvtu": __read_pvtu, - ".pvts": __read_pvts - } - # Testing first the reader that should match - if file_extension in extension_to_reader: - output_mesh = extension_to_reader.pop( file_extension )( vtk_input_file ) - if output_mesh: - return output_mesh - # If it does not match, then test all the others. - for reader in extension_to_reader.values(): - output_mesh = reader( vtk_input_file ) - if output_mesh: - return output_mesh - # No reader did work. - err_msg = f"Could not find the appropriate VTK reader for file \"{vtk_input_file}\"." - logging.error( err_msg ) - raise ValueError( err_msg ) - - -def __write_vtk( mesh: vtkUnstructuredGrid, output: str ) -> int: - logging.info( f"Writing mesh into file \"{output}\" using legacy format." ) - writer = vtkUnstructuredGridWriter() - writer.SetFileName( output ) - writer.SetInputData( mesh ) - return writer.Write() + if not os.path.exists( filepath ): + raise FileNotFoundError( f"Invalid file path: '{filepath}' does not exist." ) + _, extension = os.path.splitext( filepath ) + output_mesh: Optional[ vtkPointSet ] = None -def __write_vts( mesh: vtkStructuredGrid, output: str, toBinary: bool = False ) -> int: - logging.info( f"Writing mesh into file \"{output}\" using XML format." ) - writer = vtkXMLStructuredGridWriter() - writer.SetFileName( output ) - writer.SetInputData( mesh ) - writer.SetDataModeToBinary() if toBinary else writer.SetDataModeToAscii() - return writer.Write() + # 1. Try the reader associated with the file extension first + try: + file_format = VtkFormat( extension ) + if file_format in READER_MAP: + reader_class = READER_MAP[ file_format ] + output_mesh = _read_data( filepath, reader_class ) + except ValueError: + io_logger.warning( f"Unknown file extension '{extension}'. Trying all readers." ) + # 2. If the first attempt failed or extension was unknown, try all readers + if not output_mesh: + for reader_class in set( READER_MAP.values() ): # Use set to avoid duplicates + output_mesh = _read_data( filepath, reader_class ) + if output_mesh: + break -def __write_vtu( mesh: vtkUnstructuredGrid, output: str, toBinary: bool = False ) -> int: - logging.info( f"Writing mesh into file \"{output}\" using XML format." ) - writer = vtkXMLUnstructuredGridWriter() - writer.SetFileName( output ) - writer.SetInputData( mesh ) - writer.SetDataModeToBinary() if toBinary else writer.SetDataModeToAscii() - return writer.Write() + if not output_mesh: + raise ValueError( f"Could not find a suitable reader for '{filepath}'." ) + + return output_mesh + + +def read_unstructured_grid( filepath: str ) -> vtkUnstructuredGrid: + """ + Reads a VTK file and ensures it is a vtkUnstructuredGrid. + + This function uses the general `read_mesh` to load the data and then + validates its type. + + Args: + filepath (str): The path to the VTK file. + + Raises: + FileNotFoundError: If the input file does not exist. + ValueError: If no suitable reader can be found for the file. + TypeError: If the file is read successfully but is not a vtkUnstructuredGrid. + + Returns: + vtkUnstructuredGrid: The resulting unstructured grid data. + """ + io_logger.info(f"Reading file '{filepath}' and expecting vtkUnstructuredGrid.") + + # Reuse the generic mesh reader + mesh = read_mesh(filepath) + # Check the type of the resulting mesh + if not isinstance(mesh, vtkUnstructuredGrid): + error_msg = ( + f"File '{filepath}' was read successfully, but it is of type " + f"'{type(mesh).__name__}', not the expected vtkUnstructuredGrid." + ) + io_logger.error(error_msg) + raise TypeError(error_msg) -def write_mesh( mesh: vtkPointSet, vtk_output: VtkOutput, canOverwrite: bool = False ) -> int: - """Write mesh to disk. + io_logger.info("Validation successful. Mesh is a vtkUnstructuredGrid.") + return mesh + + +def write_mesh( mesh: vtkPointSet, vtk_output: VtkOutput, can_overwrite: bool = False ) -> int: + """ + Writes a vtkPointSet to a file. - Nothing is done if file already exists. + The format is determined by the file extension in `VtkOutput.output_path`. Args: - mesh (vtkPointSet): Grid to write - vtk_output (VtkOutput): File path. File extension will be used to select VTK file format - canOverwrite (bool, optional): Authorize overwriting the file. Defaults to False. + mesh (vtkPointSet): The grid data to write. + vtk_output (VtkOutput): Configuration for the output file. + can_overwrite (bool, optional): If False, raises an error if the file + already exists. Defaults to False. Raises: - ValueError: Invalid VTK format. + FileExistsError: If the output file exists and `can_overwrite` is False. + ValueError: If the file extension is not a supported write format. + RuntimeError: If the VTK writer fails to write the file. Returns: - int: 0 if success + int: Returns 1 on success, consistent with the VTK writer's return code. """ - if os.path.exists( vtk_output.output ) and canOverwrite: - logging.error( f"File \"{vtk_output.output}\" already exists, nothing done." ) - return 1 - file_extension = os.path.splitext( vtk_output.output )[ -1 ] - if file_extension == ".vtk": - success_code = __write_vtk( mesh, vtk_output.output ) - elif file_extension == ".vts": - success_code = __write_vts( mesh, vtk_output.output, vtk_output.is_data_mode_binary ) - elif file_extension == ".vtu": - success_code = __write_vtu( mesh, vtk_output.output, vtk_output.is_data_mode_binary ) - else: - # No writer found did work. Dying. - err_msg = f"Could not find the appropriate VTK writer for extension \"{file_extension}\"." - logging.error( err_msg ) - raise ValueError( err_msg ) - return 0 if success_code else 2 # the Write member function return 1 in case of success, 0 otherwise. + if os.path.exists( vtk_output.output_path ) and not can_overwrite: + raise FileExistsError( + f"File '{vtk_output.output_path}' already exists. Set can_overwrite=True to replace it." ) + + _, extension = os.path.splitext( vtk_output.output_path ) + + try: + file_format = VtkFormat( extension ) + if file_format not in WRITER_MAP: + raise ValueError( f"Writing to extension '{extension}' is not supported." ) + + writer_class = WRITER_MAP[ file_format ] + success_code = _write_data( mesh, writer_class, vtk_output.output_path, vtk_output.is_binary ) + + if not success_code: + raise RuntimeError( f"VTK writer failed to write file '{vtk_output.output_path}'." ) + + io_logger.info( f"Successfully wrote mesh to '{vtk_output.output_path}'." ) + return success_code # VTK writers return 1 for success + + except ValueError as e: + io_logger.error( e ) + raise From bfe251793400009d405b665c4862e95f53957c09 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 13 Aug 2025 23:14:46 -0700 Subject: [PATCH 25/52] Update mesh_action to only be given vtkUnstructuredGrid --- docs/geos_mesh_docs/doctor.rst | 53 +++++++++++++++++-- .../geos/mesh/doctor/actions/all_checks.py | 17 +++--- .../mesh/doctor/actions/check_fractures.py | 4 +- .../mesh/doctor/actions/collocated_nodes.py | 12 ++--- .../mesh/doctor/actions/element_volumes.py | 8 +-- .../doctor/actions/fix_elements_orderings.py | 7 +-- .../mesh/doctor/actions/generate_fractures.py | 4 +- .../doctor/actions/generate_global_ids.py | 7 +-- .../geos/mesh/doctor/actions/non_conformal.py | 4 +- .../actions/self_intersecting_elements.py | 14 ++--- .../mesh/doctor/actions/supported_elements.py | 4 +- 11 files changed, 92 insertions(+), 42 deletions(-) diff --git a/docs/geos_mesh_docs/doctor.rst b/docs/geos_mesh_docs/doctor.rst index 612b06680..ba9db40fe 100644 --- a/docs/geos_mesh_docs/doctor.rst +++ b/docs/geos_mesh_docs/doctor.rst @@ -1,7 +1,7 @@ Mesh Doctor ---------------- +----------- -``mesh-doctor`` is a ``python`` executable that can be used through the command line to perform various checks, validations, and tiny fixes to the ``vtk`` mesh that are meant to be used in ``geos``. +``mesh-doctor`` is a ``python`` executable that can be used through the command line to perform various checks, validations, and tiny fixes to the ``vtkUnstructuredGrid`` mesh that are meant to be used in ``geos``. ``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. @@ -314,4 +314,51 @@ It will also verify that the ``VTK_POLYHEDRON`` cells can effectively get conver options: -h, --help show this help message and exit --chunk_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 + --nproc 8 [int]: Number of threads used for parallel processing. Defaults to your CPU count 8. + + +Why only use vtkUnstructuredGrid? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The mesh doctor is designed specifically for unstructured meshes used in GEOS. +All input files are expected to be ``.vtu`` (VTK Unstructured Grid) format. +What about other formats? + +VTK Hierarchy +""""""""""""" + +Supposedly, other grid types that are part of the following VTK hierarchy could be used: + +vtkDataObject +└── vtkDataSet + └── vtkCartesianGrid + └── vtkRectilinearGrid + └── vtkImageData + └── vtkStructuredPoints + └── vtkUniformGrid + └── vtkPointSet + └── vtkExplicitStructuredGrid + └── vtkPolyData + └── vtkStructuredGrid + └── vtkUnstructuredGrid + +And when looking at specific methods used in mesh-doctor, it could suggest that other formats could be used: + +Points access: mesh.GetPoints() - Available in all vtkPointSet subclasses ✓ +Cell iteration: mesh.GetNumberOfCells(), mesh.GetCell() - Available in all vtkDataSet subclasses ✓ +Cell types: mesh.GetCellType() - Available in all vtkDataSet subclasses ✓ +Cell/Point data: mesh.GetCellData(), mesh.GetPointData() - Available in all vtkDataSet subclasses ✓ + +VTK Filter Compatibility +"""""""""""""""""""""""" + +vtkCellSizeFilter, vtkMeshQuality, and other VTK filters used in the actions expect vtkDataSet or its subclasses +vtkUnstructuredGrid is compatible with all VTK filters used. +vtkPolyData has a different data structure, not suitable for 3D volumetric meshes. + +Specific Operations Require vtkUnstructuredGrid +""""""""""""""""""""""""""""""""""""""""""""""" + +GetCellNeighbors() - Only available in vtkUnstructuredGrid +GetFaceStream() - Only available in vtkUnstructuredGrid (for polyhedron support) +GetDistinctCellTypesArray() - Only available in vtkUnstructuredGrid \ No newline at end of file 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 8f91e6703..f8f8d2c2a 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/all_checks.py @@ -1,5 +1,6 @@ from dataclasses import dataclass -from vtkmodules.vtkCommonDataModel import vtkPointSet +from typing import Any +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.register import __load_module_action from geos.mesh.doctor.parsing.cli_parsing import setup_logger @@ -7,20 +8,20 @@ @dataclass( frozen=True ) class Options: checks_to_perform: list[ str ] - checks_options: dict[ str, any ] - check_displays: dict[ str, any ] + checks_options: dict[ str, Any ] + check_displays: dict[ str, Any ] @dataclass( frozen=True ) class Result: - check_results: dict[ str, any ] + check_results: dict[ str, Any ] -def get_check_results( vtk_input: str | vtkPointSet, options: Options ) -> dict[ str, any ]: +def get_check_results( vtk_input: str | vtkUnstructuredGrid, options: Options ) -> dict[ str, Any ]: isFilepath: bool = isinstance( vtk_input, str ) - isVtkUnstructuredGrid: bool = isinstance( vtk_input, vtkPointSet ) + isVtkUnstructuredGrid: bool = isinstance( vtk_input, vtkUnstructuredGrid ) assert isFilepath | isVtkUnstructuredGrid, "Invalid input type, should either be a filepath to .vtu file" \ - " or a vtkPointSet object" + " or a vtkUnstructuredGrid object" check_results: dict[ str, any ] = dict() for check_name in options.checks_to_perform: if isVtkUnstructuredGrid: # we need to call the mesh_action function that takes a vtkPointSet as input @@ -35,5 +36,5 @@ def get_check_results( vtk_input: str | vtkPointSet, options: Options ) -> dict[ def action( vtk_input_file: str, options: Options ) -> Result: - check_results: dict[ str, any ] = get_check_results( vtk_input_file, options ) + check_results: dict[ str, Any ] = get_check_results( vtk_input_file, options ) return Result( check_results=check_results ) 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 5e054d2a5..17d3f8931 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/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" {found}) for collocated nodes {cns}." ) -def mesh_action( 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() @@ -150,7 +150,7 @@ def mesh_action( vtk_input_file: str, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: try: - return mesh_action( vtk_input_file, options ) + return __action( vtk_input_file, options ) except BaseException as 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 e39685f13..aa6d2276f 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/collocated_nodes.py @@ -2,9 +2,9 @@ from dataclasses import dataclass import numpy from vtkmodules.vtkCommonCore import reference, vtkPoints -from vtkmodules.vtkCommonDataModel import vtkIncrementalOctreePointLocator, vtkPointSet, vtkCell +from vtkmodules.vtkCommonDataModel import vtkCell, vtkIncrementalOctreePointLocator, vtkUnstructuredGrid from geos.mesh.doctor.parsing.cli_parsing import setup_logger -from geos.mesh.io.vtkIO import read_mesh +from geos.mesh.io.vtkIO import read_unstructured_grid @dataclass( frozen=True ) @@ -18,7 +18,7 @@ class Result: wrong_support_elements: list[ int ] # Element indices with support node indices appearing more than once. -def find_collocated_nodes_buckets( mesh: vtkPointSet, tolerance: float ) -> list[ tuple[ int ] ]: +def find_collocated_nodes_buckets( mesh: vtkUnstructuredGrid, tolerance: float ) -> list[ tuple[ int ] ]: points: vtkPoints = mesh.GetPoints() locator = vtkIncrementalOctreePointLocator() locator.SetTolerance( tolerance ) @@ -52,7 +52,7 @@ def find_collocated_nodes_buckets( mesh: vtkPointSet, tolerance: float ) -> list return collocated_nodes_buckets -def find_wrong_support_elements( mesh: vtkPointSet ) -> list[ int ]: +def find_wrong_support_elements( mesh: vtkUnstructuredGrid ) -> list[ int ]: # Checking that the support node indices appear only once per element. wrong_support_elements: list[ int ] = list() for c in range( mesh.GetNumberOfCells() ): @@ -63,12 +63,12 @@ def find_wrong_support_elements( mesh: vtkPointSet ) -> list[ int ]: return wrong_support_elements -def mesh_action( mesh: vtkPointSet, options: Options ) -> Result: +def mesh_action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: collocated_nodes_buckets = find_collocated_nodes_buckets( mesh, options.tolerance ) wrong_support_elements = find_wrong_support_elements( mesh ) return Result( nodes_buckets=collocated_nodes_buckets, wrong_support_elements=wrong_support_elements ) def action( vtk_input_file: str, options: Options ) -> Result: - mesh: vtkPointSet = read_mesh( vtk_input_file ) + mesh: vtkUnstructuredGrid = read_unstructured_grid( vtk_input_file ) return mesh_action( mesh, options ) 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 97496894e..888235e3f 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,11 @@ from dataclasses import dataclass from typing import List, Tuple import uuid -from vtkmodules.vtkCommonDataModel import VTK_HEXAHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_WEDGE +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, 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.mesh.io.vtkIO import read_unstructured_grid @dataclass( frozen=True ) @@ -18,7 +18,7 @@ class Result: element_volumes: List[ Tuple[ int, float ] ] -def mesh_action( mesh, options: Options ) -> Result: +def mesh_action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: cs = vtkCellSizeFilter() cs.ComputeAreaOff() @@ -67,5 +67,5 @@ def mesh_action( mesh, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: - mesh = read_mesh( vtk_input_file ) + mesh: vtkUnstructuredGrid = read_unstructured_grid( vtk_input_file ) return mesh_action( mesh, options ) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/fix_elements_orderings.py b/geos-mesh/src/geos/mesh/doctor/actions/fix_elements_orderings.py index 1b76f8bd6..3947ce51e 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/fix_elements_orderings.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/fix_elements_orderings.py @@ -1,8 +1,9 @@ from dataclasses import dataclass from typing import Dict, FrozenSet, List, Set from vtkmodules.vtkCommonCore import vtkIdList +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.utils.genericHelpers import to_vtk_id_list -from geos.mesh.io.vtkIO import VtkOutput, read_mesh, write_mesh +from geos.mesh.io.vtkIO import VtkOutput, read_unstructured_grid, write_mesh @dataclass( frozen=True ) @@ -17,7 +18,7 @@ class Result: unchanged_cell_types: FrozenSet[ int ] -def mesh_action( mesh, options: Options ) -> Result: +def mesh_action( mesh: vtkUnstructuredGrid, 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 @@ -49,5 +50,5 @@ def mesh_action( mesh, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: - mesh = read_mesh( vtk_input_file ) + mesh: vtkUnstructuredGrid = read_unstructured_grid( vtk_input_file ) return mesh_action( mesh, options ) 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 f010b7d04..953be3f16 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_fractures.py @@ -15,7 +15,7 @@ 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.mesh.io.vtkIO import VtkOutput, read_unstructured_grid, write_mesh """ 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 @@ -557,7 +557,7 @@ def mesh_action( mesh, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: try: - mesh = read_mesh( vtk_input_file ) + mesh: vtkUnstructuredGrid = read_unstructured_grid( vtk_input_file ) # Mesh cannot contain global ids before splitting. 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 " + 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 2be2c5bdf..e73e21ca7 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,8 @@ from dataclasses import dataclass from vtkmodules.vtkCommonCore import vtkIdTypeArray +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.parsing.cli_parsing import setup_logger -from geos.mesh.io.vtkIO import VtkOutput, read_mesh, write_mesh +from geos.mesh.io.vtkIO import VtkOutput, read_unstructured_grid, write_mesh @dataclass( frozen=True ) @@ -45,7 +46,7 @@ def build_global_ids( mesh, generate_cells_global_ids: bool, generate_points_glo mesh.GetCellData().SetGlobalIds( cells_global_ids ) -def mesh_action( mesh, options: Options ) -> Result: +def mesh_action( mesh: vtkUnstructuredGrid, 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}" ) @@ -53,7 +54,7 @@ def mesh_action( mesh, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: try: - mesh = read_mesh( vtk_input_file ) + mesh: vtkUnstructuredGrid = read_unstructured_grid( vtk_input_file ) return mesh_action( mesh, options ) except BaseException as e: setup_logger.error( e ) 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 a0c9000b7..94565f4e9 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py @@ -15,7 +15,7 @@ from vtkmodules.vtkFiltersModeling import vtkCollisionDetectionFilter, vtkLinearExtrusionFilter 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 +from geos.mesh.io.vtkIO import read_unstructured_grid @dataclass( frozen=True ) @@ -466,5 +466,5 @@ def mesh_action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: - mesh = read_mesh( vtk_input_file ) + mesh: vtkUnstructuredGrid = read_unstructured_grid( vtk_input_file ) return mesh_action( mesh, options ) 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 5cb1cefd0..fb796990e 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 @@ -3,8 +3,8 @@ from vtkmodules.util.numpy_support import vtk_to_numpy from vtkmodules.vtkFiltersGeneral import vtkCellValidator from vtkmodules.vtkCommonCore import vtkOutputWindow, vtkFileOutputWindow -from vtkmodules.vtkCommonDataModel import vtkPointSet -from geos.mesh.io.vtkIO import read_mesh +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from geos.mesh.io.vtkIO import read_unstructured_grid @dataclass( frozen=True ) @@ -22,7 +22,7 @@ class Result: faces_oriented_incorrectly_elements: Collection[ int ] -def get_invalid_cell_ids( mesh: vtkPointSet, min_distance: float ) -> dict[ str, list[ int ] ]: +def get_invalid_cell_ids( mesh: vtkUnstructuredGrid, min_distance: float ) -> dict[ str, list[ int ] ]: """For every cell element in a vtk mesh, check if the cell is invalid regarding 6 specific criteria: "wrong_number_of_points", "intersecting_edges", "intersecting_faces", "non_contiguous_edges","non_convex" and "faces_oriented_incorrectly". @@ -31,7 +31,7 @@ def get_invalid_cell_ids( mesh: vtkPointSet, min_distance: float ) -> dict[ str, The dict with the complete list of cell indices by criteria is returned. Args: - mesh (vtkPointSet): A vtk grid. + mesh (vtkUnstructuredGrid): A vtk grid. min_distance (float): Minimum distance in the computation. Returns: @@ -88,8 +88,8 @@ def get_invalid_cell_ids( mesh: vtkPointSet, min_distance: float ) -> dict[ str, return invalid_cell_ids -def mesh_action( mesh, options: Options ) -> Result: - invalid_cell_ids = get_invalid_cell_ids( mesh, options.min_distance ) +def mesh_action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: + invalid_cell_ids: dict[ str, list[ int ] ] = get_invalid_cell_ids( mesh, options.min_distance ) return Result( wrong_number_of_points_elements=invalid_cell_ids[ "wrong_number_of_points_elements" ], intersecting_edges_elements=invalid_cell_ids[ "intersecting_edges_elements" ], intersecting_faces_elements=invalid_cell_ids[ "intersecting_faces_elements" ], @@ -99,5 +99,5 @@ def mesh_action( mesh, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: - mesh = read_mesh( vtk_input_file ) + mesh: vtkUnstructuredGrid = read_unstructured_grid( vtk_input_file ) return mesh_action( mesh, options ) 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 782415108..2809490e5 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py @@ -11,7 +11,7 @@ VTK_WEDGE ) from geos.mesh.doctor.actions.vtk_polyhedron import build_face_to_face_connectivity_through_edges, FaceStream from geos.mesh.doctor.parsing.cli_parsing import setup_logger -from geos.mesh.io.vtkIO import read_mesh +from geos.mesh.io.vtkIO import read_unstructured_grid from geos.mesh.utils.genericHelpers import get_vtk_constant_str, vtk_iter @@ -152,5 +152,5 @@ def mesh_action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: def action( vtk_input_file: str, options: Options ) -> Result: - mesh: vtkUnstructuredGrid = read_mesh( vtk_input_file ) + mesh: vtkUnstructuredGrid = read_unstructured_grid( vtk_input_file ) return mesh_action( mesh, options ) From f99e42f2f2f41b11370b42c600bc35423edba1fa Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 13 Aug 2025 23:15:15 -0700 Subject: [PATCH 26/52] Review implementations + yapf --- .../geos/mesh/doctor/actions/generate_cube.py | 25 +++++++++++-------- .../geos/mesh/doctor/actions/non_conformal.py | 4 +-- .../mesh/doctor/actions/supported_elements.py | 4 +-- geos-mesh/src/geos/mesh/io/vtkIO.py | 20 +++++++-------- .../src/geos/mesh/utils/genericHelpers.py | 20 --------------- 5 files changed, 28 insertions(+), 45 deletions(-) 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 32396cbb3..782f1a087 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py @@ -9,6 +9,7 @@ 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.mesh.utils.arrayModifiers import createConstantAttributeDataSet @dataclass( frozen=True ) @@ -146,17 +147,21 @@ def build_rectilinear_blocks_mesh( xyzs: Iterable[ XYZ ] ) -> vtkUnstructuredGri def add_fields( mesh: vtkUnstructuredGrid, fields: Iterable[ FieldInfo ] ) -> vtkUnstructuredGrid: + """ + Add constant fields to the mesh using arrayModifiers utilities. + Each field is filled with ones (1.0) for all components. + """ for field_info in fields: - if field_info.support == "CELLS": - data = mesh.GetCellData() - n = mesh.GetNumberOfCells() - elif field_info.support == "POINTS": - data = mesh.GetPointData() - n = mesh.GetNumberOfPoints() - array = np.ones( ( n, field_info.dimension ), dtype=float ) - vtk_array = numpy_to_vtk( array ) - vtk_array.SetName( field_info.name ) - data.AddArray( vtk_array ) + onPoints = field_info.support == "POINTS" + # Create list of values (all 1.0) for each component + listValues = [ 1.0 ] * field_info.dimension + # Use the robust createConstantAttributeDataSet function + success = createConstantAttributeDataSet( dataSet=mesh, + listValues=listValues, + attributeName=field_info.name, + onPoints=onPoints ) + if not success: + setup_logger.warning( f"Failed to create field {field_info.name}" ) return mesh 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 94565f4e9..728954ad3 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/non_conformal.py @@ -59,7 +59,7 @@ def __init__( self, mesh: vtkUnstructuredGrid ): self.__is_underlying_cell_type_a_polyhedron[ ic ] = mesh.GetCell( self.__original_cells.GetValue( ic ) ).GetCellType() == VTK_POLYHEDRON # Precomputing the normals - self.__normals: np.ndarray = np.empty( ( num_cells, 3 ), dtype=np.double, + self.__normals: np.ndarray = np.empty( ( num_cells, 3 ), dtype=np.float64, order='C' ) # Do not modify the storage layout for ic in range( num_cells ): if self.__is_underlying_cell_type_a_polyhedron[ ic ]: @@ -381,7 +381,7 @@ def build_numpy_triangles( points_ids ): def compute_bounding_box( boundary_mesh: BoundaryMesh, face_tolerance: float ) -> npt.NDArray[ np.float64 ]: # Precomputing the bounding boxes. # The options are important to directly interact with memory in C++. - bounding_boxes = np.empty( ( boundary_mesh.GetNumberOfCells(), 6 ), dtype=np.double, order="C" ) + bounding_boxes = np.empty( ( boundary_mesh.GetNumberOfCells(), 6 ), dtype=np.float64, order="C" ) for i in range( boundary_mesh.GetNumberOfCells() ): bb = vtkBoundingBox( boundary_mesh.bounds( i ) ) bb.Inflate( 2 * face_tolerance ) 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 2809490e5..f19680aac 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py @@ -12,7 +12,7 @@ from geos.mesh.doctor.actions.vtk_polyhedron import build_face_to_face_connectivity_through_edges, FaceStream from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.io.vtkIO import read_unstructured_grid -from geos.mesh.utils.genericHelpers import get_vtk_constant_str, vtk_iter +from geos.mesh.utils.genericHelpers import vtk_iter @dataclass( frozen=True ) @@ -124,7 +124,7 @@ def find_unsupported_std_elements_types( mesh: vtkUnstructuredGrid ) -> list[ st mesh.GetCellTypes( vtk_cell_types ) unique_cell_types = set( vtk_iter( vtk_cell_types ) ) result_values: set[ int ] = unique_cell_types - supported_cell_types - results = [ f"{get_vtk_constant_str( i )}" for i in frozenset( result_values ) ] + results = [ f"Type {i}: {vtkCellTypes.GetClassNameFromTypeId( i )}" for i in frozenset( result_values ) ] return results diff --git a/geos-mesh/src/geos/mesh/io/vtkIO.py b/geos-mesh/src/geos/mesh/io/vtkIO.py index 16543a8fa..0201d4280 100644 --- a/geos-mesh/src/geos/mesh/io/vtkIO.py +++ b/geos-mesh/src/geos/mesh/io/vtkIO.py @@ -160,21 +160,19 @@ def read_unstructured_grid( filepath: str ) -> vtkUnstructuredGrid: Returns: vtkUnstructuredGrid: The resulting unstructured grid data. """ - io_logger.info(f"Reading file '{filepath}' and expecting vtkUnstructuredGrid.") + io_logger.info( f"Reading file '{filepath}' and expecting vtkUnstructuredGrid." ) # Reuse the generic mesh reader - mesh = read_mesh(filepath) + mesh = read_mesh( filepath ) # Check the type of the resulting mesh - if not isinstance(mesh, vtkUnstructuredGrid): - error_msg = ( - f"File '{filepath}' was read successfully, but it is of type " - f"'{type(mesh).__name__}', not the expected vtkUnstructuredGrid." - ) - io_logger.error(error_msg) - raise TypeError(error_msg) - - io_logger.info("Validation successful. Mesh is a vtkUnstructuredGrid.") + if not isinstance( mesh, vtkUnstructuredGrid ): + error_msg = ( f"File '{filepath}' was read successfully, but it is of type " + f"'{type(mesh).__name__}', not the expected vtkUnstructuredGrid." ) + io_logger.error( error_msg ) + raise TypeError( error_msg ) + + io_logger.info( "Validation successful. Mesh is a vtkUnstructuredGrid." ) return mesh diff --git a/geos-mesh/src/geos/mesh/utils/genericHelpers.py b/geos-mesh/src/geos/mesh/utils/genericHelpers.py index e45fbdb80..de0624fd9 100644 --- a/geos-mesh/src/geos/mesh/utils/genericHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/genericHelpers.py @@ -6,7 +6,6 @@ from typing import Iterator, List, Sequence, Any, Union from vtkmodules.util.numpy_support import numpy_to_vtk from vtkmodules.vtkCommonCore import vtkIdList, vtkPoints, reference -import vtkmodules.vtkCommonDataModel as vtk_dm from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, vtkMultiBlockDataSet, vtkPolyData, vtkDataSet, vtkDataObject, vtkPlane, vtkCellTypes, vtkIncrementalOctreePointLocator from vtkmodules.vtkFiltersCore import vtk3DLinearGridPlaneCutter from geos.mesh.utils.multiblockHelpers import ( getBlockElementIndexesFlatten, getBlockFromFlatIndex ) @@ -21,25 +20,6 @@ """ -def get_vtk_constant_str( vtk_int_value: int ) -> str: - """ - Finds the string name of a VTK constant from its integer value. - - Args: - vtk_int_value: The integer value of the constant (e.g., 12). - - Returns: - A string like "12: VTK_HEXAHEDRON" or "12: ". - """ - # Search through the vtkCommonDataModel module - for name in dir( vtk_dm ): - # We only want variables that start with "VTK_" - if name.startswith( "VTK_" ): - if getattr( vtk_dm, name ) == vtk_int_value: - return f"{vtk_int_value}: {name}" - return f"{vtk_int_value}: " - - def to_vtk_id_list( data: List[ int ] ) -> vtkIdList: """Utility function transforming a list of ids into a vtkIdList. From 71496c667092c04f127f7d8e2c4c118ba7c5a454 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 13 Aug 2025 23:26:01 -0700 Subject: [PATCH 27/52] Fix invalid variable names --- geos-mesh/src/geos/mesh/io/vtkIO.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/geos-mesh/src/geos/mesh/io/vtkIO.py b/geos-mesh/src/geos/mesh/io/vtkIO.py index 0201d4280..e36a011f1 100644 --- a/geos-mesh/src/geos/mesh/io/vtkIO.py +++ b/geos-mesh/src/geos/mesh/io/vtkIO.py @@ -52,8 +52,8 @@ class VtkFormat( Enum ): @dataclass( frozen=True ) class VtkOutput: """Configuration for writing a VTK file.""" - output_path: str - is_binary: bool = True + output: str + is_data_mode_binary: bool = True def _read_data( filepath: str, reader_class: vtkDataReader | vtkXMLDataReader ) -> Optional[ vtkPointSet ]: @@ -180,7 +180,7 @@ def write_mesh( mesh: vtkPointSet, vtk_output: VtkOutput, can_overwrite: bool = """ Writes a vtkPointSet to a file. - The format is determined by the file extension in `VtkOutput.output_path`. + The format is determined by the file extension in `VtkOutput.output`. Args: mesh (vtkPointSet): The grid data to write. @@ -196,11 +196,11 @@ def write_mesh( mesh: vtkPointSet, vtk_output: VtkOutput, can_overwrite: bool = Returns: int: Returns 1 on success, consistent with the VTK writer's return code. """ - if os.path.exists( vtk_output.output_path ) and not can_overwrite: + if os.path.exists( vtk_output.output ) and not can_overwrite: raise FileExistsError( - f"File '{vtk_output.output_path}' already exists. Set can_overwrite=True to replace it." ) + f"File '{vtk_output.output}' already exists. Set can_overwrite=True to replace it." ) - _, extension = os.path.splitext( vtk_output.output_path ) + _, extension = os.path.splitext( vtk_output.output ) try: file_format = VtkFormat( extension ) @@ -208,12 +208,12 @@ def write_mesh( mesh: vtkPointSet, vtk_output: VtkOutput, can_overwrite: bool = raise ValueError( f"Writing to extension '{extension}' is not supported." ) writer_class = WRITER_MAP[ file_format ] - success_code = _write_data( mesh, writer_class, vtk_output.output_path, vtk_output.is_binary ) + success_code = _write_data( mesh, writer_class, vtk_output.output, vtk_output.is_data_mode_binary ) if not success_code: - raise RuntimeError( f"VTK writer failed to write file '{vtk_output.output_path}'." ) + raise RuntimeError( f"VTK writer failed to write file '{vtk_output.output}'." ) - io_logger.info( f"Successfully wrote mesh to '{vtk_output.output_path}'." ) + io_logger.info( f"Successfully wrote mesh to '{vtk_output.output}'." ) return success_code # VTK writers return 1 for success except ValueError as e: From 40abad6127247e42ca7b6143cbb0e640f0f57112 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Thu, 14 Aug 2025 08:36:01 -0700 Subject: [PATCH 28/52] yapf --- geos-mesh/src/geos/mesh/io/vtkIO.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/geos-mesh/src/geos/mesh/io/vtkIO.py b/geos-mesh/src/geos/mesh/io/vtkIO.py index e36a011f1..dfc8951bf 100644 --- a/geos-mesh/src/geos/mesh/io/vtkIO.py +++ b/geos-mesh/src/geos/mesh/io/vtkIO.py @@ -197,8 +197,7 @@ def write_mesh( mesh: vtkPointSet, vtk_output: VtkOutput, can_overwrite: bool = int: Returns 1 on success, consistent with the VTK writer's return code. """ if os.path.exists( vtk_output.output ) and not can_overwrite: - raise FileExistsError( - f"File '{vtk_output.output}' already exists. Set can_overwrite=True to replace it." ) + raise FileExistsError( f"File '{vtk_output.output}' already exists. Set can_overwrite=True to replace it." ) _, extension = os.path.splitext( vtk_output.output ) From ab177438960b27385ab1196948e6cf487978acad Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Thu, 14 Aug 2025 09:07:44 -0700 Subject: [PATCH 29/52] Fix docs --- docs/geos_mesh_docs/doctor.rst | 58 +++++++++++++++++----------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/geos_mesh_docs/doctor.rst b/docs/geos_mesh_docs/doctor.rst index ba9db40fe..6e07353a8 100644 --- a/docs/geos_mesh_docs/doctor.rst +++ b/docs/geos_mesh_docs/doctor.rst @@ -1,9 +1,9 @@ Mesh Doctor ----------- -``mesh-doctor`` is a ``python`` executable that can be used through the command line to perform various checks, validations, and tiny fixes to the ``vtkUnstructuredGrid`` mesh that are meant to be used in ``geos``. -``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. +| ``mesh-doctor`` is a ``python`` executable that can be used through the command line to perform various checks, validations, and tiny fixes to the ``vtkUnstructuredGrid`` mesh that are meant to be used in ``geos``. + ``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 ^^^^^^^^^^^^^ @@ -321,44 +321,44 @@ Why only use vtkUnstructuredGrid? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The mesh doctor is designed specifically for unstructured meshes used in GEOS. -All input files are expected to be ``.vtu`` (VTK Unstructured Grid) format. -What about other formats? +| All input files are expected to be ``.vtu`` (VTK Unstructured Grid) format. +| What about other formats? VTK Hierarchy """"""""""""" -Supposedly, other grid types that are part of the following VTK hierarchy could be used: - -vtkDataObject -└── vtkDataSet - └── vtkCartesianGrid - └── vtkRectilinearGrid - └── vtkImageData - └── vtkStructuredPoints - └── vtkUniformGrid - └── vtkPointSet - └── vtkExplicitStructuredGrid - └── vtkPolyData - └── vtkStructuredGrid - └── vtkUnstructuredGrid +Supposedly, other grid types that are part of the following VTK hierarchy could be used:: + + vtkDataObject + └── vtkDataSet + └── vtkCartesianGrid + └── vtkRectilinearGrid + └── vtkImageData + └── vtkStructuredPoints + └── vtkUniformGrid + └── vtkPointSet + └── vtkExplicitStructuredGrid + └── vtkPolyData + └── vtkStructuredGrid + └── vtkUnstructuredGrid And when looking at specific methods used in mesh-doctor, it could suggest that other formats could be used: -Points access: mesh.GetPoints() - Available in all vtkPointSet subclasses ✓ -Cell iteration: mesh.GetNumberOfCells(), mesh.GetCell() - Available in all vtkDataSet subclasses ✓ -Cell types: mesh.GetCellType() - Available in all vtkDataSet subclasses ✓ -Cell/Point data: mesh.GetCellData(), mesh.GetPointData() - Available in all vtkDataSet subclasses ✓ +* Points access: mesh.GetPoints() - Available in all vtkPointSet subclasses ✓ +* Cell iteration: mesh.GetNumberOfCells(), mesh.GetCell() - Available in all vtkDataSet subclasses ✓ +* Cell types: mesh.GetCellType() - Available in all vtkDataSet subclasses ✓ +* Cell/Point data: mesh.GetCellData(), mesh.GetPointData() - Available in all vtkDataSet subclasses ✓ VTK Filter Compatibility """""""""""""""""""""""" -vtkCellSizeFilter, vtkMeshQuality, and other VTK filters used in the actions expect vtkDataSet or its subclasses -vtkUnstructuredGrid is compatible with all VTK filters used. -vtkPolyData has a different data structure, not suitable for 3D volumetric meshes. +| vtkCellSizeFilter, vtkMeshQuality, and other VTK filters used in the actions expect vtkDataSet or its subclasses + vtkUnstructuredGrid is compatible with all VTK filters used. +| vtkPolyData has a different data structure, not suitable for 3D volumetric meshes. Specific Operations Require vtkUnstructuredGrid """"""""""""""""""""""""""""""""""""""""""""""" -GetCellNeighbors() - Only available in vtkUnstructuredGrid -GetFaceStream() - Only available in vtkUnstructuredGrid (for polyhedron support) -GetDistinctCellTypesArray() - Only available in vtkUnstructuredGrid \ No newline at end of file +* GetCellNeighbors() - Only available in vtkUnstructuredGrid +* GetFaceStream() - Only available in vtkUnstructuredGrid (for polyhedron support) +* GetDistinctCellTypesArray() - Only available in vtkUnstructuredGrid \ No newline at end of file From 415c49dae166fb40efd50528512ddb1b5b2d4adf Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Mon, 25 Aug 2025 15:15:44 -0700 Subject: [PATCH 30/52] Replace MeshDoctorBase with MeshDoctorBaseFilter to remove vtk pipeline --- .../mesh/doctor/filters/MeshDoctorBase.py | 190 --------------- .../doctor/filters/MeshDoctorFilterBase.py | 222 ++++++++++++++++++ 2 files changed, 222 insertions(+), 190 deletions(-) delete mode 100644 geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py create mode 100644 geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py diff --git a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py deleted file mode 100644 index d85213abe..000000000 --- a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorBase.py +++ /dev/null @@ -1,190 +0,0 @@ -from typing_extensions import Self -from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase -from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector -from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid -from geos.mesh.doctor.parsing.cli_parsing import setup_logger -from geos.mesh.io.vtkIO import VtkOutput, write_mesh - -__doc__ = """ -MeshDoctorBase module provides base classes for all mesh doctor VTK filters. - -MeshDoctorBase serves as the foundation class for filters that process existing meshes, -while MeshDoctorGenerator is for filters that generate new meshes from scratch. - -These base classes provide common functionality including: -- Logger management and setup -- Grid access and manipulation methods -- File I/O operations for writing VTK unstructured grids -- Standard VTK filter interface implementation - -All mesh doctor filters should inherit from one of these base classes to ensure -consistent behavior and interface across the mesh doctor toolkit. - -Example usage patterns: - -.. code-block:: python - - # For filters that process existing meshes - from filters.MeshDoctorBase import MeshDoctorBase - - class MyProcessingFilter(MeshDoctorBase): - def __init__(self): - super().__init__(nInputPorts=1, nOutputPorts=1) - - def RequestData(self, request, inInfoVec, outInfo): - # Process input mesh and create output - pass - - # For filters that generate meshes from scratch - from filters.MeshDoctorBase import MeshDoctorGenerator - - class MyGeneratorFilter(MeshDoctorGenerator): - def __init__(self): - super().__init__(nOutputPorts=1) - - def RequestData(self, request, inInfo, outInfo): - # Generate new mesh - pass -""" - - -class MeshDoctorBase( VTKPythonAlgorithmBase ): - """Base class for all mesh doctor VTK filters. - - This class provides common functionality shared across all mesh doctor filters, - including logger management, grid access, and file writing capabilities. - """ - - def __init__( self: Self, - nInputPorts: int = 1, - nOutputPorts: int = 1, - inputType: str = 'vtkUnstructuredGrid', - outputType: str = 'vtkUnstructuredGrid' ) -> None: - """Initialize the base mesh doctor filter. - - Args: - nInputPorts (int): Number of input ports. Defaults to 1. - nOutputPorts (int): Number of output ports. Defaults to 1. - inputType (str): Input data type. Defaults to 'vtkUnstructuredGrid'. - outputType (str): Output data type. Defaults to 'vtkUnstructuredGrid'. - """ - super().__init__( nInputPorts=nInputPorts, - nOutputPorts=nOutputPorts, - inputType=inputType if nInputPorts > 0 else None, - outputType=outputType ) - self.m_logger = setup_logger - - def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: - """Inherited from VTKPythonAlgorithmBase::FillInputPortInformation. - - Args: - port (int): input port - info (vtkInformationVector): info - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - if port == 0: - info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) - return 1 - - def RequestInformation( - self: Self, - request: vtkInformation, # noqa: F841 - inInfoVec: list[ vtkInformationVector ], # noqa: F841 - outInfoVec: vtkInformationVector, - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestInformation. - - Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - executive = self.GetExecutive() # noqa: F841 - outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 - return 1 - - def SetLogger( self: Self, logger ) -> None: - """Set the logger. - - Args: - logger: Logger instance to use - """ - self.m_logger = logger - self.Modified() - - def getGrid( self: Self ) -> vtkUnstructuredGrid: - """Returns the vtkUnstructuredGrid output. - - Args: - self (Self) - - Returns: - vtkUnstructuredGrid: The output grid - """ - self.Update() # triggers RequestData - return self.GetOutputDataObject( 0 ) - - def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: - """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath. - - Args: - filepath (str): /path/to/your/file.vtu - is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. - canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. - Defaults to False. - """ - mesh: vtkUnstructuredGrid = self.getGrid() - if mesh: - vtk_output = VtkOutput( filepath, is_data_mode_binary ) - write_mesh( mesh, vtk_output, canOverwrite ) - else: - self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) - - def copyInputToOutput( self: Self, input_mesh: vtkUnstructuredGrid ) -> vtkUnstructuredGrid: - """Helper method to copy input mesh structure and attributes to a new output mesh. - - Args: - input_mesh (vtkUnstructuredGrid): Input mesh to copy from - - Returns: - vtkUnstructuredGrid: New mesh with copied structure and attributes - """ - output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() - output_mesh.CopyStructure( input_mesh ) - output_mesh.CopyAttributes( input_mesh ) - return output_mesh - - -class MeshDoctorGenerator( MeshDoctorBase ): - """Base class for mesh doctor generator filters (no input required). - - This class extends MeshDoctorBase for filters that generate meshes - from scratch without requiring input meshes. - """ - - def __init__( self: Self, nOutputPorts: int = 1, outputType: str = 'vtkUnstructuredGrid' ) -> None: - """Initialize the base mesh doctor generator filter. - - Args: - nOutputPorts (int): Number of output ports. Defaults to 1. - outputType (str): Output data type. Defaults to 'vtkUnstructuredGrid'. - """ - super().__init__( nInputPorts=0, nOutputPorts=nOutputPorts, inputType=None, outputType=outputType ) - - def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: - """Generator filters don't have input ports. - - Args: - port (int): input port (not used) - info (vtkInformationVector): info (not used) - - Returns: - int: Always returns 1 - """ - # Generator filters don't have input ports, so this method is not used - return 1 diff --git a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py new file mode 100644 index 000000000..0158fb9f9 --- /dev/null +++ b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py @@ -0,0 +1,222 @@ +from typing_extensions import Self +from typing import Union +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from geos.utils.Logger import getLogger, Logger +from geos.mesh.io.vtkIO import VtkOutput, write_mesh + +__doc__ = """ +MeshDoctorFilterBase module provides base classes for all mesh doctor filters using direct mesh manipulation. + +MeshDoctorFilterBase serves as the foundation class for filters that process existing meshes, +while MeshDoctorGenerator is for filters that generate new meshes from scratch. + +These base classes provide common functionality including: +- Logger management and setup +- Mesh access and manipulation methods +- File I/O operations for writing VTK unstructured grids +- Consistent interface across all mesh doctor filters + +Unlike the VTK pipeline-based MeshDoctorBase, these classes work with direct mesh manipulation +following the FillPartialArrays pattern for simpler, more Pythonic usage. + +Example usage patterns: + +.. code-block:: python + + # For filters that process existing meshes + from filters.MeshDoctorFilterBase import MeshDoctorFilterBase + + class MyProcessingFilter(MeshDoctorFilterBase): + def __init__(self, mesh, parameter1=default_value): + super().__init__(mesh, "My Filter Name") + self.parameter1 = parameter1 + + def applyFilter(self): + # Process self.mesh directly + # Return True on success, False on failure + pass + + # For filters that generate meshes from scratch + from filters.MeshDoctorFilterBase import MeshDoctorGeneratorBase + + class MyGeneratorFilter(MeshDoctorGeneratorBase): + def __init__(self, parameter1=default_value): + super().__init__("My Generator Name") + self.parameter1 = parameter1 + + def applyFilter(self): + # Generate new mesh and assign to self.mesh + # Return True on success, False on failure + pass +""" + + +class MeshDoctorFilterBase: + """Base class for all mesh doctor filters using direct mesh manipulation. + + This class provides common functionality shared across all mesh doctor filters, + including logger management, mesh access, and file writing capabilities. + Unlike MeshDoctorBase, this class works with direct mesh manipulation instead + of VTK pipeline patterns. + """ + + def __init__( + self: Self, + mesh: vtkUnstructuredGrid, + filter_name: str, + use_external_logger: bool = False, + ) -> None: + """Initialize the base mesh doctor filter. + + Args: + mesh (vtkUnstructuredGrid): The input mesh to process + filter_name (str): Name of the filter for logging + use_external_logger (bool): Whether to use external logger. Defaults to False. + """ + self.mesh: vtkUnstructuredGrid = mesh + self.filter_name: str = filter_name + + # Logger setup + self.logger: Logger + if not use_external_logger: + self.logger = getLogger( filter_name, True ) + else: + import logging + self.logger = logging.getLogger( filter_name ) + self.logger.setLevel( logging.INFO ) + + def setLoggerHandler( self: Self, handler ) -> None: + """Set a specific handler for the filter logger. + + Args: + handler: The logging handler to add. + """ + if not self.logger.hasHandlers(): + self.logger.addHandler( handler ) + else: + self.logger.warning( "The logger already has a handler, to use yours set 'use_external_logger' " + "to True during initialization." ) + + def getMesh( self: Self ) -> vtkUnstructuredGrid: + """Get the processed mesh. + + Returns: + vtkUnstructuredGrid: The processed mesh + """ + return self.mesh + + def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: + """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath. + + Args: + filepath (str): /path/to/your/file.vtu + is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. + canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. + Defaults to False. + """ + if self.mesh: + vtk_output = VtkOutput( filepath, is_data_mode_binary ) + write_mesh( self.mesh, vtk_output, canOverwrite ) + else: + self.logger.error( f"No mesh available. Cannot output vtkUnstructuredGrid at {filepath}." ) + + def copyMesh( self: Self, source_mesh: vtkUnstructuredGrid ) -> vtkUnstructuredGrid: + """Helper method to create a copy of a mesh with structure and attributes. + + Args: + source_mesh (vtkUnstructuredGrid): Source mesh to copy from + + Returns: + vtkUnstructuredGrid: New mesh with copied structure and attributes + """ + output_mesh: vtkUnstructuredGrid = source_mesh.NewInstance() + output_mesh.CopyStructure( source_mesh ) + output_mesh.CopyAttributes( source_mesh ) + return output_mesh + + def applyFilter( self: Self ) -> bool: + """Apply the filter operation. + + This method should be overridden by subclasses to implement specific filter logic. + + Returns: + bool: True if filter applied successfully, False otherwise. + """ + raise NotImplementedError( "Subclasses must implement applyFilter method" ) + + +class MeshDoctorGeneratorBase: + """Base class for mesh doctor generator filters (no input mesh required). + + This class provides functionality for filters that generate meshes + from scratch without requiring input meshes. + """ + + def __init__( + self: Self, + filter_name: str, + use_external_logger: bool = False, + ) -> None: + """Initialize the base mesh doctor generator filter. + + Args: + filter_name (str): Name of the filter for logging + use_external_logger (bool): Whether to use external logger. Defaults to False. + """ + self.mesh: Union[ vtkUnstructuredGrid, None ] = None + self.filter_name: str = filter_name + + # Logger setup + self.logger: Logger + if not use_external_logger: + self.logger = getLogger( filter_name, True ) + else: + import logging + self.logger = logging.getLogger( filter_name ) + self.logger.setLevel( logging.INFO ) + + def setLoggerHandler( self: Self, handler ) -> None: + """Set a specific handler for the filter logger. + + Args: + handler: The logging handler to add. + """ + if not self.logger.hasHandlers(): + self.logger.addHandler( handler ) + else: + self.logger.warning( "The logger already has a handler, to use yours set 'use_external_logger' " + "to True during initialization." ) + + def getMesh( self: Self ) -> Union[ vtkUnstructuredGrid, None ]: + """Get the generated mesh. + + Returns: + Union[vtkUnstructuredGrid, None]: The generated mesh, or None if not yet generated + """ + return self.mesh + + def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: + """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath. + + Args: + filepath (str): /path/to/your/file.vtu + is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. + canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. + Defaults to False. + """ + if self.mesh: + vtk_output = VtkOutput( filepath, is_data_mode_binary ) + write_mesh( self.mesh, vtk_output, canOverwrite ) + else: + self.logger.error( f"No mesh generated. Cannot output vtkUnstructuredGrid at {filepath}." ) + + def applyFilter( self: Self ) -> bool: + """Apply the filter operation to generate a mesh. + + This method should be overridden by subclasses to implement specific generation logic. + The generated mesh should be assigned to self.mesh. + + Returns: + bool: True if mesh generated successfully, False otherwise. + """ + raise NotImplementedError( "Subclasses must implement applyFilter method" ) From 68439c9aa69eef56044df3346b2e19c38883af02 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Mon, 25 Aug 2025 15:16:45 -0700 Subject: [PATCH 31/52] Update all filters with new base class --- .../src/geos/mesh/doctor/filters/Checks.py | 356 +++++++++++------ .../mesh/doctor/filters/CollocatedNodes.py | 228 ++++++----- .../mesh/doctor/filters/ElementVolumes.py | 204 ++++++---- .../mesh/doctor/filters/GenerateFractures.py | 378 +++++++++++------- .../doctor/filters/GenerateRectilinearGrid.py | 225 +++++++---- .../geos/mesh/doctor/filters/NonConformal.py | 289 +++++++------ .../filters/SelfIntersectingElements.py | 315 ++++++++------- 7 files changed, 1250 insertions(+), 745 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py index e972aa77a..19dd73fba 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py @@ -1,9 +1,9 @@ from types import SimpleNamespace +from typing import Any from typing_extensions import Self -from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.all_checks import Options, get_check_results -from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase +from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorFilterBase from geos.mesh.doctor.parsing._shared_checks_parsing_logic import CheckFeature, display_results from geos.mesh.doctor.parsing.all_checks_parsing import ( CHECK_FEATURES_CONFIG as cfc_all_checks, ORDERED_CHECK_NAMES as ocn_all_checks ) @@ -11,86 +11,132 @@ as ocn_main_checks ) __doc__ = """ -Checks module is a vtk filter that performs comprehensive mesh validation checks on a vtkUnstructuredGrid. +Checks module performs comprehensive mesh validation checks on a vtkUnstructuredGrid. This module contains AllChecks and MainChecks filters that run various quality checks including element validation, node validation, topology checks, and geometric integrity verification. -One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. - To use the AllChecks filter: .. code-block:: python - from filters.Checks import AllChecks + from geos.mesh.doctor.filters.Checks import AllChecks # instantiate the filter for all available checks - allChecksFilter: AllChecks = AllChecks() - - # set input mesh - allChecksFilter.SetInputData(mesh) + allChecksFilter = AllChecks(mesh) # optionally customize check parameters allChecksFilter.setCheckParameter("collocated_nodes", "tolerance", 1e-6) - allChecksFilter.setGlobalParameter("tolerance", 1e-6) # applies to all checks with tolerance parameter + allChecksFilter.setAllChecksParameter("tolerance", 1e-6) # applies to all checks with tolerance parameter # execute the checks - output_mesh: vtkUnstructuredGrid = allChecksFilter.getGrid() + success = allChecksFilter.applyFilter() # get check results check_results = allChecksFilter.getCheckResults() + # get the processed mesh + output_mesh = allChecksFilter.getMesh() + To use the MainChecks filter (subset of most important checks): .. code-block:: python - from filters.Checks import MainChecks + from geos.mesh.doctor.filters.Checks import MainChecks # instantiate the filter for main checks only - mainChecksFilter: MainChecks = MainChecks() + mainChecksFilter = MainChecks(mesh) - # set input mesh and run checks - mainChecksFilter.SetInputData(mesh) - output_mesh: vtkUnstructuredGrid = mainChecksFilter.getGrid() + # execute the checks + success = mainChecksFilter.applyFilter() """ +loggerTitle: str = "Mesh Doctor Checks Filter" + + +class MeshDoctorChecks( MeshDoctorFilterBase ): + + def __init__( + self: Self, + mesh: vtkUnstructuredGrid, + checks_to_perform: list[ str ], + check_features_config: dict[ str, CheckFeature ], + ordered_check_names: list[ str ], + use_external_logger: bool = False, + ) -> None: + """Initialize the mesh doctor checks filter. + + Args: + mesh (vtkUnstructuredGrid): The input mesh to check + checks_to_perform (list[str]): List of check names to perform + check_features_config (dict[str, CheckFeature]): Configuration for check features + ordered_check_names (list[str]): Ordered list of available check names + use_external_logger (bool): Whether to use external logger. Defaults to False. + """ + super().__init__( mesh, loggerTitle, use_external_logger ) + self.checks_to_perform: list[ str ] = checks_to_perform + self.check_parameters: dict[ str, dict[ str, Any ] ] = {} # Custom parameters override + self.check_results: dict[ str, Any ] = {} + self.check_features_config: dict[ str, CheckFeature ] = check_features_config + self.ordered_check_names: list[ str ] = ordered_check_names + + def setChecksToPerform( self: Self, checks_to_perform: list[ str ] ) -> None: + """Set which checks to perform. + + Args: + checks_to_perform (list[str]): List of check names to perform + """ + self.checks_to_perform = checks_to_perform -class MeshDoctorChecks( MeshDoctorBase ): + def setCheckParameter( self: Self, check_name: str, parameter_name: str, value: Any ) -> None: + """Set a parameter for a specific check. - def __init__( self: Self, checks_to_perform: list[ str ], check_features_config: dict[ str, CheckFeature ], - ordered_check_names: list[ str ] ) -> None: - super().__init__() - self.m_checks_to_perform: list[ str ] = checks_to_perform - self.m_check_parameters: dict[ str, dict[ str, any ] ] = dict() # Custom parameters override - self.m_check_results: dict[ str, any ] = dict() - self.m_CHECK_FEATURES_CONFIG: dict[ str, CheckFeature ] = check_features_config - self.m_ORDERED_CHECK_NAMES: list[ str ] = ordered_check_names + Args: + check_name (str): Name of the check (e.g., "collocated_nodes") + parameter_name (str): Name of the parameter (e.g., "tolerance") + value (Any): Value to set for the parameter + """ + if check_name not in self.check_parameters: + self.check_parameters[ check_name ] = {} + self.check_parameters[ check_name ][ parameter_name ] = value - def RequestData( self: Self, request: vtkInformation, inInfoVec: list[ vtkInformationVector ], - outInfo: vtkInformationVector ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestData. + def setAllChecksParameter( self: Self, parameter_name: str, value: Any ) -> None: + """Set a parameter for all checks that support it. Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects + parameter_name (str): Name of the parameter (e.g., "tolerance") + value (Any): Value to set for the parameter + """ + for check_name in self.checks_to_perform: + if check_name in self.check_features_config: + default_params = self.check_features_config[ check_name ].default_params + if parameter_name in default_params: + self.setCheckParameter( check_name, parameter_name, value ) + + def applyFilter( self: Self ) -> bool: + """Apply the mesh validation checks. Returns: - int: 1 if calculation successfully ended, 0 otherwise. + bool: True if checks completed successfully, False otherwise. """ - input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) - output = vtkUnstructuredGrid.GetData( outInfo ) + self.logger.info( f"Apply filter {self.logger.name}" ) - # Build the options using the parsing logic structure - options = self._buildOptions() - self.m_check_results = get_check_results( input_mesh, options ) + try: + # Build the options using the parsing logic structure + options = self._buildOptions() + self.check_results = get_check_results( self.mesh, options ) - results_wrapper = SimpleNamespace( check_results=self.m_check_results ) - display_results( options, results_wrapper ) + # Display results using the standard display logic + results_wrapper = SimpleNamespace( check_results=self.check_results ) + display_results( options, results_wrapper ) - output_mesh: vtkUnstructuredGrid = self.copyInputToOutput( input_mesh ) - output.ShallowCopy( output_mesh ) + self.logger.info( f"Performed {len(self.check_results)} checks" ) + self.logger.info( f"The filter {self.logger.name} succeeded" ) + return True - return 1 + except Exception as e: + self.logger.error( f"Error in mesh checks: {e}" ) + self.logger.error( f"The filter {self.logger.name} failed" ) + return False def _buildOptions( self: Self ) -> Options: """Build Options object using the same logic as the parsing system. @@ -99,133 +145,203 @@ def _buildOptions( self: Self ) -> Options: Options: Properly configured options for all checks """ # Start with default parameters for all configured checks - default_params: dict[ str, dict[ str, any ] ] = { + default_params: dict[ str, dict[ str, Any ] ] = { name: feature.default_params.copy() - for name, feature in self.m_CHECK_FEATURES_CONFIG.items() + for name, feature in self.check_features_config.items() } - final_check_params: dict[ str, dict[ str, any ] ] = { + final_check_params: dict[ str, dict[ str, Any ] ] = { name: default_params[ name ] - for name in self.m_checks_to_perform + for name in self.checks_to_perform } # Apply any custom parameter overrides - for check_name in self.m_checks_to_perform: - if check_name in self.m_check_parameters: - final_check_params[ check_name ].update( self.m_check_parameters[ check_name ] ) + for check_name in self.checks_to_perform: + if check_name in self.check_parameters: + final_check_params[ check_name ].update( self.check_parameters[ check_name ] ) # Instantiate Options objects for the selected checks - individual_check_options: dict[ str, any ] = dict() - individual_check_display: dict[ str, any ] = dict() + individual_check_options: dict[ str, Any ] = {} + individual_check_display: dict[ str, Any ] = {} - for check_name in self.m_checks_to_perform: - if check_name not in self.m_CHECK_FEATURES_CONFIG: - self.m_logger.warning( f"Check '{check_name}' is not available. Skipping." ) + for check_name in self.checks_to_perform: + if check_name not in self.check_features_config: + self.logger.warning( f"Check '{check_name}' is not available. Skipping." ) continue params = final_check_params[ check_name ] - feature_config = self.m_CHECK_FEATURES_CONFIG[ check_name ] + feature_config = self.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: - self.m_logger.error( f"Failed to create options for check '{check_name}': {e}. " - f"This check will be skipped." ) + self.logger.error( f"Failed to create options for check '{check_name}': {e}. " + f"This check will be skipped." ) return Options( checks_to_perform=list( individual_check_options.keys() ), checks_options=individual_check_options, check_displays=individual_check_display ) def getAvailableChecks( self: Self ) -> list[ str ]: - """Returns the list of available check names. + """Get the list of available check names. Returns: list[str]: List of available check names """ - return self.m_ORDERED_CHECK_NAMES - - def getCheckResults( self: Self ) -> dict[ str, any ]: - """Returns the results of all performed checks. + return self.ordered_check_names - Args: - self (Self) + def getCheckResults( self: Self ) -> dict[ str, Any ]: + """Get the results of all performed checks. Returns: - dict[str, any]: Dictionary mapping check names to their results + dict[str, Any]: Dictionary mapping check names to their results """ - return self.m_check_results + return self.check_results - def getDefaultParameters( self: Self, check_name: str ) -> dict[ str, any ]: + def getDefaultParameters( self: Self, check_name: str ) -> dict[ str, Any ]: """Get the default parameters for a specific check. Args: check_name (str): Name of the check Returns: - dict[str, any]: Dictionary of default parameters + dict[str, Any]: Dictionary of default parameters """ - if check_name in self.m_CHECK_FEATURES_CONFIG: - return self.m_CHECK_FEATURES_CONFIG[ check_name ].default_params + if check_name in self.check_features_config: + return self.check_features_config[ check_name ].default_params return {} - def setChecksToPerform( self: Self, checks_to_perform: list[ str ] ) -> None: - """Set which checks to perform. - - Args: - self (Self) - checks_to_perform (list[str]): List of check names to perform. - """ - self.m_checks_to_perform = checks_to_perform - self.Modified() - - def setCheckParameter( self: Self, check_name: str, parameter_name: str, value: any ) -> None: - """Set a parameter for a specific check. - - Args: - self (Self) - check_name (str): Name of the check (e.g., "collocated_nodes") - parameter_name (str): Name of the parameter (e.g., "tolerance") - value (any): Value to set for the parameter - """ - if check_name not in self.m_check_parameters: - self.m_check_parameters[ check_name ] = {} - self.m_check_parameters[ check_name ][ parameter_name ] = value - self.Modified() - - def setAllChecksParameter( self: Self, parameter_name: str, value: any ) -> None: - """Set a parameter for all checks that support it. - - Args: - self (Self) - parameter_name (str): Name of the parameter (e.g., "tolerance") - value (any): Value to set for the parameter - """ - for check_name in self.m_checks_to_perform: - if check_name in self.m_CHECK_FEATURES_CONFIG: - default_params = self.m_CHECK_FEATURES_CONFIG[ check_name ].default_params - if parameter_name in default_params: - self.setCheckParameter( check_name, parameter_name, value ) - self.Modified() - class AllChecks( MeshDoctorChecks ): - def __init__( self: Self ) -> None: - """Vtk filter to ... of a vtkUnstructuredGrid. + def __init__( + self: Self, + mesh: vtkUnstructuredGrid, + use_external_logger: bool = False, + ) -> None: + """Initialize the all checks filter. - Output mesh is vtkUnstructuredGrid. + Args: + mesh (vtkUnstructuredGrid): The input mesh to check + use_external_logger (bool): Whether to use external logger. Defaults to False. """ - super().__init__( checks_to_perform=ocn_all_checks, + super().__init__( mesh, + checks_to_perform=ocn_all_checks, check_features_config=cfc_all_checks, - ordered_check_names=ocn_all_checks ) + ordered_check_names=ocn_all_checks, + use_external_logger=use_external_logger ) class MainChecks( MeshDoctorChecks ): - def __init__( self: Self ) -> None: - """Vtk filter to ... of a vtkUnstructuredGrid. + def __init__( + self: Self, + mesh: vtkUnstructuredGrid, + use_external_logger: bool = False, + ) -> None: + """Initialize the main checks filter. - Output mesh is vtkUnstructuredGrid. + Args: + mesh (vtkUnstructuredGrid): The input mesh to check + use_external_logger (bool): Whether to use external logger. Defaults to False. """ - super().__init__( checks_to_perform=ocn_main_checks, + super().__init__( mesh, + checks_to_perform=ocn_main_checks, check_features_config=cfc_main_checks, - ordered_check_names=ocn_main_checks ) + ordered_check_names=ocn_main_checks, + use_external_logger=use_external_logger ) + + +# Main functions for backward compatibility and standalone use +def all_checks( + mesh: vtkUnstructuredGrid, + custom_parameters: dict[ str, dict[ str, Any ] ] = None, + write_output: bool = False, + output_path: str = "output/mesh_all_checks.vtu", +) -> tuple[ vtkUnstructuredGrid, dict[ str, Any ] ]: + """Apply all available mesh checks to a mesh. + + Args: + mesh (vtkUnstructuredGrid): The input mesh + custom_parameters (dict[str, dict[str, Any]]): Custom parameters for checks. Defaults to None. + write_output (bool): Whether to write output mesh to file. Defaults to False. + output_path (str): Output file path if write_output is True. + + Returns: + tuple[vtkUnstructuredGrid, dict[str, Any]]: + Processed mesh, check results + """ + filter_instance = AllChecks( mesh ) + + if custom_parameters: + for check_name, params in custom_parameters.items(): + for param_name, value in params.items(): + filter_instance.setCheckParameter( check_name, param_name, value ) + + success = filter_instance.applyFilter() + + if not success: + raise RuntimeError( "All checks execution failed" ) + + if write_output: + filter_instance.writeGrid( output_path ) + + return ( + filter_instance.getMesh(), + filter_instance.getCheckResults(), + ) + + +def main_checks( + mesh: vtkUnstructuredGrid, + custom_parameters: dict[ str, dict[ str, Any ] ] = None, + write_output: bool = False, + output_path: str = "output/mesh_main_checks.vtu", +) -> tuple[ vtkUnstructuredGrid, dict[ str, Any ] ]: + """Apply main mesh checks to a mesh. + + Args: + mesh (vtkUnstructuredGrid): The input mesh + custom_parameters (dict[str, dict[str, Any]]): Custom parameters for checks. Defaults to None. + write_output (bool): Whether to write output mesh to file. Defaults to False. + output_path (str): Output file path if write_output is True. + + Returns: + tuple[vtkUnstructuredGrid, dict[str, Any]]: + Processed mesh, check results + """ + filter_instance = MainChecks( mesh ) + + if custom_parameters: + for check_name, params in custom_parameters.items(): + for param_name, value in params.items(): + filter_instance.setCheckParameter( check_name, param_name, value ) + + success = filter_instance.applyFilter() + + if not success: + raise RuntimeError( "Main checks execution failed" ) + + if write_output: + filter_instance.writeGrid( output_path ) + + return ( + filter_instance.getMesh(), + filter_instance.getCheckResults(), + ) + + +# Aliases for backward compatibility +def processAllChecks( + mesh: vtkUnstructuredGrid, + custom_parameters: dict[ str, dict[ str, Any ] ] = None, +) -> tuple[ vtkUnstructuredGrid, dict[ str, Any ] ]: + """Legacy function name for backward compatibility.""" + return all_checks( mesh, custom_parameters ) + + +def processMainChecks( + mesh: vtkUnstructuredGrid, + custom_parameters: dict[ str, dict[ str, Any ] ] = None, +) -> tuple[ vtkUnstructuredGrid, dict[ str, Any ] ]: + """Legacy function name for backward compatibility.""" + return main_checks( mesh, custom_parameters ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py index a9c30dab1..b9f16b39b 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py @@ -1,146 +1,184 @@ -import numpy as np -import numpy.typing as npt from typing_extensions import Self +import numpy as np from vtkmodules.util.numpy_support import numpy_to_vtk -from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray +from vtkmodules.vtkCommonCore import vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.collocated_nodes import find_collocated_nodes_buckets, find_wrong_support_elements -from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase +from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorFilterBase __doc__ = """ -CollocatedNodes module is a vtk filter that identifies and handles duplicated/collocated nodes in a vtkUnstructuredGrid. +CollocatedNodes module identifies and handles duplicated/collocated nodes in a vtkUnstructuredGrid. The filter can detect nodes that are within a specified tolerance distance and optionally identify elements that have support nodes appearing more than once (wrong support elements). -One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. - To use the filter: .. code-block:: python - from filters.CollocatedNodes import CollocatedNodes + from geos.mesh.doctor.filters.CollocatedNodes import CollocatedNodes # instantiate the filter - collocatedNodesFilter: CollocatedNodes = CollocatedNodes() - - # set the tolerance for detecting collocated nodes - collocatedNodesFilter.setTolerance(1e-6) - - # optionally enable painting of wrong support elements - collocatedNodesFilter.setPaintWrongSupportElements(1) # 1 to enable, 0 to disable - - # set input mesh - collocatedNodesFilter.SetInputData(mesh) + collocatedNodesFilter = CollocatedNodes(mesh, tolerance=1e-6, paint_wrong_support_elements=True) # execute the filter - output_mesh: vtkUnstructuredGrid = collocatedNodesFilter.getGrid() + success = collocatedNodesFilter.applyFilter() # get results - collocated_buckets = collocatedNodesFilter.getCollocatedNodeBuckets() # list of tuples with collocated node indices - wrong_support_elements = collocatedNodesFilter.getWrongSupportElements() # list of problematic element indices + collocated_buckets = collocatedNodesFilter.getCollocatedNodeBuckets() + wrong_support_elements = collocatedNodesFilter.getWrongSupportElements() + + # get the processed mesh + output_mesh = collocatedNodesFilter.getMesh() # write the output mesh collocatedNodesFilter.writeGrid("output/mesh_with_collocated_info.vtu") """ +loggerTitle: str = "Collocated Nodes Filter" -class CollocatedNodes( MeshDoctorBase ): - def __init__( self: Self ) -> None: - """Vtk filter to find the duplicated nodes of a vtkUnstructuredGrid. +class CollocatedNodes( MeshDoctorFilterBase ): - Output mesh is vtkUnstructuredGrid. + def __init__( + self: Self, + mesh: vtkUnstructuredGrid, + tolerance: float = 0.0, + paint_wrong_support_elements: bool = False, + use_external_logger: bool = False, + ) -> None: + """Initialize the collocated nodes filter. + + Args: + mesh (vtkUnstructuredGrid): The input mesh to analyze + tolerance (float): Distance tolerance for detecting collocated nodes. Defaults to 0.0. + paint_wrong_support_elements (bool): Whether to mark wrong support elements in output. Defaults to False. + use_external_logger (bool): Whether to use external logger. Defaults to False. """ - super().__init__( nInputPorts=1, - nOutputPorts=1, - inputType='vtkUnstructuredGrid', - outputType='vtkUnstructuredGrid' ) - self.m_collocatedNodesBuckets: list[ tuple[ int ] ] = list() - self.m_paintWrongSupportElements: int = 0 - self.m_tolerance: float = 0.0 - self.m_wrongSupportElements: list[ int ] = list() - - def RequestData( self: Self, request: vtkInformation, inInfoVec: list[ vtkInformationVector ], - outInfo: vtkInformationVector ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestData. + super().__init__( mesh, loggerTitle, use_external_logger ) + self.tolerance: float = tolerance + self.paint_wrong_support_elements: bool = paint_wrong_support_elements + + # Results storage + self.collocated_node_buckets: list[ tuple[ int ] ] = [] + self.wrong_support_elements: list[ int ] = [] + + def setTolerance( self: Self, tolerance: float ) -> None: + """Set the tolerance parameter to define if two points are collocated or not. + + Args: + tolerance (float): Distance tolerance + """ + self.tolerance = tolerance + + def setPaintWrongSupportElements( self: Self, paint: bool ) -> None: + """Set whether to create arrays marking wrong support elements in output data. Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects + paint (bool): True to enable marking, False to disable + """ + self.paint_wrong_support_elements = paint + + def applyFilter( self: Self ) -> bool: + """Apply the collocated nodes analysis. Returns: - int: 1 if calculation successfully ended, 0 otherwise. + bool: True if analysis completed successfully, False otherwise. """ - input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) - output = vtkUnstructuredGrid.GetData( outInfo ) + self.logger.info( f"Apply filter {self.logger.name}" ) - self.m_collocatedNodesBuckets = find_collocated_nodes_buckets( input_mesh, self.m_tolerance ) - self.m_wrongSupportElements = find_wrong_support_elements( input_mesh ) + try: + # Find collocated nodes + self.collocated_node_buckets = find_collocated_nodes_buckets( self.mesh, self.tolerance ) + self.logger.info( f"Found {len(self.collocated_node_buckets)} groups of collocated nodes" ) - self.m_logger.info( "The following list displays the nodes buckets that contains the duplicated node indices." ) - self.m_logger.info( self.getCollocatedNodeBuckets() ) + # Find wrong support elements + self.wrong_support_elements = find_wrong_support_elements( self.mesh ) + self.logger.info( f"Found {len(self.wrong_support_elements)} elements with wrong support" ) - self.m_logger.info( "The following list displays the indexes of the cells with support node indices " - " appearing twice or more." ) - self.m_logger.info( self.getWrongSupportElements() ) + # Add marking arrays if requested + if self.paint_wrong_support_elements and self.wrong_support_elements: + self._addWrongSupportElementsArray() - output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() - output_mesh.CopyStructure( input_mesh ) - output_mesh.CopyAttributes( input_mesh ) + self.logger.info( f"The filter {self.logger.name} succeeded" ) + return True - if self.m_paintWrongSupportElements: - arrayWSP: npt.NDArray = np.zeros( ( output_mesh.GetNumberOfCells(), 1 ), dtype=int ) - arrayWSP[ self.m_wrongSupportElements ] = 1 - vtkArrayWSP: vtkDataArray = numpy_to_vtk( arrayWSP ) - vtkArrayWSP.SetName( "HasDuplicatedNodes" ) - output_mesh.GetCellData().AddArray( vtkArrayWSP ) + except Exception as e: + self.logger.error( f"Error in collocated nodes analysis: {e}" ) + self.logger.error( f"The filter {self.logger.name} failed" ) + return False - output.ShallowCopy( output_mesh ) + def _addWrongSupportElementsArray( self: Self ) -> None: + """Add array marking wrong support elements.""" + num_cells = self.mesh.GetNumberOfCells() + wrong_support_array = np.zeros( num_cells, dtype=np.int32 ) - return 1 + for element_id in self.wrong_support_elements: + if 0 <= element_id < num_cells: + wrong_support_array[ element_id ] = 1 - def getCollocatedNodeBuckets( self: Self ) -> list[ tuple[ int ] ]: - """Returns the nodes buckets that contains the duplicated node indices. + vtk_array: vtkDataArray = numpy_to_vtk( wrong_support_array ) + vtk_array.SetName( "WrongSupportElements" ) + self.mesh.GetCellData().AddArray( vtk_array ) - Args: - self (Self) + def getCollocatedNodeBuckets( self: Self ) -> list[ tuple[ int ] ]: + """Returns the nodes buckets that contain the duplicated node indices. Returns: - list[ tuple[ int ] ] + list[tuple[int]]: Groups of collocated node indices """ - return self.m_collocatedNodesBuckets + return self.collocated_node_buckets def getWrongSupportElements( self: Self ) -> list[ int ]: """Returns the element indices with support node indices appearing more than once. - Args: - self (Self) - Returns: - list[ int ] - """ - return self.m_wrongSupportElements - - def setPaintWrongSupportElements( self: Self, choice: int ) -> None: - """Set 0 or 1 to choose if you want to create a new "WrongSupportElements" array in your output data. - - Args: - self (Self) - choice (int): 0 or 1 - """ - if choice not in [ 0, 1 ]: - self.m_logger.error( f"setPaintWrongSupportElements: Please choose either 0 or 1 not '{choice}'." ) - else: - self.m_paintWrongSupportElements = choice - self.Modified() - - def setTolerance( self: Self, tolerance: float ) -> None: - """Set the tolerance parameter to define if two points are collocated or not. - - Args: - self (Self) - tolerance (float) + list[int]: Element indices with problematic support nodes """ - self.m_tolerance = tolerance - self.Modified() + return self.wrong_support_elements + + +# Main function for backward compatibility and standalone use +def collocated_nodes( + mesh: vtkUnstructuredGrid, + tolerance: float = 0.0, + paint_wrong_support_elements: bool = False, + write_output: bool = False, + output_path: str = "output/mesh_with_collocated_info.vtu", +) -> tuple[ vtkUnstructuredGrid, list[ tuple[ int ] ], list[ int ] ]: + """Apply collocated nodes analysis to a mesh. + + Args: + mesh (vtkUnstructuredGrid): The input mesh + tolerance (float): Distance tolerance for detecting collocated nodes. Defaults to 0.0. + paint_wrong_support_elements (bool): Whether to mark wrong support elements. Defaults to False. + write_output (bool): Whether to write output mesh to file. Defaults to False. + output_path (str): Output file path if write_output is True. + + Returns: + tuple[vtkUnstructuredGrid, list[tuple[int]], list[int]]: + Processed mesh, collocated node buckets, wrong support elements + """ + filter_instance = CollocatedNodes( mesh, tolerance, paint_wrong_support_elements ) + success = filter_instance.applyFilter() + + if not success: + raise RuntimeError( "Collocated nodes analysis failed" ) + + if write_output: + filter_instance.writeGrid( output_path ) + + return ( + filter_instance.getMesh(), + filter_instance.getCollocatedNodeBuckets(), + filter_instance.getWrongSupportElements(), + ) + + +# Alias for backward compatibility +def processCollocatedNodes( + mesh: vtkUnstructuredGrid, + tolerance: float = 0.0, + paint_wrong_support_elements: bool = False, +) -> tuple[ vtkUnstructuredGrid, list[ tuple[ int ] ], list[ int ] ]: + """Legacy function name for backward compatibility.""" + return collocated_nodes( mesh, tolerance, paint_wrong_support_elements ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py index abb6fbe0b..2febe51ad 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py @@ -2,121 +2,173 @@ import numpy.typing as npt from typing_extensions import Self from vtkmodules.util.numpy_support import vtk_to_numpy -from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray +from vtkmodules.vtkCommonCore import vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from vtkmodules.vtkFiltersVerdict import vtkCellSizeFilter -from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase +from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorFilterBase __doc__ = """ -ElementVolumes module is a vtk filter that calculates the volumes of all elements in a vtkUnstructuredGrid. +ElementVolumes module calculates the volumes of all elements in a vtkUnstructuredGrid. The filter can identify elements with negative or zero volumes, which typically indicate mesh quality issues such as inverted elements or degenerate cells. -One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. - To use the filter: .. code-block:: python - from filters.ElementVolumes import ElementVolumes + from geos.mesh.doctor.filters.ElementVolumes import ElementVolumes # instantiate the filter - elementVolumesFilter: ElementVolumes = ElementVolumes() - - # optionally enable detection of negative/zero volume elements - elementVolumesFilter.setReturnNegativeZeroVolumes(True) - - # set input mesh - elementVolumesFilter.SetInputData(mesh) + elementVolumesFilter = ElementVolumes(mesh, return_negative_zero_volumes=True) # execute the filter - output_mesh: vtkUnstructuredGrid = elementVolumesFilter.getGrid() + success = elementVolumesFilter.applyFilter() # get problematic elements (if enabled) - if elementVolumesFilter.m_returnNegativeZeroVolumes: - negative_zero_volumes = elementVolumesFilter.getNegativeZeroVolumes() - # returns numpy array with shape (n, 2) where first column is element index, second is volume + negative_zero_volumes = elementVolumesFilter.getNegativeZeroVolumes() + # returns numpy array with shape (n, 2) where first column is element index, second is volume + + # get the processed mesh with volume information + output_mesh = elementVolumesFilter.getMesh() # write the output mesh with volume information elementVolumesFilter.writeGrid("output/mesh_with_volumes.vtu") """ +loggerTitle: str = "Element Volumes Filter" -class ElementVolumes( MeshDoctorBase ): - def __init__( self: Self ) -> None: - """Vtk filter to calculate the volume of every element of a vtkUnstructuredGrid. +class ElementVolumes( MeshDoctorFilterBase ): - Output mesh is vtkUnstructuredGrid. + def __init__( + self: Self, + mesh: vtkUnstructuredGrid, + return_negative_zero_volumes: bool = False, + use_external_logger: bool = False, + ) -> None: + """Initialize the element volumes filter. + + Args: + mesh (vtkUnstructuredGrid): The input mesh to analyze + return_negative_zero_volumes (bool): Whether to report negative/zero volume elements. Defaults to False. + use_external_logger (bool): Whether to use external logger. Defaults to False. """ - super().__init__( nInputPorts=1, - nOutputPorts=1, - inputType='vtkUnstructuredGrid', - outputType='vtkUnstructuredGrid' ) - self.m_returnNegativeZeroVolumes: bool = False - self.m_volumes: npt.NDArray = None + super().__init__( mesh, loggerTitle, use_external_logger ) + self.return_negative_zero_volumes: bool = return_negative_zero_volumes + self.volumes: vtkDataArray = None - def RequestData( self: Self, request: vtkInformation, inInfoVec: list[ vtkInformationVector ], - outInfo: vtkInformationVector ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestData. + def setReturnNegativeZeroVolumes( self: Self, return_negative_zero_volumes: bool ) -> None: + """Set whether to report negative and zero volume elements. Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects + return_negative_zero_volumes (bool): True to enable reporting, False to disable + """ + self.return_negative_zero_volumes = return_negative_zero_volumes + + def applyFilter( self: Self ) -> bool: + """Apply the element volumes calculation. Returns: - int: 1 if calculation successfully ended, 0 otherwise. + bool: True if calculation completed successfully, False otherwise. """ - input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) - output = vtkUnstructuredGrid.GetData( outInfo ) - - cellSize = vtkCellSizeFilter() - cellSize.ComputeAreaOff() - cellSize.ComputeLengthOff() - cellSize.ComputeSumOff() - cellSize.ComputeVertexCountOff() - cellSize.ComputeVolumeOn() - volume_array_name: str = "MESH_DOCTOR_VOLUME" - cellSize.SetVolumeArrayName( volume_array_name ) - - cellSize.SetInputData( input_mesh ) - cellSize.Update() - volumes: vtkDataArray = cellSize.GetOutput().GetCellData().GetArray( volume_array_name ) - self.m_volumes = volumes - - output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() - output_mesh.CopyStructure( input_mesh ) - output_mesh.CopyAttributes( input_mesh ) - output_mesh.GetCellData().AddArray( volumes ) - output.ShallowCopy( output_mesh ) - - if self.m_returnNegativeZeroVolumes: - self.m_logger.info( "The following table displays the indexes of the cells with a zero or negative volume" ) - self.m_logger.info( self.getNegativeZeroVolumes() ) - - return 1 + self.logger.info( f"Apply filter {self.logger.name}" ) + + try: + # Use VTK's cell size filter to compute volumes + cellSize = vtkCellSizeFilter() + cellSize.ComputeAreaOff() + cellSize.ComputeLengthOff() + cellSize.ComputeSumOff() + cellSize.ComputeVertexCountOff() + cellSize.ComputeVolumeOn() + + volume_array_name: str = "MESH_DOCTOR_VOLUME" + cellSize.SetVolumeArrayName( volume_array_name ) + cellSize.SetInputData( self.mesh ) + cellSize.Update() + + # Get the computed volumes + self.volumes = cellSize.GetOutput().GetCellData().GetArray( volume_array_name ) + + # Add the volume array to our mesh + self.mesh.GetCellData().AddArray( self.volumes ) + + if self.return_negative_zero_volumes: + negative_zero_volumes = self.getNegativeZeroVolumes() + self.logger.info( f"Found {len(negative_zero_volumes)} elements with zero or negative volume" ) + if len( negative_zero_volumes ) > 0: + self.logger.info( "Element indices and volumes with zero or negative values:" ) + for idx, vol in negative_zero_volumes: + self.logger.info( f" Element {idx}: volume = {vol}" ) + + self.logger.info( f"The filter {self.logger.name} succeeded" ) + return True + + except Exception as e: + self.logger.error( f"Error in element volumes calculation: {e}" ) + self.logger.error( f"The filter {self.logger.name} failed" ) + return False def getNegativeZeroVolumes( self: Self ) -> npt.NDArray: - """Returns a numpy array of all the negative and zero volumes of the input vtkUnstructuredGrid. - - Args: - self (Self) + """Returns a numpy array of all the negative and zero volumes. Returns: - npt.NDArray + npt.NDArray: Array with shape (n, 2) where first column is element index, second is volume """ - assert self.m_volumes is not None - volumes_np: npt.NDArray = vtk_to_numpy( self.m_volumes ) + if self.volumes is None: + return np.array( [] ).reshape( 0, 2 ) + + volumes_np: npt.NDArray = vtk_to_numpy( self.volumes ) indices = np.where( volumes_np <= 0 )[ 0 ] return np.column_stack( ( indices, volumes_np[ indices ] ) ) - def setReturnNegativeZeroVolumes( self: Self, returnNegativeZeroVolumes: bool ) -> None: - """Set the condition to return or not the negative and Zero volumes when calculating the volumes. + def getVolumes( self: Self ) -> vtkDataArray: + """Get the computed volume array. - Args: - self (Self) - returnNegativeZeroVolumes (bool) + Returns: + vtkDataArray: The volume data array, or None if not computed yet """ - self.m_returnNegativeZeroVolumes = returnNegativeZeroVolumes - self.Modified() + return self.volumes + + +# Main function for backward compatibility and standalone use +def element_volumes( + mesh: vtkUnstructuredGrid, + return_negative_zero_volumes: bool = False, + write_output: bool = False, + output_path: str = "output/mesh_with_volumes.vtu", +) -> tuple[ vtkUnstructuredGrid, npt.NDArray ]: + """Apply element volumes calculation to a mesh. + + Args: + mesh (vtkUnstructuredGrid): The input mesh + return_negative_zero_volumes (bool): Whether to report negative/zero volume elements. Defaults to False. + write_output (bool): Whether to write output mesh to file. Defaults to False. + output_path (str): Output file path if write_output is True. + + Returns: + tuple[vtkUnstructuredGrid, npt.NDArray]: + Processed mesh, array of negative/zero volume elements + """ + filter_instance = ElementVolumes( mesh, return_negative_zero_volumes ) + success = filter_instance.applyFilter() + + if not success: + raise RuntimeError( "Element volumes calculation failed" ) + + if write_output: + filter_instance.writeGrid( output_path ) + + return ( + filter_instance.getMesh(), + filter_instance.getNegativeZeroVolumes(), + ) + + +# Alias for backward compatibility +def processElementVolumes( + mesh: vtkUnstructuredGrid, + return_negative_zero_volumes: bool = False, +) -> tuple[ vtkUnstructuredGrid, npt.NDArray ]: + """Legacy function name for backward compatibility.""" + return element_volumes( mesh, return_negative_zero_volumes ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py index be0246625..9fe04cc81 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py @@ -1,8 +1,7 @@ from typing_extensions import Self -from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.generate_fractures import Options, split_mesh_on_fractures -from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase +from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorFilterBase from geos.mesh.doctor.parsing.generate_fractures_parsing import convert, convert_to_fracture_policy from geos.mesh.doctor.parsing.generate_fractures_parsing import ( __FIELD_NAME, __FIELD_VALUES, __FRACTURES_DATA_MODE, __FRACTURES_OUTPUT_DIR, __FRACTURES_DATA_MODE_VALUES, @@ -11,45 +10,31 @@ from geos.mesh.utils.arrayHelpers import has_array __doc__ = """ -GenerateFractures module is a vtk filter that splits a vtkUnstructuredGrid along non-embedded fractures. +GenerateFractures module splits a vtkUnstructuredGrid along non-embedded fractures. When a fracture plane is defined between two cells, the nodes of the shared face will be duplicated to create a discontinuity. The filter generates both the split main mesh and separate fracture meshes. -One filter input is vtkUnstructuredGrid, two filter outputs which are vtkUnstructuredGrid. - To use the filter: .. code-block:: python - from filters.GenerateFractures import GenerateFractures + from geos.mesh.doctor.filters.GenerateFractures import GenerateFractures # instantiate the filter - generateFracturesFilter: GenerateFractures = GenerateFractures() - - # set the field name that defines fracture regions - generateFracturesFilter.setFieldName("fracture_field") - - # set the field values that identify fracture boundaries - generateFracturesFilter.setFieldValues("1,2") # comma-separated values - - # set fracture policy (0 for internal fractures, 1 for boundary fractures) - generateFracturesFilter.setPolicy(1) - - # set output directory for fracture meshes - generateFracturesFilter.setFracturesOutputDirectory("./fractures/") - - # optionally set data mode (0 for ASCII, 1 for binary) - generateFracturesFilter.setOutputDataMode(1) - generateFracturesFilter.setFracturesDataMode(1) - - # set input mesh - generateFracturesFilter.SetInputData(mesh) + generateFracturesFilter = GenerateFractures( + mesh, + field_name="fracture_field", + field_values="1,2", + fractures_output_dir="./fractures/", + policy=1 + ) # execute the filter - generateFracturesFilter.Update() + success = generateFracturesFilter.applyFilter() - # get the split mesh and fracture meshes - split_mesh, fracture_meshes = generateFracturesFilter.getAllGrids() + # get the results + split_mesh = generateFracturesFilter.getMesh() + fracture_meshes = generateFracturesFilter.getFractureMeshes() # write all meshes generateFracturesFilter.writeMeshes("output/split_mesh.vtu", is_data_mode_binary=True) @@ -63,132 +48,257 @@ POLICIES = __POLICIES POLICY = __POLICY +loggerTitle: str = "Generate Fractures Filter" + -class GenerateFractures( MeshDoctorBase ): +class GenerateFractures( MeshDoctorFilterBase ): - def __init__( self: Self ) -> None: - """Vtk filter to generate a simple rectilinear grid. + def __init__( + self: Self, + mesh: vtkUnstructuredGrid, + field_name: str = None, + field_values: str = None, + fractures_output_dir: str = None, + policy: int = 1, + output_data_mode: int = 0, + fractures_data_mode: int = 1, + use_external_logger: bool = False, + ) -> None: + """Initialize the generate fractures filter. - Output mesh is vtkUnstructuredGrid. + Args: + mesh (vtkUnstructuredGrid): The input mesh to split + field_name (str): Field name that defines fracture regions. Defaults to None. + field_values (str): Comma-separated field values that identify fracture boundaries. Defaults to None. + fractures_output_dir (str): Output directory for fracture meshes. Defaults to None. + policy (int): Fracture policy (0 for internal, 1 for boundary). Defaults to 1. + output_data_mode (int): Data mode for main mesh (0 for ASCII, 1 for binary). Defaults to 0. + fractures_data_mode (int): Data mode for fracture meshes (0 for ASCII, 1 for binary). Defaults to 1. + use_external_logger (bool): Whether to use external logger. Defaults to False. """ - super().__init__( nInputPorts=1, - nOutputPorts=2, - inputType='vtkUnstructuredGrid', - outputType='vtkUnstructuredGrid' ) - self.m_policy: str = POLICIES[ 1 ] - self.m_field_name: str = None - self.m_field_values: str = None - self.m_fractures_output_dir: str = None - self.m_output_modes_binary: str = { "mesh": DATA_MODE[ 0 ], "fractures": DATA_MODE[ 1 ] } - self.m_mesh_VtkOutput: VtkOutput = None - self.m_all_fractures_VtkOutput: list[ VtkOutput ] = None - - def RequestData( self: Self, request: vtkInformation, inInfoVec: list[ vtkInformationVector ], - outInfo: list[ vtkInformationVector ] ) -> int: - input_mesh = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) - if has_array( input_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." ) - self.m_logger.error( err_msg ) - return 0 - - parsed_options: dict[ str, str ] = self.getParsedOptions() - self.m_logger.critical( f"Parsed_options:\n{parsed_options}" ) - if len( parsed_options ) < 5: - self.m_logger.error( "You must set all variables before trying to create fractures." ) - return 0 - - options: Options = convert( parsed_options ) - self.m_all_fractures_VtkOutput = options.all_fractures_VtkOutput - output_mesh, fracture_meshes = split_mesh_on_fractures( input_mesh, options ) - opt = vtkUnstructuredGrid.GetData( outInfo, 0 ) - opt.ShallowCopy( output_mesh ) - - nbr_faults: int = len( fracture_meshes ) - self.SetNumberOfOutputPorts( 1 + nbr_faults ) # one output port for splitted mesh, the rest for every fault - for i in range( nbr_faults ): - opt_fault = vtkUnstructuredGrid.GetData( outInfo, i + 1 ) - opt_fault.ShallowCopy( fracture_meshes[ i ] ) - - return 1 + super().__init__( mesh, loggerTitle, use_external_logger ) + self.field_name: str = field_name + self.field_values: str = field_values + self.fractures_output_dir: str = fractures_output_dir + self.policy: str = POLICIES[ policy ] if 0 <= policy <= 1 else POLICIES[ 1 ] + self.output_data_mode: str = DATA_MODE[ output_data_mode ] if output_data_mode in [ 0, 1 ] else DATA_MODE[ 0 ] + self.fractures_data_mode: str = ( DATA_MODE[ fractures_data_mode ] + if fractures_data_mode in [ 0, 1 ] else DATA_MODE[ 1 ] ) + + # Results storage + self.fracture_meshes: list[ vtkUnstructuredGrid ] = [] + self.all_fractures_vtk_output: list[ VtkOutput ] = [] - def getAllGrids( self: Self ) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: - """Returns the vtkUnstructuredGrid with volumes. + def setFieldName( self: Self, field_name: str ) -> None: + """Set the field name that defines fracture regions. Args: - self (Self) + field_name (str): Name of the field + """ + self.field_name = field_name - Returns: - vtkUnstructuredGrid + def setFieldValues( self: Self, field_values: str ) -> None: + """Set the field values that identify fracture boundaries. + + Args: + field_values (str): Comma-separated field values """ - self.Update() # triggers RequestData - splitted_grid: vtkUnstructuredGrid = self.GetOutputDataObject( 0 ) - nbrOutputPorts: int = self.GetNumberOfOutputPorts() - fracture_meshes: list[ vtkUnstructuredGrid ] = list() - for i in range( 1, nbrOutputPorts ): - fracture_meshes.append( self.GetOutputDataObject( i ) ) - return ( splitted_grid, fracture_meshes ) - - def getParsedOptions( self: Self ) -> dict[ str, str ]: - parsed_options: dict[ str, str ] = { "output": "./mesh.vtu", "data_mode": DATA_MODE[ 0 ] } - parsed_options[ POLICY ] = self.m_policy - parsed_options[ FRACTURES_DATA_MODE ] = self.m_output_modes_binary[ "fractures" ] - if self.m_field_name: - parsed_options[ FIELD_NAME ] = self.m_field_name - else: - self.m_logger.error( "No field name provided. Please use setFieldName." ) - if self.m_field_values: - parsed_options[ FIELD_VALUES ] = self.m_field_values - else: - self.m_logger.error( "No field values provided. Please use setFieldValues." ) - if self.m_fractures_output_dir: - parsed_options[ FRACTURES_OUTPUT_DIR ] = self.m_fractures_output_dir - else: - self.m_logger.error( "No fracture output directory provided. Please use setFracturesOutputDirectory." ) - return parsed_options + self.field_values = field_values - def setFieldName( self: Self, field_name: str ) -> None: - self.m_field_name = field_name - self.Modified() + def setFracturesOutputDirectory( self: Self, directory: str ) -> None: + """Set the output directory for fracture meshes. - def setFieldValues( self: Self, field_values: str ) -> None: - self.m_field_values = field_values - self.Modified() + Args: + directory (str): Directory path + """ + self.fractures_output_dir = directory - def setFracturesDataMode( self: Self, choice: int ) -> None: + def setPolicy( self: Self, choice: int ) -> None: + """Set the fracture policy. + + Args: + choice (int): 0 for internal fractures, 1 for boundary fractures + """ if choice not in [ 0, 1 ]: - self.m_logger.error( f"setFracturesDataMode: Please choose either 0 for {DATA_MODE[ 0 ]} or 1 for", - f" {DATA_MODE[ 1 ]}, not '{choice}'." ) + self.logger.error( + f"setPolicy: Please choose either 0 for {POLICIES[0]} or 1 for {POLICIES[1]}, not '{choice}'." ) else: - self.m_output_modes_binary[ "fractures" ] = DATA_MODE[ choice ] - self.Modified() - - def setFracturesOutputDirectory( self: Self, directory: str ) -> None: - self.m_fractures_output_dir = directory - self.Modified() + self.policy = convert_to_fracture_policy( POLICIES[ choice ] ) def setOutputDataMode( self: Self, choice: int ) -> None: + """Set the data mode for the main mesh output. + + Args: + choice (int): 0 for ASCII, 1 for binary + """ if choice not in [ 0, 1 ]: - self.m_logger.error( f"setOutputDataMode: Please choose either 0 for {DATA_MODE[ 0 ]} or 1 for", - f" {DATA_MODE[ 1 ]}, not '{choice}'." ) + self.logger.error( + f"setOutputDataMode: Please choose either 0 for {DATA_MODE[0]} or 1 for {DATA_MODE[1]}, not '{choice}'." + ) else: - self.m_output_modes_binary[ "mesh" ] = DATA_MODE[ choice ] - self.Modified() + self.output_data_mode = DATA_MODE[ choice ] - def setPolicy( self: Self, choice: int ) -> None: + def setFracturesDataMode( self: Self, choice: int ) -> None: + """Set the data mode for fracture mesh outputs. + + Args: + choice (int): 0 for ASCII, 1 for binary + """ if choice not in [ 0, 1 ]: - self.m_logger.error( f"setPolicy: Please choose either 0 for {POLICIES[ 0 ]} or 1 for {POLICIES[ 1 ]}," - f" not '{choice}'." ) + self.logger.error( f"setFracturesDataMode: Please choose either 0 for {DATA_MODE[0]} " + f"or 1 for {DATA_MODE[1]}, not '{choice}'." ) else: - self.m_policy = convert_to_fracture_policy( POLICIES[ choice ] ) - self.Modified() + self.fractures_data_mode = DATA_MODE[ choice ] + + def applyFilter( self: Self ) -> bool: + """Apply the fracture generation. + + Returns: + bool: True if fractures generated successfully, False otherwise. + """ + self.logger.info( f"Apply filter {self.logger.name}" ) + + try: + # Check for global IDs which are not allowed + if has_array( self.mesh, [ "GLOBAL_IDS_POINTS", "GLOBAL_IDS_CELLS" ] ): + self.logger.error( + "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." ) + return False + + # Validate required parameters + parsed_options = self._getParsedOptions() + if len( parsed_options ) < 5: + self.logger.error( "You must set all variables before trying to create fractures." ) + return False - def writeMeshes( self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: - splitted_grid, fracture_meshes = self.getAllGrids() - if splitted_grid: - write_mesh( splitted_grid, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) + self.logger.info( f"Parsed options: {parsed_options}" ) + + # Convert options and split mesh + options: Options = convert( parsed_options ) + self.all_fractures_vtk_output = options.all_fractures_VtkOutput + + # Perform the fracture generation + output_mesh, self.fracture_meshes = split_mesh_on_fractures( self.mesh, options ) + + # Update the main mesh with the split result + self.mesh = output_mesh + + self.logger.info( f"Generated {len(self.fracture_meshes)} fracture meshes" ) + self.logger.info( f"The filter {self.logger.name} succeeded" ) + return True + + except Exception as e: + self.logger.error( f"Error in fracture generation: {e}" ) + self.logger.error( f"The filter {self.logger.name} failed" ) + return False + + def _getParsedOptions( self: Self ) -> dict[ str, str ]: + """Get parsed options for fracture generation.""" + parsed_options: dict[ str, str ] = { "output": "./mesh.vtu", "data_mode": DATA_MODE[ 0 ] } + parsed_options[ POLICY ] = self.policy + parsed_options[ FRACTURES_DATA_MODE ] = self.fractures_data_mode + + if self.field_name: + parsed_options[ FIELD_NAME ] = self.field_name else: - self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) + self.logger.error( "No field name provided. Please use setFieldName." ) - for i, fracture_mesh in enumerate( fracture_meshes ): - write_mesh( fracture_mesh, self.m_all_fractures_VtkOutput[ i ] ) + if self.field_values: + parsed_options[ FIELD_VALUES ] = self.field_values + else: + self.logger.error( "No field values provided. Please use setFieldValues." ) + + if self.fractures_output_dir: + parsed_options[ FRACTURES_OUTPUT_DIR ] = self.fractures_output_dir + else: + self.logger.error( "No fracture output directory provided. Please use setFracturesOutputDirectory." ) + + return parsed_options + + def getFractureMeshes( self: Self ) -> list[ vtkUnstructuredGrid ]: + """Get the generated fracture meshes. + + Returns: + list[vtkUnstructuredGrid]: List of fracture meshes + """ + return self.fracture_meshes + + def getAllGrids( self: Self ) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: + """Get both the split main mesh and fracture meshes. + + Returns: + tuple[vtkUnstructuredGrid, list[vtkUnstructuredGrid]]: Split mesh and fracture meshes + """ + return ( self.mesh, self.fracture_meshes ) + + def writeMeshes( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: + """Write both the split main mesh and all fracture meshes. + + Args: + filepath (str): Path for the main split mesh + is_data_mode_binary (bool): Whether to use binary format for main mesh. Defaults to True. + canOverwrite (bool): Whether to allow overwriting existing files. Defaults to False. + """ + if self.mesh: + write_mesh( self.mesh, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) + else: + self.logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) + + for i, fracture_mesh in enumerate( self.fracture_meshes ): + if i < len( self.all_fractures_vtk_output ): + write_mesh( fracture_mesh, self.all_fractures_vtk_output[ i ] ) + + +# Main function for backward compatibility and standalone use +def generate_fractures( + mesh: vtkUnstructuredGrid, + field_name: str, + field_values: str, + fractures_output_dir: str, + policy: int = 1, + output_data_mode: int = 0, + fractures_data_mode: int = 1, + write_output: bool = False, + output_path: str = "output/split_mesh.vtu", +) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: + """Apply fracture generation to a mesh. + + Args: + mesh (vtkUnstructuredGrid): The input mesh + field_name (str): Field name that defines fracture regions + field_values (str): Comma-separated field values that identify fracture boundaries + fractures_output_dir (str): Output directory for fracture meshes + policy (int): Fracture policy (0 for internal, 1 for boundary). Defaults to 1. + output_data_mode (int): Data mode for main mesh (0 for ASCII, 1 for binary). Defaults to 0. + fractures_data_mode (int): Data mode for fracture meshes (0 for ASCII, 1 for binary). Defaults to 1. + write_output (bool): Whether to write output meshes to files. Defaults to False. + output_path (str): Output file path if write_output is True. + + Returns: + tuple[vtkUnstructuredGrid, list[vtkUnstructuredGrid]]: + Split mesh and fracture meshes + """ + filter_instance = GenerateFractures( mesh, field_name, field_values, fractures_output_dir, policy, output_data_mode, + fractures_data_mode ) + success = filter_instance.applyFilter() + + if not success: + raise RuntimeError( "Fracture generation failed" ) + + if write_output: + filter_instance.writeMeshes( output_path ) + + return filter_instance.getAllGrids() + + +# Alias for backward compatibility +def processGenerateFractures( + mesh: vtkUnstructuredGrid, + field_name: str, + field_values: str, + fractures_output_dir: str, + policy: int = 1, +) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: + """Legacy function name for backward compatibility.""" + return generate_fractures( mesh, field_name, field_values, fractures_output_dir, policy ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py index 56bfe2996..b908f9a17 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py @@ -1,11 +1,10 @@ import numpy.typing as npt from typing import Iterable, Sequence from typing_extensions import Self -from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.generate_global_ids import build_global_ids from geos.mesh.doctor.actions.generate_cube import FieldInfo, add_fields, build_coordinates, build_rectilinear_grid -from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorGenerator +from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorGeneratorBase __doc__ = """ GenerateRectilinearGrid module is a vtk filter that allows to create a simple vtkUnstructuredGrid rectilinear grid. @@ -50,97 +49,185 @@ mesh: vtkUnstructuredGrid = generateRectilinearGridFilter.GetOutputDataObject( 0 ) # finally, you can write the mesh at a specific destination with: - generateRectilinearGridFilter.writeGrid( "output/filepath/of/your/grid.vtu" ) + generateRectilinearGridFilter.writeGrid("output/filepath/of/your/grid.vtu") """ +loggerTitle: str = "Generate Rectilinear Grid" -class GenerateRectilinearGrid( MeshDoctorGenerator ): - def __init__( self: Self ) -> None: - """Vtk filter to generate a simple rectilinear grid. +class GenerateRectilinearGrid( MeshDoctorGeneratorBase ): - Output mesh is vtkUnstructuredGrid. + def __init__( + self: Self, + generate_cells_global_ids: bool = False, + generate_points_global_ids: bool = False, + use_external_logger: bool = False, + ) -> None: + """Initialize the rectilinear grid generator. + + Args: + generate_cells_global_ids (bool): Whether to generate global cell IDs. Defaults to False. + generate_points_global_ids (bool): Whether to generate global point IDs. Defaults to False. + use_external_logger (bool): Whether to use external logger. Defaults to False. + """ + super().__init__( loggerTitle, use_external_logger ) + self.generate_cells_global_ids: bool = generate_cells_global_ids + self.generate_points_global_ids: bool = generate_points_global_ids + self.coords_x: Sequence[ float ] = None + self.coords_y: Sequence[ float ] = None + self.coords_z: Sequence[ float ] = None + self.number_elements_x: Sequence[ int ] = None + self.number_elements_y: Sequence[ int ] = None + self.number_elements_z: Sequence[ int ] = None + self.fields: Iterable[ FieldInfo ] = list() + + def setCoordinates( + self: Self, + coords_x: Sequence[ float ], + coords_y: Sequence[ float ], + coords_z: Sequence[ float ], + ) -> None: + """Set the coordinates of the block boundaries for the grid along X, Y and Z axis. + + Args: + coords_x (Sequence[float]): Block boundary coordinates along X axis + coords_y (Sequence[float]): Block boundary coordinates along Y axis + coords_z (Sequence[float]): Block boundary coordinates along Z axis """ - super().__init__() - self.m_generateCellsGlobalIds: bool = False - self.m_generatePointsGlobalIds: bool = False - self.m_coordsX: Sequence[ float ] = None - self.m_coordsY: Sequence[ float ] = None - self.m_coordsZ: Sequence[ float ] = None - self.m_numberElementsX: Sequence[ int ] = None - self.m_numberElementsY: Sequence[ int ] = None - self.m_numberElementsZ: Sequence[ int ] = None - self.m_fields: Iterable[ FieldInfo ] = list() - - def RequestData( self: Self, request: vtkInformation, inInfo: vtkInformationVector, - outInfo: vtkInformationVector ) -> int: - opt = vtkUnstructuredGrid.GetData( outInfo ) - x: npt.NDArray = build_coordinates( self.m_coordsX, self.m_numberElementsX ) - y: npt.NDArray = build_coordinates( self.m_coordsY, self.m_numberElementsY ) - z: npt.NDArray = build_coordinates( self.m_coordsZ, self.m_numberElementsZ ) - output: vtkUnstructuredGrid = build_rectilinear_grid( x, y, z ) - output = add_fields( output, self.m_fields ) - build_global_ids( output, self.m_generateCellsGlobalIds, self.m_generatePointsGlobalIds ) - opt.ShallowCopy( output ) - return 1 - - def setCoordinates( self: Self, coordsX: Sequence[ float ], coordsY: Sequence[ float ], - coordsZ: Sequence[ float ] ) -> None: - """Set the coordinates of the block you want to have in your grid by specifying the beginning and ending - coordinates along the X, Y and Z axis. + self.coords_x = coords_x + self.coords_y = coords_y + self.coords_z = coords_z + + def setNumberElements( + self: Self, + number_elements_x: Sequence[ int ], + number_elements_y: Sequence[ int ], + number_elements_z: Sequence[ int ], + ) -> None: + """Set the number of elements for each block along X, Y and Z axis. Args: - self (Self) - coordsX (Sequence[ float ]) - coordsY (Sequence[ float ]) - coordsZ (Sequence[ float ]) + number_elements_x (Sequence[int]): Number of elements per block along X axis + number_elements_y (Sequence[int]): Number of elements per block along Y axis + number_elements_z (Sequence[int]): Number of elements per block along Z axis """ - self.m_coordsX = coordsX - self.m_coordsY = coordsY - self.m_coordsZ = coordsZ - self.Modified() + self.number_elements_x = number_elements_x + self.number_elements_y = number_elements_y + self.number_elements_z = number_elements_z def setGenerateCellsGlobalIds( self: Self, generate: bool ) -> None: - """Set the generation of global cells ids to be True or False. + """Set whether to generate global cell IDs. Args: - self (Self) - generate (bool) + generate (bool): True to generate global cell IDs, False otherwise """ - self.m_generateCellsGlobalIds = generate - self.Modified() + self.generate_cells_global_ids = generate def setGeneratePointsGlobalIds( self: Self, generate: bool ) -> None: - """Set the generation of global points ids to be True or False. + """Set whether to generate global point IDs. Args: - self (Self) - generate (bool) + generate (bool): True to generate global point IDs, False otherwise """ - self.m_generatePointsGlobalIds = generate - self.Modified() + self.generate_points_global_ids = generate def setFields( self: Self, fields: Iterable[ FieldInfo ] ) -> None: - """Specify the cells or points array to be added to the grid. + """Set the fields (arrays) to be added to the grid. Args: - self (Self) - fields (Iterable[ FieldInfo ]) + fields (Iterable[FieldInfo]): Field information for arrays to create """ - self.m_fields = fields - self.Modified() + self.fields = fields - def setNumberElements( self: Self, numberElementsX: Sequence[ int ], numberElementsY: Sequence[ int ], - numberElementsZ: Sequence[ int ] ) -> None: - """For each block that was defined in setCoordinates, specify the number of cells that they should contain. + def applyFilter( self: Self ) -> bool: + """Generate the rectilinear grid. - Args: - self (Self) - numberElementsX (Sequence[ int ]) - numberElementsY (Sequence[ int ]) - numberElementsZ (Sequence[ int ]) + Returns: + bool: True if grid generated successfully, False otherwise. """ - self.m_numberElementsX = numberElementsX - self.m_numberElementsY = numberElementsY - self.m_numberElementsZ = numberElementsZ - self.Modified() + self.logger.info( f"Apply filter {self.logger.name}" ) + + try: + # Validate inputs + required_fields = [ + self.coords_x, self.coords_y, self.coords_z, self.number_elements_x, self.number_elements_y, + self.number_elements_z + ] + if any( field is None for field in required_fields ): + self.logger.error( "Coordinates and number of elements must be set before generating grid" ) + return False + + # Build coordinates + x: npt.NDArray = build_coordinates( self.coords_x, self.number_elements_x ) + y: npt.NDArray = build_coordinates( self.coords_y, self.number_elements_y ) + z: npt.NDArray = build_coordinates( self.coords_z, self.number_elements_z ) + + # Build the rectilinear grid + self.mesh = build_rectilinear_grid( x, y, z ) + + # Add fields if specified + if self.fields: + self.mesh = add_fields( self.mesh, self.fields ) + + # Add global IDs if requested + build_global_ids( self.mesh, self.generate_cells_global_ids, self.generate_points_global_ids ) + + self.logger.info( f"Generated rectilinear grid with {self.mesh.GetNumberOfPoints()} points " + f"and {self.mesh.GetNumberOfCells()} cells" ) + self.logger.info( f"The filter {self.logger.name} succeeded" ) + return True + + except Exception as e: + self.logger.error( f"Error in rectilinear grid generation: {e}" ) + self.logger.error( f"The filter {self.logger.name} failed" ) + return False + + +# Main function for backward compatibility and standalone use +def generate_rectilinear_grid( + coords_x: Sequence[ float ], + coords_y: Sequence[ float ], + coords_z: Sequence[ float ], + number_elements_x: Sequence[ int ], + number_elements_y: Sequence[ int ], + number_elements_z: Sequence[ int ], + fields: Iterable[ FieldInfo ] = None, + generate_cells_global_ids: bool = False, + generate_points_global_ids: bool = False, + write_output: bool = False, + output_path: str = "output/rectilinear_grid.vtu", +) -> vtkUnstructuredGrid: + """Generate a rectilinear grid mesh. + + Args: + coords_x (Sequence[float]): Block boundary coordinates along X axis + coords_y (Sequence[float]): Block boundary coordinates along Y axis + coords_z (Sequence[float]): Block boundary coordinates along Z axis + number_elements_x (Sequence[int]): Number of elements per block along X axis + number_elements_y (Sequence[int]): Number of elements per block along Y axis + number_elements_z (Sequence[int]): Number of elements per block along Z axis + fields (Iterable[FieldInfo]): Field information for arrays to create. Defaults to None. + generate_cells_global_ids (bool): Whether to generate global cell IDs. Defaults to False. + generate_points_global_ids (bool): Whether to generate global point IDs. Defaults to False. + write_output (bool): Whether to write output mesh to file. Defaults to False. + output_path (str): Output file path if write_output is True. + + Returns: + vtkUnstructuredGrid: The generated mesh + """ + filter_instance = GenerateRectilinearGrid( generate_cells_global_ids, generate_points_global_ids ) + filter_instance.setCoordinates( coords_x, coords_y, coords_z ) + filter_instance.setNumberElements( number_elements_x, number_elements_y, number_elements_z ) + + if fields: + filter_instance.setFields( fields ) + + success = filter_instance.applyFilter() + + if not success: + raise RuntimeError( "Rectilinear grid generation failed" ) + + if write_output: + filter_instance.writeGrid( output_path ) + + return filter_instance.getMesh() diff --git a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py index 22d927cad..40509ce7f 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py @@ -1,179 +1,236 @@ import numpy as np -import numpy.typing as npt from typing_extensions import Self from vtkmodules.util.numpy_support import numpy_to_vtk -from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray +from vtkmodules.vtkCommonCore import vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.non_conformal import Options, find_non_conformal_cells -from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase +from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorFilterBase __doc__ = """ -NonConformal module is a vtk filter that detects non-conformal mesh interfaces in a vtkUnstructuredGrid. +NonConformal module detects non-conformal mesh interfaces in a vtkUnstructuredGrid. Non-conformal interfaces occur when adjacent cells do not share nodes or faces properly, which can indicate mesh quality issues or intentional non-matching grid interfaces that require special handling. -One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. - To use the filter: .. code-block:: python - from filters.NonConformal import NonConformal + from geos.mesh.doctor.filters.NonConformal import NonConformal # instantiate the filter - nonConformalFilter: NonConformal = NonConformal() - - # set tolerance parameters - nonConformalFilter.setPointTolerance(1e-6) # tolerance for point matching - nonConformalFilter.setFaceTolerance(1e-6) # tolerance for face matching - nonConformalFilter.setAngleTolerance(10.0) # angle tolerance in degrees - - # optionally enable painting of non-conformal cells - nonConformalFilter.setPaintNonConformalCells(1) # 1 to enable, 0 to disable - - # set input mesh - nonConformalFilter.SetInputData(mesh) + nonConformalFilter = NonConformal( + mesh, + point_tolerance=1e-6, + face_tolerance=1e-6, + angle_tolerance=10.0, + paint_non_conformal_cells=True + ) # execute the filter - output_mesh: vtkUnstructuredGrid = nonConformalFilter.getGrid() + success = nonConformalFilter.applyFilter() # get non-conformal cell pairs non_conformal_cells = nonConformalFilter.getNonConformalCells() # returns list of tuples with (cell1_id, cell2_id) for non-conformal interfaces + # get the processed mesh + output_mesh = nonConformalFilter.getMesh() + # write the output mesh nonConformalFilter.writeGrid("output/mesh_with_nonconformal_info.vtu") """ +loggerTitle: str = "Non-Conformal Filter" -class NonConformal( MeshDoctorBase ): - def __init__( self: Self ) -> None: - """Vtk filter to ... of a vtkUnstructuredGrid. +class NonConformal( MeshDoctorFilterBase ): - Output mesh is vtkUnstructuredGrid. - """ - super().__init__( nInputPorts=1, - nOutputPorts=1, - inputType='vtkUnstructuredGrid', - outputType='vtkUnstructuredGrid' ) - self.m_angle_tolerance: float = 10.0 - self.m_face_tolerance: float = 0.0 - self.m_point_tolerance: float = 0.0 - self.m_non_conformal_cells: list[ tuple[ int, int ] ] = list() - self.m_paintNonConformalCells: int = 0 - - def RequestData( self: Self, request: vtkInformation, inInfoVec: list[ vtkInformationVector ], - outInfo: vtkInformationVector ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestData. + def __init__( + self: Self, + mesh: vtkUnstructuredGrid, + point_tolerance: float = 0.0, + face_tolerance: float = 0.0, + angle_tolerance: float = 10.0, + paint_non_conformal_cells: bool = False, + use_external_logger: bool = False, + ) -> None: + """Initialize the non-conformal detection filter. Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. + mesh (vtkUnstructuredGrid): The input mesh to analyze + point_tolerance (float): Tolerance for point matching. Defaults to 0.0. + face_tolerance (float): Tolerance for face matching. Defaults to 0.0. + angle_tolerance (float): Angle tolerance in degrees. Defaults to 10.0. + paint_non_conformal_cells (bool): Whether to mark non-conformal cells in output. Defaults to False. + use_external_logger (bool): Whether to use external logger. Defaults to False. """ - input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) - output = vtkUnstructuredGrid.GetData( outInfo ) - - options = Options( self.m_angle_tolerance, self.m_point_tolerance, self.m_face_tolerance ) - non_conformal_cells = find_non_conformal_cells( input_mesh, options ) - self.m_non_conformal_cells = non_conformal_cells + super().__init__( mesh, loggerTitle, use_external_logger ) + self.point_tolerance: float = point_tolerance + self.face_tolerance: float = face_tolerance + self.angle_tolerance: float = angle_tolerance + self.paint_non_conformal_cells: bool = paint_non_conformal_cells - non_conformal_cells_extended = [ cell_id for pair in non_conformal_cells for cell_id in pair ] - unique_non_conformal_cells = frozenset( non_conformal_cells_extended ) - self.m_logger.info( f"You have {len( unique_non_conformal_cells )} non conformal cells.\n" - f"{', '.join( map( str, sorted( non_conformal_cells_extended ) ) )}" ) + # Results storage + self.non_conformal_cells: list[ tuple[ int, int ] ] = [] - output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() - output_mesh.CopyStructure( input_mesh ) - output_mesh.CopyAttributes( input_mesh ) + def setPointTolerance( self: Self, tolerance: float ) -> None: + """Set the point tolerance parameter. - if self.m_paintNonConformalCells: - arrayNC: npt.NDArray = np.zeros( ( output_mesh.GetNumberOfCells(), 1 ), dtype=int ) - arrayNC[ unique_non_conformal_cells ] = 1 - vtkArrayNC: vtkDataArray = numpy_to_vtk( arrayNC ) - vtkArrayNC.SetName( "IsNonConformal" ) - output_mesh.GetCellData().AddArray( vtkArrayNC ) + Args: + tolerance (float): Point tolerance value + """ + self.point_tolerance = tolerance - output.ShallowCopy( output_mesh ) + def setFaceTolerance( self: Self, tolerance: float ) -> None: + """Set the face tolerance parameter. - return 1 + Args: + tolerance (float): Face tolerance value + """ + self.face_tolerance = tolerance - def getAngleTolerance( self: Self ) -> float: - """Returns the angle tolerance. + def setAngleTolerance( self: Self, tolerance: float ) -> None: + """Set the angle tolerance parameter in degrees. Args: - self (Self) - - Returns: - float + tolerance (float): Angle tolerance in degrees """ - return self.m_angle_tolerance + self.angle_tolerance = tolerance - def getfaceTolerance( self: Self ) -> float: - """Returns the face tolerance. + def setPaintNonConformalCells( self: Self, paint: bool ) -> None: + """Set whether to create arrays marking non-conformal cells in output data. Args: - self (Self) - - Returns: - float + paint (bool): True to enable marking, False to disable """ - return self.m_face_tolerance + self.paint_non_conformal_cells = paint def getPointTolerance( self: Self ) -> float: - """Returns the point tolerance. - - Args: - self (Self) + """Get the current point tolerance. Returns: - float + float: Point tolerance value """ - return self.m_point_tolerance + return self.point_tolerance - def setPaintNonConformalCells( self: Self, choice: int ) -> None: - """Set 0 or 1 to choose if you want to create a new "IsNonConformal" array in your output data. + def getFaceTolerance( self: Self ) -> float: + """Get the current face tolerance. - Args: - self (Self) - choice (int): 0 or 1 + Returns: + float: Face tolerance value """ - if choice not in [ 0, 1 ]: - self.m_logger.error( f"setPaintNonConformalCells: Please choose either 0 or 1 not '{choice}'." ) - else: - self.m_paintNonConformalCells = choice - self.Modified() + return self.face_tolerance - def setAngleTolerance( self: Self, tolerance: float ) -> None: - """Set the angle tolerance parameter in degree. + def getAngleTolerance( self: Self ) -> float: + """Get the current angle tolerance. - Args: - self (Self) - tolerance (float) + Returns: + float: Angle tolerance in degrees """ - self.m_angle_tolerance = tolerance - self.Modified() + return self.angle_tolerance - def setFaceTolerance( self: Self, tolerance: float ) -> None: - """Set the face tolerance parameter. + def applyFilter( self: Self ) -> bool: + """Apply the non-conformal detection. - Args: - self (Self) - tolerance (float) + Returns: + bool: True if detection completed successfully, False otherwise. """ - self.m_face_tolerance = tolerance - self.Modified() + self.logger.info( f"Apply filter {self.logger.name}" ) - def setPointTolerance( self: Self, tolerance: float ) -> None: - """Set the point tolerance parameter. + try: + # Create options and find non-conformal cells + options = Options( self.angle_tolerance, self.point_tolerance, self.face_tolerance ) + self.non_conformal_cells = find_non_conformal_cells( self.mesh, options ) - Args: - self (Self) - tolerance (float) + # Extract all unique cell IDs from pairs + non_conformal_cells_extended = [ cell_id for pair in self.non_conformal_cells for cell_id in pair ] + unique_non_conformal_cells = frozenset( non_conformal_cells_extended ) + + self.logger.info( f"Found {len(unique_non_conformal_cells)} non-conformal cells" ) + if non_conformal_cells_extended: + self.logger.info( + f"Non-conformal cell IDs: {', '.join(map(str, sorted(non_conformal_cells_extended)))}" ) + + # Add marking arrays if requested + if self.paint_non_conformal_cells and unique_non_conformal_cells: + self._addNonConformalCellsArray( unique_non_conformal_cells ) + + self.logger.info( f"The filter {self.logger.name} succeeded" ) + return True + + except Exception as e: + self.logger.error( f"Error in non-conformal detection: {e}" ) + self.logger.error( f"The filter {self.logger.name} failed" ) + return False + + def _addNonConformalCellsArray( self: Self, unique_non_conformal_cells: frozenset[ int ] ) -> None: + """Add array marking non-conformal cells.""" + num_cells = self.mesh.GetNumberOfCells() + non_conformal_array = np.zeros( num_cells, dtype=np.int32 ) + + for cell_id in unique_non_conformal_cells: + if 0 <= cell_id < num_cells: + non_conformal_array[ cell_id ] = 1 + + vtk_array: vtkDataArray = numpy_to_vtk( non_conformal_array ) + vtk_array.SetName( "IsNonConformal" ) + self.mesh.GetCellData().AddArray( vtk_array ) + + def getNonConformalCells( self: Self ) -> list[ tuple[ int, int ] ]: + """Get the detected non-conformal cell pairs. + + Returns: + list[tuple[int, int]]: List of cell ID pairs that are non-conformal """ - self.m_point_tolerance = tolerance - self.Modified() + return self.non_conformal_cells + + +# Main function for backward compatibility and standalone use +def non_conformal( + mesh: vtkUnstructuredGrid, + point_tolerance: float = 0.0, + face_tolerance: float = 0.0, + angle_tolerance: float = 10.0, + paint_non_conformal_cells: bool = False, + write_output: bool = False, + output_path: str = "output/mesh_with_nonconformal_info.vtu", +) -> tuple[ vtkUnstructuredGrid, list[ tuple[ int, int ] ] ]: + """Apply non-conformal detection to a mesh. + + Args: + mesh (vtkUnstructuredGrid): The input mesh + point_tolerance (float): Tolerance for point matching. Defaults to 0.0. + face_tolerance (float): Tolerance for face matching. Defaults to 0.0. + angle_tolerance (float): Angle tolerance in degrees. Defaults to 10.0. + paint_non_conformal_cells (bool): Whether to mark non-conformal cells. Defaults to False. + write_output (bool): Whether to write output mesh to file. Defaults to False. + output_path (str): Output file path if write_output is True. + + Returns: + tuple[vtkUnstructuredGrid, list[tuple[int, int]]]: + Processed mesh, non-conformal cell pairs + """ + filter_instance = NonConformal( mesh, point_tolerance, face_tolerance, angle_tolerance, paint_non_conformal_cells ) + success = filter_instance.applyFilter() + + if not success: + raise RuntimeError( "Non-conformal detection failed" ) + + if write_output: + filter_instance.writeGrid( output_path ) + + return ( + filter_instance.getMesh(), + filter_instance.getNonConformalCells(), + ) + + +# Alias for backward compatibility +def processNonConformal( + mesh: vtkUnstructuredGrid, + point_tolerance: float = 0.0, + face_tolerance: float = 0.0, + angle_tolerance: float = 10.0, +) -> tuple[ vtkUnstructuredGrid, list[ tuple[ int, int ] ] ]: + """Legacy function name for backward compatibility.""" + return non_conformal( mesh, point_tolerance, face_tolerance, angle_tolerance ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py index 5db9dc2be..d0fe751c2 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py @@ -1,39 +1,31 @@ import numpy as np -import numpy.typing as npt from typing_extensions import Self from vtkmodules.util.numpy_support import numpy_to_vtk -from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray +from vtkmodules.vtkCommonCore import vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.self_intersecting_elements import get_invalid_cell_ids -from geos.mesh.doctor.filters.MeshDoctorBase import MeshDoctorBase +from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorFilterBase __doc__ = """ -SelfIntersectingElements module is a vtk filter that identifies various types of invalid or problematic elements +SelfIntersectingElements module identifies various types of invalid or problematic elements in a vtkUnstructuredGrid. It detects elements with intersecting edges, intersecting faces, non-contiguous edges, non-convex shapes, incorrectly oriented faces, and wrong number of points. -One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. - To use the filter: .. code-block:: python - from filters.SelfIntersectingElements import SelfIntersectingElements + from geos.mesh.doctor.filters.SelfIntersectingElements import SelfIntersectingElements # instantiate the filter - selfIntersectingElementsFilter: SelfIntersectingElements = SelfIntersectingElements() - - # set minimum distance parameter for intersection detection - selfIntersectingElementsFilter.setMinDistance(1e-6) - - # optionally enable painting of invalid elements - selfIntersectingElementsFilter.setPaintInvalidElements(1) # 1 to enable, 0 to disable - - # set input mesh - selfIntersectingElementsFilter.SetInputData(mesh) + selfIntersectingElementsFilter = SelfIntersectingElements( + mesh, + min_distance=1e-6, + paint_invalid_elements=True + ) # execute the filter - output_mesh: vtkUnstructuredGrid = selfIntersectingElementsFilter.getGrid() + success = selfIntersectingElementsFilter.applyFilter() # get different types of problematic elements wrong_points_elements = selfIntersectingElementsFilter.getWrongNumberOfPointsElements() @@ -43,176 +35,229 @@ non_convex_elements = selfIntersectingElementsFilter.getNonConvexElements() wrong_oriented_faces_elements = selfIntersectingElementsFilter.getFacesOrientedIncorrectlyElements() + # get the processed mesh + output_mesh = selfIntersectingElementsFilter.getMesh() + # write the output mesh selfIntersectingElementsFilter.writeGrid("output/mesh_with_invalid_elements.vtu") """ +loggerTitle: str = "Self-Intersecting Elements Filter" -class SelfIntersectingElements( MeshDoctorBase ): - def __init__( self: Self ) -> None: - """Vtk filter to find invalid elements of a vtkUnstructuredGrid. +class SelfIntersectingElements( MeshDoctorFilterBase ): - Output mesh is vtkUnstructuredGrid. - """ - super().__init__( nInputPorts=1, - nOutputPorts=1, - inputType='vtkUnstructuredGrid', - outputType='vtkUnstructuredGrid' ) - self.m_min_distance: float = 0.0 - self.m_wrong_number_of_points_elements: list[ int ] = list() - self.m_intersecting_edges_elements: list[ int ] = list() - self.m_intersecting_faces_elements: list[ int ] = list() - self.m_non_contiguous_edges_elements: list[ int ] = list() - self.m_non_convex_elements: list[ int ] = list() - self.m_faces_oriented_incorrectly_elements: list[ int ] = list() - self.m_paintInvalidElements: int = 0 - - def RequestData( self: Self, request: vtkInformation, inInfoVec: list[ vtkInformationVector ], - outInfo: vtkInformationVector ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestData. + def __init__( + self: Self, + mesh: vtkUnstructuredGrid, + min_distance: float = 0.0, + paint_invalid_elements: bool = False, + use_external_logger: bool = False, + ) -> None: + """Initialize the self-intersecting elements detection filter. Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects + mesh (vtkUnstructuredGrid): The input mesh to analyze + min_distance (float): Minimum distance parameter for intersection detection. Defaults to 0.0. + paint_invalid_elements (bool): Whether to mark invalid elements in output. Defaults to False. + use_external_logger (bool): Whether to use external logger. Defaults to False. + """ + super().__init__( mesh, loggerTitle, use_external_logger ) + self.min_distance: float = min_distance + self.paint_invalid_elements: bool = paint_invalid_elements + + # Results storage + self.wrong_number_of_points_elements: list[ int ] = [] + self.intersecting_edges_elements: list[ int ] = [] + self.intersecting_faces_elements: list[ int ] = [] + self.non_contiguous_edges_elements: list[ int ] = [] + self.non_convex_elements: list[ int ] = [] + self.faces_oriented_incorrectly_elements: list[ int ] = [] - Returns: - int: 1 if calculation successfully ended, 0 otherwise. + def setMinDistance( self: Self, distance: float ) -> None: + """Set the minimum distance parameter for intersection detection. + + Args: + distance (float): Minimum distance value """ - input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) - output = vtkUnstructuredGrid.GetData( outInfo ) + self.min_distance = distance + + def setPaintInvalidElements( self: Self, paint: bool ) -> None: + """Set whether to create arrays marking invalid elements in output data. - invalid_cells = get_invalid_cell_ids( input_mesh, self.m_min_distance ) + Args: + paint (bool): True to enable marking, False to disable + """ + self.paint_invalid_elements = paint - self.m_wrong_number_of_points_elements = invalid_cells.get( "wrong_number_of_points_elements", [] ) - self.m_intersecting_edges_elements = invalid_cells.get( "intersecting_edges_elements", [] ) - self.m_intersecting_faces_elements = invalid_cells.get( "intersecting_faces_elements", [] ) - self.m_non_contiguous_edges_elements = invalid_cells.get( "non_contiguous_edges_elements", [] ) - self.m_non_convex_elements = invalid_cells.get( "non_convex_elements", [] ) - self.m_faces_oriented_incorrectly_elements = invalid_cells.get( "faces_oriented_incorrectly_elements", [] ) + def getMinDistance( self: Self ) -> float: + """Get the current minimum distance parameter. - # Log the results - total_invalid = sum( len( invalid_list ) for invalid_list in invalid_cells.values() ) - self.m_logger.info( f"Found {total_invalid} invalid elements:" ) - for criterion, cell_list in invalid_cells.items(): - if cell_list: - self.m_logger.info( f" {criterion}: {len( cell_list )} elements - {cell_list}" ) + Returns: + float: Minimum distance value + """ + return self.min_distance - output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() - output_mesh.CopyStructure( input_mesh ) - output_mesh.CopyAttributes( input_mesh ) + def applyFilter( self: Self ) -> bool: + """Apply the self-intersecting elements detection. - if self.m_paintInvalidElements: - # Create arrays to mark invalid elements + Returns: + bool: True if detection completed successfully, False otherwise. + """ + self.logger.info( f"Apply filter {self.logger.name}" ) + + try: + # Get invalid cell IDs + invalid_cells = get_invalid_cell_ids( self.mesh, self.min_distance ) + + # Store results + self.wrong_number_of_points_elements = invalid_cells.get( "wrong_number_of_points_elements", [] ) + self.intersecting_edges_elements = invalid_cells.get( "intersecting_edges_elements", [] ) + self.intersecting_faces_elements = invalid_cells.get( "intersecting_faces_elements", [] ) + self.non_contiguous_edges_elements = invalid_cells.get( "non_contiguous_edges_elements", [] ) + self.non_convex_elements = invalid_cells.get( "non_convex_elements", [] ) + self.faces_oriented_incorrectly_elements = invalid_cells.get( "faces_oriented_incorrectly_elements", [] ) + + # Log the results + total_invalid = sum( len( invalid_list ) for invalid_list in invalid_cells.values() ) + self.logger.info( f"Found {total_invalid} invalid elements:" ) for criterion, cell_list in invalid_cells.items(): if cell_list: - array: npt.NDArray = np.zeros( ( output_mesh.GetNumberOfCells(), 1 ), dtype=int ) - array[ cell_list ] = 1 - vtkArray: vtkDataArray = numpy_to_vtk( array ) - vtkArray.SetName( f"Is{criterion.replace('_', '').title()}" ) - output_mesh.GetCellData().AddArray( vtkArray ) + self.logger.info( f" {criterion}: {len(cell_list)} elements - {cell_list}" ) - output.ShallowCopy( output_mesh ) + # Add marking arrays if requested + if self.paint_invalid_elements: + self._addInvalidElementsArrays( invalid_cells ) - return 1 + self.logger.info( f"The filter {self.logger.name} succeeded" ) + return True - def getMinDistance( self: Self ) -> float: - """Returns the minimum distance. + except Exception as e: + self.logger.error( f"Error in self-intersecting elements detection: {e}" ) + self.logger.error( f"The filter {self.logger.name} failed" ) + return False - Args: - self (Self) + def _addInvalidElementsArrays( self: Self, invalid_cells: dict[ str, list[ int ] ] ) -> None: + """Add arrays marking different types of invalid elements.""" + num_cells = self.mesh.GetNumberOfCells() - Returns: - float - """ - return self.m_min_distance + for criterion, cell_list in invalid_cells.items(): + if cell_list: + array = np.zeros( num_cells, dtype=np.int32 ) + for cell_id in cell_list: + if 0 <= cell_id < num_cells: + array[ cell_id ] = 1 - def getWrongNumberOfPointsElements( self: Self ) -> list[ int ]: - """Returns elements with wrong number of points. + vtk_array: vtkDataArray = numpy_to_vtk( array ) + # Convert criterion name to CamelCase for array name + array_name = f"Is{criterion.replace('_', '').title()}" + vtk_array.SetName( array_name ) + self.mesh.GetCellData().AddArray( vtk_array ) - Args: - self (Self) + def getWrongNumberOfPointsElements( self: Self ) -> list[ int ]: + """Get elements with wrong number of points. Returns: - list[int] + list[int]: Element indices with wrong number of points """ - return self.m_wrong_number_of_points_elements + return self.wrong_number_of_points_elements def getIntersectingEdgesElements( self: Self ) -> list[ int ]: - """Returns elements with intersecting edges. - - Args: - self (Self) + """Get elements with intersecting edges. Returns: - list[int] + list[int]: Element indices with intersecting edges """ - return self.m_intersecting_edges_elements + return self.intersecting_edges_elements def getIntersectingFacesElements( self: Self ) -> list[ int ]: - """Returns elements with intersecting faces. - - Args: - self (Self) + """Get elements with intersecting faces. Returns: - list[int] + list[int]: Element indices with intersecting faces """ - return self.m_intersecting_faces_elements + return self.intersecting_faces_elements def getNonContiguousEdgesElements( self: Self ) -> list[ int ]: - """Returns elements with non-contiguous edges. - - Args: - self (Self) + """Get elements with non-contiguous edges. Returns: - list[int] + list[int]: Element indices with non-contiguous edges """ - return self.m_non_contiguous_edges_elements + return self.non_contiguous_edges_elements def getNonConvexElements( self: Self ) -> list[ int ]: - """Returns non-convex elements. - - Args: - self (Self) + """Get non-convex elements. Returns: - list[int] + list[int]: Non-convex element indices """ - return self.m_non_convex_elements + return self.non_convex_elements def getFacesOrientedIncorrectlyElements( self: Self ) -> list[ int ]: - """Returns elements with incorrectly oriented faces. - - Args: - self (Self) + """Get elements with incorrectly oriented faces. Returns: - list[int] + list[int]: Element indices with incorrectly oriented faces """ - return self.m_faces_oriented_incorrectly_elements + return self.faces_oriented_incorrectly_elements - def setPaintInvalidElements( self: Self, choice: int ) -> None: - """Set 0 or 1 to choose if you want to create arrays marking invalid elements in your output data. + def getAllInvalidElements( self: Self ) -> dict[ str, list[ int ] ]: + """Get all invalid elements organized by type. - Args: - self (Self) - choice (int): 0 or 1 - """ - if choice not in [ 0, 1 ]: - self.m_logger.error( f"setPaintInvalidElements: Please choose either 0 or 1 not '{choice}'." ) - else: - self.m_paintInvalidElements = choice - self.Modified() - - def setMinDistance( self: Self, distance: float ) -> None: - """Set the minimum distance parameter. - - Args: - self (Self) - distance (float) + Returns: + dict[str, list[int]]: Dictionary mapping invalid element types to their IDs """ - self.m_min_distance = distance - self.Modified() + return { + "wrong_number_of_points_elements": self.wrong_number_of_points_elements, + "intersecting_edges_elements": self.intersecting_edges_elements, + "intersecting_faces_elements": self.intersecting_faces_elements, + "non_contiguous_edges_elements": self.non_contiguous_edges_elements, + "non_convex_elements": self.non_convex_elements, + "faces_oriented_incorrectly_elements": self.faces_oriented_incorrectly_elements, + } + + +# Main function for backward compatibility and standalone use +def self_intersecting_elements( + mesh: vtkUnstructuredGrid, + min_distance: float = 0.0, + paint_invalid_elements: bool = False, + write_output: bool = False, + output_path: str = "output/mesh_with_invalid_elements.vtu", +) -> tuple[ vtkUnstructuredGrid, dict[ str, list[ int ] ] ]: + """Apply self-intersecting elements detection to a mesh. + + Args: + mesh (vtkUnstructuredGrid): The input mesh + min_distance (float): Minimum distance parameter for intersection detection. Defaults to 0.0. + paint_invalid_elements (bool): Whether to mark invalid elements. Defaults to False. + write_output (bool): Whether to write output mesh to file. Defaults to False. + output_path (str): Output file path if write_output is True. + + Returns: + tuple[vtkUnstructuredGrid, dict[str, list[int]]]: + Processed mesh, dictionary of invalid element types and their IDs + """ + filter_instance = SelfIntersectingElements( mesh, min_distance, paint_invalid_elements ) + success = filter_instance.applyFilter() + + if not success: + raise RuntimeError( "Self-intersecting elements detection failed" ) + + if write_output: + filter_instance.writeGrid( output_path ) + + return ( + filter_instance.getMesh(), + filter_instance.getAllInvalidElements(), + ) + + +# Alias for backward compatibility +def processSelfIntersectingElements( + mesh: vtkUnstructuredGrid, + min_distance: float = 0.0, +) -> tuple[ vtkUnstructuredGrid, dict[ str, list[ int ] ] ]: + """Legacy function name for backward compatibility.""" + return self_intersecting_elements( mesh, min_distance ) From a4c4c3b8bea4b06deac376c1a33009366e4ed8c5 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Fri, 29 Aug 2025 16:42:55 -0700 Subject: [PATCH 32/52] Reimplement to use applyFilter approach --- .../mesh/doctor/actions/element_volumes.py | 70 ++- .../actions/self_intersecting_elements.py | 41 +- .../mesh/doctor/actions/supported_elements.py | 61 ++- .../src/geos/mesh/doctor/filters/Checks.py | 359 +++++++------ .../mesh/doctor/filters/CollocatedNodes.py | 182 ++++--- .../mesh/doctor/filters/ElementVolumes.py | 206 ++++---- .../mesh/doctor/filters/GenerateFractures.py | 339 +++++++------ .../doctor/filters/GenerateRectilinearGrid.py | 262 +++++----- .../doctor/filters/MeshDoctorFilterBase.py | 69 +-- .../geos/mesh/doctor/filters/NonConformal.py | 257 +++++----- .../filters/SelfIntersectingElements.py | 258 ++++------ .../mesh/doctor/filters/SupportedElements.py | 472 +++++++++--------- .../parsing/collocated_nodes_parsing.py | 40 +- .../doctor/parsing/element_volumes_parsing.py | 28 +- .../doctor/parsing/non_conformal_parsing.py | 27 +- .../self_intersecting_elements_parsing.py | 46 +- .../parsing/supported_elements_parsing.py | 42 +- 17 files changed, 1401 insertions(+), 1358 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 888235e3f..c3d46a8c1 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/element_volumes.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from typing import List, Tuple import uuid +from vtkmodules.vtkCommonCore import vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, VTK_HEXAHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_WEDGE from vtkmodules.vtkFiltersVerdict import vtkCellSizeFilter, vtkMeshQuality from vtkmodules.util.numpy_support import vtk_to_numpy @@ -15,26 +15,22 @@ class Options: @dataclass( frozen=True ) class Result: - element_volumes: List[ Tuple[ int, float ] ] + element_volumes: list[ tuple[ int, float ] ] -def mesh_action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: - cs = vtkCellSizeFilter() +SUPPORTED_TYPES = [ VTK_HEXAHEDRON, VTK_TETRA ] - cs.ComputeAreaOff() - cs.ComputeLengthOff() - cs.ComputeSumOff() - cs.ComputeVertexCountOff() - cs.ComputeVolumeOn() - volume_array_name = "__MESH_DOCTOR_VOLUME-" + str( uuid.uuid4() ) # Making the name unique - cs.SetVolumeArrayName( volume_array_name ) - cs.SetInputData( mesh ) - cs.Update() +def get_mesh_quality( mesh: vtkUnstructuredGrid ) -> vtkDataArray: + """Get the quality of the mesh. - mq = vtkMeshQuality() - SUPPORTED_TYPES = [ VTK_HEXAHEDRON, VTK_TETRA ] + Args: + mesh (vtkUnstructuredGrid): The input mesh. + Returns: + vtkDataArray: The array containing the quality of each cell. + """ + mq = vtkMeshQuality() mq.SetTetQualityMeasureToVolume() mq.SetHexQualityMeasureToVolume() if hasattr( mq, "SetPyramidQualityMeasureToVolume" ): # This feature is quite recent @@ -46,18 +42,50 @@ def mesh_action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: setup_logger.warning( "Your \"pyvtk\" version does not bring pyramid nor wedge support with vtkMeshQuality. Using the fallback solution." ) - mq.SetInputData( mesh ) mq.Update() - volume = cs.GetOutput().GetCellData().GetArray( volume_array_name ) - quality = mq.GetOutput().GetCellData().GetArray( "Quality" ) # Name is imposed by vtk. + return mq.GetOutput().GetCellData().GetArray( "Quality" ) # Name is imposed by vtk. + + +def get_mesh_volume( mesh: vtkUnstructuredGrid ) -> vtkDataArray: + """Get the volume of the mesh. + + Args: + mesh (vtkUnstructuredGrid): The input mesh. + + Returns: + vtkDataArray: The array containing the volume of each cell. + """ + cs = vtkCellSizeFilter() + cs.ComputeAreaOff() + cs.ComputeLengthOff() + cs.ComputeSumOff() + cs.ComputeVertexCountOff() + cs.ComputeVolumeOn() + + volume_array_name: str = "__MESH_DOCTOR_VOLUME-" + str( uuid.uuid4() ) # Making the name unique + cs.SetVolumeArrayName( volume_array_name ) + cs.SetInputData( mesh ) + cs.Update() + + return cs.GetOutput().GetCellData().GetArray( volume_array_name ) + + +def mesh_action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: + volume: vtkDataArray = get_mesh_volume( mesh ) + if not volume: + setup_logger.error( "Volume computation failed." ) + raise RuntimeError( "Volume computation failed." ) + + quality: vtkDataArray = get_mesh_quality( mesh ) + if not quality: + setup_logger.error( "Quality computation failed." ) + raise RuntimeError( "Quality computation failed." ) - assert volume is not None - assert quality is not None volume = vtk_to_numpy( volume ) quality = vtk_to_numpy( quality ) - small_volumes: List[ Tuple[ int, float ] ] = [] + small_volumes: list[ tuple[ int, float ] ] = [] for i, pack in enumerate( zip( volume, quality ) ): v, q = pack vol = q if mesh.GetCellType( i ) in SUPPORTED_TYPES else v 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 fb796990e..7c4922ae6 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 @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Collection from vtkmodules.util.numpy_support import vtk_to_numpy from vtkmodules.vtkFiltersGeneral import vtkCellValidator from vtkmodules.vtkCommonCore import vtkOutputWindow, vtkFileOutputWindow @@ -14,18 +13,13 @@ class Options: @dataclass( frozen=True ) class Result: - wrong_number_of_points_elements: Collection[ int ] - intersecting_edges_elements: Collection[ int ] - intersecting_faces_elements: Collection[ int ] - non_contiguous_edges_elements: Collection[ int ] - non_convex_elements: Collection[ int ] - faces_oriented_incorrectly_elements: Collection[ int ] + invalid_cell_ids: dict[ str, list[ int ] ] def get_invalid_cell_ids( mesh: vtkUnstructuredGrid, min_distance: float ) -> dict[ str, list[ int ] ]: - """For every cell element in a vtk mesh, check if the cell is invalid regarding 6 specific criteria: - "wrong_number_of_points", "intersecting_edges", "intersecting_faces", - "non_contiguous_edges","non_convex" and "faces_oriented_incorrectly". + """For every cell element in a vtk mesh, check if the cell is invalid regarding 8 specific criteria: + "wrong_number_of_points", "intersecting_edges", "intersecting_faces", "non_contiguous_edges","non_convex", + "faces_oriented_incorrectly", "non_planar_faces_elements" and "degenerate_faces_elements". If any of this criteria was met, the cell index is added to a list corresponding to this specific criteria. The dict with the complete list of cell indices by criteria is returned. @@ -42,7 +36,9 @@ def get_invalid_cell_ids( mesh: vtkUnstructuredGrid, min_distance: float ) -> di "intersecting_faces": [ ... ], "non_contiguous_edges": [ ... ], "non_convex": [ ... ], - "faces_oriented_incorrectly": [ ... ] + "faces_oriented_incorrectly": [ ... ], + "non_planar_faces_elements": [ ... ], + "degenerate_faces_elements": [ ... ] } """ # The goal of this first block is to silence the standard error output from VTK. The vtkCellValidator can be very @@ -53,14 +49,16 @@ def get_invalid_cell_ids( mesh: vtkUnstructuredGrid, min_distance: float ) -> di vtk_std_err_out.SetInstance( err_out ) # Different types of cell invalidity are defined as hexadecimal values, specific to vtkCellValidator - # Here NonPlanarFaces and DegenerateFaces can also be obtained. + # Complete set of validity checks available in vtkCellValidator error_masks: dict[ str, int ] = { - "wrong_number_of_points_elements": 0x01, # 0000 0001 - "intersecting_edges_elements": 0x02, # 0000 0010 - "intersecting_faces_elements": 0x04, # 0000 0100 - "non_contiguous_edges_elements": 0x08, # 0000 1000 - "non_convex_elements": 0x10, # 0001 0000 - "faces_oriented_incorrectly_elements": 0x20, # 0010 0000 + "wrongNumberOfPointsElements": 0x01, # 0000 0001 + "intersectingEdgesElements": 0x02, # 0000 0010 + "intersectingFacesElements": 0x04, # 0000 0100 + "nonContiguousEdgesElements": 0x08, # 0000 1000 + "nonConvexElements": 0x10, # 0001 0000 + "facesOrientedIncorrectlyElements": 0x20, # 0010 0000 + "nonPlanarFacesElements": 0x40, # 0100 0000 + "degenerateFacesElements": 0x80, # 1000 0000 } # The results can be stored in a dictionary where keys are the error names @@ -90,12 +88,7 @@ def get_invalid_cell_ids( mesh: vtkUnstructuredGrid, min_distance: float ) -> di def mesh_action( mesh: vtkUnstructuredGrid, options: Options ) -> Result: invalid_cell_ids: dict[ str, list[ int ] ] = get_invalid_cell_ids( mesh, options.min_distance ) - return Result( wrong_number_of_points_elements=invalid_cell_ids[ "wrong_number_of_points_elements" ], - intersecting_edges_elements=invalid_cell_ids[ "intersecting_edges_elements" ], - intersecting_faces_elements=invalid_cell_ids[ "intersecting_faces_elements" ], - non_contiguous_edges_elements=invalid_cell_ids[ "non_contiguous_edges_elements" ], - non_convex_elements=invalid_cell_ids[ "non_convex_elements" ], - faces_oriented_incorrectly_elements=invalid_cell_ids[ "faces_oriented_incorrectly_elements" ] ) + return Result( invalid_cell_ids ) def action( vtk_input_file: str, options: Options ) -> Result: 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 f19680aac..52e42d4cf 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/supported_elements.py @@ -7,8 +7,8 @@ from vtkmodules.util.numpy_support import vtk_to_numpy from vtkmodules.vtkCommonCore import vtkIdList from vtkmodules.vtkCommonDataModel import ( vtkCellTypes, vtkUnstructuredGrid, VTK_HEXAGONAL_PRISM, VTK_HEXAHEDRON, - VTK_PENTAGONAL_PRISM, VTK_POLYHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_VOXEL, - VTK_WEDGE ) + VTK_LINE, VTK_PENTAGONAL_PRISM, VTK_POLYGON, VTK_POLYHEDRON, VTK_PYRAMID, + VTK_QUAD, VTK_TETRA, VTK_TRIANGLE, VTK_VERTEX, VTK_VOXEL, VTK_WEDGE ) from geos.mesh.doctor.actions.vtk_polyhedron import build_face_to_face_connectivity_through_edges, FaceStream from geos.mesh.doctor.parsing.cli_parsing import setup_logger from geos.mesh.io.vtkIO import read_unstructured_grid @@ -40,8 +40,8 @@ def init_worker( mesh_to_init: vtkUnstructuredGrid ) -> None: supported_cell_types: set[ int ] = { - VTK_HEXAGONAL_PRISM, VTK_HEXAHEDRON, VTK_PENTAGONAL_PRISM, VTK_POLYHEDRON, VTK_PYRAMID, VTK_TETRA, VTK_VOXEL, - VTK_WEDGE + VTK_HEXAGONAL_PRISM, VTK_HEXAHEDRON, VTK_LINE, VTK_PENTAGONAL_PRISM, VTK_POLYGON, VTK_POLYHEDRON, VTK_PYRAMID, + VTK_QUAD, VTK_TETRA, VTK_TRIANGLE, VTK_VERTEX, VTK_VOXEL, VTK_WEDGE } @@ -50,11 +50,14 @@ 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 base (e.g., pentagonal prism gets n = 5). + name (str): A human-readable name for logging purposes. + + Returns: + networkx.Graph: A graph instance representing the prism. """ tmp = networkx.cycle_graph( n ) for node in range( n ): @@ -82,11 +85,13 @@ 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. - 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. + """Checks if a polyhedron can be converted into a supported cell. + + Args: + face_stream: 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 ) for reference_graph in self.__reference_graphs[ cell_graph.order() ]: @@ -95,10 +100,13 @@ def __is_polyhedron_supported( self, face_stream ) -> str: 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. + """Check if a vtk polyhedron cell can be converted into a supported GEOS element. + + Args: + ic (int): The index of the 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 @@ -117,6 +125,14 @@ def __call__( self, ic: int ) -> int: def find_unsupported_std_elements_types( mesh: vtkUnstructuredGrid ) -> list[ str ]: + """Find unsupported standard element types in the mesh. + + Args: + mesh (vtkUnstructuredGrid): The input mesh to analyze. + + Returns: + list[ str ]: List of unsupported element type descriptions. + """ if hasattr( mesh, "GetDistinctCellTypesArray" ): # For more recent versions of vtk. unique_cell_types = set( vtk_to_numpy( mesh.GetDistinctCellTypesArray() ) ) else: @@ -129,6 +145,15 @@ def find_unsupported_std_elements_types( mesh: vtkUnstructuredGrid ) -> list[ st def find_unsupported_polyhedron_elements( mesh: vtkUnstructuredGrid, options: Options ) -> list[ int ]: + """Find unsupported polyhedron elements in the mesh. + + Args: + mesh (vtkUnstructuredGrid): The input mesh to analyze. + options (Options): The options for processing. + + Returns: + list[ int ]: List of element indices for unsupported polyhedrons. + """ # Dealing with polyhedron elements. num_cells: int = mesh.GetNumberOfCells() result = ones( num_cells, dtype=int ) * -1 diff --git a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py index 19dd73fba..b2a5e2bbf 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py @@ -7,15 +7,16 @@ from geos.mesh.doctor.parsing._shared_checks_parsing_logic import CheckFeature, display_results from geos.mesh.doctor.parsing.all_checks_parsing import ( CHECK_FEATURES_CONFIG as cfc_all_checks, ORDERED_CHECK_NAMES as ocn_all_checks ) -from geos.mesh.doctor.parsing.main_checks_parsing import ( CHECK_FEATURES_CONFIG as cfc_main_checks, ORDERED_CHECK_NAMES - as ocn_main_checks ) +from geos.mesh.doctor.parsing.main_checks_parsing import ( CHECK_FEATURES_CONFIG as cfcMainChecks, ORDERED_CHECK_NAMES + as ocnMainChecks ) __doc__ = """ Checks module performs comprehensive mesh validation checks on a vtkUnstructuredGrid. This module contains AllChecks and MainChecks filters that run various quality checks including element validation, node validation, topology checks, and geometric integrity verification. -To use the AllChecks filter: +To use the AllChecks filter +--------------------------- .. code-block:: python @@ -32,12 +33,13 @@ success = allChecksFilter.applyFilter() # get check results - check_results = allChecksFilter.getCheckResults() + checkResults = allChecksFilter.getCheckResults() # get the processed mesh output_mesh = allChecksFilter.getMesh() -To use the MainChecks filter (subset of most important checks): +To use the MainChecks filter (subset of most important checks) +-------------------------------------------------------------- .. code-block:: python @@ -48,300 +50,279 @@ # execute the checks success = mainChecksFilter.applyFilter() + + # get check results + checkResults = mainChecksFilter.getCheckResults() + +For standalone use without creating a filter instance +----------------------------------------------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.Checks import allChecks, mainChecks + + # apply all checks directly + outputMesh, checkResults = allChecks( + mesh, + customParameters={"collocated_nodes": {"tolerance": 1e-6}} + ) + + # or apply main checks only + outputMesh, checkResults = mainChecks( + mesh, + customParameters={"element_volumes": {"minVolume": 0.0}} + ) """ loggerTitle: str = "Mesh Doctor Checks Filter" -class MeshDoctorChecks( MeshDoctorFilterBase ): +class Checks( MeshDoctorFilterBase ): def __init__( self: Self, mesh: vtkUnstructuredGrid, - checks_to_perform: list[ str ], - check_features_config: dict[ str, CheckFeature ], - ordered_check_names: list[ str ], - use_external_logger: bool = False, + checksToPerform: list[ str ], + checkFeaturesConfig: dict[ str, CheckFeature ], + orderedCheckNames: list[ str ], + useExternalLogger: bool = False, ) -> None: """Initialize the mesh doctor checks filter. Args: mesh (vtkUnstructuredGrid): The input mesh to check - checks_to_perform (list[str]): List of check names to perform - check_features_config (dict[str, CheckFeature]): Configuration for check features - ordered_check_names (list[str]): Ordered list of available check names - use_external_logger (bool): Whether to use external logger. Defaults to False. + checksToPerform (list[str]): List of check names to perform + checkFeaturesConfig (dict[str, CheckFeature]): Configuration for check features + orderedCheckNames (list[str]): Ordered list of available check names + useExternalLogger (bool): Whether to use external logger. Defaults to False. """ - super().__init__( mesh, loggerTitle, use_external_logger ) - self.checks_to_perform: list[ str ] = checks_to_perform - self.check_parameters: dict[ str, dict[ str, Any ] ] = {} # Custom parameters override - self.check_results: dict[ str, Any ] = {} - self.check_features_config: dict[ str, CheckFeature ] = check_features_config - self.ordered_check_names: list[ str ] = ordered_check_names - - def setChecksToPerform( self: Self, checks_to_perform: list[ str ] ) -> None: - """Set which checks to perform. + super().__init__( mesh, loggerTitle, useExternalLogger ) + self.checksToPerform: list[ str ] = checksToPerform + self.checkParameters: dict[ str, dict[ str, Any ] ] = {} # Custom parameters override + self.checkResults: dict[ str, Any ] = {} + self.checkFeaturesConfig: dict[ str, CheckFeature ] = checkFeaturesConfig + self.orderedCheckNames: list[ str ] = orderedCheckNames - Args: - checks_to_perform (list[str]): List of check names to perform + def applyFilter( self: Self ) -> bool: + """Apply the mesh validation checks. + + Returns: + bool: True if checks completed successfully, False otherwise. """ - self.checks_to_perform = checks_to_perform + self.logger.info( f"Apply filter {self.logger.name}" ) - def setCheckParameter( self: Self, check_name: str, parameter_name: str, value: Any ) -> None: - """Set a parameter for a specific check. + # Build the options using the parsing logic structure + options = self._buildOptions() + self.checkResults = get_check_results( self.mesh, options ) + + # Display results using the standard display logic + resultsWrapper = SimpleNamespace( checkResults=self.checkResults ) + display_results( options, resultsWrapper ) + + self.logger.info( f"Performed {len(self.checkResults)} checks" ) + self.logger.info( f"The filter {self.logger.name} succeeded" ) + return True + + def getAvailableChecks( self: Self ) -> list[ str ]: + """Get the list of available check names. + + Returns: + list[str]: List of available check names + """ + return self.orderedCheckNames + + def getCheckResults( self: Self ) -> dict[ str, Any ]: + """Get the results of all performed checks. + + Returns: + dict[str, Any]: Dictionary mapping check names to their results. + """ + return self.checkResults + + def getDefaultParameters( self: Self, checkName: str ) -> dict[ str, Any ]: + """Get the default parameters for a specific check. Args: - check_name (str): Name of the check (e.g., "collocated_nodes") - parameter_name (str): Name of the parameter (e.g., "tolerance") - value (Any): Value to set for the parameter + checkName (str): Name of the check. + + Returns: + dict[str, Any]: Dictionary of default parameters. """ - if check_name not in self.check_parameters: - self.check_parameters[ check_name ] = {} - self.check_parameters[ check_name ][ parameter_name ] = value + if checkName in self.checkFeaturesConfig: + return self.checkFeaturesConfig[ checkName ].default_params + return {} - def setAllChecksParameter( self: Self, parameter_name: str, value: Any ) -> None: + def setAllChecksParameter( self: Self, parameterName: str, value: Any ) -> None: """Set a parameter for all checks that support it. Args: - parameter_name (str): Name of the parameter (e.g., "tolerance") + parameterName (str): Name of the parameter (e.g., "tolerance") value (Any): Value to set for the parameter """ - for check_name in self.checks_to_perform: - if check_name in self.check_features_config: - default_params = self.check_features_config[ check_name ].default_params - if parameter_name in default_params: - self.setCheckParameter( check_name, parameter_name, value ) + for checkName in self.checksToPerform: + if checkName in self.checkFeaturesConfig: + defaultParams = self.checkFeaturesConfig[ checkName ].default_params + if parameterName in defaultParams: + self.setCheckParameter( checkName, parameterName, value ) - def applyFilter( self: Self ) -> bool: - """Apply the mesh validation checks. + def setCheckParameter( self: Self, checkName: str, parameterName: str, value: Any ) -> None: + """Set a parameter for a specific check. - Returns: - bool: True if checks completed successfully, False otherwise. + Args: + checkName (str): Name of the check (e.g., "collocated_nodes") + parameterName (str): Name of the parameter (e.g., "tolerance") + value (Any): Value to set for the parameter """ - self.logger.info( f"Apply filter {self.logger.name}" ) - - try: - # Build the options using the parsing logic structure - options = self._buildOptions() - self.check_results = get_check_results( self.mesh, options ) - - # Display results using the standard display logic - results_wrapper = SimpleNamespace( check_results=self.check_results ) - display_results( options, results_wrapper ) + if checkName not in self.checkParameters: + self.checkParameters[ checkName ] = {} + self.checkParameters[ checkName ][ parameterName ] = value - self.logger.info( f"Performed {len(self.check_results)} checks" ) - self.logger.info( f"The filter {self.logger.name} succeeded" ) - return True + def setChecksToPerform( self: Self, checksToPerform: list[ str ] ) -> None: + """Set which checks to perform. - except Exception as e: - self.logger.error( f"Error in mesh checks: {e}" ) - self.logger.error( f"The filter {self.logger.name} failed" ) - return False + Args: + checksToPerform (list[str]): List of check names to perform + """ + self.checksToPerform = checksToPerform def _buildOptions( self: Self ) -> Options: """Build Options object using the same logic as the parsing system. Returns: - Options: Properly configured options for all checks + Options: Properly configured options for all checks. """ # Start with default parameters for all configured checks - default_params: dict[ str, dict[ str, Any ] ] = { + defaultParams: dict[ str, dict[ str, Any ] ] = { name: feature.default_params.copy() - for name, feature in self.check_features_config.items() + for name, feature in self.checkFeaturesConfig.items() } - final_check_params: dict[ str, dict[ str, Any ] ] = { - name: default_params[ name ] - for name in self.checks_to_perform + finalCheckParams: dict[ str, dict[ str, Any ] ] = { + name: defaultParams[ name ] + for name in self.checksToPerform } # Apply any custom parameter overrides - for check_name in self.checks_to_perform: - if check_name in self.check_parameters: - final_check_params[ check_name ].update( self.check_parameters[ check_name ] ) + for checkName in self.checksToPerform: + if checkName in self.checkParameters: + finalCheckParams[ checkName ].update( self.checkParameters[ checkName ] ) # Instantiate Options objects for the selected checks - individual_check_options: dict[ str, Any ] = {} - individual_check_display: dict[ str, Any ] = {} + individualCheckOptions: dict[ str, Any ] = {} + individualCheckDisplay: dict[ str, Any ] = {} - for check_name in self.checks_to_perform: - if check_name not in self.check_features_config: - self.logger.warning( f"Check '{check_name}' is not available. Skipping." ) + for checkName in self.checksToPerform: + if checkName not in self.checkFeaturesConfig: + self.logger.warning( f"Check '{checkName}' is not available. Skipping." ) continue - params = final_check_params[ check_name ] - feature_config = self.check_features_config[ check_name ] + params = finalCheckParams[ checkName ] + featureConfig = self.checkFeaturesConfig[ checkName ] try: - individual_check_options[ check_name ] = feature_config.options_cls( **params ) - individual_check_display[ check_name ] = feature_config.display + individualCheckOptions[ checkName ] = featureConfig.options_cls( **params ) + individualCheckDisplay[ checkName ] = featureConfig.display except Exception as e: - self.logger.error( f"Failed to create options for check '{check_name}': {e}. " + self.logger.error( f"Failed to create options for check '{checkName}': {e}. " f"This check will be skipped." ) - return Options( checks_to_perform=list( individual_check_options.keys() ), - checks_options=individual_check_options, - check_displays=individual_check_display ) - - def getAvailableChecks( self: Self ) -> list[ str ]: - """Get the list of available check names. - - Returns: - list[str]: List of available check names - """ - return self.ordered_check_names - - def getCheckResults( self: Self ) -> dict[ str, Any ]: - """Get the results of all performed checks. - - Returns: - dict[str, Any]: Dictionary mapping check names to their results - """ - return self.check_results - - def getDefaultParameters( self: Self, check_name: str ) -> dict[ str, Any ]: - """Get the default parameters for a specific check. + return Options( checksToPerform=list( individualCheckOptions.keys() ), + checks_options=individualCheckOptions, + check_displays=individualCheckDisplay ) - Args: - check_name (str): Name of the check - Returns: - dict[str, Any]: Dictionary of default parameters - """ - if check_name in self.check_features_config: - return self.check_features_config[ check_name ].default_params - return {} - - -class AllChecks( MeshDoctorChecks ): +class AllChecks( Checks ): def __init__( self: Self, mesh: vtkUnstructuredGrid, - use_external_logger: bool = False, + useExternalLogger: bool = False, ) -> None: - """Initialize the all checks filter. + """Initialize the all_checks filter. Args: - mesh (vtkUnstructuredGrid): The input mesh to check - use_external_logger (bool): Whether to use external logger. Defaults to False. + mesh (vtkUnstructuredGrid): The input mesh to check. + useExternalLogger (bool): Whether to use external logger. Defaults to False. """ super().__init__( mesh, - checks_to_perform=ocn_all_checks, - check_features_config=cfc_all_checks, - ordered_check_names=ocn_all_checks, - use_external_logger=use_external_logger ) + checksToPerform=ocn_all_checks, + checkFeaturesConfig=cfc_all_checks, + orderedCheckNames=ocn_all_checks, + useExternalLogger=useExternalLogger ) -class MainChecks( MeshDoctorChecks ): +class MainChecks( Checks ): def __init__( self: Self, mesh: vtkUnstructuredGrid, - use_external_logger: bool = False, + useExternalLogger: bool = False, ) -> None: """Initialize the main checks filter. Args: - mesh (vtkUnstructuredGrid): The input mesh to check - use_external_logger (bool): Whether to use external logger. Defaults to False. + mesh (vtkUnstructuredGrid): The input mesh to check. + useExternalLogger (bool): Whether to use external logger. Defaults to False. """ super().__init__( mesh, - checks_to_perform=ocn_main_checks, - check_features_config=cfc_main_checks, - ordered_check_names=ocn_main_checks, - use_external_logger=use_external_logger ) + checksToPerform=ocnMainChecks, + checkFeaturesConfig=cfcMainChecks, + orderedCheckNames=ocnMainChecks, + useExternalLogger=useExternalLogger ) # Main functions for backward compatibility and standalone use -def all_checks( +def allChecks( mesh: vtkUnstructuredGrid, - custom_parameters: dict[ str, dict[ str, Any ] ] = None, - write_output: bool = False, - output_path: str = "output/mesh_all_checks.vtu", + customParameters: dict[ str, dict[ str, Any ] ] = None ) -> tuple[ vtkUnstructuredGrid, dict[ str, Any ] ]: """Apply all available mesh checks to a mesh. Args: - mesh (vtkUnstructuredGrid): The input mesh - custom_parameters (dict[str, dict[str, Any]]): Custom parameters for checks. Defaults to None. - write_output (bool): Whether to write output mesh to file. Defaults to False. - output_path (str): Output file path if write_output is True. + mesh (vtkUnstructuredGrid): The input mesh to check. + customParameters (dict[str, dict[str, Any]]): Custom parameters for checks. Defaults to None. Returns: tuple[vtkUnstructuredGrid, dict[str, Any]]: Processed mesh, check results """ - filter_instance = AllChecks( mesh ) + filterInstance = AllChecks( mesh ) - if custom_parameters: - for check_name, params in custom_parameters.items(): + if customParameters: + for checkName, params in customParameters.items(): for param_name, value in params.items(): - filter_instance.setCheckParameter( check_name, param_name, value ) - - success = filter_instance.applyFilter() + filterInstance.setCheckParameter( checkName, param_name, value ) - if not success: - raise RuntimeError( "All checks execution failed" ) - - if write_output: - filter_instance.writeGrid( output_path ) + filterInstance.applyFilter() return ( - filter_instance.getMesh(), - filter_instance.getCheckResults(), + filterInstance.getMesh(), + filterInstance.getCheckResults(), ) -def main_checks( +def mainChecks( mesh: vtkUnstructuredGrid, - custom_parameters: dict[ str, dict[ str, Any ] ] = None, - write_output: bool = False, - output_path: str = "output/mesh_main_checks.vtu", + customParameters: dict[ str, dict[ str, Any ] ] = None ) -> tuple[ vtkUnstructuredGrid, dict[ str, Any ] ]: """Apply main mesh checks to a mesh. Args: - mesh (vtkUnstructuredGrid): The input mesh - custom_parameters (dict[str, dict[str, Any]]): Custom parameters for checks. Defaults to None. - write_output (bool): Whether to write output mesh to file. Defaults to False. - output_path (str): Output file path if write_output is True. + mesh (vtkUnstructuredGrid): The input mesh to check. + customParameters (dict[str, dict[str, Any]]): Custom parameters for checks. Defaults to None. Returns: tuple[vtkUnstructuredGrid, dict[str, Any]]: Processed mesh, check results """ - filter_instance = MainChecks( mesh ) + filterInstance = MainChecks( mesh ) - if custom_parameters: - for check_name, params in custom_parameters.items(): + if customParameters: + for checkName, params in customParameters.items(): for param_name, value in params.items(): - filter_instance.setCheckParameter( check_name, param_name, value ) - - success = filter_instance.applyFilter() - - if not success: - raise RuntimeError( "Main checks execution failed" ) + filterInstance.setCheckParameter( checkName, param_name, value ) - if write_output: - filter_instance.writeGrid( output_path ) + filterInstance.applyFilter() return ( - filter_instance.getMesh(), - filter_instance.getCheckResults(), + filterInstance.getMesh(), + filterInstance.getCheckResults(), ) - - -# Aliases for backward compatibility -def processAllChecks( - mesh: vtkUnstructuredGrid, - custom_parameters: dict[ str, dict[ str, Any ] ] = None, -) -> tuple[ vtkUnstructuredGrid, dict[ str, Any ] ]: - """Legacy function name for backward compatibility.""" - return all_checks( mesh, custom_parameters ) - - -def processMainChecks( - mesh: vtkUnstructuredGrid, - custom_parameters: dict[ str, dict[ str, Any ] ] = None, -) -> tuple[ vtkUnstructuredGrid, dict[ str, Any ] ]: - """Legacy function name for backward compatibility.""" - return main_checks( mesh, custom_parameters ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py index b9f16b39b..0afa5fdf3 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py @@ -5,33 +5,50 @@ from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.collocated_nodes import find_collocated_nodes_buckets, find_wrong_support_elements from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorFilterBase +from geos.mesh.doctor.parsing.collocated_nodes_parsing import logger_results __doc__ = """ CollocatedNodes module identifies and handles duplicated/collocated nodes in a vtkUnstructuredGrid. The filter can detect nodes that are within a specified tolerance distance and optionally identify elements that have support nodes appearing more than once (wrong support elements). -To use the filter: +To use the filter +----------------- .. code-block:: python from geos.mesh.doctor.filters.CollocatedNodes import CollocatedNodes # instantiate the filter - collocatedNodesFilter = CollocatedNodes(mesh, tolerance=1e-6, paint_wrong_support_elements=True) + collocatedNodesFilter = CollocatedNodes(mesh, tolerance=1e-6, writeWrongSupportElements=True) # execute the filter success = collocatedNodesFilter.applyFilter() # get results - collocated_buckets = collocatedNodesFilter.getCollocatedNodeBuckets() - wrong_support_elements = collocatedNodesFilter.getWrongSupportElements() + collocatedBuckets = collocatedNodesFilter.getCollocatedNodeBuckets() + wrongSupportElements = collocatedNodesFilter.getWrongSupportElements() # get the processed mesh - output_mesh = collocatedNodesFilter.getMesh() + outputMesh = collocatedNodesFilter.getMesh() # write the output mesh collocatedNodesFilter.writeGrid("output/mesh_with_collocated_info.vtu") + +For standalone use without creating a filter instance +----------------------------------------------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.CollocatedNodes import collocatedNodes + + # apply filter directly + outputMesh, collocatedBuckets, wrongSupportElements = collocatedNodes( + mesh, + outputPath="output/mesh_with_collocated_info.vtu", + tolerance=1e-6, + writeWrongSupportElements=True + ) """ loggerTitle: str = "Collocated Nodes Filter" @@ -43,40 +60,22 @@ def __init__( self: Self, mesh: vtkUnstructuredGrid, tolerance: float = 0.0, - paint_wrong_support_elements: bool = False, - use_external_logger: bool = False, + writeWrongSupportElements: bool = False, + useExternalLogger: bool = False, ) -> None: """Initialize the collocated nodes filter. Args: - mesh (vtkUnstructuredGrid): The input mesh to analyze + mesh (vtkUnstructuredGrid): The input mesh to analyze. tolerance (float): Distance tolerance for detecting collocated nodes. Defaults to 0.0. - paint_wrong_support_elements (bool): Whether to mark wrong support elements in output. Defaults to False. - use_external_logger (bool): Whether to use external logger. Defaults to False. + writeWrongSupportElements (bool): Whether to mark wrong support elements in output. Defaults to False. + useExternalLogger (bool): Whether to use external logger. Defaults to False. """ - super().__init__( mesh, loggerTitle, use_external_logger ) + super().__init__( mesh, loggerTitle, useExternalLogger ) self.tolerance: float = tolerance - self.paint_wrong_support_elements: bool = paint_wrong_support_elements - - # Results storage - self.collocated_node_buckets: list[ tuple[ int ] ] = [] - self.wrong_support_elements: list[ int ] = [] - - def setTolerance( self: Self, tolerance: float ) -> None: - """Set the tolerance parameter to define if two points are collocated or not. - - Args: - tolerance (float): Distance tolerance - """ - self.tolerance = tolerance - - def setPaintWrongSupportElements( self: Self, paint: bool ) -> None: - """Set whether to create arrays marking wrong support elements in output data. - - Args: - paint (bool): True to enable marking, False to disable - """ - self.paint_wrong_support_elements = paint + self.writeWrongSupportElements: bool = writeWrongSupportElements + self.collocatedNodeBuckets: list[ tuple[ int ] ] = [] + self.wrongSupportElements: list[ int ] = [] def applyFilter( self: Self ) -> bool: """Apply the collocated nodes analysis. @@ -86,99 +85,88 @@ def applyFilter( self: Self ) -> bool: """ self.logger.info( f"Apply filter {self.logger.name}" ) - try: - # Find collocated nodes - self.collocated_node_buckets = find_collocated_nodes_buckets( self.mesh, self.tolerance ) - self.logger.info( f"Found {len(self.collocated_node_buckets)} groups of collocated nodes" ) - - # Find wrong support elements - self.wrong_support_elements = find_wrong_support_elements( self.mesh ) - self.logger.info( f"Found {len(self.wrong_support_elements)} elements with wrong support" ) - - # Add marking arrays if requested - if self.paint_wrong_support_elements and self.wrong_support_elements: - self._addWrongSupportElementsArray() + self.collocatedNodeBuckets: list[ tuple[ int ] ] = find_collocated_nodes_buckets( self.mesh, self.tolerance ) + self.wrongSupportElements: list[ int ] = find_wrong_support_elements( self.mesh ) - self.logger.info( f"The filter {self.logger.name} succeeded" ) - return True - - except Exception as e: - self.logger.error( f"Error in collocated nodes analysis: {e}" ) - self.logger.error( f"The filter {self.logger.name} failed" ) - return False - - def _addWrongSupportElementsArray( self: Self ) -> None: - """Add array marking wrong support elements.""" - num_cells = self.mesh.GetNumberOfCells() - wrong_support_array = np.zeros( num_cells, dtype=np.int32 ) + # Add marking arrays if requested + if self.writeWrongSupportElements and self.wrongSupportElements: + self._addWrongSupportElementsArray() - for element_id in self.wrong_support_elements: - if 0 <= element_id < num_cells: - wrong_support_array[ element_id ] = 1 + logger_results( self.logger, self.collocatedNodeBuckets, self.wrongSupportElements ) - vtk_array: vtkDataArray = numpy_to_vtk( wrong_support_array ) - vtk_array.SetName( "WrongSupportElements" ) - self.mesh.GetCellData().AddArray( vtk_array ) + self.logger.info( f"The filter {self.logger.name} succeeded." ) + return True def getCollocatedNodeBuckets( self: Self ) -> list[ tuple[ int ] ]: """Returns the nodes buckets that contain the duplicated node indices. Returns: - list[tuple[int]]: Groups of collocated node indices + list[tuple[int]]: Groups of collocated node indices. """ - return self.collocated_node_buckets + return self.collocatedNodeBuckets def getWrongSupportElements( self: Self ) -> list[ int ]: """Returns the element indices with support node indices appearing more than once. Returns: - list[int]: Element indices with problematic support nodes + list[int]: Element indices with problematic support nodes. + """ + return self.wrongSupportElements + + def setTolerance( self: Self, tolerance: float ) -> None: + """Set the tolerance parameter to define if two points are collocated or not. + + Args: + tolerance (float): Distance tolerance. + """ + self.tolerance = tolerance + + def setWriteWrongSupportElements( self: Self, write: bool ) -> None: + """Set whether to create arrays marking wrong support elements in output data. + + Args: + write (bool): True to enable marking, False to disable. """ - return self.wrong_support_elements + self.writeWrongSupportElements = write + + def _addWrongSupportElementsArray( self: Self ) -> None: + """Add array marking wrong support elements.""" + numCells: int = self.mesh.GetNumberOfCells() + wrongSupportArray = np.zeros( numCells, dtype=np.int32 ) + wrongSupportArray[ self.wrongSupportElements ] = 1 + + vtkArray: vtkDataArray = numpy_to_vtk( wrongSupportArray ) + vtkArray.SetName( "WrongSupportElements" ) + self.mesh.GetCellData().AddArray( vtkArray ) -# Main function for backward compatibility and standalone use -def collocated_nodes( +# Main function for standalone use +def collocatedNodes( mesh: vtkUnstructuredGrid, + outputPath: str, tolerance: float = 0.0, - paint_wrong_support_elements: bool = False, - write_output: bool = False, - output_path: str = "output/mesh_with_collocated_info.vtu", + writeWrongSupportElements: bool = False, ) -> tuple[ vtkUnstructuredGrid, list[ tuple[ int ] ], list[ int ] ]: """Apply collocated nodes analysis to a mesh. Args: - mesh (vtkUnstructuredGrid): The input mesh + mesh (vtkUnstructuredGrid): The input mesh to analyze. + outputPath (str): Output file path if writeOutput is True. tolerance (float): Distance tolerance for detecting collocated nodes. Defaults to 0.0. - paint_wrong_support_elements (bool): Whether to mark wrong support elements. Defaults to False. - write_output (bool): Whether to write output mesh to file. Defaults to False. - output_path (str): Output file path if write_output is True. + writeWrongSupportElements (bool): Whether to mark wrong support elements. Defaults to False. + writeOutput (bool): Whether to write output mesh to file. Defaults to False. Returns: tuple[vtkUnstructuredGrid, list[tuple[int]], list[int]]: - Processed mesh, collocated node buckets, wrong support elements + Processed mesh, collocated node buckets, wrong support elements. """ - filter_instance = CollocatedNodes( mesh, tolerance, paint_wrong_support_elements ) - success = filter_instance.applyFilter() - - if not success: - raise RuntimeError( "Collocated nodes analysis failed" ) - - if write_output: - filter_instance.writeGrid( output_path ) + filterInstance = CollocatedNodes( mesh, tolerance, writeWrongSupportElements ) + filterInstance.applyFilter() + if writeWrongSupportElements: # If we are painting wrong support elements, we need to write the output + filterInstance.writeGrid( outputPath ) return ( - filter_instance.getMesh(), - filter_instance.getCollocatedNodeBuckets(), - filter_instance.getWrongSupportElements(), + filterInstance.getMesh(), + filterInstance.getCollocatedNodeBuckets(), + filterInstance.getWrongSupportElements(), ) - - -# Alias for backward compatibility -def processCollocatedNodes( - mesh: vtkUnstructuredGrid, - tolerance: float = 0.0, - paint_wrong_support_elements: bool = False, -) -> tuple[ vtkUnstructuredGrid, list[ tuple[ int ] ], list[ int ] ]: - """Legacy function name for backward compatibility.""" - return collocated_nodes( mesh, tolerance, paint_wrong_support_elements ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py index 2febe51ad..6302c1fa5 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py @@ -1,38 +1,55 @@ import numpy as np import numpy.typing as npt from typing_extensions import Self -from vtkmodules.util.numpy_support import vtk_to_numpy +from vtkmodules.util.numpy_support import numpy_to_vtk, vtk_to_numpy from vtkmodules.vtkCommonCore import vtkDataArray from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid -from vtkmodules.vtkFiltersVerdict import vtkCellSizeFilter +from geos.mesh.doctor.actions.element_volumes import get_mesh_quality, get_mesh_volume, SUPPORTED_TYPES from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorFilterBase +from geos.mesh.doctor.parsing.element_volumes_parsing import logger_results __doc__ = """ ElementVolumes module calculates the volumes of all elements in a vtkUnstructuredGrid. -The filter can identify elements with negative or zero volumes, which typically indicate mesh quality issues -such as inverted elements or degenerate cells. +The filter can identify elements with volume inferior to a specified threshold (usually 0.0), which typically indicate +mesh quality issues such as inverted elements or degenerate cells. -To use the filter: +To use the filter +----------------- .. code-block:: python from geos.mesh.doctor.filters.ElementVolumes import ElementVolumes # instantiate the filter - elementVolumesFilter = ElementVolumes(mesh, return_negative_zero_volumes=True) + elementVolumesFilter = ElementVolumes(mesh, minVolume=0.0, writeIsBelowVolume=True) # execute the filter success = elementVolumesFilter.applyFilter() - # get problematic elements (if enabled) - negative_zero_volumes = elementVolumesFilter.getNegativeZeroVolumes() - # returns numpy array with shape (n, 2) where first column is element index, second is volume + # get problematic elements + invalidVolumes = elementVolumesFilter.getInvalidVolumes() + # returns the list of tuples (element index, volume) # get the processed mesh with volume information - output_mesh = elementVolumesFilter.getMesh() + outputMesh = elementVolumesFilter.getMesh() # write the output mesh with volume information elementVolumesFilter.writeGrid("output/mesh_with_volumes.vtu") + +For standalone use without creating a filter instance +----------------------------------------------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.ElementVolumes import elementVolumes + + # apply filter directly + outputMesh, volumes, belowVolumes = elementVolumes( + mesh, + outputPath="output/mesh_with_volumes.vtu", + minVolume=0.0, + writeIsBelowVolume=True + ) """ loggerTitle: str = "Element Volumes Filter" @@ -43,27 +60,24 @@ class ElementVolumes( MeshDoctorFilterBase ): def __init__( self: Self, mesh: vtkUnstructuredGrid, - return_negative_zero_volumes: bool = False, - use_external_logger: bool = False, + minVolume: float = 0.0, + writeIsBelowVolume: bool = False, + useExternalLogger: bool = False, ) -> None: """Initialize the element volumes filter. Args: - mesh (vtkUnstructuredGrid): The input mesh to analyze - return_negative_zero_volumes (bool): Whether to report negative/zero volume elements. Defaults to False. - use_external_logger (bool): Whether to use external logger. Defaults to False. + mesh (vtkUnstructuredGrid): The input mesh to analyze. + minVolume (float): Minimum volume threshold for elements. Defaults to 0.0. + writeIsBelowVolume (bool): Whether to add new CellData array with values 0 or 1 if below the minimum volume. + Defaults to False. + useExternalLogger (bool): Whether to use external logger. Defaults to False. """ - super().__init__( mesh, loggerTitle, use_external_logger ) - self.return_negative_zero_volumes: bool = return_negative_zero_volumes + super().__init__( mesh, loggerTitle, useExternalLogger ) + self.minVolume: float = minVolume self.volumes: vtkDataArray = None - - def setReturnNegativeZeroVolumes( self: Self, return_negative_zero_volumes: bool ) -> None: - """Set whether to report negative and zero volume elements. - - Args: - return_negative_zero_volumes (bool): True to enable reporting, False to disable - """ - self.return_negative_zero_volumes = return_negative_zero_volumes + self.belowVolumes: list[ tuple[ int, float ] ] = [] + self.writeIsBelowVolume: bool = writeIsBelowVolume def applyFilter( self: Self ) -> bool: """Apply the element volumes calculation. @@ -73,102 +87,100 @@ def applyFilter( self: Self ) -> bool: """ self.logger.info( f"Apply filter {self.logger.name}" ) - try: - # Use VTK's cell size filter to compute volumes - cellSize = vtkCellSizeFilter() - cellSize.ComputeAreaOff() - cellSize.ComputeLengthOff() - cellSize.ComputeSumOff() - cellSize.ComputeVertexCountOff() - cellSize.ComputeVolumeOn() - - volume_array_name: str = "MESH_DOCTOR_VOLUME" - cellSize.SetVolumeArrayName( volume_array_name ) - cellSize.SetInputData( self.mesh ) - cellSize.Update() - - # Get the computed volumes - self.volumes = cellSize.GetOutput().GetCellData().GetArray( volume_array_name ) - - # Add the volume array to our mesh - self.mesh.GetCellData().AddArray( self.volumes ) - - if self.return_negative_zero_volumes: - negative_zero_volumes = self.getNegativeZeroVolumes() - self.logger.info( f"Found {len(negative_zero_volumes)} elements with zero or negative volume" ) - if len( negative_zero_volumes ) > 0: - self.logger.info( "Element indices and volumes with zero or negative values:" ) - for idx, vol in negative_zero_volumes: - self.logger.info( f" Element {idx}: volume = {vol}" ) - - self.logger.info( f"The filter {self.logger.name} succeeded" ) - return True - - except Exception as e: - self.logger.error( f"Error in element volumes calculation: {e}" ) - self.logger.error( f"The filter {self.logger.name} failed" ) + volume: vtkDataArray = get_mesh_volume( self.mesh ) + if not volume: + self.logger.error( "Volume computation failed." ) return False - def getNegativeZeroVolumes( self: Self ) -> npt.NDArray: - """Returns a numpy array of all the negative and zero volumes. + quality: vtkDataArray = get_mesh_quality( self.mesh ) + if not quality: + self.logger.error( "Quality computation failed." ) + return False + + volume = vtk_to_numpy( volume ) + quality = vtk_to_numpy( quality ) + self.belowVolumes = [] + for i, pack in enumerate( zip( volume, quality ) ): + v, q = pack + vol = q if self.mesh.GetCellType( i ) in SUPPORTED_TYPES else v + if vol < self.minVolume: + self.belowVolumes.append( ( i, float( vol ) ) ) + + logger_results( self.logger, self.belowVolumes ) + + if self.writeIsBelowVolume and self.belowVolumes: + self._addBelowVolumeArray() + + self.logger.info( f"The filter {self.logger.name} succeeded." ) + return True + + def getBelowVolumes( self: Self ) -> list[ tuple[ int, float ] ]: + """Get the list of volumes below the minimum threshold. Returns: - npt.NDArray: Array with shape (n, 2) where first column is element index, second is volume + list[ tuple[ int, float ] ]: List of tuples containing element index and volume. """ - if self.volumes is None: - return np.array( [] ).reshape( 0, 2 ) - - volumes_np: npt.NDArray = vtk_to_numpy( self.volumes ) - indices = np.where( volumes_np <= 0 )[ 0 ] - return np.column_stack( ( indices, volumes_np[ indices ] ) ) + return self.belowVolumes def getVolumes( self: Self ) -> vtkDataArray: """Get the computed volume array. Returns: - vtkDataArray: The volume data array, or None if not computed yet + vtkDataArray: The volume data array, or None if not computed yet. """ return self.volumes + def setWriteIsBelowVolume( self: Self, write: bool ) -> None: + """Set whether to write elements below the volume threshold. + + Args: + write (bool): True to enable writing, False to disable. + """ + self.writeIsBelowVolume = write -# Main function for backward compatibility and standalone use -def element_volumes( + def _addBelowVolumeArray( self: Self ) -> None: + """Add an array marking elements below the minimum volume threshold on the mesh.""" + self.logger.info( "Adding CellData array marking elements below the minimum volume threshold." ) + numCells = self.mesh.GetNumberOfCells() + belowVolumeArray = np.zeros( numCells, dtype=np.int32 ) + belowVolumeArray[ [ i for i, _ in self.belowVolumes ] ] = 1 + + vtkArray: vtkDataArray = numpy_to_vtk( belowVolumeArray ) + vtkArray.SetName( "BelowVolumeThresholdOf" + str( self.minVolume ) ) + self.mesh.GetCellData().AddArray( vtkArray ) + + +# Main function for standalone use +def elementVolumes( mesh: vtkUnstructuredGrid, - return_negative_zero_volumes: bool = False, - write_output: bool = False, - output_path: str = "output/mesh_with_volumes.vtu", -) -> tuple[ vtkUnstructuredGrid, npt.NDArray ]: + outputPath: str, + minVolume: float = 0.0, + writeIsBelowVolume: bool = False, +) -> tuple[ vtkUnstructuredGrid, npt.NDArray, list[ tuple[ int, float ] ] ]: """Apply element volumes calculation to a mesh. Args: - mesh (vtkUnstructuredGrid): The input mesh - return_negative_zero_volumes (bool): Whether to report negative/zero volume elements. Defaults to False. - write_output (bool): Whether to write output mesh to file. Defaults to False. - output_path (str): Output file path if write_output is True. + mesh (vtkUnstructuredGrid): The input mesh to analyze. + minVolume (float): Minimum volume threshold for elements. Defaults to 0.0. + writeIsBelowVolume (bool): Whether to write elements below the volume threshold. Defaults to False. + writeOutput (bool): Whether to write output mesh to file. Defaults to False. + outputPath (str): Output file path if writeOutput is True. Returns: - tuple[vtkUnstructuredGrid, npt.NDArray]: - Processed mesh, array of negative/zero volume elements + tuple[vtkUnstructuredGrid, npt.NDArray, list[ tuple[ int, float ] ]]: + Processed mesh, array of volumes, list of volumes below the threshold. """ - filter_instance = ElementVolumes( mesh, return_negative_zero_volumes ) - success = filter_instance.applyFilter() + filterInstance = ElementVolumes( mesh, minVolume, writeIsBelowVolume ) + success = filterInstance.applyFilter() if not success: - raise RuntimeError( "Element volumes calculation failed" ) + raise RuntimeError( "Element volumes calculation failed." ) - if write_output: - filter_instance.writeGrid( output_path ) + if writeIsBelowVolume: + filterInstance.writeGrid( outputPath ) return ( - filter_instance.getMesh(), - filter_instance.getNegativeZeroVolumes(), + filterInstance.getMesh(), + filterInstance.getVolumes(), + filterInstance.getBelowVolumes(), ) - - -# Alias for backward compatibility -def processElementVolumes( - mesh: vtkUnstructuredGrid, - return_negative_zero_volumes: bool = False, -) -> tuple[ vtkUnstructuredGrid, npt.NDArray ]: - """Legacy function name for backward compatibility.""" - return element_volumes( mesh, return_negative_zero_volumes ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py index 9fe04cc81..87a111e6a 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py @@ -14,7 +14,8 @@ When a fracture plane is defined between two cells, the nodes of the shared face will be duplicated to create a discontinuity. The filter generates both the split main mesh and separate fracture meshes. -To use the filter: +To use the filter +----------------- .. code-block:: python @@ -23,9 +24,9 @@ # instantiate the filter generateFracturesFilter = GenerateFractures( mesh, - field_name="fracture_field", - field_values="1,2", - fractures_output_dir="./fractures/", + fieldName="fracture_field", + fieldValues="1,2", + fracturesOutputDir="./fractures/", policy=1 ) @@ -33,11 +34,30 @@ success = generateFracturesFilter.applyFilter() # get the results - split_mesh = generateFracturesFilter.getMesh() - fracture_meshes = generateFracturesFilter.getFractureMeshes() + splitMesh = generateFracturesFilter.getMesh() + fractureMeshes = generateFracturesFilter.getFractureMeshes() # write all meshes - generateFracturesFilter.writeMeshes("output/split_mesh.vtu", is_data_mode_binary=True) + generateFracturesFilter.writeMeshes("output/split_mesh.vtu", isDataModeBinary=True) + +For standalone use without creating a filter instance +----------------------------------------------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.GenerateFractures import generateFractures + + # apply fracture generation directly + splitMesh, fractureMeshes = generateFractures( + mesh, + outputPath="output/split_mesh.vtu", + fieldName="fracture_field", + fieldValues="1,2", + fracturesOutputDir="./fractures/", + policy=1, + outputDataMode=0, + fracturesDataMode=1 + ) """ FIELD_NAME = __FIELD_NAME @@ -56,249 +76,226 @@ class GenerateFractures( MeshDoctorFilterBase ): def __init__( self: Self, mesh: vtkUnstructuredGrid, - field_name: str = None, - field_values: str = None, - fractures_output_dir: str = None, + fieldName: str = None, + fieldValues: str = None, + fracturesOutputDir: str = None, policy: int = 1, - output_data_mode: int = 0, - fractures_data_mode: int = 1, - use_external_logger: bool = False, + outputDataMode: int = 0, + fracturesDataMode: int = 1, + useExternalLogger: bool = False, ) -> None: """Initialize the generate fractures filter. Args: - mesh (vtkUnstructuredGrid): The input mesh to split - field_name (str): Field name that defines fracture regions. Defaults to None. - field_values (str): Comma-separated field values that identify fracture boundaries. Defaults to None. - fractures_output_dir (str): Output directory for fracture meshes. Defaults to None. + mesh (vtkUnstructuredGrid): The input mesh to split. + fieldName (str): Field name that defines fracture regions. Defaults to None. + fieldValues (str): Comma-separated field values that identify fracture boundaries. Defaults to None. + fracturesOutputDir (str): Output directory for fracture meshes. Defaults to None. policy (int): Fracture policy (0 for internal, 1 for boundary). Defaults to 1. - output_data_mode (int): Data mode for main mesh (0 for ASCII, 1 for binary). Defaults to 0. - fractures_data_mode (int): Data mode for fracture meshes (0 for ASCII, 1 for binary). Defaults to 1. - use_external_logger (bool): Whether to use external logger. Defaults to False. + outputDataMode (int): Data mode for main mesh (0 for ASCII, 1 for binary). Defaults to 0. + fracturesDataMode (int): Data mode for fracture meshes (0 for ASCII, 1 for binary). Defaults to 1. + useExternalLogger (bool): Whether to use external logger. Defaults to False. """ - super().__init__( mesh, loggerTitle, use_external_logger ) - self.field_name: str = field_name - self.field_values: str = field_values - self.fractures_output_dir: str = fractures_output_dir + super().__init__( mesh, loggerTitle, useExternalLogger ) + self.fieldName: str = fieldName + self.fieldValues: str = fieldValues + self.fracturesOutputDir: str = fracturesOutputDir self.policy: str = POLICIES[ policy ] if 0 <= policy <= 1 else POLICIES[ 1 ] - self.output_data_mode: str = DATA_MODE[ output_data_mode ] if output_data_mode in [ 0, 1 ] else DATA_MODE[ 0 ] - self.fractures_data_mode: str = ( DATA_MODE[ fractures_data_mode ] - if fractures_data_mode in [ 0, 1 ] else DATA_MODE[ 1 ] ) + self.outputDataMode: str = DATA_MODE[ outputDataMode ] if outputDataMode in [ 0, 1 ] else DATA_MODE[ 0 ] + self.fracturesDataMode: str = ( DATA_MODE[ fracturesDataMode ] + if fracturesDataMode in [ 0, 1 ] else DATA_MODE[ 1 ] ) + self.fractureMeshes: list[ vtkUnstructuredGrid ] = [] + self.allFracturesVtkOutput: list[ VtkOutput ] = [] + + def applyFilter( self: Self ) -> bool: + """Apply the fracture generation. + + Returns: + bool: True if fractures generated successfully, False otherwise. + """ + self.logger.info( f"Apply filter {self.logger.name}" ) + + # Check for global IDs which are not allowed + if has_array( self.mesh, [ "GLOBAL_IDS_POINTS", "GLOBAL_IDS_CELLS" ] ): + self.logger.error( + "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." ) + return False + + # Validate required parameters + parsedOptions = self._buildParsedOptions() + if len( parsedOptions ) < 5: + self.logger.error( "You must set all variables before trying to create fractures." ) + return False + + self.logger.info( f"Parsed options: {parsedOptions}" ) + + # Convert options and split mesh + options: Options = convert( parsedOptions ) + self.allFracturesVtkOutput = options.all_fractures_VtkOutput + + # Perform the fracture generation + output_mesh, self.fractureMeshes = split_mesh_on_fractures( self.mesh, options ) + + # Update the main mesh with the split result + self.mesh = output_mesh - # Results storage - self.fracture_meshes: list[ vtkUnstructuredGrid ] = [] - self.all_fractures_vtk_output: list[ VtkOutput ] = [] + self.logger.info( f"Generated {len(self.fractureMeshes)} fracture meshes." ) + self.logger.info( f"The filter {self.logger.name} succeeded." ) + return True - def setFieldName( self: Self, field_name: str ) -> None: + def getAllGrids( self: Self ) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: + """Get both the split main mesh and fracture meshes. + + Returns: + tuple[vtkUnstructuredGrid, list[vtkUnstructuredGrid]]: Split mesh and fracture meshes. + """ + return ( self.mesh, self.fractureMeshes ) + + def getFractureMeshes( self: Self ) -> list[ vtkUnstructuredGrid ]: + """Get the generated fracture meshes. + + Returns: + list[vtkUnstructuredGrid]: List of fracture meshes. + """ + return self.fractureMeshes + + def setFieldName( self: Self, fieldName: str ) -> None: """Set the field name that defines fracture regions. Args: - field_name (str): Name of the field + fieldName (str): Name of the field. """ - self.field_name = field_name + self.fieldName = fieldName - def setFieldValues( self: Self, field_values: str ) -> None: + def setFieldValues( self: Self, fieldValues: str ) -> None: """Set the field values that identify fracture boundaries. Args: - field_values (str): Comma-separated field values + fieldValues (str): Comma-separated field values. """ - self.field_values = field_values + self.fieldValues = fieldValues - def setFracturesOutputDirectory( self: Self, directory: str ) -> None: - """Set the output directory for fracture meshes. + def setFracturesDataMode( self: Self, choice: int ) -> None: + """Set the data mode for fracture mesh outputs. Args: - directory (str): Directory path + choice (int): 0 for ASCII, 1 for binary. """ - self.fractures_output_dir = directory + if choice not in [ 0, 1 ]: + self.logger.error( f"setFracturesDataMode: Please choose either 0 for {DATA_MODE[0]} " + f"or 1 for {DATA_MODE[1]}, not '{choice}'." ) + else: + self.fracturesDataMode = DATA_MODE[ choice ] - def setPolicy( self: Self, choice: int ) -> None: - """Set the fracture policy. + def setFracturesOutputDirectory( self: Self, directory: str ) -> None: + """Set the output directory for fracture meshes. Args: - choice (int): 0 for internal fractures, 1 for boundary fractures + directory (str): Directory path. """ - if choice not in [ 0, 1 ]: - self.logger.error( - f"setPolicy: Please choose either 0 for {POLICIES[0]} or 1 for {POLICIES[1]}, not '{choice}'." ) - else: - self.policy = convert_to_fracture_policy( POLICIES[ choice ] ) + self.fracturesOutputDir = directory def setOutputDataMode( self: Self, choice: int ) -> None: """Set the data mode for the main mesh output. Args: - choice (int): 0 for ASCII, 1 for binary + choice (int): 0 for ASCII, 1 for binary. """ if choice not in [ 0, 1 ]: self.logger.error( f"setOutputDataMode: Please choose either 0 for {DATA_MODE[0]} or 1 for {DATA_MODE[1]}, not '{choice}'." ) else: - self.output_data_mode = DATA_MODE[ choice ] + self.outputDataMode = DATA_MODE[ choice ] - def setFracturesDataMode( self: Self, choice: int ) -> None: - """Set the data mode for fracture mesh outputs. + def setPolicy( self: Self, choice: int ) -> None: + """Set the fracture policy. Args: - choice (int): 0 for ASCII, 1 for binary + choice (int): 0 for field, 1 for internal surfaces. """ if choice not in [ 0, 1 ]: - self.logger.error( f"setFracturesDataMode: Please choose either 0 for {DATA_MODE[0]} " - f"or 1 for {DATA_MODE[1]}, not '{choice}'." ) + self.logger.error( + f"setPolicy: Please choose either 0 for {POLICIES[0]} or 1 for {POLICIES[1]}, not '{choice}'." ) else: - self.fractures_data_mode = DATA_MODE[ choice ] - - def applyFilter( self: Self ) -> bool: - """Apply the fracture generation. - - Returns: - bool: True if fractures generated successfully, False otherwise. - """ - self.logger.info( f"Apply filter {self.logger.name}" ) - - try: - # Check for global IDs which are not allowed - if has_array( self.mesh, [ "GLOBAL_IDS_POINTS", "GLOBAL_IDS_CELLS" ] ): - self.logger.error( - "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." ) - return False - - # Validate required parameters - parsed_options = self._getParsedOptions() - if len( parsed_options ) < 5: - self.logger.error( "You must set all variables before trying to create fractures." ) - return False - - self.logger.info( f"Parsed options: {parsed_options}" ) - - # Convert options and split mesh - options: Options = convert( parsed_options ) - self.all_fractures_vtk_output = options.all_fractures_VtkOutput - - # Perform the fracture generation - output_mesh, self.fracture_meshes = split_mesh_on_fractures( self.mesh, options ) - - # Update the main mesh with the split result - self.mesh = output_mesh - - self.logger.info( f"Generated {len(self.fracture_meshes)} fracture meshes" ) - self.logger.info( f"The filter {self.logger.name} succeeded" ) - return True - - except Exception as e: - self.logger.error( f"Error in fracture generation: {e}" ) - self.logger.error( f"The filter {self.logger.name} failed" ) - return False + self.policy = convert_to_fracture_policy( POLICIES[ choice ] ) - def _getParsedOptions( self: Self ) -> dict[ str, str ]: - """Get parsed options for fracture generation.""" - parsed_options: dict[ str, str ] = { "output": "./mesh.vtu", "data_mode": DATA_MODE[ 0 ] } - parsed_options[ POLICY ] = self.policy - parsed_options[ FRACTURES_DATA_MODE ] = self.fractures_data_mode + def _buildParsedOptions( self: Self ) -> dict[ str, str ]: + """Build parsed options to be used for an Options object.""" + parsedOptions: dict[ str, str ] = { "output": "./mesh.vtu", "data_mode": DATA_MODE[ 0 ] } + parsedOptions[ POLICY ] = self.policy + parsedOptions[ FRACTURES_DATA_MODE ] = self.fracturesDataMode - if self.field_name: - parsed_options[ FIELD_NAME ] = self.field_name + if self.fieldName: + parsedOptions[ FIELD_NAME ] = self.fieldName else: self.logger.error( "No field name provided. Please use setFieldName." ) - if self.field_values: - parsed_options[ FIELD_VALUES ] = self.field_values + if self.fieldValues: + parsedOptions[ FIELD_VALUES ] = self.fieldValues else: self.logger.error( "No field values provided. Please use setFieldValues." ) - if self.fractures_output_dir: - parsed_options[ FRACTURES_OUTPUT_DIR ] = self.fractures_output_dir + if self.fracturesOutputDir: + parsedOptions[ FRACTURES_OUTPUT_DIR ] = self.fracturesOutputDir else: self.logger.error( "No fracture output directory provided. Please use setFracturesOutputDirectory." ) - return parsed_options - - def getFractureMeshes( self: Self ) -> list[ vtkUnstructuredGrid ]: - """Get the generated fracture meshes. - - Returns: - list[vtkUnstructuredGrid]: List of fracture meshes - """ - return self.fracture_meshes - - def getAllGrids( self: Self ) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: - """Get both the split main mesh and fracture meshes. - - Returns: - tuple[vtkUnstructuredGrid, list[vtkUnstructuredGrid]]: Split mesh and fracture meshes - """ - return ( self.mesh, self.fracture_meshes ) + return parsedOptions - def writeMeshes( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: + def writeMeshes( self: Self, filepath: str, isDataModeBinary: bool = True, canOverwrite: bool = False ) -> None: """Write both the split main mesh and all fracture meshes. Args: - filepath (str): Path for the main split mesh - is_data_mode_binary (bool): Whether to use binary format for main mesh. Defaults to True. + filepath (str): Path for the main split mesh. + isDataModeBinary (bool): Whether to use binary format for main mesh. Defaults to True. canOverwrite (bool): Whether to allow overwriting existing files. Defaults to False. """ if self.mesh: - write_mesh( self.mesh, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) + write_mesh( self.mesh, VtkOutput( filepath, isDataModeBinary ), canOverwrite ) else: self.logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) - for i, fracture_mesh in enumerate( self.fracture_meshes ): - if i < len( self.all_fractures_vtk_output ): - write_mesh( fracture_mesh, self.all_fractures_vtk_output[ i ] ) + for i, fractureMesh in enumerate( self.fractureMeshes ): + if i < len( self.allFracturesVtkOutput ): + write_mesh( fractureMesh, self.allFracturesVtkOutput[ i ] ) -# Main function for backward compatibility and standalone use -def generate_fractures( +# Main function for standalone use +def generateFractures( mesh: vtkUnstructuredGrid, - field_name: str, - field_values: str, - fractures_output_dir: str, + outputPath: str, + fieldName: str, + fieldValues: str, + fracturesOutputDir: str, policy: int = 1, - output_data_mode: int = 0, - fractures_data_mode: int = 1, - write_output: bool = False, - output_path: str = "output/split_mesh.vtu", + outputDataMode: int = 0, + fracturesDataMode: int = 1 ) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: """Apply fracture generation to a mesh. Args: - mesh (vtkUnstructuredGrid): The input mesh - field_name (str): Field name that defines fracture regions - field_values (str): Comma-separated field values that identify fracture boundaries - fractures_output_dir (str): Output directory for fracture meshes + mesh (vtkUnstructuredGrid): The input mesh. + outputPath (str): Output file path if write_output is True. + fieldName (str): Field name that defines fracture regions. + fieldValues (str): Comma-separated field values that identify fracture boundaries. + fracturesOutputDir (str): Output directory for fracture meshes. policy (int): Fracture policy (0 for internal, 1 for boundary). Defaults to 1. - output_data_mode (int): Data mode for main mesh (0 for ASCII, 1 for binary). Defaults to 0. - fractures_data_mode (int): Data mode for fracture meshes (0 for ASCII, 1 for binary). Defaults to 1. - write_output (bool): Whether to write output meshes to files. Defaults to False. - output_path (str): Output file path if write_output is True. + outputDataMode (int): Data mode for main mesh (0 for ASCII, 1 for binary). Defaults to 0. + fracturesDataMode (int): Data mode for fracture meshes (0 for ASCII, 1 for binary). Defaults to 1. Returns: tuple[vtkUnstructuredGrid, list[vtkUnstructuredGrid]]: - Split mesh and fracture meshes + Split mesh and fracture meshes. """ - filter_instance = GenerateFractures( mesh, field_name, field_values, fractures_output_dir, policy, output_data_mode, - fractures_data_mode ) - success = filter_instance.applyFilter() + filterInstance = GenerateFractures( mesh, fieldName, fieldValues, fracturesOutputDir, policy, outputDataMode, + fracturesDataMode ) + success = filterInstance.applyFilter() if not success: - raise RuntimeError( "Fracture generation failed" ) - - if write_output: - filter_instance.writeMeshes( output_path ) - - return filter_instance.getAllGrids() + raise RuntimeError( "Fracture generation failed." ) + filterInstance.writeMeshes( outputPath ) -# Alias for backward compatibility -def processGenerateFractures( - mesh: vtkUnstructuredGrid, - field_name: str, - field_values: str, - fractures_output_dir: str, - policy: int = 1, -) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: - """Legacy function name for backward compatibility.""" - return generate_fractures( mesh, field_name, field_values, fractures_output_dir, policy ) + return filterInstance.getAllGrids() diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py index b908f9a17..70d82878b 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py @@ -13,13 +13,14 @@ No filter input and one filter output which is vtkUnstructuredGrid. -To use the filter: +To use the filter +----------------- .. code-block:: python from filters.GenerateRectilinearGrid import GenerateRectilinearGrid - # instanciate the filter + # instantiate the filter generateRectilinearGridFilter: GenerateRectilinearGrid = GenerateRectilinearGrid() # set the coordinates of each block border for the X, Y and Z axis @@ -39,17 +40,32 @@ points_dim3 = FieldInfo( "point3", 3, "POINTS" ) # array "point3" of shape ( number of points, 3 ) generateRectilinearGridFilter.setFields( [ cells_dim1, cells_dim3, points_dim1, points_dim3 ] ) - # then, to obtain the constructed mesh out of all these operations, 2 solutions are available + # execute the filter + success = elementVolumesFilter.applyFilter() - # solution1 - mesh: vtkUnstructuredGrid = generateRectilinearGridFilter.getGrid() + # get the generated mesh + outputMesh = generateRectilinearGridFilter.getGrid() - # solution2, which calls the same method as above - generateRectilinearGridFilter.Update() - mesh: vtkUnstructuredGrid = generateRectilinearGridFilter.GetOutputDataObject( 0 ) +For standalone use without creating a filter instance +----------------------------------------------------- - # finally, you can write the mesh at a specific destination with: - generateRectilinearGridFilter.writeGrid("output/filepath/of/your/grid.vtu") +.. code-block:: python + + from filters.GenerateRectilinearGrid import generateRectilinearGrid, FieldInfo + + # generate grid directly + outputMesh = generateRectilinearGrid( + coordsX=[0.0, 5.0, 10.0], + coordsY=[0.0, 5.0, 10.0], + coordsZ=[0.0, 10.0], + numberElementsX=[5, 5], + numberElementsY=[5, 5], + numberElementsZ=[10], + outputPath="output/rectilinear_grid.vtu", + fields=[FieldInfo("cell1", 1, "CELLS")], + generateCellsGlobalIds=True, + generatePointsGlobalIds=True + ) """ loggerTitle: str = "Generate Rectilinear Grid" @@ -59,86 +75,29 @@ class GenerateRectilinearGrid( MeshDoctorGeneratorBase ): def __init__( self: Self, - generate_cells_global_ids: bool = False, - generate_points_global_ids: bool = False, - use_external_logger: bool = False, + generateCellsGlobalIds: bool = False, + generatePointsGlobalIds: bool = False, + useExternalLogger: bool = False, ) -> None: """Initialize the rectilinear grid generator. Args: - generate_cells_global_ids (bool): Whether to generate global cell IDs. Defaults to False. - generate_points_global_ids (bool): Whether to generate global point IDs. Defaults to False. - use_external_logger (bool): Whether to use external logger. Defaults to False. + generateCellsGlobalIds (bool): Whether to generate global cell IDs. Defaults to False. + generatePointsGlobalIds (bool): Whether to generate global point IDs. Defaults to False. + useExternalLogger (bool): Whether to use external logger. Defaults to False. """ - super().__init__( loggerTitle, use_external_logger ) - self.generate_cells_global_ids: bool = generate_cells_global_ids - self.generate_points_global_ids: bool = generate_points_global_ids - self.coords_x: Sequence[ float ] = None - self.coords_y: Sequence[ float ] = None - self.coords_z: Sequence[ float ] = None - self.number_elements_x: Sequence[ int ] = None - self.number_elements_y: Sequence[ int ] = None + super().__init__( loggerTitle, useExternalLogger ) + self.generateCellsGlobalIds: bool = generateCellsGlobalIds + self.generatePointsGlobalIds: bool = generatePointsGlobalIds + self.coordsX: Sequence[ float ] = None + self.coordsY: Sequence[ float ] = None + self.coordsZ: Sequence[ float ] = None + self.numberElementsX: Sequence[ int ] = None + self.numberElementsY: Sequence[ int ] = None + self.numberElementsZ: Sequence[ int ] = None self.number_elements_z: Sequence[ int ] = None self.fields: Iterable[ FieldInfo ] = list() - def setCoordinates( - self: Self, - coords_x: Sequence[ float ], - coords_y: Sequence[ float ], - coords_z: Sequence[ float ], - ) -> None: - """Set the coordinates of the block boundaries for the grid along X, Y and Z axis. - - Args: - coords_x (Sequence[float]): Block boundary coordinates along X axis - coords_y (Sequence[float]): Block boundary coordinates along Y axis - coords_z (Sequence[float]): Block boundary coordinates along Z axis - """ - self.coords_x = coords_x - self.coords_y = coords_y - self.coords_z = coords_z - - def setNumberElements( - self: Self, - number_elements_x: Sequence[ int ], - number_elements_y: Sequence[ int ], - number_elements_z: Sequence[ int ], - ) -> None: - """Set the number of elements for each block along X, Y and Z axis. - - Args: - number_elements_x (Sequence[int]): Number of elements per block along X axis - number_elements_y (Sequence[int]): Number of elements per block along Y axis - number_elements_z (Sequence[int]): Number of elements per block along Z axis - """ - self.number_elements_x = number_elements_x - self.number_elements_y = number_elements_y - self.number_elements_z = number_elements_z - - def setGenerateCellsGlobalIds( self: Self, generate: bool ) -> None: - """Set whether to generate global cell IDs. - - Args: - generate (bool): True to generate global cell IDs, False otherwise - """ - self.generate_cells_global_ids = generate - - def setGeneratePointsGlobalIds( self: Self, generate: bool ) -> None: - """Set whether to generate global point IDs. - - Args: - generate (bool): True to generate global point IDs, False otherwise - """ - self.generate_points_global_ids = generate - - def setFields( self: Self, fields: Iterable[ FieldInfo ] ) -> None: - """Set the fields (arrays) to be added to the grid. - - Args: - fields (Iterable[FieldInfo]): Field information for arrays to create - """ - self.fields = fields - def applyFilter( self: Self ) -> bool: """Generate the rectilinear grid. @@ -150,17 +109,17 @@ def applyFilter( self: Self ) -> bool: try: # Validate inputs required_fields = [ - self.coords_x, self.coords_y, self.coords_z, self.number_elements_x, self.number_elements_y, - self.number_elements_z + self.coordsX, self.coordsY, self.coordsZ, self.numberElementsX, self.numberElementsY, + self.numberElementsZ ] if any( field is None for field in required_fields ): - self.logger.error( "Coordinates and number of elements must be set before generating grid" ) + self.logger.error( "Coordinates and number of elements must be set before generating grid." ) return False # Build coordinates - x: npt.NDArray = build_coordinates( self.coords_x, self.number_elements_x ) - y: npt.NDArray = build_coordinates( self.coords_y, self.number_elements_y ) - z: npt.NDArray = build_coordinates( self.coords_z, self.number_elements_z ) + x: npt.NDArray = build_coordinates( self.coordsX, self.numberElementsX ) + y: npt.NDArray = build_coordinates( self.coordsY, self.numberElementsY ) + z: npt.NDArray = build_coordinates( self.coordsZ, self.numberElementsZ ) # Build the rectilinear grid self.mesh = build_rectilinear_grid( x, y, z ) @@ -170,64 +129,119 @@ def applyFilter( self: Self ) -> bool: self.mesh = add_fields( self.mesh, self.fields ) # Add global IDs if requested - build_global_ids( self.mesh, self.generate_cells_global_ids, self.generate_points_global_ids ) + build_global_ids( self.mesh, self.generateCellsGlobalIds, self.generatePointsGlobalIds ) self.logger.info( f"Generated rectilinear grid with {self.mesh.GetNumberOfPoints()} points " - f"and {self.mesh.GetNumberOfCells()} cells" ) - self.logger.info( f"The filter {self.logger.name} succeeded" ) + f"and {self.mesh.GetNumberOfCells()} cells." ) + self.logger.info( f"The filter {self.logger.name} succeeded." ) return True except Exception as e: self.logger.error( f"Error in rectilinear grid generation: {e}" ) - self.logger.error( f"The filter {self.logger.name} failed" ) + self.logger.error( f"The filter {self.logger.name} failed." ) return False + def setCoordinates( + self: Self, + coordsX: Sequence[ float ], + coordsY: Sequence[ float ], + coordsZ: Sequence[ float ], + ) -> None: + """Set the coordinates of the block boundaries for the grid along X, Y and Z axis. + + Args: + coordsX (Sequence[float]): Block boundary coordinates along X axis. + coordsY (Sequence[float]): Block boundary coordinates along Y axis. + coordsZ (Sequence[float]): Block boundary coordinates along Z axis. + """ + self.coordsX = coordsX + self.coordsY = coordsY + self.coordsZ = coordsZ + + def setFields( self: Self, fields: Iterable[ FieldInfo ] ) -> None: + """Set the fields (arrays) to be added to the grid. -# Main function for backward compatibility and standalone use -def generate_rectilinear_grid( - coords_x: Sequence[ float ], - coords_y: Sequence[ float ], - coords_z: Sequence[ float ], - number_elements_x: Sequence[ int ], - number_elements_y: Sequence[ int ], - number_elements_z: Sequence[ int ], + Args: + fields (Iterable[FieldInfo]): Field information for arrays to create. + """ + self.fields = fields + + def setGenerateCellsGlobalIds( self: Self, generate: bool ) -> None: + """Set whether to generate global cell IDs. + + Args: + generate (bool): True to generate global cell IDs, False otherwise. + """ + self.generateCellsGlobalIds = generate + + def setGeneratePointsGlobalIds( self: Self, generate: bool ) -> None: + """Set whether to generate global point IDs. + + Args: + generate (bool): True to generate global point IDs, False otherwise. + """ + self.generatePointsGlobalIds = generate + + def setNumberElements( + self: Self, + numberElementsX: Sequence[ int ], + numberElementsY: Sequence[ int ], + numberElementsZ: Sequence[ int ], + ) -> None: + """Set the number of elements for each block along X, Y and Z axis. + + Args: + numberElementsX (Sequence[int]): Number of elements per block along X axis. + numberElementsY (Sequence[int]): Number of elements per block along Y axis. + numberElementsZ (Sequence[int]): Number of elements per block along Z axis. + """ + self.numberElementsX = numberElementsX + self.numberElementsY = numberElementsY + self.numberElementsZ = numberElementsZ + + +# Main function for standalone use +def generateRectilinearGrid( + coordsX: Sequence[ float ], + coordsY: Sequence[ float ], + coordsZ: Sequence[ float ], + numberElementsX: Sequence[ int ], + numberElementsY: Sequence[ int ], + numberElementsZ: Sequence[ int ], + outputPath: str, fields: Iterable[ FieldInfo ] = None, - generate_cells_global_ids: bool = False, - generate_points_global_ids: bool = False, - write_output: bool = False, - output_path: str = "output/rectilinear_grid.vtu", + generateCellsGlobalIds: bool = False, + generatePointsGlobalIds: bool = False, ) -> vtkUnstructuredGrid: """Generate a rectilinear grid mesh. Args: - coords_x (Sequence[float]): Block boundary coordinates along X axis - coords_y (Sequence[float]): Block boundary coordinates along Y axis - coords_z (Sequence[float]): Block boundary coordinates along Z axis - number_elements_x (Sequence[int]): Number of elements per block along X axis - number_elements_y (Sequence[int]): Number of elements per block along Y axis - number_elements_z (Sequence[int]): Number of elements per block along Z axis + coordsX (Sequence[float]): Block boundary coordinates along X axis. + coordsY (Sequence[float]): Block boundary coordinates along Y axis. + coordsZ (Sequence[float]): Block boundary coordinates along Z axis. + numberElementsX (Sequence[int]): Number of elements per block along X axis. + numberElementsY (Sequence[int]): Number of elements per block along Y axis. + numberElementsZ (Sequence[int]): Number of elements per block along Z axis. + outputPath (str): Output file path if write_output is True. fields (Iterable[FieldInfo]): Field information for arrays to create. Defaults to None. - generate_cells_global_ids (bool): Whether to generate global cell IDs. Defaults to False. - generate_points_global_ids (bool): Whether to generate global point IDs. Defaults to False. - write_output (bool): Whether to write output mesh to file. Defaults to False. - output_path (str): Output file path if write_output is True. + generateCellsGlobalIds (bool): Whether to generate global cell IDs. Defaults to False. + generatePointsGlobalIds (bool): Whether to generate global point IDs. Defaults to False. Returns: - vtkUnstructuredGrid: The generated mesh + vtkUnstructuredGrid: The generated mesh. """ - filter_instance = GenerateRectilinearGrid( generate_cells_global_ids, generate_points_global_ids ) - filter_instance.setCoordinates( coords_x, coords_y, coords_z ) - filter_instance.setNumberElements( number_elements_x, number_elements_y, number_elements_z ) + filterInstance = GenerateRectilinearGrid( generateCellsGlobalIds, generatePointsGlobalIds ) + filterInstance.setCoordinates( coordsX, coordsY, coordsZ ) + filterInstance.setNumberElements( numberElementsX, numberElementsY, numberElementsZ ) if fields: - filter_instance.setFields( fields ) + filterInstance.setFields( fields ) - success = filter_instance.applyFilter() + success = filterInstance.applyFilter() if not success: - raise RuntimeError( "Rectilinear grid generation failed" ) + raise RuntimeError( "Rectilinear grid generation failed." ) - if write_output: - filter_instance.writeGrid( output_path ) + filterInstance.writeGrid( outputPath ) - return filter_instance.getMesh() + return filterInstance.getMesh() diff --git a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py index 0158fb9f9..00dcd4a83 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py @@ -19,7 +19,8 @@ Unlike the VTK pipeline-based MeshDoctorBase, these classes work with direct mesh manipulation following the FillPartialArrays pattern for simpler, more Pythonic usage. -Example usage patterns: +Example usage patterns +---------------------- .. code-block:: python @@ -63,26 +64,26 @@ class MeshDoctorFilterBase: def __init__( self: Self, mesh: vtkUnstructuredGrid, - filter_name: str, - use_external_logger: bool = False, + filterName: str, + useExternalLogger: bool = False, ) -> None: """Initialize the base mesh doctor filter. Args: mesh (vtkUnstructuredGrid): The input mesh to process - filter_name (str): Name of the filter for logging - use_external_logger (bool): Whether to use external logger. Defaults to False. + filterName (str): Name of the filter for logging + useExternalLogger (bool): Whether to use external logger. Defaults to False. """ self.mesh: vtkUnstructuredGrid = mesh - self.filter_name: str = filter_name + self.filterName: str = filterName # Logger setup self.logger: Logger - if not use_external_logger: - self.logger = getLogger( filter_name, True ) + if not useExternalLogger: + self.logger = getLogger( filterName, True ) else: import logging - self.logger = logging.getLogger( filter_name ) + self.logger = logging.getLogger( filterName ) self.logger.setLevel( logging.INFO ) def setLoggerHandler( self: Self, handler ) -> None: @@ -94,7 +95,7 @@ def setLoggerHandler( self: Self, handler ) -> None: if not self.logger.hasHandlers(): self.logger.addHandler( handler ) else: - self.logger.warning( "The logger already has a handler, to use yours set 'use_external_logger' " + self.logger.warning( "The logger already has a handler, to use yours set 'useExternalLogger' " "to True during initialization." ) def getMesh( self: Self ) -> vtkUnstructuredGrid: @@ -105,33 +106,33 @@ def getMesh( self: Self ) -> vtkUnstructuredGrid: """ return self.mesh - def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: + def writeGrid( self: Self, filepath: str, isDataModeBinary: bool = True, canOverwrite: bool = False ) -> None: """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath. Args: filepath (str): /path/to/your/file.vtu - is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. + isDataModeBinary (bool, optional): Writes the file in binary format or ascii. Defaults to True. canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. Defaults to False. """ if self.mesh: - vtk_output = VtkOutput( filepath, is_data_mode_binary ) + vtk_output = VtkOutput( filepath, isDataModeBinary ) write_mesh( self.mesh, vtk_output, canOverwrite ) else: self.logger.error( f"No mesh available. Cannot output vtkUnstructuredGrid at {filepath}." ) - def copyMesh( self: Self, source_mesh: vtkUnstructuredGrid ) -> vtkUnstructuredGrid: + def copyMesh( self: Self, sourceMesh: vtkUnstructuredGrid ) -> vtkUnstructuredGrid: """Helper method to create a copy of a mesh with structure and attributes. Args: - source_mesh (vtkUnstructuredGrid): Source mesh to copy from + sourceMesh (vtkUnstructuredGrid): Source mesh to copy from. Returns: - vtkUnstructuredGrid: New mesh with copied structure and attributes + vtkUnstructuredGrid: New mesh with copied structure and attributes. """ - output_mesh: vtkUnstructuredGrid = source_mesh.NewInstance() - output_mesh.CopyStructure( source_mesh ) - output_mesh.CopyAttributes( source_mesh ) + output_mesh: vtkUnstructuredGrid = sourceMesh.NewInstance() + output_mesh.CopyStructure( sourceMesh ) + output_mesh.CopyAttributes( sourceMesh ) return output_mesh def applyFilter( self: Self ) -> bool: @@ -142,7 +143,7 @@ def applyFilter( self: Self ) -> bool: Returns: bool: True if filter applied successfully, False otherwise. """ - raise NotImplementedError( "Subclasses must implement applyFilter method" ) + raise NotImplementedError( "Subclasses must implement applyFilter method." ) class MeshDoctorGeneratorBase: @@ -154,25 +155,25 @@ class MeshDoctorGeneratorBase: def __init__( self: Self, - filter_name: str, - use_external_logger: bool = False, + filterName: str, + useExternalLogger: bool = False, ) -> None: """Initialize the base mesh doctor generator filter. Args: - filter_name (str): Name of the filter for logging - use_external_logger (bool): Whether to use external logger. Defaults to False. + filterName (str): Name of the filter for logging. + useExternalLogger (bool): Whether to use external logger. Defaults to False. """ self.mesh: Union[ vtkUnstructuredGrid, None ] = None - self.filter_name: str = filter_name + self.filterName: str = filterName # Logger setup self.logger: Logger - if not use_external_logger: - self.logger = getLogger( filter_name, True ) + if not useExternalLogger: + self.logger = getLogger( filterName, True ) else: import logging - self.logger = logging.getLogger( filter_name ) + self.logger = logging.getLogger( filterName ) self.logger.setLevel( logging.INFO ) def setLoggerHandler( self: Self, handler ) -> None: @@ -184,28 +185,28 @@ def setLoggerHandler( self: Self, handler ) -> None: if not self.logger.hasHandlers(): self.logger.addHandler( handler ) else: - self.logger.warning( "The logger already has a handler, to use yours set 'use_external_logger' " + self.logger.warning( "The logger already has a handler, to use yours set 'useExternalLogger' " "to True during initialization." ) def getMesh( self: Self ) -> Union[ vtkUnstructuredGrid, None ]: """Get the generated mesh. Returns: - Union[vtkUnstructuredGrid, None]: The generated mesh, or None if not yet generated + Union[vtkUnstructuredGrid, None]: The generated mesh, or None if not yet generated. """ return self.mesh - def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: + def writeGrid( self: Self, filepath: str, isDataModeBinary: bool = True, canOverwrite: bool = False ) -> None: """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath. Args: filepath (str): /path/to/your/file.vtu - is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. + isDataModeBinary (bool, optional): Writes the file in binary format or ascii. Defaults to True. canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing file. Defaults to False. """ if self.mesh: - vtk_output = VtkOutput( filepath, is_data_mode_binary ) + vtk_output = VtkOutput( filepath, isDataModeBinary ) write_mesh( self.mesh, vtk_output, canOverwrite ) else: self.logger.error( f"No mesh generated. Cannot output vtkUnstructuredGrid at {filepath}." ) @@ -219,4 +220,4 @@ def applyFilter( self: Self ) -> bool: Returns: bool: True if mesh generated successfully, False otherwise. """ - raise NotImplementedError( "Subclasses must implement applyFilter method" ) + raise NotImplementedError( "Subclasses must implement applyFilter method." ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py index 40509ce7f..1660a2e6c 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py @@ -5,13 +5,15 @@ from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.non_conformal import Options, find_non_conformal_cells from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorFilterBase +from geos.mesh.doctor.parsing.non_conformal_parsing import logger_results __doc__ = """ NonConformal module detects non-conformal mesh interfaces in a vtkUnstructuredGrid. Non-conformal interfaces occur when adjacent cells do not share nodes or faces properly, which can indicate mesh quality issues or intentional non-matching grid interfaces that require special handling. -To use the filter: +To use the filter +----------------- .. code-block:: python @@ -20,24 +22,41 @@ # instantiate the filter nonConformalFilter = NonConformal( mesh, - point_tolerance=1e-6, - face_tolerance=1e-6, - angle_tolerance=10.0, - paint_non_conformal_cells=True + pointTolerance=1e-6, + faceTolerance=1e-6, + angleTolerance=10.0, + writeNonConformalCells=True ) # execute the filter success = nonConformalFilter.applyFilter() # get non-conformal cell pairs - non_conformal_cells = nonConformalFilter.getNonConformalCells() + nonConformalCells = nonConformalFilter.getNonConformalCells() # returns list of tuples with (cell1_id, cell2_id) for non-conformal interfaces # get the processed mesh output_mesh = nonConformalFilter.getMesh() # write the output mesh - nonConformalFilter.writeGrid("output/mesh_with_nonconformal_info.vtu") + nonConformalFilter.writeGrid("output/mesh_with_non_conformal_info.vtu") + +For standalone use without creating a filter instance +----------------------------------------------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.NonConformal import nonConformal + + # apply filter directly + outputMesh, nonConformalCells = nonConformal( + mesh, + outputPath="output/mesh_with_non_conformal_info.vtu", + pointTolerance=1e-6, + faceTolerance=1e-6, + angleTolerance=10.0, + writeNonConformalCells=True + ) """ loggerTitle: str = "Non-Conformal Filter" @@ -48,189 +67,159 @@ class NonConformal( MeshDoctorFilterBase ): def __init__( self: Self, mesh: vtkUnstructuredGrid, - point_tolerance: float = 0.0, - face_tolerance: float = 0.0, - angle_tolerance: float = 10.0, - paint_non_conformal_cells: bool = False, - use_external_logger: bool = False, + pointTolerance: float = 0.0, + faceTolerance: float = 0.0, + angleTolerance: float = 10.0, + writeNonConformalCells: bool = False, + useExternalLogger: bool = False, ) -> None: """Initialize the non-conformal detection filter. Args: - mesh (vtkUnstructuredGrid): The input mesh to analyze - point_tolerance (float): Tolerance for point matching. Defaults to 0.0. - face_tolerance (float): Tolerance for face matching. Defaults to 0.0. - angle_tolerance (float): Angle tolerance in degrees. Defaults to 10.0. - paint_non_conformal_cells (bool): Whether to mark non-conformal cells in output. Defaults to False. - use_external_logger (bool): Whether to use external logger. Defaults to False. + mesh (vtkUnstructuredGrid): The input mesh to analyze. + pointTolerance (float): Tolerance for point matching. Defaults to 0.0. + faceTolerance (float): Tolerance for face matching. Defaults to 0.0. + angleTolerance (float): Angle tolerance in degrees. Defaults to 10.0. + writeNonConformalCells (bool): Whether to mark non-conformal cells in output. Defaults to False. + useExternalLogger (bool): Whether to use external logger. Defaults to False. """ - super().__init__( mesh, loggerTitle, use_external_logger ) - self.point_tolerance: float = point_tolerance - self.face_tolerance: float = face_tolerance - self.angle_tolerance: float = angle_tolerance - self.paint_non_conformal_cells: bool = paint_non_conformal_cells + super().__init__( mesh, loggerTitle, useExternalLogger ) + self.pointTolerance: float = pointTolerance + self.faceTolerance: float = faceTolerance + self.angleTolerance: float = angleTolerance + self.writeNonConformalCells: bool = writeNonConformalCells # Results storage - self.non_conformal_cells: list[ tuple[ int, int ] ] = [] + self.nonConformalCells: list[ tuple[ int, int ] ] = [] - def setPointTolerance( self: Self, tolerance: float ) -> None: - """Set the point tolerance parameter. - - Args: - tolerance (float): Point tolerance value - """ - self.point_tolerance = tolerance - - def setFaceTolerance( self: Self, tolerance: float ) -> None: - """Set the face tolerance parameter. + def applyFilter( self: Self ) -> bool: + """Apply the non-conformal detection. - Args: - tolerance (float): Face tolerance value + Returns: + bool: True if detection completed successfully, False otherwise. """ - self.face_tolerance = tolerance + self.logger.info( f"Apply filter {self.logger.name}" ) - def setAngleTolerance( self: Self, tolerance: float ) -> None: - """Set the angle tolerance parameter in degrees. + # Create options and find non-conformal cells + options = Options( self.angleTolerance, self.pointTolerance, self.faceTolerance ) + self.nonConformalCells = find_non_conformal_cells( self.mesh, options ) - Args: - tolerance (float): Angle tolerance in degrees - """ - self.angle_tolerance = tolerance + logger_results( self.logger, self.nonConformalCells ) - def setPaintNonConformalCells( self: Self, paint: bool ) -> None: - """Set whether to create arrays marking non-conformal cells in output data. + # Add marking arrays if requested + if self.writeNonConformalCells and self.nonConformalCells: + self._addNonConformalCellsArray( self.nonConformalCells ) - Args: - paint (bool): True to enable marking, False to disable - """ - self.paint_non_conformal_cells = paint + self.logger.info( f"The filter {self.logger.name} succeeded." ) + return True - def getPointTolerance( self: Self ) -> float: - """Get the current point tolerance. + def getAngleTolerance( self: Self ) -> float: + """Get the current angle tolerance. Returns: - float: Point tolerance value + float: Angle tolerance in degrees. """ - return self.point_tolerance + return self.angleTolerance def getFaceTolerance( self: Self ) -> float: """Get the current face tolerance. Returns: - float: Face tolerance value + float: Face tolerance value. """ - return self.face_tolerance + return self.faceTolerance - def getAngleTolerance( self: Self ) -> float: - """Get the current angle tolerance. + def getNonConformalCells( self: Self ) -> list[ tuple[ int, int ] ]: + """Get the detected non-conformal cell pairs. Returns: - float: Angle tolerance in degrees + list[tuple[int, int]]: List of cell ID pairs that are non-conformal. """ - return self.angle_tolerance + return self.nonConformalCells - def applyFilter( self: Self ) -> bool: - """Apply the non-conformal detection. + def getPointTolerance( self: Self ) -> float: + """Get the current point tolerance. Returns: - bool: True if detection completed successfully, False otherwise. + float: Point tolerance value. """ - self.logger.info( f"Apply filter {self.logger.name}" ) + return self.pointTolerance - try: - # Create options and find non-conformal cells - options = Options( self.angle_tolerance, self.point_tolerance, self.face_tolerance ) - self.non_conformal_cells = find_non_conformal_cells( self.mesh, options ) - - # Extract all unique cell IDs from pairs - non_conformal_cells_extended = [ cell_id for pair in self.non_conformal_cells for cell_id in pair ] - unique_non_conformal_cells = frozenset( non_conformal_cells_extended ) + def setAngleTolerance( self: Self, tolerance: float ) -> None: + """Set the angle tolerance parameter in degrees. - self.logger.info( f"Found {len(unique_non_conformal_cells)} non-conformal cells" ) - if non_conformal_cells_extended: - self.logger.info( - f"Non-conformal cell IDs: {', '.join(map(str, sorted(non_conformal_cells_extended)))}" ) + Args: + tolerance (float): Angle tolerance in degrees. + """ + self.angleTolerance = tolerance - # Add marking arrays if requested - if self.paint_non_conformal_cells and unique_non_conformal_cells: - self._addNonConformalCellsArray( unique_non_conformal_cells ) + def setFaceTolerance( self: Self, tolerance: float ) -> None: + """Set the face tolerance parameter. - self.logger.info( f"The filter {self.logger.name} succeeded" ) - return True + Args: + tolerance (float): Face tolerance value. + """ + self.faceTolerance = tolerance - except Exception as e: - self.logger.error( f"Error in non-conformal detection: {e}" ) - self.logger.error( f"The filter {self.logger.name} failed" ) - return False + def setPointTolerance( self: Self, tolerance: float ) -> None: + """Set the point tolerance parameter. - def _addNonConformalCellsArray( self: Self, unique_non_conformal_cells: frozenset[ int ] ) -> None: - """Add array marking non-conformal cells.""" - num_cells = self.mesh.GetNumberOfCells() - non_conformal_array = np.zeros( num_cells, dtype=np.int32 ) + Args: + tolerance (float): Point tolerance value. + """ + self.pointTolerance = tolerance - for cell_id in unique_non_conformal_cells: - if 0 <= cell_id < num_cells: - non_conformal_array[ cell_id ] = 1 + def setWriteNonConformalCells( self: Self, write: bool ) -> None: + """Set whether to create anarray marking non-conformal cells in output data. - vtk_array: vtkDataArray = numpy_to_vtk( non_conformal_array ) - vtk_array.SetName( "IsNonConformal" ) - self.mesh.GetCellData().AddArray( vtk_array ) + Args: + write (bool): True to enable marking, False to disable. + """ + self.writeNonConformalCells = write - def getNonConformalCells( self: Self ) -> list[ tuple[ int, int ] ]: - """Get the detected non-conformal cell pairs. + def _addNonConformalCellsArray( self: Self ) -> None: + """Add array marking non-conformal cells.""" + numCells: int = self.mesh.GetNumberOfCells() + uniqueNonConformalCells = frozenset( [ cell_id for pair in self.nonConformalCells for cell_id in pair ] ) + nonConformalArray = np.zeros( numCells, dtype=np.int32 ) + nonConformalArray[ list( uniqueNonConformalCells ) ] = 1 - Returns: - list[tuple[int, int]]: List of cell ID pairs that are non-conformal - """ - return self.non_conformal_cells + vtkArray: vtkDataArray = numpy_to_vtk( nonConformalArray ) + vtkArray.SetName( "IsNonConformal" ) + self.mesh.GetCellData().AddArray( vtkArray ) -# Main function for backward compatibility and standalone use -def non_conformal( +# Main function for standalone use +def nonConformal( mesh: vtkUnstructuredGrid, - point_tolerance: float = 0.0, - face_tolerance: float = 0.0, - angle_tolerance: float = 10.0, - paint_non_conformal_cells: bool = False, - write_output: bool = False, - output_path: str = "output/mesh_with_nonconformal_info.vtu", + outputPath: str, + pointTolerance: float = 0.0, + faceTolerance: float = 0.0, + angleTolerance: float = 10.0, + writeNonConformalCells: bool = False ) -> tuple[ vtkUnstructuredGrid, list[ tuple[ int, int ] ] ]: """Apply non-conformal detection to a mesh. Args: - mesh (vtkUnstructuredGrid): The input mesh - point_tolerance (float): Tolerance for point matching. Defaults to 0.0. - face_tolerance (float): Tolerance for face matching. Defaults to 0.0. - angle_tolerance (float): Angle tolerance in degrees. Defaults to 10.0. - paint_non_conformal_cells (bool): Whether to mark non-conformal cells. Defaults to False. + mesh (vtkUnstructuredGrid): The input mesh to analyze. + pointTolerance (float): Tolerance for point matching. Defaults to 0.0. + faceTolerance (float): Tolerance for face matching. Defaults to 0.0. + angleTolerance (float): Angle tolerance in degrees. Defaults to 10.0. + writeNonConformalCells (bool): Whether to mark non-conformal cells. Defaults to False. write_output (bool): Whether to write output mesh to file. Defaults to False. output_path (str): Output file path if write_output is True. Returns: tuple[vtkUnstructuredGrid, list[tuple[int, int]]]: - Processed mesh, non-conformal cell pairs + Processed mesh, non-conformal cell pairs. """ - filter_instance = NonConformal( mesh, point_tolerance, face_tolerance, angle_tolerance, paint_non_conformal_cells ) - success = filter_instance.applyFilter() - - if not success: - raise RuntimeError( "Non-conformal detection failed" ) + filterInstance = NonConformal( mesh, pointTolerance, faceTolerance, angleTolerance, writeNonConformalCells ) + filterInstance.applyFilter() - if write_output: - filter_instance.writeGrid( output_path ) + if writeNonConformalCells: + filterInstance.writeGrid( outputPath ) return ( - filter_instance.getMesh(), - filter_instance.getNonConformalCells(), + filterInstance.getMesh(), + filterInstance.getNonConformalCells(), ) - - -# Alias for backward compatibility -def processNonConformal( - mesh: vtkUnstructuredGrid, - point_tolerance: float = 0.0, - face_tolerance: float = 0.0, - angle_tolerance: float = 10.0, -) -> tuple[ vtkUnstructuredGrid, list[ tuple[ int, int ] ] ]: - """Legacy function name for backward compatibility.""" - return non_conformal( mesh, point_tolerance, face_tolerance, angle_tolerance ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py index d0fe751c2..bcd794a00 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py @@ -5,13 +5,16 @@ from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.doctor.actions.self_intersecting_elements import get_invalid_cell_ids from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorFilterBase +from geos.mesh.doctor.parsing.self_intersecting_elements_parsing import logger_results __doc__ = """ SelfIntersectingElements module identifies various types of invalid or problematic elements in a vtkUnstructuredGrid. It detects elements with intersecting edges, intersecting faces, non-contiguous edges, -non-convex shapes, incorrectly oriented faces, and wrong number of points. +non-convex shapes, incorrectly oriented faces, wrong number of points, non planar faces elements and degenerate +faces elements. -To use the filter: +To use the filter +----------------- .. code-block:: python @@ -20,26 +23,36 @@ # instantiate the filter selfIntersectingElementsFilter = SelfIntersectingElements( mesh, - min_distance=1e-6, - paint_invalid_elements=True + minDistance=1e-6, + writeInvalidElements=True ) # execute the filter success = selfIntersectingElementsFilter.applyFilter() - # get different types of problematic elements - wrong_points_elements = selfIntersectingElementsFilter.getWrongNumberOfPointsElements() - intersecting_edges_elements = selfIntersectingElementsFilter.getIntersectingEdgesElements() - intersecting_faces_elements = selfIntersectingElementsFilter.getIntersectingFacesElements() - non_contiguous_edges_elements = selfIntersectingElementsFilter.getNonContiguousEdgesElements() - non_convex_elements = selfIntersectingElementsFilter.getNonConvexElements() - wrong_oriented_faces_elements = selfIntersectingElementsFilter.getFacesOrientedIncorrectlyElements() + # get the ids of problematic elements for every error types + selfIntersectingElementsFilter.getInvalidCells() # get the processed mesh output_mesh = selfIntersectingElementsFilter.getMesh() # write the output mesh selfIntersectingElementsFilter.writeGrid("output/mesh_with_invalid_elements.vtu") + +For standalone use without creating a filter instance +----------------------------------------------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.SelfIntersectingElements import selfIntersectingElements + + # apply filter directly + outputMesh, invalidCellIds = selfIntersectingElements( + mesh, + outputPath="output/mesh_with_invalid_elements.vtu", + minDistance=1e-6, + writeInvalidElements=True + ) """ loggerTitle: str = "Self-Intersecting Elements Filter" @@ -50,53 +63,22 @@ class SelfIntersectingElements( MeshDoctorFilterBase ): def __init__( self: Self, mesh: vtkUnstructuredGrid, - min_distance: float = 0.0, - paint_invalid_elements: bool = False, - use_external_logger: bool = False, + minDistance: float = 0.0, + writeInvalidElements: bool = False, + useExternalLogger: bool = False, ) -> None: """Initialize the self-intersecting elements detection filter. Args: - mesh (vtkUnstructuredGrid): The input mesh to analyze - min_distance (float): Minimum distance parameter for intersection detection. Defaults to 0.0. - paint_invalid_elements (bool): Whether to mark invalid elements in output. Defaults to False. - use_external_logger (bool): Whether to use external logger. Defaults to False. - """ - super().__init__( mesh, loggerTitle, use_external_logger ) - self.min_distance: float = min_distance - self.paint_invalid_elements: bool = paint_invalid_elements - - # Results storage - self.wrong_number_of_points_elements: list[ int ] = [] - self.intersecting_edges_elements: list[ int ] = [] - self.intersecting_faces_elements: list[ int ] = [] - self.non_contiguous_edges_elements: list[ int ] = [] - self.non_convex_elements: list[ int ] = [] - self.faces_oriented_incorrectly_elements: list[ int ] = [] - - def setMinDistance( self: Self, distance: float ) -> None: - """Set the minimum distance parameter for intersection detection. - - Args: - distance (float): Minimum distance value + mesh (vtkUnstructuredGrid): The input mesh to analyze. + minDistance (float): Minimum distance parameter for intersection detection. Defaults to 0.0. + writeInvalidElements (bool): Whether to mark invalid elements in output. Defaults to False. + useExternalLogger (bool): Whether to use external logger. Defaults to False. """ - self.min_distance = distance - - def setPaintInvalidElements( self: Self, paint: bool ) -> None: - """Set whether to create arrays marking invalid elements in output data. - - Args: - paint (bool): True to enable marking, False to disable - """ - self.paint_invalid_elements = paint - - def getMinDistance( self: Self ) -> float: - """Get the current minimum distance parameter. - - Returns: - float: Minimum distance value - """ - return self.min_distance + super().__init__( mesh, loggerTitle, useExternalLogger ) + self.invalidCellIds: dict[ str, list[ int ] ] = {} + self.minDistance: float = minDistance + self.writeInvalidElements: bool = writeInvalidElements def applyFilter( self: Self ) -> bool: """Apply the self-intersecting elements detection. @@ -106,158 +88,90 @@ def applyFilter( self: Self ) -> bool: """ self.logger.info( f"Apply filter {self.logger.name}" ) - try: - # Get invalid cell IDs - invalid_cells = get_invalid_cell_ids( self.mesh, self.min_distance ) - - # Store results - self.wrong_number_of_points_elements = invalid_cells.get( "wrong_number_of_points_elements", [] ) - self.intersecting_edges_elements = invalid_cells.get( "intersecting_edges_elements", [] ) - self.intersecting_faces_elements = invalid_cells.get( "intersecting_faces_elements", [] ) - self.non_contiguous_edges_elements = invalid_cells.get( "non_contiguous_edges_elements", [] ) - self.non_convex_elements = invalid_cells.get( "non_convex_elements", [] ) - self.faces_oriented_incorrectly_elements = invalid_cells.get( "faces_oriented_incorrectly_elements", [] ) - - # Log the results - total_invalid = sum( len( invalid_list ) for invalid_list in invalid_cells.values() ) - self.logger.info( f"Found {total_invalid} invalid elements:" ) - for criterion, cell_list in invalid_cells.items(): - if cell_list: - self.logger.info( f" {criterion}: {len(cell_list)} elements - {cell_list}" ) - - # Add marking arrays if requested - if self.paint_invalid_elements: - self._addInvalidElementsArrays( invalid_cells ) - - self.logger.info( f"The filter {self.logger.name} succeeded" ) - return True - - except Exception as e: - self.logger.error( f"Error in self-intersecting elements detection: {e}" ) - self.logger.error( f"The filter {self.logger.name} failed" ) - return False - - def _addInvalidElementsArrays( self: Self, invalid_cells: dict[ str, list[ int ] ] ) -> None: - """Add arrays marking different types of invalid elements.""" - num_cells = self.mesh.GetNumberOfCells() - - for criterion, cell_list in invalid_cells.items(): - if cell_list: - array = np.zeros( num_cells, dtype=np.int32 ) - for cell_id in cell_list: - if 0 <= cell_id < num_cells: - array[ cell_id ] = 1 + self.invalidCells = get_invalid_cell_ids( self.mesh, self.minDistance ) + logger_results( self.logger, self.invalidCells ) - vtk_array: vtkDataArray = numpy_to_vtk( array ) - # Convert criterion name to CamelCase for array name - array_name = f"Is{criterion.replace('_', '').title()}" - vtk_array.SetName( array_name ) - self.mesh.GetCellData().AddArray( vtk_array ) + # Add marking arrays if requested + if self.writeInvalidElements: + self._addInvalidElementsArrays() - def getWrongNumberOfPointsElements( self: Self ) -> list[ int ]: - """Get elements with wrong number of points. + self.logger.info( f"The filter {self.logger.name} succeeded." ) + return True - Returns: - list[int]: Element indices with wrong number of points - """ - return self.wrong_number_of_points_elements - - def getIntersectingEdgesElements( self: Self ) -> list[ int ]: - """Get elements with intersecting edges. - - Returns: - list[int]: Element indices with intersecting edges - """ - return self.intersecting_edges_elements - - def getIntersectingFacesElements( self: Self ) -> list[ int ]: - """Get elements with intersecting faces. + def getInvalidCellIds( self: Self ) -> dict[ str, list[ int ] ]: + """Get all invalid elements organized by type. Returns: - list[int]: Element indices with intersecting faces + dict[str, list[int]]: Dictionary mapping invalid element types to their IDs. """ - return self.intersecting_faces_elements + return self.invalidCellIds - def getNonContiguousEdgesElements( self: Self ) -> list[ int ]: - """Get elements with non-contiguous edges. + def getMinDistance( self: Self ) -> float: + """Get the current minimum distance parameter. Returns: - list[int]: Element indices with non-contiguous edges + float: Minimum distance value. """ - return self.non_contiguous_edges_elements + return self.minDistance - def getNonConvexElements( self: Self ) -> list[ int ]: - """Get non-convex elements. + def setMinDistance( self: Self, distance: float ) -> None: + """Set the minimum distance parameter for intersection detection. - Returns: - list[int]: Non-convex element indices + Args: + distance (float): Minimum distance value. """ - return self.non_convex_elements + self.minDistance = distance - def getFacesOrientedIncorrectlyElements( self: Self ) -> list[ int ]: - """Get elements with incorrectly oriented faces. + def setWriteInvalidElements( self: Self, write: bool ) -> None: + """Set whether to create arrays marking invalid elements in output data. - Returns: - list[int]: Element indices with incorrectly oriented faces + Args: + write (bool): True to enable marking, False to disable. """ - return self.faces_oriented_incorrectly_elements + self.writeInvalidElements = write - def getAllInvalidElements( self: Self ) -> dict[ str, list[ int ] ]: - """Get all invalid elements organized by type. - - Returns: - dict[str, list[int]]: Dictionary mapping invalid element types to their IDs - """ - return { - "wrong_number_of_points_elements": self.wrong_number_of_points_elements, - "intersecting_edges_elements": self.intersecting_edges_elements, - "intersecting_faces_elements": self.intersecting_faces_elements, - "non_contiguous_edges_elements": self.non_contiguous_edges_elements, - "non_convex_elements": self.non_convex_elements, - "faces_oriented_incorrectly_elements": self.faces_oriented_incorrectly_elements, - } - - -# Main function for backward compatibility and standalone use -def self_intersecting_elements( + def _addInvalidElementsArrays( self: Self ) -> None: + """Add arrays marking different types of invalid elements.""" + numCells: int = self.mesh.GetNumberOfCells() + if self.writeInvalidElements: + for invalidTypeName, invalidTypeData in self.invalidCellIds.items(): + if invalidTypeData: + array = np.zeros( numCells, dtype=np.int32 ) + array[ invalidTypeData ] = 1 + vtkArray: vtkDataArray = numpy_to_vtk( array ) + vtkArray.SetName( f"Is{invalidTypeName}" ) + self.mesh.GetCellData().AddArray( vtkArray ) + + +# Main function for standalone use +def selfIntersectingElements( mesh: vtkUnstructuredGrid, - min_distance: float = 0.0, - paint_invalid_elements: bool = False, - write_output: bool = False, - output_path: str = "output/mesh_with_invalid_elements.vtu", + outputPath: str, + minDistance: float = 0.0, + writeInvalidElements: bool = False ) -> tuple[ vtkUnstructuredGrid, dict[ str, list[ int ] ] ]: """Apply self-intersecting elements detection to a mesh. Args: - mesh (vtkUnstructuredGrid): The input mesh - min_distance (float): Minimum distance parameter for intersection detection. Defaults to 0.0. - paint_invalid_elements (bool): Whether to mark invalid elements. Defaults to False. - write_output (bool): Whether to write output mesh to file. Defaults to False. - output_path (str): Output file path if write_output is True. + mesh (vtkUnstructuredGrid): The input mesh to analyze. + outputPath (str): Output file path if write_output is True. + minDistance (float): Minimum distance parameter for intersection detection. Defaults to 0.0. + writeInvalidElements (bool): Whether to mark invalid elements. Defaults to False. Returns: tuple[vtkUnstructuredGrid, dict[str, list[int]]]: Processed mesh, dictionary of invalid element types and their IDs """ - filter_instance = SelfIntersectingElements( mesh, min_distance, paint_invalid_elements ) + filter_instance = SelfIntersectingElements( mesh, minDistance, writeInvalidElements ) success = filter_instance.applyFilter() if not success: raise RuntimeError( "Self-intersecting elements detection failed" ) - if write_output: - filter_instance.writeGrid( output_path ) + if writeInvalidElements: + filter_instance.writeGrid( outputPath ) return ( filter_instance.getMesh(), - filter_instance.getAllInvalidElements(), + filter_instance.getInvalidCellIds(), ) - - -# Alias for backward compatibility -def processSelfIntersectingElements( - mesh: vtkUnstructuredGrid, - min_distance: float = 0.0, -) -> tuple[ vtkUnstructuredGrid, dict[ str, list[ int ] ] ]: - """Legacy function name for backward compatibility.""" - return self_intersecting_elements( mesh, min_distance ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py index 4a3e26ea8..ac0833596 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py @@ -1,229 +1,243 @@ -# TODO Find an implementation to keep multiprocessing while using vtkFilter - -# import numpy as np -# import numpy.typing as npt -# from typing_extensions import Self -# from vtkmodules.util.numpy_support import numpy_to_vtk -# from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase -# from vtkmodules.vtkCommonCore import vtkInformation, vtkInformationVector, vtkDataArray, VTK_INT -# from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid -# from geos.mesh.doctor.actions.supported_elements import ( Options, find_unsupported_std_elements_types, -# find_unsupported_polyhedron_elements ) -# from geos.mesh.io.vtkIO import VtkOutput, write_mesh -# from geos.utils.Logger import Logger, getLogger - -# __doc__ = """ -# SupportedElements module is a vtk filter that identifies unsupported element types and problematic polyhedron -# elements in a vtkUnstructuredGrid. It checks for element types that are not supported by GEOS and validates -# polyhedron elements for geometric correctness. -# -# One filter input is vtkUnstructuredGrid, one filter output which is vtkUnstructuredGrid. -# -# To use the filter: -# -# .. code-block:: python -# -# from filters.SupportedElements import SupportedElements -# -# # instantiate the filter -# supportedElementsFilter: SupportedElements = SupportedElements() -# -# # optionally enable painting of unsupported element types -# supportedElementsFilter.setPaintUnsupportedElementTypes(1) # 1 to enable, 0 to disable -# -# # set input mesh -# supportedElementsFilter.SetInputData(mesh) -# -# # execute the filter -# output_mesh: vtkUnstructuredGrid = supportedElementsFilter.getGrid() -# -# # get unsupported elements -# unsupported_elements = supportedElementsFilter.getUnsupportedElements() -# -# # write the output mesh -# supportedElementsFilter.writeGrid("output/mesh_with_support_info.vtu") -# -# Note: This filter is currently disabled due to multiprocessing requirements. -# """ - -# class SupportedElements( VTKPythonAlgorithmBase ): - -# def __init__( self: Self ) -> None: -# """Vtk filter to ... a vtkUnstructuredGrid. - -# Output mesh is vtkUnstructuredGrid. -# """ -# super().__init__( nInputPorts=1, -# nOutputPorts=1, -# inputType='vtkUnstructuredGrid', -# outputType='vtkUnstructuredGrid' ) -# self.m_paintUnsupportedElementTypes: int = 0 -# # TODO Needs parallelism to work -# # self.m_paintUnsupportedPolyhedrons: int = 0 -# # self.m_chunk_size: int = 1 -# # self.m_num_proc: int = 1 -# self.m_logger: Logger = getLogger( "Element Volumes Filter" ) - -# def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: -# """Inherited from VTKPythonAlgorithmBase::RequestInformation. - -# Args: -# port (int): input port -# info (vtkInformationVector): info - -# Returns: -# int: 1 if calculation successfully ended, 0 otherwise. -# """ -# if port == 0: -# info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkUnstructuredGrid" ) -# return 1 - -# def RequestInformation( -# self: Self, -# request: vtkInformation, # noqa: F841 -# inInfoVec: list[ vtkInformationVector ], # noqa: F841 -# outInfoVec: vtkInformationVector, -# ) -> int: -# """Inherited from VTKPythonAlgorithmBase::RequestInformation. - -# Args: -# request (vtkInformation): request -# inInfoVec (list[vtkInformationVector]): input objects -# outInfoVec (vtkInformationVector): output objects - -# Returns: -# int: 1 if calculation successfully ended, 0 otherwise. -# """ -# executive = self.GetExecutive() # noqa: F841 -# outInfo = outInfoVec.GetInformationObject( 0 ) # noqa: F841 -# return 1 - -# def RequestData( -# self: Self, -# request: vtkInformation, -# inInfoVec: list[ vtkInformationVector ], -# outInfo: vtkInformationVector -# ) -> int: -# """Inherited from VTKPythonAlgorithmBase::RequestData. - -# Args: -# request (vtkInformation): request -# inInfoVec (list[vtkInformationVector]): input objects -# outInfoVec (vtkInformationVector): output objects - -# Returns: -# int: 1 if calculation successfully ended, 0 otherwise. -# """ -# input_mesh: vtkUnstructuredGrid = vtkUnstructuredGrid.GetData( inInfoVec[ 0 ] ) -# output = vtkUnstructuredGrid.GetData( outInfo ) - -# output_mesh: vtkUnstructuredGrid = input_mesh.NewInstance() -# output_mesh.CopyStructure( input_mesh ) -# output_mesh.CopyAttributes( input_mesh ) - -# unsupported_std_elt_types: set[ int ] = find_unsupported_std_elements_types( input_mesh ) -# if len( unsupported_std_elt_types ) > 0: -# self.m_logger.info( "The following vtk element types in your mesh are not supported by GEOS:" ) -# self.m_logger.info( unsupported_std_elt_types ) - -# if self.m_paintUnsupportedElementTypes: -# nbr_cells: int = output_mesh.GetNumberOfCells() -# arrayCellTypes: npt.NDArray = np.zeros( nbr_cells, dtype=int ) -# for i in range( nbr_cells ): -# arrayCellTypes[ i ] = output_mesh.GetCellType(i) - -# arrayUET: npt.NDArray = np.zeros( nbr_cells, dtype=int ) -# arrayUET[ np.isin( arrayCellTypes, list( unsupported_std_elt_types ) ) ] = 1 -# vtkArrayWSP: vtkDataArray = numpy_to_vtk( arrayUET ) -# vtkArrayWSP.SetName( "HasUnsupportedType" ) -# output_mesh.GetCellData().AddArray( vtkArrayWSP ) - -# # TODO Needs parallelism to work -# # options = Options( self.m_num_proc, self.m_chunk_size ) -# # unsupported_polyhedron_elts: list[ int ] = find_unsupported_polyhedron_elements( input_mesh, options ) -# # if len( unsupported_polyhedron_elts ) > 0: -# # self.m_logger.info( "These vtk polyhedron cell indexes in your mesh are not supported by GEOS:" ) -# # self.m_logger.info( unsupported_polyhedron_elts ) - -# # if self.m_paintUnsupportedPolyhedrons: -# # arrayUP: npt.NDArray = np.zeros( output_mesh.GetNumberOfCells(), dtype=int ) -# # arrayUP[ unsupported_polyhedron_elts ] = 1 -# # self.m_logger.info( f"arrayUP: {arrayUP}" ) -# # vtkArrayWSP: vtkDataArray = numpy_to_vtk( arrayUP ) -# # vtkArrayWSP.SetName( "IsUnsupportedPolyhedron" ) -# # output_mesh.GetCellData().AddArray( vtkArrayWSP ) - -# output.ShallowCopy( output_mesh ) - -# return 1 - -# def SetLogger( self: Self, logger: Logger ) -> None: -# """Set the logger. - -# Args: -# logger (Logger): logger -# """ -# self.m_logger = logger -# self.Modified() - -# def getGrid( self: Self ) -> vtkUnstructuredGrid: -# """Returns the vtkUnstructuredGrid with volumes. - -# Args: -# self (Self) - -# Returns: -# vtkUnstructuredGrid -# """ -# self.Update() # triggers RequestData -# return self.GetOutputDataObject( 0 ) - -# def setPaintUnsupportedElementTypes( self: Self, choice: int ) -> None: -# """Set 0 or 1 to choose if you want to create a new "HasUnsupportedType" array in your output data. - -# Args: -# self (Self) -# choice (int): 0 or 1 -# """ -# if choice not in [ 0, 1 ]: -# self.m_logger.error( f"setPaintUnsupportedElementTypes: Please choose either 0 or 1 not '{choice}'." ) -# else: -# self.m_paintUnsupportedElementTypes = choice -# self.Modified() - -# # TODO Needs parallelism to work -# # def setPaintUnsupportedPolyhedrons( self: Self, choice: int ) -> None: -# # """Set 0 or 1 to choose if you want to create a new "IsUnsupportedPolyhedron" array in your output data. - -# # Args: -# # self (Self) -# # choice (int): 0 or 1 -# # """ -# # if choice not in [ 0, 1 ]: -# # self.m_logger.error( f"setPaintUnsupportedPolyhedrons: Please choose either 0 or 1 not '{choice}'." ) -# # else: -# # self.m_paintUnsupportedPolyhedrons = choice -# # self.Modified() - -# # def setChunkSize( self: Self, new_chunk_size: int ) -> None: -# # self.m_chunk_size = new_chunk_size -# # self.Modified() - -# # def setNumProc( self: Self, new_num_proc: int ) -> None: -# # self.m_num_proc = new_num_proc -# # self.Modified() - -# def writeGrid( self: Self, filepath: str, is_data_mode_binary: bool = True, canOverwrite: bool = False ) -> None: -# """Writes a .vtu file of the vtkUnstructuredGrid at the specified filepath with volumes. - -# Args: -# filepath (str): /path/to/your/file.vtu -# is_data_mode_binary (bool, optional): Writes the file in binary format or ascii. Defaults to True. -# canOverwrite (bool, optional): Allows or not to overwrite if the filepath already leads to an existing -# file. Defaults to False. -# """ -# mesh: vtkUnstructuredGrid = self.getGrid() -# if mesh: -# write_mesh( filepath, VtkOutput( filepath, is_data_mode_binary ), canOverwrite ) -# else: -# self.m_logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) +import numpy as np +from typing_extensions import Self +from vtkmodules.util.numpy_support import numpy_to_vtk +from vtkmodules.vtkCommonCore import vtkDataArray +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from geos.mesh.doctor.actions.supported_elements import ( Options, find_unsupported_std_elements_types, + find_unsupported_polyhedron_elements ) +from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorFilterBase +from geos.mesh.doctor.parsing.supported_elements_parsing import logger_results + +__doc__ = """ +SupportedElements module identifies unsupported element types and problematic polyhedron +elements in a vtkUnstructuredGrid. It checks for element types that are not supported by GEOS and validates +polyhedron elements for geometric correctness. + +To use the filter +----------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.SupportedElements import SupportedElements + + # instantiate the filter + supportedElementsFilter = SupportedElements(mesh, writeUnsupportedElementTypes=True, + writeUnsupportedPolyhedrons=True, + numProc=4, chunkSize=1000) + + # execute the filter + success = supportedElementsFilter.applyFilter() + + # get unsupported element types + unsupportedTypes = supportedElementsFilter.getUnsupportedElementTypes() + + # get unsupported polyhedron elements + unsupportedPolyhedrons = supportedElementsFilter.getUnsupportedPolyhedronElements() + + # get the processed mesh with support information + outputMesh = supportedElementsFilter.getMesh() + + # write the output mesh + supportedElementsFilter.writeGrid("output/mesh_with_support_info.vtu") + +For standalone use without creating a filter instance +----------------------------------------------------- + +.. code-block:: python + + from geos.mesh.doctor.filters.SupportedElements import supportedElements + + # apply filter directly + outputMesh, unsupportedTypes, unsupportedPolyhedrons = supportedElements( + mesh, + outputPath="output/mesh_with_support_info.vtu", + numProc=4, + chunkSize=1000, + writeUnsupportedElementTypes=True, + writeUnsupportedPolyhedrons=True + ) +""" + +loggerTitle: str = "Supported Elements Filter" + + +class SupportedElements( MeshDoctorFilterBase ): + + def __init__( + self: Self, + mesh: vtkUnstructuredGrid, + numProc: int = 1, + chunkSize: int = 1, + writeUnsupportedElementTypes: bool = False, + writeUnsupportedPolyhedrons: bool = False, + useExternalLogger: bool = False, + ) -> None: + """Initialize the supported elements filter. + + Args: + mesh (vtkUnstructuredGrid): The input mesh to analyze. + writeUnsupportedElementTypes (bool): Whether to add new CellData array marking unsupported element types. + Defaults to False. + writeUnsupportedPolyhedrons (bool): Whether to add new CellData array marking unsupported polyhedrons. + Defaults to False. + numProc (int): Number of processes for multiprocessing. Defaults to 1. + chunkSize (int): Chunk size for multiprocessing. Defaults to 1. + useExternalLogger (bool): Whether to use external logger. Defaults to False. + """ + super().__init__( mesh, loggerTitle, useExternalLogger ) + self.numProc: int = numProc + self.chunkSize: int = chunkSize + self.unsupportedElementTypes: list[ str ] = [] + self.unsupportedPolyhedronElements: list[ int ] = [] + self.writeUnsupportedElementTypes: bool = writeUnsupportedElementTypes + self.writeUnsupportedPolyhedrons: bool = writeUnsupportedPolyhedrons + + def applyFilter( self: Self ) -> bool: + """Apply the supported elements analysis. + + Returns: + bool: True if analysis completed successfully, False otherwise. + """ + self.logger.info( f"Apply filter {self.logger.name}." ) + + # Find unsupported standard element types + self.unsupportedElementTypes = find_unsupported_std_elements_types( self.mesh ) + + if len( self.unsupportedElementTypes ) > 0: + if self.writeUnsupportedElementTypes: + self._addUnsupportedElementTypesArray() + + # Find unsupported polyhedron elements + options = Options( self.numProc, self.chunkSize ) + self.unsupportedPolyhedronElements = find_unsupported_polyhedron_elements( self.mesh, options ) + + if len( self.unsupportedPolyhedronElements ) > 0: + if self.writeUnsupportedPolyhedrons: + self._addUnsupportedPolyhedronsArray() + + logger_results( self.logger, self.unsupportedPolyhedronElements, self.unsupportedElementTypes ) + + self.logger.info( f"The filter {self.logger.name} succeeded." ) + return True + + def getUnsupportedElementTypes( self: Self ) -> list[ str ]: + """Get the list of unsupported element types. + + Returns: + list[ str ]: List of unsupported element type descriptions. + """ + return self.unsupportedElementTypes + + def getUnsupportedPolyhedronElements( self: Self ) -> list[ int ]: + """Get the list of unsupported polyhedron element indices. + + Returns: + list[ int ]: List of element indices for unsupported polyhedrons. + """ + return self.unsupportedPolyhedronElements + + def setWriteUnsupportedElementTypes( self: Self, write: bool ) -> None: + """Set whether to write unsupported element types. + + Args: + write (bool): True to enable writing, False to disable. + """ + self.writeUnsupportedElementTypes = write + + def setWriteUnsupportedPolyhedrons( self: Self, write: bool ) -> None: + """Set whether to write unsupported polyhedrons. + + Args: + write (bool): True to enable writing, False to disable. + """ + self.writeUnsupportedPolyhedrons = write + + def setNumProc( self: Self, numProc: int ) -> None: + """Set the number of processes for multiprocessing. + + Args: + numProc (int): Number of processes. + """ + self.numProc = numProc + + def setChunkSize( self: Self, chunkSize: int ) -> None: + """Set the chunk size for multiprocessing. + + Args: + chunkSize (int): Chunk size. + """ + self.chunkSize = chunkSize + + def _addUnsupportedElementTypesArray( self: Self ) -> None: + """Add an array marking elements with unsupported types on the mesh.""" + self.logger.info( "Adding CellData array marking elements with unsupported types." ) + + numCells: int = self.mesh.GetNumberOfCells() + unsupportedTypesArray = np.zeros( numCells, dtype=np.int32 ) + + # Get unsupported type IDs from the string descriptions + unsupportedTypeIds = set() + for description in self.unsupportedElementTypes: + # Extract type ID from description like "Type 42: vtkSomeElementType" + if description.startswith( "Type " ): + typeId = int( description.split( ":" )[ 0 ].replace( "Type ", "" ) ) + unsupportedTypeIds.add( typeId ) + + # Mark cells with unsupported types + for i in range( numCells ): + cellType: int = self.mesh.GetCellType( i ) + if cellType in unsupportedTypeIds: + unsupportedTypesArray[ i ] = 1 + + vtkArray: vtkDataArray = numpy_to_vtk( unsupportedTypesArray ) + vtkArray.SetName( "HasUnsupportedType" ) + self.mesh.GetCellData().AddArray( vtkArray ) + + def _addUnsupportedPolyhedronsArray( self: Self ) -> None: + """Add an array marking unsupported polyhedron elements on the mesh.""" + self.logger.info( "Adding CellData array marking unsupported polyhedron elements." ) + + numCells: int = self.mesh.GetNumberOfCells() + unsupportedPolyhedronsArray = np.zeros( numCells, dtype=np.int32 ) + unsupportedPolyhedronsArray[ self.unsupportedPolyhedronElements ] = 1 + + vtkArray: vtkDataArray = numpy_to_vtk( unsupportedPolyhedronsArray ) + vtkArray.SetName( "IsUnsupportedPolyhedron" ) + self.mesh.GetCellData().AddArray( vtkArray ) + + +# Main function for standalone use +def supportedElements( + mesh: vtkUnstructuredGrid, + outputPath: str, + numProc: int = 1, + chunkSize: int = 1, + writeUnsupportedElementTypes: bool = False, + writeUnsupportedPolyhedrons: bool = False, +) -> tuple[ vtkUnstructuredGrid, list[ str ], list[ int ] ]: + """Apply supported elements analysis to a mesh. + + Args: + mesh (vtkUnstructuredGrid): The input mesh to analyze. + outputPath (str): Output file path for writing the mesh. + numProc (int): Number of processes for multiprocessing. Defaults to 1. + chunkSize (int): Chunk size for multiprocessing. Defaults to 1. + writeUnsupportedElementTypes (bool): Whether to write unsupported element types. Defaults to False. + writeUnsupportedPolyhedrons (bool): Whether to write unsupported polyhedrons. Defaults to False. + + Returns: + tuple[vtkUnstructuredGrid, list[ str ], list[ int ]]: + Processed mesh, list of unsupported element types, list of unsupported polyhedron indices. + """ + filterInstance = SupportedElements( mesh, writeUnsupportedElementTypes, writeUnsupportedPolyhedrons, numProc, + chunkSize ) + filterInstance.applyFilter() + + if writeUnsupportedElementTypes or writeUnsupportedPolyhedrons: + filterInstance.writeGrid( outputPath ) + + return ( + filterInstance.getMesh(), + filterInstance.getUnsupportedElementTypes(), + filterInstance.getUnsupportedPolyhedronElements(), + ) 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 ac93feb85..a06ee0f75 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 @@ -25,24 +25,40 @@ def fill_subparser( subparsers ) -> None: def display_results( options: Options, result: Result ): setup_logger.results( get_options_used_message( options ) ) + logger_results( setup_logger, result.nodes_buckets, result.wrong_support_elements ) + + +def logger_results( logger, nodes_buckets: list[ tuple[ int ] ], wrong_support_elements: list[ int ] ): + """Log the results of the collocated nodes check. + + Args: + logger: Logger instance for output. + nodes_buckets (list[ tuple[ int ] ]): List of collocated nodes buckets. + wrong_support_elements (list[ int ]): List of elements with wrong support nodes. + """ + # Accounts for external logging object that would not contain 'results' attribute + log_method = logger.info + if hasattr(logger, 'results'): + log_method = logger.results + all_collocated_nodes: list[ int ] = [] - for bucket in result.nodes_buckets: + for bucket in 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 = list( set( all_collocated_nodes ) ) if all_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." ) + log_method( f"You have {len( all_collocated_nodes )} collocated nodes." ) + log_method( "Here are all the buckets of collocated nodes." ) tmp: list[ str ] = [] - for bucket in result.nodes_buckets: + for bucket in nodes_buckets: tmp.append( f"({', '.join(map(str, bucket))})" ) - setup_logger.results( f"({', '.join(tmp)})" ) + log_method( f"({', '.join(tmp)})" ) else: - setup_logger.results( "You have no collocated node." ) + log_method( "You have no collocated node." ) - if result.wrong_support_elements: - tmp: str = ", ".join( map( str, result.wrong_support_elements ) ) - setup_logger.results( - f"You have {len(result.wrong_support_elements)} elements with duplicated support nodes.\n" + tmp ) + if wrong_support_elements: + tmp: str = ", ".join( map( str, wrong_support_elements ) ) + log_method( + f"You have {len(wrong_support_elements)} elements with duplicated support nodes.\n" + tmp ) else: - setup_logger.results( "You have no element with duplicated support nodes." ) + log_method( "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 233157851..db8bad93d 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 @@ -31,11 +31,23 @@ 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( "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 ) ) + logger_results( setup_logger, result.element_volumes ) + + +def logger_results( logger, element_volumes: list[ tuple[ int, float ] ] ) -> None: + """Show the results of the element volumes check. + + Args: + logger: Logger instance for output. + element_volumes (list[ tuple[ int, float ] ]): List of element volumes. + """ + # Accounts for external logging object that would not contain 'results' attribute + log_method = logger.info + if hasattr(logger, 'results'): + log_method = logger.results + + log_method( "Elements index | Volumes calculated" ) + log_method( "-----------------------------------" ) + max_length: int = len( "Elements index " ) + for ( ind, volume ) in element_volumes: + log_method( 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 801e04f4b..89d567eda 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 @@ -47,9 +47,24 @@ 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." ) - setup_logger.results( f"{', '.join( map( str, sorted( non_conformal_cells ) ) )}" ) + logger_results( setup_logger, result.non_conformal_cells ) + + +def logger_results( logger, non_conformal_cells: list[ tuple[ int, int ] ] ) -> None: + """Log the results of the non-conformal cells check. + + Args: + logger: Logger instance for output. + non_conformal_cells (list[ tuple[ int, int ] ]): List of non-conformal cells. + """ + # Accounts for external logging object that would not contain 'results' attribute + log_method = logger.info + if hasattr(logger, 'results'): + log_method = logger.results + + unique_cells: list[ int ] = [] + for i, j in non_conformal_cells: + unique_cells += i, j + unique_cells: frozenset[ int ] = frozenset( unique_cells ) + log_method( f"You have {len( unique_cells )} non conformal cells." ) + log_method( f"{', '.join( map( str, sorted( unique_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 430a25325..6e838c2cc 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 @@ -13,9 +13,8 @@ def convert( parsed_options ) -> Options: min_distance = parsed_options[ __MIN_DISTANCE ] if min_distance == 0: - setup_logger.warning( - "Having minimum distance set to 0 can induce lots of false positive results (adjacent faces may be considered intersecting)." - ) + 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: raise ValueError( f"Negative minimum distance ({min_distance}) in the {SELF_INTERSECTING_ELEMENTS} check is not allowed." ) @@ -31,14 +30,43 @@ 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=( "[float]: The minimum distance in the computation." + f" Defaults to your machine precision {__MIN_DISTANCE_DEFAULT}." ) ) 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" + - ", ".join( map( str, result.intersecting_faces_elements ) ) ) + logger_results( setup_logger, result.invalid_cell_ids ) + + +def logger_results( logger, invalid_cell_ids ) -> None: + """Log the results of the self-intersecting elements check. + + Args: + logger: Logger instance for output. + invalid_cell_ids: Dictionary of invalid cell IDs by error type. + """ + # Accounts for external logging object that would not contain 'results' attribute + log_method = logger.info + if hasattr(logger, 'results'): + log_method = logger.results + + # Human-readable descriptions for each error type + error_descriptions = { + 'wrongNumberOfPointsElements': 'elements with wrong number of points', + 'intersectingEdgesElements': 'elements with intersecting edges', + 'intersectingFacesElements': 'elements with self intersecting faces', + 'nonContiguousEdgesElements': 'elements with non-contiguous edges', + 'nonConvexElements': 'non-convex elements', + 'facesOrientedIncorrectlyElements': 'elements with incorrectly oriented faces', + 'nonPlanarFacesElements': 'elements with non-planar faces', + 'degenerateFacesElements': 'elements with degenerate faces' + } + + # Log results for each error type that has invalid elements + for error_type, invalid_ids in invalid_cell_ids.items(): + if invalid_ids: + description = error_descriptions.get(error_type, f'elements with {error_type}') + log_method(f"You have {len(invalid_ids)} {description}.") + log_method("The elements indices are:\n" + ", ".join(map(str, invalid_ids))) 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 f9f8dd84a..b24756b1a 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 @@ -19,7 +19,7 @@ def convert( parsed_options ) -> Options: def fill_subparser( subparsers ) -> None: p = subparsers.add_parser( SUPPORTED_ELEMENTS, - help="Check that all the elements of the mesh are supported by GEOSX." ) + help="Check that all the elements of the mesh are supported by GEOS." ) p.add_argument( '--' + __CHUNK_SIZE, type=int, required=False, @@ -38,17 +38,33 @@ 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." - ) - setup_logger.results( - f"The list of the unsupported polyhedra is\n{tuple(sorted(result.unsupported_polyhedron_elements))}." ) + logger_results( setup_logger, result.unsupported_polyhedron_elements, result.unsupported_std_elements_types ) + + +def logger_results( logger, + unsupported_polyhedron_elements: frozenset[ int ], + unsupported_std_elements_types: list[ str ] ) -> None: + """Log the results of the supported elements check. + + Args: + logger: Logger instance for output. + unsupported_polyhedron_elements (frozenset[ int ]): List of unsupported polyhedron elements. + unsupported_std_elements_types (list[ str ]): List of unsupported standard element types. + """ + # Accounts for external logging object that would not contain 'results' attribute + log_method = logger.info + if hasattr(logger, 'results'): + log_method = logger.results + + if unsupported_polyhedron_elements: + log_method( f"There is/are {len(unsupported_polyhedron_elements)} polyhedra that may not be converted to" + " supported elements." ) + log_method( "The list of the unsupported polyhedra is\n" + f"{tuple(sorted(unsupported_polyhedron_elements))}." ) else: - setup_logger.results( "All the polyhedra (if any) can be converted to supported elements." ) - if result.unsupported_std_elements_types: - 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))}." - ) + log_method( "All the polyhedra (if any) can be converted to supported elements." ) + if unsupported_std_elements_types: + log_method( "There are unsupported vtk standard element types. The list of those vtk types is" + f" {tuple(sorted(unsupported_std_elements_types))}.") else: - setup_logger.results( "All the standard vtk element types (if any) are supported." ) + log_method( "All the standard vtk element types (if any) are supported." ) From dd9cecb30fba771cb1be95e9d375e1c129ce45f5 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Fri, 29 Aug 2025 16:43:35 -0700 Subject: [PATCH 33/52] Update documentation --- docs/geos_mesh_docs/doctor.rst | 22 +- docs/geos_mesh_docs/filters/AllChecks.rst | 88 ------- docs/geos_mesh_docs/filters/Checks.rst | 18 ++ .../filters/CollocatedNodes.rst | 89 +------ .../geos_mesh_docs/filters/ElementVolumes.rst | 103 +------- .../filters/GenerateFractures.rst | 232 ++++-------------- .../filters/GenerateRectilinearGrid.rst | 214 +--------------- docs/geos_mesh_docs/filters/MainChecks.rst | 98 -------- .../filters/MeshDoctorFilterBase.rst | 14 ++ docs/geos_mesh_docs/filters/NonConformal.rst | 195 +-------------- .../filters/SelfIntersectingElements.rst | 181 +------------- .../filters/SupportedElements.rst | 225 ++--------------- docs/geos_mesh_docs/filters/index.rst | 66 ++--- 13 files changed, 140 insertions(+), 1405 deletions(-) delete mode 100644 docs/geos_mesh_docs/filters/AllChecks.rst create mode 100644 docs/geos_mesh_docs/filters/Checks.rst delete mode 100644 docs/geos_mesh_docs/filters/MainChecks.rst create mode 100644 docs/geos_mesh_docs/filters/MeshDoctorFilterBase.rst diff --git a/docs/geos_mesh_docs/doctor.rst b/docs/geos_mesh_docs/doctor.rst index 6e07353a8..48fcc3363 100644 --- a/docs/geos_mesh_docs/doctor.rst +++ b/docs/geos_mesh_docs/doctor.rst @@ -320,7 +320,7 @@ It will also verify that the ``VTK_POLYHEDRON`` cells can effectively get conver Why only use vtkUnstructuredGrid? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The mesh doctor is designed specifically for unstructured meshes used in GEOS. +| The mesh doctor is designed specifically for unstructured meshes used in GEOS. | All input files are expected to be ``.vtu`` (VTK Unstructured Grid) format. | What about other formats? @@ -344,21 +344,21 @@ Supposedly, other grid types that are part of the following VTK hierarchy could And when looking at specific methods used in mesh-doctor, it could suggest that other formats could be used: -* Points access: mesh.GetPoints() - Available in all vtkPointSet subclasses ✓ -* Cell iteration: mesh.GetNumberOfCells(), mesh.GetCell() - Available in all vtkDataSet subclasses ✓ -* Cell types: mesh.GetCellType() - Available in all vtkDataSet subclasses ✓ -* Cell/Point data: mesh.GetCellData(), mesh.GetPointData() - Available in all vtkDataSet subclasses ✓ +* Points access: ``mesh.GetPoints()`` - Available in all vtkPointSet subclasses ✓ +* Cell iteration: ``mesh.GetNumberOfCells()``, ``mesh.GetCell()`` - Available in all vtkDataSet subclasses ✓ +* Cell types: ``mesh.GetCellType()`` - Available in all vtkDataSet subclasses ✓ +* Cell/Point data: ``mesh.GetCellData()``, ``mesh.GetPointData()`` - Available in all vtkDataSet subclasses ✓ VTK Filter Compatibility """""""""""""""""""""""" -| vtkCellSizeFilter, vtkMeshQuality, and other VTK filters used in the actions expect vtkDataSet or its subclasses - vtkUnstructuredGrid is compatible with all VTK filters used. -| vtkPolyData has a different data structure, not suitable for 3D volumetric meshes. +| ``vtkCellSizeFilter``, ``vtkMeshQuality``, and other VTK filters used in the actions expect ``vtkDataSet`` or its subclasses + ``vtkUnstructuredGrid`` is compatible with all VTK filters used. +| ``vtkPolyData`` has a different data structure, not suitable for 3D volumetric meshes. Specific Operations Require vtkUnstructuredGrid """"""""""""""""""""""""""""""""""""""""""""""" -* GetCellNeighbors() - Only available in vtkUnstructuredGrid -* GetFaceStream() - Only available in vtkUnstructuredGrid (for polyhedron support) -* GetDistinctCellTypesArray() - Only available in vtkUnstructuredGrid \ No newline at end of file +* ``GetCellNeighbors()`` - Only available in vtkUnstructuredGrid +* ``GetFaceStream()`` - Only available in vtkUnstructuredGrid (for polyhedron support) +* ``GetDistinctCellTypesArray()`` - Only available in vtkUnstructuredGrid \ No newline at end of file diff --git a/docs/geos_mesh_docs/filters/AllChecks.rst b/docs/geos_mesh_docs/filters/AllChecks.rst deleted file mode 100644 index d7dd6fd52..000000000 --- a/docs/geos_mesh_docs/filters/AllChecks.rst +++ /dev/null @@ -1,88 +0,0 @@ -AllChecks Filter -================ - -.. autoclass:: geos.mesh.doctor.filters.Checks.AllChecks - :members: - :undoc-members: - :show-inheritance: - -Overview --------- - -The AllChecks filter performs comprehensive mesh validation by running all available quality checks on a vtkUnstructuredGrid. This filter is part of the mesh doctor toolkit and provides detailed analysis of mesh quality, topology, and geometric integrity. - -Features --------- - -* Comprehensive mesh validation with all available quality checks -* Configurable check parameters for customized validation -* Detailed reporting of found issues -* Integration with mesh doctor parsing system -* Support for both individual check parameter customization and global parameter setting - -Usage Example -------------- - -.. code-block:: python - - from geos.mesh.doctor.filters.Checks import AllChecks - - # Instantiate the filter for all available checks - allChecksFilter = AllChecks() - - # Set input mesh - allChecksFilter.SetInputData(mesh) - - # Optionally customize check parameters - allChecksFilter.setCheckParameter("collocated_nodes", "tolerance", 1e-6) - allChecksFilter.setGlobalParameter("tolerance", 1e-6) # applies to all checks with tolerance parameter - - # Execute the checks and get output - output_mesh = allChecksFilter.getGrid() - - # Get check results - check_results = allChecksFilter.getCheckResults() - - # Write the output mesh - allChecksFilter.writeGrid("output/mesh_with_check_results.vtu") - -Parameters ----------- - -The AllChecks filter supports parameter customization for individual checks: - -* **setCheckParameter(check_name, parameter_name, value)**: Set specific parameter for a named check -* **setGlobalParameter(parameter_name, value)**: Apply parameter to all checks that support it - -Common parameters include: - -* **tolerance**: Distance tolerance for geometric checks (e.g., collocated nodes, non-conformal interfaces) -* **angle_tolerance**: Angular tolerance for orientation checks -* **min_volume**: Minimum acceptable element volume - -Available Checks ----------------- - -The AllChecks filter includes all checks available in the mesh doctor system: - -* Collocated nodes detection -* Element volume validation -* Self-intersecting elements detection -* Non-conformal interface detection -* Supported element type validation -* And many more quality checks - -Output ------- - -* **Input**: vtkUnstructuredGrid -* **Output**: vtkUnstructuredGrid (copy of input with potential additional arrays marking issues) -* **Check Results**: Detailed dictionary with results from all performed checks - -See Also --------- - -* :doc:`MainChecks ` - Subset of most important checks -* :doc:`CollocatedNodes ` - Individual collocated nodes check -* :doc:`ElementVolumes ` - Individual element volume check -* :doc:`SelfIntersectingElements ` - Individual self-intersection check diff --git a/docs/geos_mesh_docs/filters/Checks.rst b/docs/geos_mesh_docs/filters/Checks.rst new file mode 100644 index 000000000..7828c4b17 --- /dev/null +++ b/docs/geos_mesh_docs/filters/Checks.rst @@ -0,0 +1,18 @@ +AllChecks and MainChecks Filters +================================ + +.. automodule:: geos.mesh.doctor.filters.Checks + :members: + :undoc-members: + :show-inheritance: + +See Also +-------- + +* :doc:`CollocatedNodes ` - Individual collocated nodes check +* :doc:`ElementVolumes ` - Individual element volume check +* :doc:`NonConformal ` - Individual non-conformal interface check +* :doc:`SelfIntersectingElements ` - Individual self-intersection check +* :doc:`SupportedElements ` - Individual supported elements check + + diff --git a/docs/geos_mesh_docs/filters/CollocatedNodes.rst b/docs/geos_mesh_docs/filters/CollocatedNodes.rst index 96edae1a7..80adcbf58 100644 --- a/docs/geos_mesh_docs/filters/CollocatedNodes.rst +++ b/docs/geos_mesh_docs/filters/CollocatedNodes.rst @@ -6,77 +6,6 @@ CollocatedNodes Filter :undoc-members: :show-inheritance: -Overview --------- - -The CollocatedNodes filter identifies and handles duplicated/collocated nodes in a vtkUnstructuredGrid. Collocated nodes are nodes that are positioned very close to each other (within a specified tolerance), which can indicate mesh quality issues or modeling problems. - -Features --------- - -* Detection of collocated/duplicated nodes within specified tolerance -* Identification of elements with wrong support (nodes appearing multiple times) -* Optional marking of problematic elements in output mesh -* Configurable tolerance for distance-based node comparison -* Detailed reporting of found collocated node groups - -Usage Example -------------- - -.. code-block:: python - - from geos.mesh.doctor.filters.CollocatedNodes import CollocatedNodes - - # Instantiate the filter - collocatedNodesFilter = CollocatedNodes() - - # Set the tolerance for detecting collocated nodes - collocatedNodesFilter.setTolerance(1e-6) - - # Optionally enable painting of wrong support elements - collocatedNodesFilter.setPaintWrongSupportElements(1) # 1 to enable, 0 to disable - - # Set input mesh - collocatedNodesFilter.SetInputData(mesh) - - # Execute the filter and get output - output_mesh = collocatedNodesFilter.getGrid() - - # Get results - collocated_buckets = collocatedNodesFilter.getCollocatedNodeBuckets() # list of tuples with collocated node indices - wrong_support_elements = collocatedNodesFilter.getWrongSupportElements() # list of problematic element indices - - # Write the output mesh - collocatedNodesFilter.writeGrid("output/mesh_with_collocated_info.vtu") - -Parameters ----------- - -setTolerance(tolerance) - Set the distance tolerance for determining if two nodes are collocated. - - * **tolerance** (float): Distance threshold below which nodes are considered collocated - * **Default**: 0.0 - -setPaintWrongSupportElements(choice) - Enable/disable creation of array marking elements with wrong support nodes. - - * **choice** (int): 1 to enable marking, 0 to disable - * **Default**: 0 - -Results Access --------------- - -getCollocatedNodeBuckets() - Returns groups of collocated node indices. - - * **Returns**: list[tuple[int]] - Each tuple contains indices of nodes that are collocated - -getWrongSupportElements() - Returns element indices that have support nodes appearing more than once. - - * **Returns**: list[int] - Element indices with problematic support nodes - Understanding the Results ------------------------- @@ -94,7 +23,7 @@ Each bucket is a tuple containing node indices that are within the specified tol **Wrong Support Elements** -Elements where the same node appears multiple times in the element's connectivity. This usually indicates: +Cell element IDs where the same node appears multiple times in the element's connectivity. This usually indicates: * Degenerate elements * Mesh generation errors @@ -108,24 +37,14 @@ Common Use Cases * **Debugging**: Understand why meshes might have connectivity problems * **Validation**: Ensure mesh meets quality standards for specific applications -Output ------- +I/O +--- * **Input**: vtkUnstructuredGrid * **Output**: vtkUnstructuredGrid with optional arrays marking problematic elements * **Additional Data**: When painting is enabled, adds "WrongSupportElements" array to cell data -Best Practices --------------- - -* Set tolerance based on mesh scale and precision requirements -* Use smaller tolerances for high-precision meshes -* Enable painting to visualize problematic areas in ParaView -* Check both collocated buckets and wrong support elements for comprehensive analysis - See Also -------- -* :doc:`AllChecks ` - Includes collocated nodes check among others -* :doc:`MainChecks ` - Includes collocated nodes check in main set -* :doc:`SelfIntersectingElements ` - Related geometric validation +* :doc:`AllChecks and MainChecks ` - Includes collocated nodes check among others diff --git a/docs/geos_mesh_docs/filters/ElementVolumes.rst b/docs/geos_mesh_docs/filters/ElementVolumes.rst index 2a9910a65..d7afd45f5 100644 --- a/docs/geos_mesh_docs/filters/ElementVolumes.rst +++ b/docs/geos_mesh_docs/filters/ElementVolumes.rst @@ -6,69 +6,6 @@ ElementVolumes Filter :undoc-members: :show-inheritance: -Overview --------- - -The ElementVolumes filter calculates the volumes of all elements in a vtkUnstructuredGrid and can identify elements with negative or zero volumes. Such elements typically indicate serious mesh quality issues including inverted elements, degenerate cells, or incorrect node ordering. - -Features --------- - -* Volume calculation for all element types -* Detection of negative and zero volume elements -* Detailed reporting of problematic elements with their volumes -* Integration with VTK's cell size computation -* Optional filtering to return only problematic elements - -Usage Example -------------- - -.. code-block:: python - - from geos.mesh.doctor.filters.ElementVolumes import ElementVolumes - - # Instantiate the filter - elementVolumesFilter = ElementVolumes() - - # Optionally enable detection of negative/zero volume elements - elementVolumesFilter.setReturnNegativeZeroVolumes(True) - - # Set input mesh - elementVolumesFilter.SetInputData(mesh) - - # Execute the filter and get output - output_mesh = elementVolumesFilter.getGrid() - - # Get problematic elements (if enabled) - if elementVolumesFilter.m_returnNegativeZeroVolumes: - negative_zero_volumes = elementVolumesFilter.getNegativeZeroVolumes() - # Returns numpy array with shape (n, 2) where first column is element index, second is volume - - # Write the output mesh with volume information - elementVolumesFilter.writeGrid("output/mesh_with_volumes.vtu") - -Parameters ----------- - -setReturnNegativeZeroVolumes(returnNegativeZeroVolumes) - Enable/disable specific detection and return of elements with negative or zero volumes. - - * **returnNegativeZeroVolumes** (bool): True to enable detection, False to disable - * **Default**: False - -Results Access --------------- - -getNegativeZeroVolumes() - Returns detailed information about elements with negative or zero volumes. - - * **Returns**: numpy.ndarray with shape (n, 2) - - * Column 0: Element indices with problematic volumes - * Column 1: Corresponding volume values (≤ 0) - - * **Note**: Only available when returnNegativeZeroVolumes is enabled - Understanding Volume Issues --------------------------- @@ -112,49 +49,15 @@ Common Fixes 2. **Merge collocated nodes**: Fix duplicate node issues 3. **Improve mesh quality**: Regenerate with better settings -Output ------- +I/O +--- * **Input**: vtkUnstructuredGrid * **Output**: vtkUnstructuredGrid with volume information added as cell data * **Volume Array**: "Volume" array added to cell data containing computed volumes * **Additional Data**: When returnNegativeZeroVolumes is enabled, provides detailed problematic element information -Integration with Other Filters ------------------------------- - -The ElementVolumes filter works well in combination with: - -* **CollocatedNodes**: Fix node duplication that can cause zero volumes -* **SelfIntersectingElements**: Detect other geometric problems -* **AllChecks/MainChecks**: Comprehensive mesh validation including volume checks - -Example Workflow ----------------- - -.. code-block:: python - - # Complete volume analysis workflow - volumeFilter = ElementVolumes() - volumeFilter.setReturnNegativeZeroVolumes(True) - volumeFilter.SetInputData(mesh) - - output_mesh = volumeFilter.getGrid() - - # Analyze results - problematic = volumeFilter.getNegativeZeroVolumes() - - if len(problematic) > 0: - print(f"Found {len(problematic)} elements with non-positive volumes:") - for idx, volume in problematic: - print(f" Element {idx}: volume = {volume}") - else: - print("All elements have positive volumes - mesh is good!") - See Also -------- -* :doc:`AllChecks ` - Includes element volume check among others -* :doc:`MainChecks ` - Includes element volume check in main set -* :doc:`CollocatedNodes ` - Can help fix zero volume issues -* :doc:`SelfIntersectingElements ` - Related geometric validation +* :doc:`AllChecks and MainChecks ` - Includes element volumes check among others diff --git a/docs/geos_mesh_docs/filters/GenerateFractures.rst b/docs/geos_mesh_docs/filters/GenerateFractures.rst index 78c6feab7..03ead101f 100644 --- a/docs/geos_mesh_docs/filters/GenerateFractures.rst +++ b/docs/geos_mesh_docs/filters/GenerateFractures.rst @@ -6,143 +6,55 @@ GenerateFractures Filter :undoc-members: :show-inheritance: -Overview --------- - -The GenerateFractures filter splits a vtkUnstructuredGrid along non-embedded fractures. When a fracture plane is defined between two cells, the nodes of the shared face are duplicated to create a discontinuity. The filter generates both the split main mesh and separate fracture meshes. - -Features --------- - -* Mesh splitting along fracture planes with node duplication -* Multiple fracture policy support (internal vs boundary fractures) -* Configurable fracture identification via field values -* Generation of separate fracture mesh outputs -* Flexible output data modes (ASCII/binary) -* Automatic fracture mesh file management - -Usage Example -------------- - -.. code-block:: python - - from geos.mesh.doctor.filters.GenerateFractures import GenerateFractures - - # Instantiate the filter - generateFracturesFilter = GenerateFractures() - - # Set the field name that defines fracture regions - generateFracturesFilter.setFieldName("fracture_field") - - # Set the field values that identify fracture boundaries - generateFracturesFilter.setFieldValues("1,2") # comma-separated values - - # Set fracture policy (0 for internal fractures, 1 for boundary fractures) - generateFracturesFilter.setPolicy(1) - - # Set output directory for fracture meshes - generateFracturesFilter.setFracturesOutputDirectory("./fractures/") - - # Optionally set data mode (0 for ASCII, 1 for binary) - generateFracturesFilter.setOutputDataMode(1) - generateFracturesFilter.setFracturesDataMode(1) - - # Set input mesh - generateFracturesFilter.SetInputData(mesh) - - # Execute the filter - generateFracturesFilter.Update() - - # Get the split mesh and fracture meshes - split_mesh, fracture_meshes = generateFracturesFilter.getAllGrids() - - # Write all meshes - generateFracturesFilter.writeMeshes("output/split_mesh.vtu", is_data_mode_binary=True) - -Parameters ----------- - -setFieldName(field_name) - Set the name of the cell data field that defines fracture regions. - - * **field_name** (str): Name of the field in cell data - -setFieldValues(field_values) - Set the field values that identify fracture boundaries. - - * **field_values** (str): Comma-separated list of values (e.g., "1,2,3") - -setPolicy(choice) - Set the fracture generation policy. - - * **choice** (int): - - * 0: Internal fractures policy - * 1: Boundary fractures policy - -setFracturesOutputDirectory(directory) - Set the output directory for individual fracture mesh files. - - * **directory** (str): Path to output directory - -setOutputDataMode(choice) - Set the data format for the main output mesh. - - * **choice** (int): - - * 0: ASCII format - * 1: Binary format - -setFracturesDataMode(choice) - Set the data format for fracture mesh files. - - * **choice** (int): - - * 0: ASCII format - * 1: Binary format - Fracture Policies ----------------- -**Internal Fractures Policy (0)** - -* Creates fractures within the mesh interior -* Duplicates nodes at internal interfaces -* Suitable for modeling embedded fracture networks - -**Boundary Fractures Policy (1)** - -* Creates fractures at mesh boundaries -* Handles fractures that extend to domain edges -* Suitable for modeling fault systems extending beyond the domain - -Results Access --------------- - -getAllGrids() - Returns both the split mesh and individual fracture meshes. - - * **Returns**: tuple (split_mesh, fracture_meshes) - - * **split_mesh**: vtkUnstructuredGrid - Main mesh with fractures applied - * **fracture_meshes**: list[vtkUnstructuredGrid] - Individual fracture surfaces - -writeMeshes(filepath, is_data_mode_binary, canOverwrite) - Write all generated meshes to files. - - * **filepath** (str): Path for main split mesh - * **is_data_mode_binary** (bool): Use binary format - * **canOverwrite** (bool): Allow overwriting existing files +**Field Fractures Policy (field)** + +| Obtained by setting ``--policy field``. +| +| Creates fractures by using internal interfaces between cells regions. +| These interfaces corresponds to a change in the values of a CellData array stored in your mesh. +| Suppose you have a CellData array named "Regions" with 3 different values indicating geological layers. 1 is caprock, 2 is reservoir, and 3 is underburden as represented here (below is a 2D representation along Z for simplicity): +| +| 111111111111111111 +| 111111222222222222 +| 222222222223333333 +| 333333333333333333 +| +| In this example, I define the interface between reservoir and caprock as a fracture, and the interface between reservoir and underburden as another fracture. +| So when specifying your field values as ``--values 1,2:2,3``, the filter will create a fracture along the interface between these two regions. The result will be a new mesh with the fracture represented as a separate entity. The nodes will be split along these interfaces, allowing for discontinuities in the mesh: +| +| 111111111111111111 +| 111111------------ +| ------222222222222 +| 22222222222+++++++ +| +++++++++++3333333 +| 333333333333333333 +| +| with ``----`` representing the first fracture and ``+++++++`` representing the second fracture. + +**Internal Fractures Policy (internal_surfaces)** + +| Obtained by setting ``--policy internal_surfaces``. +| +| Creates fractures by using tagged 2D cells that are identified as a fracture. +| In VTK, ``2D cells`` are refering to ``VTK_TRIANGLE`` or ``VTK_QUAD`` cell types. +| These 2D cells should be part of the input mesh and tagged with a specific CellData array value. +| Suppose you have a CellData array named "isFault" with 3 different values to tag fractures. 0 is for all non fault cells, 1 is fault1, and 2 is fault2 as represented here (below is a 2D representation along Z for simplicity): +| +| 000000000000000000 +| 000000111111111111 +| 111111000000000000 +| 000000000002222222 +| 222222222220000000 +| 000000000000000000 +| +| So when specifying your field values as ``--values 1:2``, the filter will create one fracture for each cells tagged with 1 and 2. Understanding Fracture Generation --------------------------------- -**Input Requirements** - -1. **Fracture Field**: Cell data array identifying regions separated by fractures -2. **Field Values**: Specific values indicating fracture boundaries -3. **Policy**: How to handle fracture creation - **Process** 1. **Identification**: Find interfaces between cells with different field values @@ -155,66 +67,12 @@ Understanding Fracture Generation * **Split Mesh**: Original mesh with fractures as discontinuities * **Fracture Meshes**: Individual surface meshes for each fracture -Common Use Cases ----------------- - -* **Geomechanics**: Modeling fault systems in geological domains -* **Fluid Flow**: Creating discrete fracture networks -* **Contact Mechanics**: Preparing meshes for contact simulations -* **Multi-physics**: Coupling different physics across fracture interfaces - -Example Workflow ----------------- - -.. code-block:: python - - # Complete fracture generation workflow - fracture_filter = GenerateFractures() - - # Configure fracture detection - fracture_filter.setFieldName("material_id") - fracture_filter.setFieldValues("1,2") # Fracture between materials 1 and 2 - fracture_filter.setPolicy(1) # Boundary fractures - - # Configure output - fracture_filter.setFracturesOutputDirectory("./fractures/") - fracture_filter.setOutputDataMode(1) # Binary for efficiency - fracture_filter.setFracturesDataMode(1) - - # Process mesh - fracture_filter.SetInputData(original_mesh) - fracture_filter.Update() - - # Get results - split_mesh, fracture_surfaces = fracture_filter.getAllGrids() - - print(f"Generated {len(fracture_surfaces)} fracture surfaces") - - # Write all outputs - fracture_filter.writeMeshes("output/domain_with_fractures.vtu") +I/O +--- -Output ------- - -* **Input**: vtkUnstructuredGrid with fracture identification field +* **Input**: vtkUnstructuredGrid with fracture identification field or tagged 2D cells * **Outputs**: * Split mesh with fractures as discontinuities * Individual fracture surface meshes * **File Output**: Automatic writing of fracture meshes to specified directory - -Best Practices --------------- - -* Ensure fracture field values are properly assigned to cells -* Use appropriate policy based on fracture geometry -* Check that fractures form continuous surfaces -* Verify mesh quality after fracture generation -* Use binary format for large meshes to improve I/O performance - -See Also --------- - -* :doc:`GenerateRectilinearGrid ` - Basic mesh generation -* :doc:`CollocatedNodes ` - May be needed after fracture generation -* :doc:`ElementVolumes ` - Quality check after splitting diff --git a/docs/geos_mesh_docs/filters/GenerateRectilinearGrid.rst b/docs/geos_mesh_docs/filters/GenerateRectilinearGrid.rst index a29540ee2..fba3cc930 100644 --- a/docs/geos_mesh_docs/filters/GenerateRectilinearGrid.rst +++ b/docs/geos_mesh_docs/filters/GenerateRectilinearGrid.rst @@ -6,213 +6,9 @@ GenerateRectilinearGrid Filter :undoc-members: :show-inheritance: -Overview --------- +I/O +--- -The GenerateRectilinearGrid filter creates simple rectilinear (structured) grids as vtkUnstructuredGrid objects. This filter is useful for generating regular meshes for testing, validation, or as starting points for more complex mesh generation workflows. - -Features --------- - -* Generation of 3D rectilinear grids with customizable dimensions -* Flexible block-based coordinate specification -* Variable element density per block -* Optional global ID generation for points and cells -* Customizable field generation with arbitrary dimensions -* Direct mesh generation without input requirements - -Usage Example -------------- - -.. code-block:: python - - from geos.mesh.doctor.filters.GenerateRectilinearGrid import GenerateRectilinearGrid - from geos.mesh.doctor.actions.generate_cube import FieldInfo - - # Instantiate the filter - generateRectilinearGridFilter = GenerateRectilinearGrid() - - # Set the coordinates of each block border for the X, Y and Z axis - generateRectilinearGridFilter.setCoordinates([0.0, 5.0, 10.0], [0.0, 5.0, 10.0], [0.0, 10.0]) - - # For each block defined, specify the number of cells that they should contain in the X, Y, Z axis - generateRectilinearGridFilter.setNumberElements([5, 5], [5, 5], [10]) - - # To add the GlobalIds for cells and points, set to True the generate global ids options - generateRectilinearGridFilter.setGenerateCellsGlobalIds(True) - generateRectilinearGridFilter.setGeneratePointsGlobalIds(True) - - # To create new arrays with a specific dimension, you can use the following commands - cells_dim1 = FieldInfo("cell1", 1, "CELLS") # array "cell1" of shape (number of cells, 1) - cells_dim3 = FieldInfo("cell3", 3, "CELLS") # array "cell3" of shape (number of cells, 3) - points_dim1 = FieldInfo("point1", 1, "POINTS") # array "point1" of shape (number of points, 1) - points_dim3 = FieldInfo("point3", 3, "POINTS") # array "point3" of shape (number of points, 3) - generateRectilinearGridFilter.setFields([cells_dim1, cells_dim3, points_dim1, points_dim3]) - - # Then, to obtain the constructed mesh out of all these operations, 2 solutions are available - - # Solution1 (recommended) - mesh = generateRectilinearGridFilter.getGrid() - - # Solution2 (manual) - generateRectilinearGridFilter.Update() - mesh = generateRectilinearGridFilter.GetOutputDataObject(0) - - # Finally, you can write the mesh at a specific destination with: - generateRectilinearGridFilter.writeGrid("output/filepath/of/your/grid.vtu") - -Parameters ----------- - -setCoordinates(coordsX, coordsY, coordsZ) - Set the coordinates that define block boundaries along each axis. - - * **coordsX** (Sequence[float]): X-coordinates of block boundaries - * **coordsY** (Sequence[float]): Y-coordinates of block boundaries - * **coordsZ** (Sequence[float]): Z-coordinates of block boundaries - -setNumberElements(numberElementsX, numberElementsY, numberElementsZ) - Set the number of elements in each block along each axis. - - * **numberElementsX** (Sequence[int]): Number of elements in each X-block - * **numberElementsY** (Sequence[int]): Number of elements in each Y-block - * **numberElementsZ** (Sequence[int]): Number of elements in each Z-block - -setGenerateCellsGlobalIds(generate) - Enable/disable generation of global cell IDs. - - * **generate** (bool): True to generate global cell IDs - -setGeneratePointsGlobalIds(generate) - Enable/disable generation of global point IDs. - - * **generate** (bool): True to generate global point IDs - -setFields(fields) - Specify additional cell or point arrays to be added to the grid. - - * **fields** (Iterable[FieldInfo]): List of field specifications - -Field Specification -------------------- - -Fields are specified using FieldInfo objects: - -.. code-block:: python - - from geos.mesh.doctor.actions.generate_cube import FieldInfo - - # Create a field specification - field = FieldInfo(name, dimension, location) - -**Parameters:** - -* **name** (str): Name of the array -* **dimension** (int): Number of components (1 for scalars, 3 for vectors, etc.) -* **location** (str): "CELLS" for cell data, "POINTS" for point data - -**Examples:** - -.. code-block:: python - - # Scalar cell field - pressure = FieldInfo("pressure", 1, "CELLS") - - # Vector point field - velocity = FieldInfo("velocity", 3, "POINTS") - - # Tensor cell field - stress = FieldInfo("stress", 6, "CELLS") # 6 components for symmetric tensor - -Grid Construction Logic ------------------------ - -**Coordinate Specification** - -Coordinates define block boundaries. For example: - -.. code-block:: python - - coordsX = [0.0, 5.0, 10.0] # Creates 2 blocks in X: [0,5] and [5,10] - numberElementsX = [5, 10] # 5 elements in first block, 10 in second - -**Element Distribution** - -Each block can have different element densities: - -* Block 1: 5 elements distributed uniformly in [0.0, 5.0] -* Block 2: 10 elements distributed uniformly in [5.0, 10.0] - -**Total Grid Size** - -* Total elements: numberElementsX[0] × numberElementsY[0] × numberElementsZ[0] + ... (for each block combination) -* Total points: (sum(numberElementsX) + len(numberElementsX)) × (sum(numberElementsY) + len(numberElementsY)) × (sum(numberElementsZ) + len(numberElementsZ)) - -Example: Multi-Block Grid -------------------------- - -.. code-block:: python - - # Create a grid with 3 blocks in X, 2 in Y, 1 in Z - filter = GenerateRectilinearGrid() - - # Define block boundaries - filter.setCoordinates( - [0.0, 1.0, 3.0, 5.0], # 3 blocks: [0,1], [1,3], [3,5] - [0.0, 2.0, 4.0], # 2 blocks: [0,2], [2,4] - [0.0, 1.0] # 1 block: [0,1] - ) - - # Define element counts per block - filter.setNumberElements( - [10, 20, 10], # 10, 20, 10 elements in X blocks - [15, 15], # 15, 15 elements in Y blocks - [5] # 5 elements in Z block - ) - - # Add global IDs and custom fields - filter.setGenerateCellsGlobalIds(True) - filter.setGeneratePointsGlobalIds(True) - - material_id = FieldInfo("material", 1, "CELLS") - coordinates = FieldInfo("coords", 3, "POINTS") - filter.setFields([material_id, coordinates]) - - # Generate the grid - mesh = filter.getGrid() - -Output ------- - -* **Input**: None (generator filter) -* **Output**: vtkUnstructuredGrid with hexahedral elements -* **Additional Arrays**: - - * Global IDs (if enabled) - * Custom fields (if specified) - * All arrays filled with constant value 1.0 - -Use Cases ---------- - -* **Testing**: Create simple grids for algorithm testing -* **Validation**: Generate known geometries for code validation -* **Prototyping**: Quick mesh generation for initial simulations -* **Benchmarking**: Standard grids for performance testing -* **Education**: Simple examples for learning mesh-based methods - -Best Practices --------------- - -* Start with simple single-block grids before using multi-block configurations -* Ensure coordinate sequences are monotonically increasing -* Match the length of numberElements arrays with coordinate block counts -* Use meaningful field names for better mesh organization -* Enable global IDs when mesh will be used in parallel computations - -See Also --------- - -* :doc:`GenerateFractures ` - Advanced mesh modification -* :doc:`ElementVolumes ` - Quality assessment for generated meshes -* :doc:`AllChecks ` - Comprehensive validation of generated meshes +* **Input**: Not needed +* **Output**: vtkUnstructuredGrid +* **Additional Data**: New fields on points or cells if user requests them. \ No newline at end of file diff --git a/docs/geos_mesh_docs/filters/MainChecks.rst b/docs/geos_mesh_docs/filters/MainChecks.rst deleted file mode 100644 index 722e98188..000000000 --- a/docs/geos_mesh_docs/filters/MainChecks.rst +++ /dev/null @@ -1,98 +0,0 @@ -MainChecks Filter -================= - -.. autoclass:: geos.mesh.doctor.filters.Checks.MainChecks - :members: - :undoc-members: - :show-inheritance: - -Overview --------- - -The MainChecks filter performs essential mesh validation by running the most important quality checks on a vtkUnstructuredGrid. This filter provides a streamlined subset of checks that are most critical for mesh quality assessment. - -Features --------- - -* Essential mesh validation with the most important quality checks -* Faster execution compared to AllChecks -* Configurable check parameters -* Focused on critical mesh quality issues -* Same interface as AllChecks for consistency - -Usage Example -------------- - -.. code-block:: python - - from geos.mesh.doctor.filters.Checks import MainChecks - - # Instantiate the filter for main checks only - mainChecksFilter = MainChecks() - - # Set input mesh - mainChecksFilter.SetInputData(mesh) - - # Optionally customize check parameters - mainChecksFilter.setCheckParameter("collocated_nodes", "tolerance", 1e-6) - - # Execute the checks and get output - output_mesh = mainChecksFilter.getGrid() - - # Get check results - check_results = mainChecksFilter.getCheckResults() - - # Write the output mesh - mainChecksFilter.writeGrid("output/mesh_main_checks.vtu") - -Main Checks Included --------------------- - -The MainChecks filter includes a curated subset of the most important checks: - -* **Collocated nodes**: Detect duplicate/overlapping nodes -* **Element volumes**: Identify negative or zero volume elements -* **Self-intersecting elements**: Find geometrically invalid elements -* **Basic topology validation**: Ensure mesh connectivity is valid - -These checks cover the most common and critical mesh quality issues that can affect simulation stability and accuracy. - -When to Use MainChecks vs AllChecks ------------------------------------ - -**Use MainChecks when:** - -* You need quick mesh validation -* You're doing routine quality checks -* Performance is important -* You want to focus on critical issues only - -**Use AllChecks when:** - -* You need comprehensive mesh analysis -* You're debugging complex mesh issues -* You have time for thorough validation -* You need detailed reporting on all aspects - -Parameters ----------- - -Same parameter interface as AllChecks: - -* **setCheckParameter(check_name, parameter_name, value)**: Set specific parameter for a named check -* **setGlobalParameter(parameter_name, value)**: Apply parameter to all checks that support it - -Output ------- - -* **Input**: vtkUnstructuredGrid -* **Output**: vtkUnstructuredGrid (copy of input with potential additional arrays marking issues) -* **Check Results**: Dictionary with results from performed main checks - -See Also --------- - -* :doc:`AllChecks ` - Comprehensive mesh validation with all checks -* :doc:`CollocatedNodes ` - Individual collocated nodes check -* :doc:`ElementVolumes ` - Individual element volume check -* :doc:`SelfIntersectingElements ` - Individual self-intersection check diff --git a/docs/geos_mesh_docs/filters/MeshDoctorFilterBase.rst b/docs/geos_mesh_docs/filters/MeshDoctorFilterBase.rst new file mode 100644 index 000000000..d9a24feeb --- /dev/null +++ b/docs/geos_mesh_docs/filters/MeshDoctorFilterBase.rst @@ -0,0 +1,14 @@ +MeshDoctorFilterBase +==================== + +.. automodule:: geos.mesh.doctor.filters.MeshDoctorFilterBase + :members: + :undoc-members: + :show-inheritance: + +See Also +-------- + +* :doc:`AllChecks and MainChecks ` - Comprehensive mesh quality assessment using MeshDoctorFilterBase +* :doc:`GenerateRectilinearGrid ` - Grid generation using MeshDoctorGeneratorBase +* :doc:`GenerateFractures ` - Fracture mesh generation using MeshDoctorFilterBase diff --git a/docs/geos_mesh_docs/filters/NonConformal.rst b/docs/geos_mesh_docs/filters/NonConformal.rst index 44991af86..4e8df5ee0 100644 --- a/docs/geos_mesh_docs/filters/NonConformal.rst +++ b/docs/geos_mesh_docs/filters/NonConformal.rst @@ -6,101 +6,6 @@ NonConformal Filter :undoc-members: :show-inheritance: -Overview --------- - -The NonConformal filter detects non-conformal mesh interfaces in a vtkUnstructuredGrid. Non-conformal interfaces occur when adjacent cells do not share nodes or faces properly, which can indicate mesh quality issues or intentional non-matching grid interfaces that require special handling in simulations. - -Features --------- - -* Detection of non-conformal interfaces between mesh elements -* Configurable tolerance parameters for different geometric aspects -* Optional marking of non-conformal cells in output mesh -* Support for point, face, and angle tolerance specifications -* Detailed reporting of non-conformal cell pairs - -Usage Example -------------- - -.. code-block:: python - - from geos.mesh.doctor.filters.NonConformal import NonConformal - - # Instantiate the filter - nonConformalFilter = NonConformal() - - # Set tolerance parameters - nonConformalFilter.setPointTolerance(1e-6) # tolerance for point matching - nonConformalFilter.setFaceTolerance(1e-6) # tolerance for face matching - nonConformalFilter.setAngleTolerance(10.0) # angle tolerance in degrees - - # Optionally enable painting of non-conformal cells - nonConformalFilter.setPaintNonConformalCells(1) # 1 to enable, 0 to disable - - # Set input mesh - nonConformalFilter.SetInputData(mesh) - - # Execute the filter and get output - output_mesh = nonConformalFilter.getGrid() - - # Get non-conformal cell pairs - non_conformal_cells = nonConformalFilter.getNonConformalCells() - # Returns list of tuples with (cell1_id, cell2_id) for non-conformal interfaces - - # Write the output mesh - nonConformalFilter.writeGrid("output/mesh_with_nonconformal_info.vtu") - -Parameters ----------- - -setPointTolerance(tolerance) - Set the tolerance for point position matching. - - * **tolerance** (float): Distance below which points are considered coincident - * **Default**: 0.0 - -setFaceTolerance(tolerance) - Set the tolerance for face geometry matching. - - * **tolerance** (float): Distance tolerance for face-to-face matching - * **Default**: 0.0 - -setAngleTolerance(tolerance) - Set the tolerance for face normal angle differences. - - * **tolerance** (float): Maximum angle difference in degrees - * **Default**: 10.0 - -setPaintNonConformalCells(choice) - Enable/disable creation of array marking non-conformal cells. - - * **choice** (int): 1 to enable marking, 0 to disable - * **Default**: 0 - -Results Access --------------- - -getNonConformalCells() - Returns pairs of cell indices that have non-conformal interfaces. - - * **Returns**: list[tuple[int, int]] - Each tuple contains (cell1_id, cell2_id) - -getAngleTolerance() - Get the current angle tolerance setting. - - * **Returns**: float - Current angle tolerance in degrees - -getFaceTolerance() - Get the current face tolerance setting. - - * **Returns**: float - Current face tolerance - -getPointTolerance() - Get the current point tolerance setting. - - * **Returns**: float - Current point tolerance - Understanding Non-Conformal Interfaces --------------------------------------- @@ -129,102 +34,6 @@ Understanding Non-Conformal Interfaces 3. **Gap Interfaces**: Small gaps between elements that should be connected 4. **Overlapping Interfaces**: Elements that overlap slightly due to meshing errors -Tolerance Parameters Explained ------------------------------- - -**Point Tolerance** - -Controls how close points must be to be considered the same: - -* **Too small**: May miss near-coincident points that should match -* **Too large**: May incorrectly group distinct points -* **Typical values**: 1e-6 to 1e-12 depending on mesh scale - -**Face Tolerance** - -Controls how closely face geometries must match: - -* **Distance-based**: Maximum separation between face centroids or boundaries -* **Affects**: Detection of faces that should be conformal but have small gaps -* **Typical values**: 1e-6 to 1e-10 - -**Angle Tolerance** - -Controls how closely face normals must align: - -* **In degrees**: Maximum angle between face normal vectors -* **Affects**: Detection of faces that should be coplanar but have slight orientation differences -* **Typical values**: 0.1 to 10.0 degrees - -Common Causes of Non-Conformity -------------------------------- - -1. **Mesh Generation Issues**: - - * Different mesh densities in adjacent regions - * Boundary misalignment during mesh merging - * Floating-point precision errors - -2. **Intentional Design**: - - * Adaptive mesh refinement interfaces - * Multi-scale coupling boundaries - * Domain decomposition interfaces - -3. **Mesh Processing Errors**: - - * Node merging tolerances too strict - * Coordinate transformation errors - * File format conversion issues - -Impact on Simulations ---------------------- - -**Potential Problems**: - -* **Gaps**: Can cause fluid/heat leakage in flow simulations -* **Overlaps**: May create artificial constraints or stress concentrations -* **Inconsistent Physics**: Different discretizations across interfaces - -**When Non-Conformity is Acceptable**: - -* **Mortar Methods**: Designed to handle non-matching grids -* **Penalty Methods**: Use constraints to enforce continuity -* **Adaptive Refinement**: Temporary non-conformity during adaptation - -Example Analysis Workflow -------------------------- - -.. code-block:: python - - # Comprehensive non-conformity analysis - nc_filter = NonConformal() - - # Configure for sensitive detection - nc_filter.setPointTolerance(1e-8) - nc_filter.setFaceTolerance(1e-8) - nc_filter.setAngleTolerance(1.0) # 1 degree tolerance - - # Enable visualization - nc_filter.setPaintNonConformalCells(1) - - # Process mesh - nc_filter.SetInputData(mesh) - output_mesh = nc_filter.getGrid() - - # Analyze results - non_conformal_pairs = nc_filter.getNonConformalCells() - - if len(non_conformal_pairs) == 0: - print("Mesh is fully conformal - all interfaces match properly") - else: - print(f"Found {len(non_conformal_pairs)} non-conformal interfaces:") - for cell1, cell2 in non_conformal_pairs[:10]: # Show first 10 - print(f" Cells {cell1} and {cell2} have non-conformal interface") - - # Write mesh with marking for visualization - nc_filter.writeGrid("output/mesh_nonconformal_marked.vtu") - Output ------ @@ -245,6 +54,4 @@ Best Practices See Also -------- -* :doc:`AllChecks ` - Includes non-conformal check among others -* :doc:`CollocatedNodes ` - Related to point matching issues -* :doc:`SelfIntersectingElements ` - Related geometric validation +* :doc:`AllChecks ` - Includes non-conformal check among others diff --git a/docs/geos_mesh_docs/filters/SelfIntersectingElements.rst b/docs/geos_mesh_docs/filters/SelfIntersectingElements.rst index 059f7e568..0dfd97d88 100644 --- a/docs/geos_mesh_docs/filters/SelfIntersectingElements.rst +++ b/docs/geos_mesh_docs/filters/SelfIntersectingElements.rst @@ -6,109 +6,6 @@ SelfIntersectingElements Filter :undoc-members: :show-inheritance: -Overview --------- - -The SelfIntersectingElements filter identifies various types of invalid or problematic elements in a vtkUnstructuredGrid. It performs comprehensive geometric validation to detect elements with intersecting edges, intersecting faces, non-contiguous edges, non-convex shapes, incorrectly oriented faces, and wrong number of points. - -Features --------- - -* Detection of multiple types of geometric element problems -* Configurable minimum distance parameter for intersection detection -* Optional marking of invalid elements in output mesh -* Detailed classification of different problem types -* Comprehensive reporting of all detected issues - -Usage Example -------------- - -.. code-block:: python - - from geos.mesh.doctor.filters.SelfIntersectingElements import SelfIntersectingElements - - # Instantiate the filter - selfIntersectingElementsFilter = SelfIntersectingElements() - - # Set minimum distance parameter for intersection detection - selfIntersectingElementsFilter.setMinDistance(1e-6) - - # Optionally enable painting of invalid elements - selfIntersectingElementsFilter.setPaintInvalidElements(1) # 1 to enable, 0 to disable - - # Set input mesh - selfIntersectingElementsFilter.SetInputData(mesh) - - # Execute the filter and get output - output_mesh = selfIntersectingElementsFilter.getGrid() - - # Get different types of problematic elements - wrong_points_elements = selfIntersectingElementsFilter.getWrongNumberOfPointsElements() - intersecting_edges_elements = selfIntersectingElementsFilter.getIntersectingEdgesElements() - intersecting_faces_elements = selfIntersectingElementsFilter.getIntersectingFacesElements() - non_contiguous_edges_elements = selfIntersectingElementsFilter.getNonContiguousEdgesElements() - non_convex_elements = selfIntersectingElementsFilter.getNonConvexElements() - wrong_oriented_faces_elements = selfIntersectingElementsFilter.getFacesOrientedIncorrectlyElements() - - # Write the output mesh - selfIntersectingElementsFilter.writeGrid("output/mesh_with_invalid_elements.vtu") - -Parameters ----------- - -setMinDistance(distance) - Set the minimum distance parameter for intersection detection. - - * **distance** (float): Minimum distance threshold for geometric calculations - * **Default**: 0.0 - * **Usage**: Smaller values detect more subtle problems, larger values ignore minor issues - -setPaintInvalidElements(choice) - Enable/disable creation of arrays marking invalid elements. - - * **choice** (int): 1 to enable marking, 0 to disable - * **Default**: 0 - * **Effect**: When enabled, adds arrays to cell data identifying different problem types - -Types of Problems Detected --------------------------- - -getWrongNumberOfPointsElements() - Elements with incorrect number of points for their cell type. - - * **Returns**: list[int] - Element indices with wrong point counts - * **Examples**: Triangle with 4 points, hexahedron with 7 points - -getIntersectingEdgesElements() - Elements where edges intersect themselves. - - * **Returns**: list[int] - Element indices with self-intersecting edges - * **Examples**: Twisted quadrilaterals, folded triangles - -getIntersectingFacesElements() - Elements where faces intersect each other. - - * **Returns**: list[int] - Element indices with self-intersecting faces - * **Examples**: Inverted tetrahedra, twisted hexahedra - -getNonContiguousEdgesElements() - Elements where edges are not properly connected. - - * **Returns**: list[int] - Element indices with connectivity issues - * **Examples**: Disconnected edge loops, gaps in element boundaries - -getNonConvexElements() - Elements that are not convex as required. - - * **Returns**: list[int] - Element indices that are non-convex - * **Examples**: Concave quadrilaterals, non-convex polygons - -getFacesOrientedIncorrectlyElements() - Elements with incorrectly oriented faces. - - * **Returns**: list[int] - Element indices with orientation problems - * **Examples**: Inward-pointing face normals, inconsistent winding - Understanding Element Problems ------------------------------ @@ -203,68 +100,6 @@ Common Causes and Solutions * **Cause**: Inconsistent node ordering, mesh processing errors * **Solution**: Fix element node ordering, use mesh repair tools -Example Comprehensive Analysis ------------------------------- - -.. code-block:: python - - # Detailed element validation workflow - validator = SelfIntersectingElements() - validator.setMinDistance(1e-8) # Very sensitive detection - validator.setPaintInvalidElements(1) # Enable visualization - - validator.SetInputData(mesh) - output_mesh = validator.getGrid() - - # Collect all problems - problems = { - 'Wrong points': validator.getWrongNumberOfPointsElements(), - 'Intersecting edges': validator.getIntersectingEdgesElements(), - 'Intersecting faces': validator.getIntersectingFacesElements(), - 'Non-contiguous edges': validator.getNonContiguousEdgesElements(), - 'Non-convex': validator.getNonConvexElements(), - 'Wrong orientation': validator.getFacesOrientedIncorrectlyElements() - } - - # Report results - total_problems = sum(len(elements) for elements in problems.values()) - - if total_problems == 0: - print("✓ All elements are geometrically valid!") - else: - print(f"⚠ Found {total_problems} problematic elements:") - for problem_type, elements in problems.items(): - if elements: - print(f" {problem_type}: {len(elements)} elements") - print(f" Examples: {elements[:5]}") # Show first 5 - - # Save results for visualization - validator.writeGrid("output/mesh_validation_results.vtu") - -Impact on Simulations ---------------------- - -**Numerical Issues** - -* Poor convergence -* Solver instabilities -* Incorrect results -* Simulation crashes - -**Physical Accuracy** - -* Wrong material volumes -* Incorrect flow paths -* Bad stress/strain calculations -* Energy conservation violations - -**Performance Impact** - -* Slower convergence -* Need for smaller time steps -* Additional stabilization methods -* Increased computational cost - Output ------ @@ -273,21 +108,7 @@ Output * **Problem Lists**: Separate lists for each type of geometric problem * **Marking Arrays**: When painting is enabled, adds arrays identifying problem types -Best Practices --------------- - -* **Set appropriate minimum distance** based on mesh precision -* **Enable painting** to visualize problems in ParaView -* **Check all problem types** for comprehensive validation -* **Fix problems before simulation** to avoid numerical issues -* **Use with other validators** for complete mesh assessment -* **Document any intentionally invalid elements** if they serve a purpose - See Also -------- -* :doc:`AllChecks ` - Includes self-intersection check among others -* :doc:`MainChecks ` - Includes self-intersection check in main set -* :doc:`ElementVolumes ` - Related to geometric validity -* :doc:`CollocatedNodes ` - Can help fix some geometric issues -* :doc:`NonConformal ` - Related interface validation +* :doc:`AllChecks and MainChecks ` - Includes self-intersection check among others \ No newline at end of file diff --git a/docs/geos_mesh_docs/filters/SupportedElements.rst b/docs/geos_mesh_docs/filters/SupportedElements.rst index a2fb59e69..3bb366239 100644 --- a/docs/geos_mesh_docs/filters/SupportedElements.rst +++ b/docs/geos_mesh_docs/filters/SupportedElements.rst @@ -6,223 +6,40 @@ SupportedElements Filter :undoc-members: :show-inheritance: -Overview --------- - -The SupportedElements filter identifies unsupported element types and problematic polyhedron elements in a vtkUnstructuredGrid. It validates that all elements in the mesh are supported by GEOS and checks polyhedron elements for geometric correctness. - -.. note:: - This filter is currently disabled due to multiprocessing requirements that are incompatible with the VTK filter framework. The implementation exists but is commented out in the source code. - -Features (When Available) -------------------------- - -* Detection of unsupported VTK element types -* Validation of polyhedron element geometry -* Optional marking of unsupported elements in output mesh -* Integration with parallel processing for large meshes -* Detailed reporting of element type compatibility - -Intended Usage Example ----------------------- - -.. code-block:: python - - from geos.mesh.doctor.filters.SupportedElements import SupportedElements - - # Instantiate the filter - supportedElementsFilter = SupportedElements() - - # Optionally enable painting of unsupported element types - supportedElementsFilter.setPaintUnsupportedElementTypes(1) # 1 to enable, 0 to disable - - # Set input mesh - supportedElementsFilter.SetInputData(mesh) - - # Execute the filter and get output - output_mesh = supportedElementsFilter.getGrid() - - # Get unsupported elements - unsupported_elements = supportedElementsFilter.getUnsupportedElements() - - # Write the output mesh - supportedElementsFilter.writeGrid("output/mesh_with_support_info.vtu") - GEOS Supported Element Types ---------------------------- GEOS supports the following VTK element types: -**Standard Elements** - -* **VTK_VERTEX** (1): Point elements -* **VTK_LINE** (3): Line segments -* **VTK_TRIANGLE** (5): Triangular elements -* **VTK_QUAD** (9): Quadrilateral elements -* **VTK_TETRA** (10): Tetrahedral elements -* **VTK_HEXAHEDRON** (12): Hexahedral (brick) elements -* **VTK_WEDGE** (13): Wedge/prism elements -* **VTK_PYRAMID** (14): Pyramid elements - -**Higher-Order Elements** - -* **VTK_QUADRATIC_TRIANGLE** (22): 6-node triangles -* **VTK_QUADRATIC_QUAD** (23): 8-node quadrilaterals -* **VTK_QUADRATIC_TETRA** (24): 10-node tetrahedra -* **VTK_QUADRATIC_HEXAHEDRON** (25): 20-node hexahedra - -**Special Elements** - -* **VTK_POLYHEDRON** (42): General polyhedra (with validation) - -Unsupported Element Types -------------------------- - -Elements not supported by GEOS include: - -* **VTK_PIXEL** (8): Axis-aligned rectangles -* **VTK_VOXEL** (11): Axis-aligned boxes -* **VTK_PENTAGONAL_PRISM** (15): 5-sided prisms -* **VTK_HEXAGONAL_PRISM** (16): 6-sided prisms -* Various specialized or experimental VTK cell types - -Polyhedron Validation ---------------------- - -For polyhedron elements (VTK_POLYHEDRON), additional checks are performed: - -**Geometric Validation** - -* Face planarity -* Edge connectivity -* Volume calculation -* Normal consistency - -**Topological Validation** - -* Manifold surface verification -* Closed surface check -* Face orientation consistency - -**Quality Checks** - -* Aspect ratio limits -* Volume positivity -* Face area positivity - -Common Issues and Solutions ---------------------------- - -**Unsupported Standard Elements** - -* **Problem**: Mesh contains VTK_PIXEL or VTK_VOXEL elements -* **Solution**: Convert to VTK_QUAD or VTK_HEXAHEDRON respectively -* **Tools**: VTK conversion filters or mesh processing software - -**Invalid Polyhedra** - -* **Problem**: Non-manifold or self-intersecting polyhedra -* **Solution**: Use mesh repair tools or regenerate with better quality settings -* **Prevention**: Validate polyhedra during mesh generation - -**Mixed Element Types** - -* **Problem**: Mesh contains both supported and unsupported elements -* **Solution**: Selective element type conversion or mesh region separation - -Current Status and Alternatives -------------------------------- - -**Why Disabled** - -The SupportedElements filter requires multiprocessing capabilities for efficient polyhedron validation on large meshes. However, the VTK Python filter framework doesn't integrate well with multiprocessing, leading to: - -* Process spawning issues -* Memory management problems -* Inconsistent results across platforms - -**Alternative Approaches** - -1. **Command-Line Tool**: Use the ``mesh-doctor supported_elements`` command instead -2. **Direct Function Calls**: Import and use the underlying functions directly -3. **Manual Validation**: Check element types programmatically - -**Command-Line Alternative** - -.. code-block:: bash - - # Use mesh-doctor command line tool instead - mesh-doctor -i input_mesh.vtu supported_elements --help - -**Direct Function Usage** - -.. code-block:: python - - from geos.mesh.doctor.actions.supported_elements import ( - find_unsupported_std_elements_types, - find_unsupported_polyhedron_elements - ) - - # Direct function usage (not as VTK filter) - unsupported_std = find_unsupported_std_elements_types(mesh) - # Note: polyhedron validation requires multiprocessing setup - -Future Development ------------------- - -**Planned Improvements** +**0D & 1D Elements** -* Integration with VTK's parallel processing capabilities -* Alternative implementation without multiprocessing dependency -* Better error handling and reporting -* Performance optimizations for large meshes +* VTK_VERTEX (1): These are individual point elements. +* VTK_LINE (3): These are linear, 1D elements defined by two points. -**Workaround Implementation** +**2D Elements** -Until the filter is re-enabled, users can: +* VTK_TRIANGLE (5): These are 2D, three-noded triangular elements. +* VTK_POLYGON (7): This is a generic cell for 2D elements with more than four vertices. +* VTK_QUAD (9): These are 2D, four-noded quadrilateral elements. -1. Use the command-line interface -2. Implement custom validation loops -3. Use external mesh validation tools -4. Perform validation in separate processes +**3D Elements** -Example Manual Validation -------------------------- +* VTK_TETRA (10): These are four-noded tetrahedral elements (a triangular pyramid). +* VTK_HEXAHEDRON (12): These are eight-noded hexahedral elements, often called bricks or cubes. +* VTK_WEDGE (13): These are six-noded wedge or triangular prism elements. +* VTK_PYRAMID (14): These are five-noded pyramids with a quadrilateral base. +* VTK_PENTAGONAL_PRISM (15): These are ten-noded prisms with a pentagonal base. +* VTK_HEXAGONAL_PRISM (16): These are twelve-noded prisms with a hexagonal base. +* VTK_POLYHEDRON (42): Corresponds to Polyhedron as well as HeptagonalPrism, OctagonalPrism, NonagonalPrism, DecagonalPrism, and HendecagonalPrism. This is a general-purpose 3D cell type for arbitrary polyhedra not covered by the other specific types. -.. code-block:: python +I/O +--- - import vtk - - def check_supported_elements(mesh): - """Manual check for supported element types.""" - supported_types = { - vtk.VTK_VERTEX, vtk.VTK_LINE, vtk.VTK_TRIANGLE, vtk.VTK_QUAD, - vtk.VTK_TETRA, vtk.VTK_HEXAHEDRON, vtk.VTK_WEDGE, vtk.VTK_PYRAMID, - vtk.VTK_QUADRATIC_TRIANGLE, vtk.VTK_QUADRATIC_QUAD, - vtk.VTK_QUADRATIC_TETRA, vtk.VTK_QUADRATIC_HEXAHEDRON, - vtk.VTK_POLYHEDRON - } - - unsupported = [] - for i in range(mesh.GetNumberOfCells()): - cell_type = mesh.GetCellType(i) - if cell_type not in supported_types: - unsupported.append((i, cell_type)) - - return unsupported - - # Usage - unsupported_elements = check_supported_elements(mesh) - if unsupported_elements: - print(f"Found {len(unsupported_elements)} unsupported elements") - for cell_id, cell_type in unsupported_elements[:5]: - print(f" Cell {cell_id}: type {cell_type}") +* **Input**: vtkUnstructuredGrid +* **Output**: vtkUnstructuredGrid with optional arrays marking problematic elements +* **Additional Data**: When painting is enabled, adds "WrongSupportElements" array to cell data See Also -------- -* :doc:`AllChecks ` - Would include supported elements check when available -* :doc:`SelfIntersectingElements ` - Related geometric validation -* :doc:`ElementVolumes ` - Basic element validation -* GEOS documentation on supported element types -* VTK documentation on cell types +* :doc:`AllChecks ` - Includes supported-elements check among others \ No newline at end of file diff --git a/docs/geos_mesh_docs/filters/index.rst b/docs/geos_mesh_docs/filters/index.rst index 456dd2e85..e6dbf88be 100644 --- a/docs/geos_mesh_docs/filters/index.rst +++ b/docs/geos_mesh_docs/filters/index.rst @@ -11,8 +11,7 @@ These filters analyze existing meshes for various quality issues and geometric p .. toctree:: :maxdepth: 1 - AllChecks - MainChecks + Checks CollocatedNodes ElementVolumes SelfIntersectingElements @@ -39,33 +38,6 @@ These filters perform specialized processing and validation tasks. SupportedElements -Common Usage Pattern -==================== - -All mesh doctor filters follow a consistent usage pattern: - -.. code-block:: python - - from geos.mesh.doctor.filters.FilterName import FilterName - - # Instantiate the filter - filter = FilterName() - - # Configure filter parameters - filter.setParameter(value) - - # Set input data (for processing filters, not needed for generators) - filter.SetInputData(mesh) - - # Execute the filter and get output - output_mesh = filter.getGrid() - - # Access specific results (filter-dependent) - results = filter.getSpecificResults() - - # Write results to file - filter.writeGrid("output/result.vtu") - Filter Categories Explained =========================== @@ -121,11 +93,11 @@ Quick Reference Filter Selection Guide ---------------------- -**For routine mesh validation**: - Use :doc:`MainChecks ` for fast, essential checks +**For mesh validation**: + Use :doc:`MainChecks ` for fast, essential checks **For comprehensive analysis**: - Use :doc:`AllChecks ` for detailed validation + Use :doc:`AllChecks ` for detailed validation **For specific problems**: - Duplicate nodes → :doc:`CollocatedNodes ` @@ -143,12 +115,7 @@ Filter Selection Guide Parameter Guidelines -------------------- -**Tolerance Parameters**: - - High precision meshes: 1e-12 to 1e-8 - - Standard meshes: 1e-8 to 1e-6 - - Coarse meshes: 1e-6 to 1e-4 - -**Painting Options**: +**Painting New Properties**: - Enable (1) for visualization in ParaView - Disable (0) for performance in batch processing @@ -167,14 +134,6 @@ Workflow Integration 3. **Validate fixes** by re-running quality checks 4. **Document mesh quality** for simulation reference -Performance Considerations --------------------------- - -- Use appropriate tolerances (not unnecessarily strict) -- Enable painting only when needed for visualization -- Use binary output for large meshes -- Run comprehensive checks during development, lighter checks in production - Error Handling -------------- @@ -183,10 +142,19 @@ Error Handling - Document known issues and their acceptable thresholds - Use multiple validation approaches for critical applications +Base Classes +============ + +The fundamental base classes that all mesh doctor filters inherit from. + +.. toctree:: + :maxdepth: 1 + + MeshDoctorFilterBase + See Also ======== - **GEOS Documentation**: Supported element types and mesh requirements -- **VTK Documentation**: VTK data formats and cell types -- **ParaView**: Visualization of mesh quality results -- **Mesh Generation Tools**: Creating high-quality input meshes +- **VTK Documentation**: VTK data formats and cell types +- **ParaView**: Visualization of mesh quality results \ No newline at end of file From 1ba9b08d3eeb29fe86f90fd3f1a3b95c14156129 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Fri, 29 Aug 2025 17:46:48 -0700 Subject: [PATCH 34/52] Remove previous tests for filters --- geos-mesh/tests/test_Checks.py | 361 ---------------- geos-mesh/tests/test_collocated_nodes.py | 210 ---------- geos-mesh/tests/test_element_volumes.py | 260 ------------ geos-mesh/tests/test_generate_cube.py | 43 -- geos-mesh/tests/test_generate_fractures.py | 27 -- .../tests/test_self_intersecting_elements.py | 180 -------- geos-mesh/tests/test_supported_elements.py | 395 ------------------ 7 files changed, 1476 deletions(-) delete mode 100644 geos-mesh/tests/test_Checks.py delete mode 100644 geos-mesh/tests/test_collocated_nodes.py delete mode 100644 geos-mesh/tests/test_element_volumes.py delete mode 100644 geos-mesh/tests/test_self_intersecting_elements.py delete mode 100644 geos-mesh/tests/test_supported_elements.py diff --git a/geos-mesh/tests/test_Checks.py b/geos-mesh/tests/test_Checks.py deleted file mode 100644 index 5661c0689..000000000 --- a/geos-mesh/tests/test_Checks.py +++ /dev/null @@ -1,361 +0,0 @@ -import pytest -from vtkmodules.vtkCommonCore import vtkPoints -from vtkmodules.vtkCommonDataModel import ( vtkCellArray, vtkTetra, vtkUnstructuredGrid, VTK_TETRA ) -from geos.mesh.doctor.filters.Checks import AllChecks, MainChecks - - -@pytest.fixture -def simple_mesh_with_issues() -> vtkUnstructuredGrid: - """Create a simple test mesh with known issues for testing checks. - - This mesh includes: - - Collocated nodes (points 0 and 3 are at the same location) - - A very small volume element - - Wrong support elements (duplicate node indices in cells) - - Returns: - vtkUnstructuredGrid: Test mesh with various issues - """ - mesh = vtkUnstructuredGrid() - - # Create points with some collocated nodes - points = vtkPoints() - points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 0 - points.InsertNextPoint( 1.0, 0.0, 0.0 ) # Point 1 - points.InsertNextPoint( 0.0, 1.0, 0.0 ) # Point 2 - points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 3 - duplicate of Point 0 - points.InsertNextPoint( 0.0, 0.0, 1.0 ) # Point 4 - points.InsertNextPoint( 2.0, 0.0, 0.0 ) # Point 5 - points.InsertNextPoint( 2.01, 0.0, 0.0 ) # Point 6 - very close to Point 5 (small volume) - points.InsertNextPoint( 2.0, 0.01, 0.0 ) # Point 7 - creates tiny element - points.InsertNextPoint( 2.0, 0.0, 0.01 ) # Point 8 - creates tiny element - mesh.SetPoints( points ) - - # Create cells - cells = vtkCellArray() - cell_types = [] - - # Normal tetrahedron - tet1 = vtkTetra() - tet1.GetPointIds().SetId( 0, 0 ) - tet1.GetPointIds().SetId( 1, 1 ) - tet1.GetPointIds().SetId( 2, 2 ) - tet1.GetPointIds().SetId( 3, 4 ) - cells.InsertNextCell( tet1 ) - cell_types.append( VTK_TETRA ) - - # Tetrahedron with duplicate node indices (wrong support) - tet2 = vtkTetra() - tet2.GetPointIds().SetId( 0, 3 ) # This is collocated with point 0 - tet2.GetPointIds().SetId( 1, 1 ) - tet2.GetPointIds().SetId( 2, 2 ) - tet2.GetPointIds().SetId( 3, 0 ) # Duplicate reference to same location - cells.InsertNextCell( tet2 ) - cell_types.append( VTK_TETRA ) - - # Very small volume tetrahedron - tet3 = vtkTetra() - tet3.GetPointIds().SetId( 0, 5 ) - tet3.GetPointIds().SetId( 1, 6 ) - tet3.GetPointIds().SetId( 2, 7 ) - tet3.GetPointIds().SetId( 3, 8 ) - cells.InsertNextCell( tet3 ) - cell_types.append( VTK_TETRA ) - - mesh.SetCells( cell_types, cells ) - return mesh - - -@pytest.fixture -def clean_mesh() -> vtkUnstructuredGrid: - """Create a clean test mesh without issues. - - Returns: - vtkUnstructuredGrid: Clean test mesh - """ - mesh = vtkUnstructuredGrid() - - # Create well-separated points - points = vtkPoints() - points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 0 - points.InsertNextPoint( 1.0, 0.0, 0.0 ) # Point 1 - points.InsertNextPoint( 0.0, 1.0, 0.0 ) # Point 2 - points.InsertNextPoint( 0.0, 0.0, 1.0 ) # Point 3 - mesh.SetPoints( points ) - - # Create a single clean tetrahedron - cells = vtkCellArray() - cell_types = [] - - tet = vtkTetra() - tet.GetPointIds().SetId( 0, 0 ) - tet.GetPointIds().SetId( 1, 1 ) - tet.GetPointIds().SetId( 2, 2 ) - tet.GetPointIds().SetId( 3, 3 ) - cells.InsertNextCell( tet ) - cell_types.append( VTK_TETRA ) - - mesh.SetCells( cell_types, cells ) - return mesh - - -@pytest.fixture -def all_checks_filter() -> AllChecks: - """Create a fresh AllChecks filter for each test.""" - return AllChecks() - - -@pytest.fixture -def main_checks_filter() -> MainChecks: - """Create a fresh MainChecks filter for each test.""" - return MainChecks() - - -class TestAllChecks: - """Test class for AllChecks filter functionality.""" - - def test_filter_creation( self, all_checks_filter: AllChecks ): - """Test that AllChecks filter can be created successfully.""" - assert all_checks_filter is not None - assert hasattr( all_checks_filter, 'getAvailableChecks' ) - assert hasattr( all_checks_filter, 'setChecksToPerform' ) - assert hasattr( all_checks_filter, 'setCheckParameter' ) - - def test_available_checks( self, all_checks_filter: AllChecks ): - """Test that all expected checks are available.""" - available_checks = all_checks_filter.getAvailableChecks() - - # Check that we have the expected checks for AllChecks - expected_checks = [ - 'collocated_nodes', 'element_volumes', 'non_conformal', 'self_intersecting_elements', 'supported_elements' - ] - - for check in expected_checks: - assert check in available_checks, f"Check '{check}' should be available" - - def test_default_parameters( self, all_checks_filter: AllChecks ): - """Test that default parameters are correctly retrieved.""" - available_checks = all_checks_filter.getAvailableChecks() - - for check_name in available_checks: - defaults = all_checks_filter.getDefaultParameters( check_name ) - assert isinstance( defaults, dict ), f"Default parameters for '{check_name}' should be a dict" - - # Test specific known defaults - collocated_defaults = all_checks_filter.getDefaultParameters( 'collocated_nodes' ) - assert 'tolerance' in collocated_defaults - - volume_defaults = all_checks_filter.getDefaultParameters( 'element_volumes' ) - assert 'min_volume' in volume_defaults - - def test_set_checks_to_perform( self, all_checks_filter: AllChecks ): - """Test setting specific checks to perform.""" - # Set specific checks - checks_to_perform = [ 'collocated_nodes', 'element_volumes' ] - all_checks_filter.setChecksToPerform( checks_to_perform ) - - # Verify by checking if the filter state changed - assert hasattr( all_checks_filter, 'm_checks_to_perform' ) - assert all_checks_filter.m_checks_to_perform == checks_to_perform - - def test_set_check_parameter( self, all_checks_filter: AllChecks ): - """Test setting parameters for specific checks.""" - # Set a tolerance parameter for collocated nodes - all_checks_filter.setCheckParameter( 'collocated_nodes', 'tolerance', 1e-6 ) - - # Set minimum volume for element volumes - all_checks_filter.setCheckParameter( 'element_volumes', 'min_volume', 0.001 ) - - # Verify parameters are stored - assert 'collocated_nodes' in all_checks_filter.m_check_parameters - assert all_checks_filter.m_check_parameters[ 'collocated_nodes' ][ 'tolerance' ] == 1e-6 - assert all_checks_filter.m_check_parameters[ 'element_volumes' ][ 'min_volume' ] == 0.001 - - def test_set_all_checks_parameter( self, all_checks_filter: AllChecks ): - """Test setting a parameter that applies to all compatible checks.""" - # Set tolerance for all checks that support it - all_checks_filter.setAllChecksParameter( 'tolerance', 1e-8 ) - - # Check that tolerance was set for checks that support it - if 'collocated_nodes' in all_checks_filter.m_check_parameters: - assert all_checks_filter.m_check_parameters[ 'collocated_nodes' ][ 'tolerance' ] == 1e-8 - - def test_process_mesh_with_issues( self, all_checks_filter: AllChecks, - simple_mesh_with_issues: vtkUnstructuredGrid ): - """Test processing a mesh with known issues.""" - # Configure for specific checks - all_checks_filter.setChecksToPerform( [ 'collocated_nodes', 'element_volumes' ] ) - all_checks_filter.setCheckParameter( 'collocated_nodes', 'tolerance', 1e-12 ) - all_checks_filter.setCheckParameter( 'element_volumes', 'min_volume', 1e-3 ) - - # Process the mesh - all_checks_filter.SetInputDataObject( 0, simple_mesh_with_issues ) - all_checks_filter.Update() - - # Check results - results = all_checks_filter.getCheckResults() - - assert 'collocated_nodes' in results - assert 'element_volumes' in results - - # Check that collocated nodes were found - collocated_result = results[ 'collocated_nodes' ] - assert hasattr( collocated_result, 'nodes_buckets' ) - # We expect to find collocated nodes (points 0 and 3) - assert len( collocated_result.nodes_buckets ) > 0 - - # Check that volume issues were detected - volume_result = results[ 'element_volumes' ] - assert hasattr( volume_result, 'element_volumes' ) - - def test_process_clean_mesh( self, all_checks_filter: AllChecks, clean_mesh: vtkUnstructuredGrid ): - """Test processing a clean mesh without issues.""" - # Configure checks - all_checks_filter.setChecksToPerform( [ 'collocated_nodes', 'element_volumes' ] ) - all_checks_filter.setCheckParameter( 'collocated_nodes', 'tolerance', 1e-12 ) - all_checks_filter.setCheckParameter( 'element_volumes', 'min_volume', 1e-6 ) - - # Process the mesh - all_checks_filter.SetInputDataObject( 0, clean_mesh ) - all_checks_filter.Update() - - # Check results - results = all_checks_filter.getCheckResults() - - assert 'collocated_nodes' in results - assert 'element_volumes' in results - - # Check that no issues were found - collocated_result = results[ 'collocated_nodes' ] - assert len( collocated_result.nodes_buckets ) == 0 - - volume_result = results[ 'element_volumes' ] - assert len( volume_result.element_volumes ) == 0 - - def test_output_mesh_unchanged( self, all_checks_filter: AllChecks, clean_mesh: vtkUnstructuredGrid ): - """Test that the output mesh is unchanged from the input (checks don't modify geometry).""" - original_num_points = clean_mesh.GetNumberOfPoints() - original_num_cells = clean_mesh.GetNumberOfCells() - - # Process the mesh - all_checks_filter.SetInputDataObject( 0, clean_mesh ) - all_checks_filter.Update() - - # Get output mesh - output_mesh = all_checks_filter.getGrid() - - # Verify structure is unchanged - assert output_mesh.GetNumberOfPoints() == original_num_points - assert output_mesh.GetNumberOfCells() == original_num_cells - - # Verify points are the same - for i in range( original_num_points ): - original_point = clean_mesh.GetPoint( i ) - output_point = output_mesh.GetPoint( i ) - assert original_point == output_point - - -class TestMainChecks: - """Test class for MainChecks filter functionality.""" - - def test_filter_creation( self, main_checks_filter: MainChecks ): - """Test that MainChecks filter can be created successfully.""" - assert main_checks_filter is not None - assert hasattr( main_checks_filter, 'getAvailableChecks' ) - assert hasattr( main_checks_filter, 'setChecksToPerform' ) - assert hasattr( main_checks_filter, 'setCheckParameter' ) - - def test_available_checks( self, main_checks_filter: MainChecks ): - """Test that main checks are available (subset of all checks).""" - available_checks = main_checks_filter.getAvailableChecks() - - # MainChecks should have a subset of checks - expected_main_checks = [ 'collocated_nodes', 'element_volumes', 'self_intersecting_elements' ] - - for check in expected_main_checks: - assert check in available_checks, f"Main check '{check}' should be available" - - def test_process_mesh( self, main_checks_filter: MainChecks, simple_mesh_with_issues: vtkUnstructuredGrid ): - """Test processing a mesh with MainChecks.""" - # Process the mesh with default configuration - main_checks_filter.SetInputDataObject( 0, simple_mesh_with_issues ) - main_checks_filter.Update() - - # Check that results are obtained - results = main_checks_filter.getCheckResults() - assert isinstance( results, dict ) - assert len( results ) > 0 - - # Check that main checks were performed - available_checks = main_checks_filter.getAvailableChecks() - for check_name in available_checks: - if check_name in results: - result = results[ check_name ] - assert result is not None - - -class TestFilterComparison: - """Test class for comparing AllChecks and MainChecks filters.""" - - def test_all_checks_vs_main_checks_availability( self, all_checks_filter: AllChecks, - main_checks_filter: MainChecks ): - """Test that MainChecks is a subset of AllChecks.""" - all_checks = set( all_checks_filter.getAvailableChecks() ) - main_checks = set( main_checks_filter.getAvailableChecks() ) - - # MainChecks should be a subset of AllChecks - assert main_checks.issubset( all_checks ), "MainChecks should be a subset of AllChecks" - - # AllChecks should have more checks than MainChecks - assert len( all_checks ) >= len( main_checks ), "AllChecks should have at least as many checks as MainChecks" - - def test_parameter_consistency( self, all_checks_filter: AllChecks, main_checks_filter: MainChecks ): - """Test that parameter handling is consistent between filters.""" - # Get common checks - all_checks = set( all_checks_filter.getAvailableChecks() ) - main_checks = set( main_checks_filter.getAvailableChecks() ) - common_checks = all_checks.intersection( main_checks ) - - # Test that default parameters are the same for common checks - for check_name in common_checks: - all_defaults = all_checks_filter.getDefaultParameters( check_name ) - main_defaults = main_checks_filter.getDefaultParameters( check_name ) - assert all_defaults == main_defaults, f"Default parameters should be the same for '{check_name}'" - - -class TestErrorHandling: - """Test class for error handling and edge cases.""" - - def test_invalid_check_name( self, all_checks_filter: AllChecks ): - """Test handling of invalid check names.""" - # Try to set an invalid check - invalid_checks = [ 'nonexistent_check' ] - all_checks_filter.setChecksToPerform( invalid_checks ) - - # The filter should handle this gracefully - # (The actual behavior depends on implementation - it might warn or ignore) - assert all_checks_filter.m_checks_to_perform == invalid_checks - - def test_invalid_parameter_name( self, all_checks_filter: AllChecks ): - """Test handling of invalid parameter names.""" - # Try to set an invalid parameter - all_checks_filter.setCheckParameter( 'collocated_nodes', 'invalid_param', 123 ) - - # This should not crash the filter - assert 'collocated_nodes' in all_checks_filter.m_check_parameters - assert 'invalid_param' in all_checks_filter.m_check_parameters[ 'collocated_nodes' ] - - def test_empty_mesh( self, all_checks_filter: AllChecks ): - """Test handling of empty mesh.""" - # Create an empty mesh - empty_mesh = vtkUnstructuredGrid() - empty_mesh.SetPoints( vtkPoints() ) - - # Process the empty mesh - all_checks_filter.setChecksToPerform( [ 'collocated_nodes' ] ) - all_checks_filter.SetInputDataObject( 0, empty_mesh ) - all_checks_filter.Update() - - # Should complete without error - results = all_checks_filter.getCheckResults() - assert isinstance( results, dict ) diff --git a/geos-mesh/tests/test_collocated_nodes.py b/geos-mesh/tests/test_collocated_nodes.py deleted file mode 100644 index c4de479b0..000000000 --- a/geos-mesh/tests/test_collocated_nodes.py +++ /dev/null @@ -1,210 +0,0 @@ -import pytest -from typing import Iterator, Tuple -from vtkmodules.vtkCommonCore import vtkPoints -from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkTetra, vtkUnstructuredGrid, VTK_TETRA -from geos.mesh.doctor.actions.collocated_nodes import Options, mesh_action -from geos.mesh.doctor.filters.CollocatedNodes import CollocatedNodes - - -def get_points() -> Iterator[ Tuple[ vtkPoints, int ] ]: - """Generates the data for the cases. - One case has two nodes at the exact same position. - The other has two differente nodes - :return: Generator to (vtk points, number of expected duplicated locations) - """ - for p0, p1 in ( ( 0, 0, 0 ), ( 1, 1, 1 ) ), ( ( 0, 0, 0 ), ( 0, 0, 0 ) ): - points = vtkPoints() - points.SetNumberOfPoints( 2 ) - points.SetPoint( 0, p0 ) - points.SetPoint( 1, p1 ) - num_nodes_bucket = 1 if p0 == p1 else 0 - yield points, num_nodes_bucket - - -@pytest.mark.parametrize( "data", get_points() ) -def test_simple_collocated_points( data: Tuple[ vtkPoints, int ] ): - points, num_nodes_bucket = data - - mesh = vtkUnstructuredGrid() - mesh.SetPoints( points ) - - result = mesh_action( mesh, Options( tolerance=1.e-12 ) ) - - assert len( result.wrong_support_elements ) == 0 - assert len( result.nodes_buckets ) == num_nodes_bucket - if num_nodes_bucket == 1: - assert len( result.nodes_buckets[ 0 ] ) == points.GetNumberOfPoints() - - -def test_wrong_support_elements(): - points = vtkPoints() - points.SetNumberOfPoints( 4 ) - points.SetPoint( 0, ( 0, 0, 0 ) ) - points.SetPoint( 1, ( 1, 0, 0 ) ) - points.SetPoint( 2, ( 0, 1, 0 ) ) - points.SetPoint( 3, ( 0, 0, 1 ) ) - - cell_types = [ VTK_TETRA ] - cells = vtkCellArray() - cells.AllocateExact( 1, 4 ) - - tet = vtkTetra() - tet.GetPointIds().SetId( 0, 0 ) - tet.GetPointIds().SetId( 1, 1 ) - tet.GetPointIds().SetId( 2, 2 ) - tet.GetPointIds().SetId( 3, 0 ) # Intentionally wrong - cells.InsertNextCell( tet ) - - mesh = vtkUnstructuredGrid() - mesh.SetPoints( points ) - mesh.SetCells( cell_types, cells ) - - result = mesh_action( mesh, Options( tolerance=1.e-12 ) ) - - assert len( result.nodes_buckets ) == 0 - assert len( result.wrong_support_elements ) == 1 - assert result.wrong_support_elements[ 0 ] == 0 - - -# Create a test mesh with collocated nodes -@pytest.fixture -def sample_mesh() -> vtkUnstructuredGrid: - # Create a simple mesh with duplicate points - mesh = vtkUnstructuredGrid() - - # Create points - points = vtkPoints() - points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 0 - points.InsertNextPoint( 1.0, 0.0, 0.0 ) # Point 1 - points.InsertNextPoint( 0.0, 1.0, 0.0 ) # Point 2 - points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 3 - duplicate of Point 0 - points.InsertNextPoint( 2.0, 0.0, 0.0 ) # Point 4 - mesh.SetPoints( points ) - - # Create cells - cells = vtkCellArray() - # Create a triangular cell with normal connectivity - cells.InsertNextCell( 3 ) - cells.InsertCellPoint( 0 ) - cells.InsertCellPoint( 1 ) - cells.InsertCellPoint( 2 ) - - # Create a cell with duplicate point indices - cells.InsertNextCell( 3 ) - cells.InsertCellPoint( 3 ) # This is a duplicate of point 0 - cells.InsertCellPoint( 1 ) - cells.InsertCellPoint( 4 ) - mesh.SetCells( 5, cells ) # 5 is VTK_TRIANGLE - return mesh - - -@pytest.fixture -def collocated_nodes_filter() -> CollocatedNodes: - return CollocatedNodes() - - -def test_init( collocated_nodes_filter: CollocatedNodes ): - """Test initialization of the CollocatedNodes filter.""" - assert collocated_nodes_filter.m_collocatedNodesBuckets == list() - assert collocated_nodes_filter.m_paintWrongSupportElements == 0 - assert collocated_nodes_filter.m_tolerance == 0.0 - assert collocated_nodes_filter.m_wrongSupportElements == list() - - -def test_collocated_nodes_detection( sample_mesh: vtkUnstructuredGrid, collocated_nodes_filter: CollocatedNodes ): - """Test the filter's ability to detect collocated nodes.""" - # Set input mesh - collocated_nodes_filter.SetInputDataObject( sample_mesh ) - - # Set tolerance - collocated_nodes_filter.setTolerance( 1e-6 ) - - # Run filter - collocated_nodes_filter.Update() - - # Check results - buckets = collocated_nodes_filter.getCollocatedNodeBuckets() - assert len( buckets ) > 0 - - # We expect points 0 and 3 to be in the same bucket - bucket_with_duplicates = None - for bucket in buckets: - if 0 in bucket and 3 in bucket: - bucket_with_duplicates = bucket - break - - assert bucket_with_duplicates is not None, "Failed to detect collocated nodes 0 and 3" - - -def test_wrong_support_elements2( sample_mesh: vtkUnstructuredGrid, collocated_nodes_filter: CollocatedNodes ): - """Test the filter's ability to detect elements with wrong support.""" - # Set input mesh - collocated_nodes_filter.SetInputDataObject( sample_mesh ) - - # Run filter - collocated_nodes_filter.Update() - - # Check results - wrong_elements = collocated_nodes_filter.getWrongSupportElements() - - # In our test mesh, we don't have cells with duplicate point indices within the same cell - # So this should be empty - assert isinstance( wrong_elements, list ) - - -def test_paint_wrong_support_elements( sample_mesh: vtkUnstructuredGrid, collocated_nodes_filter: CollocatedNodes ): - """Test the painting of wrong support elements.""" - # Set input mesh - collocated_nodes_filter.SetInputDataObject( sample_mesh ) - - # Enable painting - collocated_nodes_filter.setPaintWrongSupportElements( 1 ) - - # Run filter - collocated_nodes_filter.Update() - - # Get output mesh - output_mesh = collocated_nodes_filter.getGrid() - - # Check if the array was added - cell_data = output_mesh.GetCellData() - has_array = cell_data.HasArray( "HasDuplicatedNodes" ) - assert has_array, "The HasDuplicatedNodes array wasn't added to cell data" - - -def test_set_paint_wrong_support_elements( collocated_nodes_filter: CollocatedNodes ): - """Test setPaintWrongSupportElements method.""" - # Valid input - collocated_nodes_filter.setPaintWrongSupportElements( 1 ) - assert collocated_nodes_filter.m_paintWrongSupportElements == 1 - - # Valid input - collocated_nodes_filter.setPaintWrongSupportElements( 0 ) - assert collocated_nodes_filter.m_paintWrongSupportElements == 0 - - # Invalid input - collocated_nodes_filter.setPaintWrongSupportElements( 2 ) - # Should remain unchanged - assert collocated_nodes_filter.m_paintWrongSupportElements == 0 - - -def test_set_tolerance( collocated_nodes_filter: CollocatedNodes ): - """Test setTolerance method.""" - collocated_nodes_filter.setTolerance( 0.001 ) - assert collocated_nodes_filter.m_tolerance == 0.001 - - -def test_get_collocated_node_buckets( collocated_nodes_filter: CollocatedNodes ): - """Test getCollocatedNodeBuckets method.""" - # Set a value manually - collocated_nodes_filter.m_collocatedNodesBuckets = [ ( 0, 1 ), ( 2, 3 ) ] - result = collocated_nodes_filter.getCollocatedNodeBuckets() - assert result == [ ( 0, 1 ), ( 2, 3 ) ] - - -def test_get_wrong_support_elements( collocated_nodes_filter: CollocatedNodes ): - """Test getWrongSupportElements method.""" - # Set a value manually - collocated_nodes_filter.m_wrongSupportElements = [ 0, 3, 5 ] - result = collocated_nodes_filter.getWrongSupportElements() - assert result == [ 0, 3, 5 ] diff --git a/geos-mesh/tests/test_element_volumes.py b/geos-mesh/tests/test_element_volumes.py deleted file mode 100644 index 64b903bd2..000000000 --- a/geos-mesh/tests/test_element_volumes.py +++ /dev/null @@ -1,260 +0,0 @@ -import numpy as np -import numpy.typing as npt -import pytest -from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkHexahedron, vtkTetra, vtkUnstructuredGrid, VTK_TETRA -from vtkmodules.vtkCommonCore import vtkPoints, vtkIdList -from vtkmodules.util.numpy_support import vtk_to_numpy -from geos.mesh.doctor.actions.element_volumes import Options, mesh_action -from geos.mesh.doctor.filters.ElementVolumes import ElementVolumes - - -@pytest.fixture -def tetra_mesh() -> vtkUnstructuredGrid: - """Create a simple tetrahedron with known volume (1/6)""" - points = vtkPoints() - points.InsertNextPoint( 0, 0, 0 ) # Point 0 - points.InsertNextPoint( 1, 0, 0 ) # Point 1 - points.InsertNextPoint( 0, 1, 0 ) # Point 2 - points.InsertNextPoint( 0, 0, 1 ) # Point 3 - - tetra = vtkTetra() - tetra.GetPointIds().SetId( 0, 0 ) - tetra.GetPointIds().SetId( 1, 1 ) - tetra.GetPointIds().SetId( 2, 2 ) - tetra.GetPointIds().SetId( 3, 3 ) - - ug = vtkUnstructuredGrid() - ug.SetPoints( points ) - ug.InsertNextCell( tetra.GetCellType(), tetra.GetPointIds() ) - return ug - - -@pytest.fixture -def hexa_mesh() -> vtkUnstructuredGrid: - """Create a simple hexahedron with known volume (1.0)""" - points = vtkPoints() - points.InsertNextPoint( 0, 0, 0 ) # Point 0 - points.InsertNextPoint( 1, 0, 0 ) # Point 1 - points.InsertNextPoint( 1, 1, 0 ) # Point 2 - points.InsertNextPoint( 0, 1, 0 ) # Point 3 - points.InsertNextPoint( 0, 0, 1 ) # Point 4 - points.InsertNextPoint( 1, 0, 1 ) # Point 5 - points.InsertNextPoint( 1, 1, 1 ) # Point 6 - points.InsertNextPoint( 0, 1, 1 ) # Point 7 - - hexa = vtkHexahedron() - for i in range( 8 ): - hexa.GetPointIds().SetId( i, i ) - - ug = vtkUnstructuredGrid() - ug.SetPoints( points ) - ug.InsertNextCell( hexa.GetCellType(), hexa.GetPointIds() ) - return ug - - -@pytest.fixture -def negative_vol_mesh() -> vtkUnstructuredGrid: - """Create a tetrahedron with negative volume (wrong winding)""" - points = vtkPoints() - points.InsertNextPoint( 0, 0, 0 ) # Point 0 - points.InsertNextPoint( 1, 0, 0 ) # Point 1 - points.InsertNextPoint( 0, 1, 0 ) # Point 2 - points.InsertNextPoint( 0, 0, 1 ) # Point 3 - - tetra = vtkTetra() - # Switch two points to create negative volume - tetra.GetPointIds().SetId( 0, 0 ) - tetra.GetPointIds().SetId( 1, 2 ) # Swapped from normal order - tetra.GetPointIds().SetId( 2, 1 ) # Swapped from normal order - tetra.GetPointIds().SetId( 3, 3 ) - - ug = vtkUnstructuredGrid() - ug.SetPoints( points ) - ug.InsertNextCell( tetra.GetCellType(), tetra.GetPointIds() ) - return ug - - -@pytest.fixture -def zero_vol_mesh() -> vtkUnstructuredGrid: - """Create a tetrahedron with zero volume (coplanar points)""" - points = vtkPoints() - points.InsertNextPoint( 0, 0, 0 ) # Point 0 - points.InsertNextPoint( 1, 0, 0 ) # Point 1 - points.InsertNextPoint( 0, 1, 0 ) # Point 2 - points.InsertNextPoint( 1, 1, 0 ) # Point 3 (coplanar with others) - - tetra = vtkTetra() - tetra.GetPointIds().SetId( 0, 0 ) - tetra.GetPointIds().SetId( 1, 1 ) - tetra.GetPointIds().SetId( 2, 2 ) - tetra.GetPointIds().SetId( 3, 3 ) - - ug = vtkUnstructuredGrid() - ug.SetPoints( points ) - ug.InsertNextCell( tetra.GetCellType(), tetra.GetPointIds() ) - return ug - - -@pytest.fixture -def volume_filter() -> ElementVolumes: - """Create a fresh ElementVolumes filter for each test""" - return ElementVolumes() - - -def test_tetrahedron_volume( tetra_mesh: vtkUnstructuredGrid, volume_filter: ElementVolumes ) -> None: - """Test volume calculation for a regular tetrahedron""" - volume_filter.SetInputDataObject( 0, tetra_mesh ) - volume_filter.Update() - output: vtkUnstructuredGrid = volume_filter.getGrid() - - volumes: npt.NDArray = vtk_to_numpy( output.GetCellData().GetArray( "MESH_DOCTOR_VOLUME" ) ) - expected_volume: float = 1 / 6 # Tetrahedron volume - - assert len( volumes ) == 1 - assert volumes[ 0 ] == pytest.approx( expected_volume, abs=1e-6 ) - - -def test_hexahedron_volume( hexa_mesh: vtkUnstructuredGrid, volume_filter: ElementVolumes ) -> None: - """Test volume calculation for a regular hexahedron""" - volume_filter.SetInputDataObject( 0, hexa_mesh ) - volume_filter.Update() - output: vtkUnstructuredGrid = volume_filter.getGrid() - - volumes: npt.NDArray = vtk_to_numpy( output.GetCellData().GetArray( "MESH_DOCTOR_VOLUME" ) ) - expected_volume: float = 1.0 # Unit cube volume - - assert len( volumes ) == 1 - assert volumes[ 0 ] == pytest.approx( expected_volume, abs=1e-6 ) - - -def test_negative_volume_detection( negative_vol_mesh: vtkUnstructuredGrid, volume_filter: ElementVolumes ) -> None: - """Test detection of negative volumes""" - volume_filter.SetInputDataObject( 0, negative_vol_mesh ) - volume_filter.setReturnNegativeZeroVolumes( True ) - volume_filter.Update() - - output: vtkUnstructuredGrid = volume_filter.getGrid() - volumes: npt.NDArray = vtk_to_numpy( output.GetCellData().GetArray( "MESH_DOCTOR_VOLUME" ) ) - - assert len( volumes ) == 1 - assert volumes[ 0 ] < 0 - - # Test getNegativeZeroVolumes method - negative_zero_volumes: npt.NDArray = volume_filter.getNegativeZeroVolumes() - assert len( negative_zero_volumes ) == 1 - assert negative_zero_volumes[ 0, 0 ] == 0 # First cell index - assert negative_zero_volumes[ 0, 1 ] == volumes[ 0 ] # Volume value - - -def test_zero_volume_detection( zero_vol_mesh: vtkUnstructuredGrid, volume_filter: ElementVolumes ) -> None: - """Test detection of zero volumes""" - volume_filter.SetInputDataObject( 0, zero_vol_mesh ) - volume_filter.setReturnNegativeZeroVolumes( True ) - volume_filter.Update() - - output: vtkUnstructuredGrid = volume_filter.getGrid() - volumes: npt.NDArray = vtk_to_numpy( output.GetCellData().GetArray( "MESH_DOCTOR_VOLUME" ) ) - - assert len( volumes ) == 1 - assert volumes[ 0 ] == pytest.approx( 0.0, abs=1e-6 ) - - # Test getNegativeZeroVolumes method - negative_zero_volumes: npt.NDArray = volume_filter.getNegativeZeroVolumes() - assert len( negative_zero_volumes ) == 1 - assert negative_zero_volumes[ 0, 0 ] == 0 # First cell index - assert negative_zero_volumes[ 0, 1 ] == pytest.approx( 0.0, abs=1e-6 ) # Volume value - - -def test_return_negative_zero_volumes_flag( volume_filter: ElementVolumes ) -> None: - """Test setting and getting the returnNegativeZeroVolumes flag""" - # Default should be False - assert not volume_filter.m_returnNegativeZeroVolumes - - # Set to True and verify - volume_filter.setReturnNegativeZeroVolumes( True ) - assert volume_filter.m_returnNegativeZeroVolumes - - # Set to False and verify - volume_filter.setReturnNegativeZeroVolumes( False ) - assert not volume_filter.m_returnNegativeZeroVolumes - - -def test_mixed_mesh( tetra_mesh: vtkUnstructuredGrid, hexa_mesh: vtkUnstructuredGrid, - volume_filter: ElementVolumes ) -> None: - """Test with a combined mesh containing multiple element types""" - # Create a mixed mesh with both tet and hex - mixed_mesh = vtkUnstructuredGrid() - - # Copy points from tetra_mesh - tetra_points: vtkPoints = tetra_mesh.GetPoints() - points = vtkPoints() - for i in range( tetra_points.GetNumberOfPoints() ): - points.InsertNextPoint( tetra_points.GetPoint( i ) ) - - # Add points from hexa_mesh with offset - hexa_points: vtkPoints = hexa_mesh.GetPoints() - offset: int = points.GetNumberOfPoints() - for i in range( hexa_points.GetNumberOfPoints() ): - x, y, z = hexa_points.GetPoint( i ) - points.InsertNextPoint( x + 2, y, z ) # Offset in x-direction - - mixed_mesh.SetPoints( points ) - - # Add tetra cell - tetra_cell: vtkTetra = tetra_mesh.GetCell( 0 ) - ids: vtkIdList = tetra_cell.GetPointIds() - mixed_mesh.InsertNextCell( tetra_cell.GetCellType(), ids.GetNumberOfIds(), - [ ids.GetId( i ) for i in range( ids.GetNumberOfIds() ) ] ) - - # Add hexa cell with offset ids - hexa_cell: vtkHexahedron = hexa_mesh.GetCell( 0 ) - ids: vtkIdList = hexa_cell.GetPointIds() - hexa_ids: list[ int ] = [ ids.GetId( i ) + offset for i in range( ids.GetNumberOfIds() ) ] - mixed_mesh.InsertNextCell( hexa_cell.GetCellType(), len( hexa_ids ), hexa_ids ) - - # Apply filter - volume_filter.SetInputDataObject( 0, mixed_mesh ) - volume_filter.Update() - output: vtkUnstructuredGrid = volume_filter.getGrid() - - # Check volumes - volumes: npt.NDArray = vtk_to_numpy( output.GetCellData().GetArray( "MESH_DOCTOR_VOLUME" ) ) - - assert len( volumes ) == 2 - assert volumes[ 0 ] == pytest.approx( 1 / 6, abs=1e-6 ) # Tetrahedron volume - assert volumes[ 1 ] == pytest.approx( 1.0, abs=1e-6 ) # Hexahedron volume - - -def test_simple_tet(): - # creating a simple tetrahedron - points = vtkPoints() - points.SetNumberOfPoints( 4 ) - points.SetPoint( 0, ( 0, 0, 0 ) ) - points.SetPoint( 1, ( 1, 0, 0 ) ) - points.SetPoint( 2, ( 0, 1, 0 ) ) - points.SetPoint( 3, ( 0, 0, 1 ) ) - - cell_types = [ VTK_TETRA ] - cells = vtkCellArray() - cells.AllocateExact( 1, 4 ) - - tet = vtkTetra() - tet.GetPointIds().SetId( 0, 0 ) - tet.GetPointIds().SetId( 1, 1 ) - tet.GetPointIds().SetId( 2, 2 ) - tet.GetPointIds().SetId( 3, 3 ) - cells.InsertNextCell( tet ) - - mesh = vtkUnstructuredGrid() - mesh.SetPoints( points ) - mesh.SetCells( cell_types, cells ) - - result = mesh_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 * np.finfo( float ).eps - - result = mesh_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 22d0ef06b..3e877eb7f 100644 --- a/geos-mesh/tests/test_generate_cube.py +++ b/geos-mesh/tests/test_generate_cube.py @@ -1,47 +1,4 @@ -import pytest -from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, vtkPointData, vtkCellData -from vtkmodules.vtkCommonCore import vtkDataArray from geos.mesh.doctor.actions.generate_cube import FieldInfo, Options, __build -from geos.mesh.doctor.filters.GenerateRectilinearGrid import GenerateRectilinearGrid - - -@pytest.fixture -def generate_rectilinear_grid_filter() -> GenerateRectilinearGrid: - filter = GenerateRectilinearGrid() - filter.setCoordinates( [ 0.0, 5.0, 10.0 ], [ 0.0, 10.0, 20.0 ], [ 0.0, 50.0 ] ) - filter.setNumberElements( [ 5, 5 ], [ 5, 5 ], [ 10 ] ) # 10 cells along X, Y, Z axis - filter.setGenerateCellsGlobalIds( True ) - filter.setGeneratePointsGlobalIds( True ) - - cells_dim1 = FieldInfo( "cell1", 1, "CELLS" ) - cells_dim3 = FieldInfo( "cell3", 3, "CELLS" ) - points_dim1 = FieldInfo( "point1", 1, "POINTS" ) - points_dim3 = FieldInfo( "point3", 3, "POINTS" ) - filter.setFields( [ cells_dim1, cells_dim3, points_dim1, points_dim3 ] ) - - return filter - - -def test_generate_rectilinear_grid( generate_rectilinear_grid_filter: GenerateRectilinearGrid ) -> None: - generate_rectilinear_grid_filter.Update() - mesh = generate_rectilinear_grid_filter.GetOutputDataObject( 0 ) - - assert isinstance( mesh, vtkUnstructuredGrid ) - assert mesh.GetNumberOfCells() == 1000 - assert mesh.GetNumberOfPoints() == 1331 - assert mesh.GetBounds() == ( 0.0, 10.0, 0.0, 20.0, 0.0, 50.0 ) - - pointData: vtkPointData = mesh.GetPointData() - ptArray1: vtkDataArray = pointData.GetArray( "point1" ) - ptArray3: vtkDataArray = pointData.GetArray( "point3" ) - assert ptArray1.GetNumberOfComponents() == 1 - assert ptArray3.GetNumberOfComponents() == 3 - - cellData: vtkCellData = mesh.GetCellData() - cellArray1: vtkDataArray = cellData.GetArray( "cell1" ) - cellArray3: vtkDataArray = cellData.GetArray( "cell3" ) - assert cellArray1.GetNumberOfComponents() == 1 - assert cellArray3.GetNumberOfComponents() == 3 def test_generate_cube(): diff --git a/geos-mesh/tests/test_generate_fractures.py b/geos-mesh/tests/test_generate_fractures.py index fb6fd978f..1e36be7e7 100644 --- a/geos-mesh/tests/test_generate_fractures.py +++ b/geos-mesh/tests/test_generate_fractures.py @@ -8,7 +8,6 @@ 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.doctor.filters.GenerateFractures import GenerateFractures from geos.mesh.utils.genericHelpers import to_vtk_id_list FaceNodesCoords = tuple[ tuple[ float ] ] @@ -215,32 +214,6 @@ def test_generate_fracture( test_case: TestCase ): assert len( res ) == test_case.result.fracture_mesh_num_points -@pytest.mark.parametrize( "test_case_filter", __generate_test_data() ) -def test_GenerateFracture( test_case_filter: TestCase ): - genFracFilter = GenerateFractures() - genFracFilter.SetInputDataObject( 0, test_case_filter.input_mesh ) - genFracFilter.setFieldName( test_case_filter.options.field ) - field_values: str = ','.join( map( str, test_case_filter.options.field_values_combined ) ) - genFracFilter.setFieldValues( field_values ) - genFracFilter.setFracturesOutputDirectory( "." ) - if test_case_filter.options.policy == FracturePolicy.FIELD: - genFracFilter.setPolicy( 0 ) - else: - genFracFilter.setPolicy( 1 ) - genFracFilter.Update() - - main_mesh, fracture_meshes = genFracFilter.getAllGrids() - fracture_mesh: vtkUnstructuredGrid = fracture_meshes[ 0 ] - assert main_mesh.GetNumberOfPoints() == test_case_filter.result.main_mesh_num_points - assert main_mesh.GetNumberOfCells() == test_case_filter.result.main_mesh_num_cells - assert fracture_mesh.GetNumberOfPoints() == test_case_filter.result.fracture_mesh_num_points - assert fracture_mesh.GetNumberOfCells() == test_case_filter.result.fracture_mesh_num_cells - - res = format_collocated_nodes( fracture_mesh ) - assert res == test_case_filter.collocated_nodes - assert len( res ) == test_case_filter.result.fracture_mesh_num_points - - def add_simplified_field_for_cells( mesh: vtkUnstructuredGrid, field_name: str, field_dimension: int ): """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. diff --git a/geos-mesh/tests/test_self_intersecting_elements.py b/geos-mesh/tests/test_self_intersecting_elements.py deleted file mode 100644 index 992992223..000000000 --- a/geos-mesh/tests/test_self_intersecting_elements.py +++ /dev/null @@ -1,180 +0,0 @@ -from vtkmodules.vtkCommonCore import vtkPoints -from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkHexahedron, vtkUnstructuredGrid, VTK_HEXAHEDRON -from geos.mesh.doctor.actions.self_intersecting_elements import Options, mesh_action -from geos.mesh.doctor.filters.SelfIntersectingElements import SelfIntersectingElements -import pytest - - -def test_jumbled_hex(): - # creating a simple hexahedron - points = vtkPoints() - points.SetNumberOfPoints( 8 ) - points.SetPoint( 0, ( 0, 0, 0 ) ) - points.SetPoint( 1, ( 1, 0, 0 ) ) - points.SetPoint( 2, ( 1, 1, 0 ) ) - points.SetPoint( 3, ( 0, 1, 0 ) ) - points.SetPoint( 4, ( 0, 0, 1 ) ) - points.SetPoint( 5, ( 1, 0, 1 ) ) - points.SetPoint( 6, ( 1, 1, 1 ) ) - points.SetPoint( 7, ( 0, 1, 1 ) ) - - cell_types = [ VTK_HEXAHEDRON ] - cells = vtkCellArray() - cells.AllocateExact( 1, 8 ) - - hex = vtkHexahedron() - hex.GetPointIds().SetId( 0, 0 ) - hex.GetPointIds().SetId( 1, 1 ) - hex.GetPointIds().SetId( 2, 3 ) # Intentionally wrong - hex.GetPointIds().SetId( 3, 2 ) # Intentionally wrong - hex.GetPointIds().SetId( 4, 4 ) - hex.GetPointIds().SetId( 5, 5 ) - hex.GetPointIds().SetId( 6, 6 ) - hex.GetPointIds().SetId( 7, 7 ) - cells.InsertNextCell( hex ) - - mesh = vtkUnstructuredGrid() - mesh.SetPoints( points ) - mesh.SetCells( cell_types, cells ) - - result = mesh_action( mesh, Options( min_distance=0. ) ) - - assert len( result.intersecting_faces_elements ) == 1 - assert result.intersecting_faces_elements[ 0 ] == 0 - - -@pytest.fixture -def jumbled_hex_mesh(): - """Create a hexahedron with intentionally swapped nodes to create self-intersecting faces.""" - points = vtkPoints() - points.SetNumberOfPoints( 8 ) - points.SetPoint( 0, ( 0, 0, 0 ) ) - points.SetPoint( 1, ( 1, 0, 0 ) ) - points.SetPoint( 2, ( 1, 1, 0 ) ) - points.SetPoint( 3, ( 0, 1, 0 ) ) - points.SetPoint( 4, ( 0, 0, 1 ) ) - points.SetPoint( 5, ( 1, 0, 1 ) ) - points.SetPoint( 6, ( 1, 1, 1 ) ) - points.SetPoint( 7, ( 0, 1, 1 ) ) - - cell_types = [ VTK_HEXAHEDRON ] - cells = vtkCellArray() - cells.AllocateExact( 1, 8 ) - - hex = vtkHexahedron() - hex.GetPointIds().SetId( 0, 0 ) - hex.GetPointIds().SetId( 1, 1 ) - hex.GetPointIds().SetId( 2, 3 ) # Intentionally wrong - hex.GetPointIds().SetId( 3, 2 ) # Intentionally wrong - hex.GetPointIds().SetId( 4, 4 ) - hex.GetPointIds().SetId( 5, 5 ) - hex.GetPointIds().SetId( 6, 6 ) - hex.GetPointIds().SetId( 7, 7 ) - cells.InsertNextCell( hex ) - - mesh = vtkUnstructuredGrid() - mesh.SetPoints( points ) - mesh.SetCells( cell_types, cells ) - return mesh - - -@pytest.fixture -def valid_hex_mesh(): - """Create a properly ordered hexahedron with no self-intersecting faces.""" - points = vtkPoints() - points.SetNumberOfPoints( 8 ) - points.SetPoint( 0, ( 0, 0, 0 ) ) - points.SetPoint( 1, ( 1, 0, 0 ) ) - points.SetPoint( 2, ( 1, 1, 0 ) ) - points.SetPoint( 3, ( 0, 1, 0 ) ) - points.SetPoint( 4, ( 0, 0, 1 ) ) - points.SetPoint( 5, ( 1, 0, 1 ) ) - points.SetPoint( 6, ( 1, 1, 1 ) ) - points.SetPoint( 7, ( 0, 1, 1 ) ) - - cell_types = [ VTK_HEXAHEDRON ] - cells = vtkCellArray() - cells.AllocateExact( 1, 8 ) - - hex = vtkHexahedron() - for i in range( 8 ): - hex.GetPointIds().SetId( i, i ) - cells.InsertNextCell( hex ) - - mesh = vtkUnstructuredGrid() - mesh.SetPoints( points ) - mesh.SetCells( cell_types, cells ) - return mesh - - -def test_self_intersecting_elements_filter_detects_invalid_elements( jumbled_hex_mesh ): - """Test that the SelfIntersectingElements filter correctly detects invalid elements.""" - filter = SelfIntersectingElements() - filter.setMinDistance( 0.0 ) - filter.SetInputDataObject( 0, jumbled_hex_mesh ) - filter.Update() - - output = filter.getGrid() - # Check that the filter detected the invalid element - intersecting_faces = filter.getIntersectingFacesElements() - assert len( intersecting_faces ) == 1 - assert intersecting_faces[ 0 ] == 0 - - # Check that output mesh has same structure - assert output.GetNumberOfCells() == 1 - assert output.GetNumberOfPoints() == 8 - - -def test_self_intersecting_elements_filter_valid_mesh( valid_hex_mesh ): - """Test that the SelfIntersectingElements filter finds no issues in a valid mesh.""" - filter = SelfIntersectingElements() - filter.setMinDistance( 1e-12 ) # Use small tolerance instead of 0.0 - filter.SetInputDataObject( 0, valid_hex_mesh ) - filter.Update() - - output = filter.getGrid() - # Check that no invalid elements were found - assert len( filter.getIntersectingFacesElements() ) == 0 - assert len( filter.getWrongNumberOfPointsElements() ) == 0 - assert len( filter.getIntersectingEdgesElements() ) == 0 - assert len( filter.getNonContiguousEdgesElements() ) == 0 - assert len( filter.getNonConvexElements() ) == 0 - assert len( filter.getFacesOrientedIncorrectlyElements() ) == 0 - - # Check that output mesh has same structure - assert output.GetNumberOfCells() == 1 - assert output.GetNumberOfPoints() == 8 - - -def test_self_intersecting_elements_filter_paint_invalid_elements( jumbled_hex_mesh ): - """Test that the SelfIntersectingElements filter can paint invalid elements.""" - filter = SelfIntersectingElements() - filter.setMinDistance( 0.0 ) - filter.setPaintInvalidElements( 1 ) # Enable painting - filter.SetInputDataObject( 0, jumbled_hex_mesh ) - filter.Update() - - output = filter.getGrid() - # Check that painting arrays were added to the output - cell_data = output.GetCellData() - - # Should have arrays marking the invalid elements - # The exact array names depend on the implementation - assert cell_data.GetNumberOfArrays() > 0 - - # Check that invalid elements were detected - intersecting_faces = filter.getIntersectingFacesElements() - assert len( intersecting_faces ) == 1 - - -def test_self_intersecting_elements_filter_getters_setters(): - """Test getter and setter methods of the SelfIntersectingElements filter.""" - filter = SelfIntersectingElements() - - # Test min distance getter/setter - filter.setMinDistance( 0.5 ) - assert filter.getMinDistance() == 0.5 - - # Test paint invalid elements setter (no getter available) - filter.setPaintInvalidElements( 1 ) - # No exception should be raised diff --git a/geos-mesh/tests/test_supported_elements.py b/geos-mesh/tests/test_supported_elements.py deleted file mode 100644 index 297c3899b..000000000 --- a/geos-mesh/tests/test_supported_elements.py +++ /dev/null @@ -1,395 +0,0 @@ -# import os -import pytest -import numpy as np -from typing import Tuple -from vtkmodules.vtkCommonCore import vtkIdList, vtkPoints -from vtkmodules.vtkCommonDataModel import ( vtkUnstructuredGrid, vtkQuad, vtkTetra, vtkHexahedron, vtkPolyhedron, - vtkCellArray, VTK_POLYHEDRON, VTK_QUAD, VTK_TETRA, VTK_HEXAHEDRON ) -# 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.doctor.filters.SupportedElements import SupportedElements -from geos.mesh.utils.genericHelpers import to_vtk_id_list - - -# TODO Update this test to have access to another meshTests file -@pytest.mark.parametrize( "base_name", ( "supportedElements.vtk", "supportedElementsAsVTKPolyhedra.vtk" ) ) -def test_supported_elements( base_name ) -> None: - """Testing that the supported elements are properly detected as supported! - :param base_name: Supported elements are provided as standard elements or polyhedron elements. - """ - ... - # directory = os.path.dirname( os.path.realpath( __file__ ) ) - # supported_elements_file_name = os.path.join( directory, "../../../../unitTests/meshTests", base_name ) - # options = Options( chunk_size=1, num_proc=4 ) - # result = check( supported_elements_file_name, options ) - # assert not result.unsupported_std_elements_types - # assert not result.unsupported_polyhedron_elements - - -def make_dodecahedron() -> Tuple[ vtkPoints, vtkIdList ]: - """Returns the points and faces for a dodecahedron. - This code was adapted from an official vtk example. - :return: The tuple of points and faces (as vtk instances). - """ - # yapf: disable - points = ( - (1.21412, 0, 1.58931), - (0.375185, 1.1547, 1.58931), - (-0.982247, 0.713644, 1.58931), - (-0.982247, -0.713644, 1.58931), - (0.375185, -1.1547, 1.58931), - (1.96449, 0, 0.375185), - (0.607062, 1.86835, 0.375185), - (-1.58931, 1.1547, 0.375185), - (-1.58931, -1.1547, 0.375185), - (0.607062, -1.86835, 0.375185), - (1.58931, 1.1547, -0.375185), - (-0.607062, 1.86835, -0.375185), - (-1.96449, 0, -0.375185), - (-0.607062, -1.86835, -0.375185), - (1.58931, -1.1547, -0.375185), - (0.982247, 0.713644, -1.58931), - (-0.375185, 1.1547, -1.58931), - (-1.21412, 0, -1.58931), - (-0.375185, -1.1547, -1.58931), - (0.982247, -0.713644, -1.58931) - ) - - faces = (12, # number of faces - 5, 0, 1, 2, 3, 4, # number of ids on face, ids - 5, 0, 5, 10, 6, 1, - 5, 1, 6, 11, 7, 2, - 5, 2, 7, 12, 8, 3, - 5, 3, 8, 13, 9, 4, - 5, 4, 9, 14, 5, 0, - 5, 15, 10, 5, 14, 19, - 5, 16, 11, 6, 10, 15, - 5, 17, 12, 7, 11, 16, - 5, 18, 13, 8, 12, 17, - 5, 19, 14, 9, 13, 18, - 5, 19, 18, 17, 16, 15) - # yapf: enable - - p = vtkPoints() - p.Allocate( len( points ) ) - for coords in points: - p.InsertNextPoint( coords ) - - f = to_vtk_id_list( faces ) - - return p, f - - -# TODO make this test work -def test_dodecahedron() -> None: - """Tests whether a dodecahedron is support by GEOS or not. - """ - points, faces = make_dodecahedron() - mesh = vtkUnstructuredGrid() - mesh.Allocate( 1 ) - mesh.SetPoints( points ) - mesh.InsertNextCell( VTK_POLYHEDRON, faces ) - - # TODO Why does __check triggers an assertion error with 'assert MESH is not None' ? - # result = __check( mesh, Options( num_proc=1, chunk_size=1 ) ) - # assert set( result.unsupported_polyhedron_elements ) == { 0 } - # assert not result.unsupported_std_elements_types - - -def test_parse_face_stream() -> None: - _, faces = make_dodecahedron() - result = parse_face_stream( faces ) - # yapf: disable - expected = ( - (0, 1, 2, 3, 4), - (0, 5, 10, 6, 1), - (1, 6, 11, 7, 2), - (2, 7, 12, 8, 3), - (3, 8, 13, 9, 4), - (4, 9, 14, 5, 0), - (15, 10, 5, 14, 19), - (16, 11, 6, 10, 15), - (17, 12, 7, 11, 16), - (18, 13, 8, 12, 17), - (19, 14, 9, 13, 18), - (19, 18, 17, 16, 15) - ) - # yapf: enable - assert result == expected - face_stream = FaceStream.build_from_vtk_id_list( faces ) - assert face_stream.num_faces == 12 - assert face_stream.num_support_points == 20 - - -def create_simple_tetra_grid(): - """Create a simple tetrahedral grid for testing""" - # Create an unstructured grid - points_tetras: vtkPoints = vtkPoints() - points_tetras_coords: list[ tuple[ float ] ] = [ - ( 1.0, 0.5, 0.0 ), # point0 - ( 1.0, 0.0, 1.0 ), - ( 1.0, 1.0, 1.0 ), - ( 0.0, 0.5, 0.5 ), - ( 2.0, 0.5, 0.5 ), - ( 1.0, 0.5, 2.0 ), # point5 - ( 0.0, 0.5, 1.5 ), - ( 2.0, 0.5, 1.5 ) - ] - for point_tetra in points_tetras_coords: - points_tetras.InsertNextPoint( point_tetra ) - - tetra1: vtkTetra = vtkTetra() - tetra1.GetPointIds().SetId( 0, 0 ) - tetra1.GetPointIds().SetId( 1, 1 ) - tetra1.GetPointIds().SetId( 2, 2 ) - tetra1.GetPointIds().SetId( 3, 3 ) - - tetra2: vtkTetra = vtkTetra() - tetra2.GetPointIds().SetId( 0, 0 ) - tetra2.GetPointIds().SetId( 1, 2 ) - tetra2.GetPointIds().SetId( 2, 1 ) - tetra2.GetPointIds().SetId( 3, 4 ) - - tetra3: vtkTetra = vtkTetra() - tetra3.GetPointIds().SetId( 0, 1 ) - tetra3.GetPointIds().SetId( 1, 5 ) - tetra3.GetPointIds().SetId( 2, 2 ) - tetra3.GetPointIds().SetId( 3, 6 ) - - tetra4: vtkTetra = vtkTetra() - tetra4.GetPointIds().SetId( 0, 1 ) - tetra4.GetPointIds().SetId( 1, 2 ) - tetra4.GetPointIds().SetId( 2, 5 ) - tetra4.GetPointIds().SetId( 3, 7 ) - - tetras_cells: vtkCellArray = vtkCellArray() - tetras_cells.InsertNextCell( tetra1 ) - tetras_cells.InsertNextCell( tetra2 ) - tetras_cells.InsertNextCell( tetra3 ) - tetras_cells.InsertNextCell( tetra4 ) - - tetras_grid: vtkUnstructuredGrid = vtkUnstructuredGrid() - tetras_grid.SetPoints( points_tetras ) - tetras_grid.SetCells( VTK_TETRA, tetras_cells ) - return tetras_grid - - -def create_mixed_grid(): - """Create a grid with supported and unsupported cell types, 4 Hexahedrons with 2 quad fracs vertical""" - # Create an unstructured grid - four_hexs_points: vtkPoints = vtkPoints() - four_hexs_points_coords: list[ tuple[ float ] ] = [ - ( 0.0, 0.0, 0.0 ), # point0 - ( 1.0, 0.0, 0.0 ), # point1 - ( 2.0, 0.0, 0.0 ), # point2 - ( 0.0, 1.0, 0.0 ), # point3 - ( 1.0, 1.0, 0.0 ), # point4 - ( 2.0, 1.0, 0.0 ), # point5 - ( 0.0, 0.0, 1.0 ), # point6 - ( 1.0, 0.0, 1.0 ), # point7 - ( 2.0, 0.0, 1.0 ), # point8 - ( 0.0, 1.0, 1.0 ), # point9 - ( 1.0, 1.0, 1.0 ), # point10 - ( 2.0, 1.0, 1.0 ), # point11 - ( 0.0, 0.0, 2.0 ), # point12 - ( 1.0, 0.0, 2.0 ), # point13 - ( 2.0, 0.0, 2.0 ), # point14 - ( 0.0, 1.0, 2.0 ), # point15 - ( 1.0, 1.0, 2.0 ), # point16 - ( 2.0, 1.0, 2.0 ) - ] - for four_hexs_point in four_hexs_points_coords: - four_hexs_points.InsertNextPoint( four_hexs_point ) - - # hex1 - four_hex1: vtkHexahedron = vtkHexahedron() - four_hex1.GetPointIds().SetId( 0, 0 ) - four_hex1.GetPointIds().SetId( 1, 1 ) - four_hex1.GetPointIds().SetId( 2, 4 ) - four_hex1.GetPointIds().SetId( 3, 3 ) - four_hex1.GetPointIds().SetId( 4, 6 ) - four_hex1.GetPointIds().SetId( 5, 7 ) - four_hex1.GetPointIds().SetId( 6, 10 ) - four_hex1.GetPointIds().SetId( 7, 9 ) - - # hex2 - four_hex2: vtkHexahedron = vtkHexahedron() - four_hex2.GetPointIds().SetId( 0, 0 + 1 ) - four_hex2.GetPointIds().SetId( 1, 1 + 1 ) - four_hex2.GetPointIds().SetId( 2, 4 + 1 ) - four_hex2.GetPointIds().SetId( 3, 3 + 1 ) - four_hex2.GetPointIds().SetId( 4, 6 + 1 ) - four_hex2.GetPointIds().SetId( 5, 7 + 1 ) - four_hex2.GetPointIds().SetId( 6, 10 + 1 ) - four_hex2.GetPointIds().SetId( 7, 9 + 1 ) - - # hex3 - four_hex3: vtkHexahedron = vtkHexahedron() - four_hex3.GetPointIds().SetId( 0, 0 + 6 ) - four_hex3.GetPointIds().SetId( 1, 1 + 6 ) - four_hex3.GetPointIds().SetId( 2, 4 + 6 ) - four_hex3.GetPointIds().SetId( 3, 3 + 6 ) - four_hex3.GetPointIds().SetId( 4, 6 + 6 ) - four_hex3.GetPointIds().SetId( 5, 7 + 6 ) - four_hex3.GetPointIds().SetId( 6, 10 + 6 ) - four_hex3.GetPointIds().SetId( 7, 9 + 6 ) - - # hex4 - four_hex4: vtkHexahedron = vtkHexahedron() - four_hex4.GetPointIds().SetId( 0, 0 + 7 ) - four_hex4.GetPointIds().SetId( 1, 1 + 7 ) - four_hex4.GetPointIds().SetId( 2, 4 + 7 ) - four_hex4.GetPointIds().SetId( 3, 3 + 7 ) - four_hex4.GetPointIds().SetId( 4, 6 + 7 ) - four_hex4.GetPointIds().SetId( 5, 7 + 7 ) - four_hex4.GetPointIds().SetId( 6, 10 + 7 ) - four_hex4.GetPointIds().SetId( 7, 9 + 7 ) - - # quad1 - four_hex_quad1: vtkQuad = vtkQuad() - four_hex_quad1.GetPointIds().SetId( 0, 1 ) - four_hex_quad1.GetPointIds().SetId( 1, 4 ) - four_hex_quad1.GetPointIds().SetId( 2, 10 ) - four_hex_quad1.GetPointIds().SetId( 3, 7 ) - - # quad2 - four_hex_quad2: vtkQuad = vtkQuad() - four_hex_quad2.GetPointIds().SetId( 0, 1 + 6 ) - four_hex_quad2.GetPointIds().SetId( 1, 4 + 6 ) - four_hex_quad2.GetPointIds().SetId( 2, 10 + 6 ) - four_hex_quad2.GetPointIds().SetId( 3, 7 + 6 ) - - four_hex_grid_2_quads = vtkUnstructuredGrid() - four_hex_grid_2_quads.SetPoints( four_hexs_points ) - all_cell_types_four_hex_grid_2_quads = [ VTK_HEXAHEDRON ] * 4 + [ VTK_QUAD ] * 2 - all_cells_four_hex_grid_2_quads = [ four_hex1, four_hex2, four_hex3, four_hex4, four_hex_quad1, four_hex_quad2 ] - for cell_type, cell in zip( all_cell_types_four_hex_grid_2_quads, all_cells_four_hex_grid_2_quads ): - four_hex_grid_2_quads.InsertNextCell( cell_type, cell.GetPointIds() ) - return four_hex_grid_2_quads - - -def create_unsupported_polyhedron_grid(): - """Create a grid with an unsupported polyhedron (non-convex)""" - grid = vtkUnstructuredGrid() - # Create points for the grid - points = vtkPoints() # Need to import vtkPoints - # Create points for a non-convex polyhedron - point_coords = np.array( [ - [ 0.0, 0.0, 0.0 ], # 0 - [ 1.0, 0.0, 0.0 ], # 1 - [ 1.0, 1.0, 0.0 ], # 2 - [ 0.0, 1.0, 0.0 ], # 3 - [ 0.0, 0.0, 1.0 ], # 4 - [ 1.0, 0.0, 1.0 ], # 5 - [ 1.0, 1.0, 1.0 ], # 6 - [ 0.0, 1.0, 1.0 ], # 7 - [ 0.5, 0.5, -0.5 ] # 8 (point makes it non-convex) - ] ) - # Add points to the points array - for point in point_coords: - points.InsertNextPoint( point ) - # Set the points in the grid - grid.SetPoints( points ) - # Create a polyhedron - polyhedron = vtkPolyhedron() - # For simplicity, we'll create a polyhedron that would be recognized as unsupported - # This is a simplified example - you may need to adjust based on your actual implementation - polyhedron.GetPointIds().SetNumberOfIds( 9 ) - for i in range( 9 ): - polyhedron.GetPointIds().SetId( i, i ) - # Add the polyhedron to the grid - grid.InsertNextCell( polyhedron.GetCellType(), polyhedron.GetPointIds() ) - return grid - - -# TODO reimplement once SupportedElements can handle multiprocessing -# class TestSupportedElements: - -# def test_only_supported_elements( self ): -# """Test a grid with only supported element types""" -# # Create grid with only supported elements (tetra) -# grid = create_simple_tetra_grid() -# # Apply the filter -# filter = SupportedElements() -# filter.SetInputDataObject( grid ) -# filter.Update() -# result = filter.getGrid() -# assert result is not None -# # Verify no arrays were added (since all elements are supported) -# assert result.GetCellData().GetArray( "HasUnsupportedType" ) is None -# assert result.GetCellData().GetArray( "IsUnsupportedPolyhedron" ) is None - -# def test_unsupported_element_types( self ): -# """Test a grid with unsupported element types""" -# # Create grid with unsupported elements -# grid = create_mixed_grid() -# # Apply the filter with painting enabled -# filter = SupportedElements() -# filter.m_logger.critical( "test_unsupported_element_types" ) -# filter.SetInputDataObject( grid ) -# filter.setPaintUnsupportedElementTypes( 1 ) -# filter.Update() -# result = filter.getGrid() -# assert result is not None -# # Verify the array was added -# unsupported_array = result.GetCellData().GetArray( "HasUnsupportedType" ) -# assert unsupported_array is not None -# for i in range( 0, 4 ): -# assert unsupported_array.GetValue( i ) == 0 # Hexahedron should be supported -# for j in range( 4, 6 ): -# assert unsupported_array.GetValue( j ) == 1 # Quad should not be supported - -# TODO Needs parallelism to work -# def test_unsupported_polyhedron( self ): -# """Test a grid with unsupported polyhedron""" -# # Create grid with unsupported polyhedron -# grid = create_unsupported_polyhedron_grid() -# # Apply the filter with painting enabled -# filter = SupportedElements() -# filter.m_logger.critical( "test_unsupported_polyhedron" ) -# filter.SetInputDataObject( grid ) -# filter.setPaintUnsupportedPolyhedrons( 1 ) -# filter.Update() -# result = filter.getGrid() -# assert result is not None -# # Verify the array was added -# polyhedron_array = result.GetCellData().GetArray( "IsUnsupportedPolyhedron" ) -# assert polyhedron_array is None -# # Since we created an unsupported polyhedron, it should be marked -# assert polyhedron_array.GetValue( 0 ) == 1 - -# def test_paint_flags( self ): -# """Test setting invalid paint flags""" -# filter = SupportedElements() -# # Should log an error but not raise an exception -# filter.setPaintUnsupportedElementTypes( 2 ) # Invalid value -# filter.setPaintUnsupportedPolyhedrons( 2 ) # Invalid value -# # Values should remain unchanged -# assert filter.m_paintUnsupportedElementTypes == 0 -# assert filter.m_paintUnsupportedPolyhedrons == 0 - -# def test_set_chunk_size( self ): -# """Test that setChunkSize properly updates the chunk size""" -# # Create filter instance -# filter = SupportedElements() -# # Note the initial value -# initial_chunk_size = filter.m_chunk_size -# # Set a new chunk size -# new_chunk_size = 100 -# filter.setChunkSize( new_chunk_size ) -# # Verify the chunk size was updated -# assert filter.m_chunk_size == new_chunk_size -# assert filter.m_chunk_size != initial_chunk_size - -# def test_set_num_proc( self ): -# """Test that setNumProc properly updates the number of processors""" -# # Create filter instance -# filter = SupportedElements() -# # Note the initial value -# initial_num_proc = filter.m_num_proc -# # Set a new number of processors -# new_num_proc = 4 -# filter.setNumProc( new_num_proc ) -# # Verify the number of processors was updated -# assert filter.m_num_proc == new_num_proc -# assert filter.m_num_proc != initial_num_proc From 88ccfce61d4a3138af4dee5c9cd2edb5b0c759b7 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 2 Sep 2025 18:02:05 -0700 Subject: [PATCH 35/52] Update test_generate_cube to include GenerateRectilinearGrid --- geos-mesh/tests/test_generate_cube.py | 114 ++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/geos-mesh/tests/test_generate_cube.py b/geos-mesh/tests/test_generate_cube.py index 3e877eb7f..59b82d1d3 100644 --- a/geos-mesh/tests/test_generate_cube.py +++ b/geos-mesh/tests/test_generate_cube.py @@ -1,4 +1,7 @@ from geos.mesh.doctor.actions.generate_cube import FieldInfo, Options, __build +from geos.mesh.doctor.filters.GenerateRectilinearGrid import GenerateRectilinearGrid, generateRectilinearGrid +import tempfile +import os def test_generate_cube(): @@ -18,3 +21,114 @@ def test_generate_cube(): assert output.GetCellData().GetArray( "test" ).GetNumberOfComponents() == 2 assert output.GetCellData().GetGlobalIds() assert not output.GetPointData().GetGlobalIds() + + +def test_generate_rectilinear_grid_filter(): + """Test the GenerateRectilinearGrid filter class.""" + # Create filter instance + filter_instance = GenerateRectilinearGrid( generateCellsGlobalIds=True, generatePointsGlobalIds=True ) + + # Set coordinates and number of elements + filter_instance.setCoordinates( [ 0.0, 5.0, 10.0 ], [ 0.0, 5.0, 10.0 ], [ 0.0, 10.0 ] ) + filter_instance.setNumberElements( [ 5, 5 ], [ 5, 5 ], [ 10 ] ) + + # Add fields + cells_dim1 = FieldInfo( "cell1", 1, "CELLS" ) + cells_dim3 = FieldInfo( "cell3", 3, "CELLS" ) + points_dim1 = FieldInfo( "point1", 1, "POINTS" ) + points_dim3 = FieldInfo( "point3", 3, "POINTS" ) + filter_instance.setFields( [ cells_dim1, cells_dim3, points_dim1, points_dim3 ] ) + + # Apply filter + success = filter_instance.applyFilter() + assert success, "Filter should succeed" + + # Get the generated mesh + output_mesh = filter_instance.getMesh() + + # Verify mesh properties + assert output_mesh is not None, "Output mesh should not be None" + assert output_mesh.GetNumberOfCells() == 1000, "Should have 1000 cells (10x10x10)" + assert output_mesh.GetNumberOfPoints() == 1331, "Should have 1331 points (11x11x11)" + + # Verify global IDs + assert output_mesh.GetCellData().GetGlobalIds() is not None, "Should have cell global IDs" + assert output_mesh.GetPointData().GetGlobalIds() is not None, "Should have point global IDs" + + # Verify fields + assert output_mesh.GetCellData().GetArray( "cell1" ) is not None, "Should have cell1 array" + assert output_mesh.GetCellData().GetArray( "cell1" ).GetNumberOfComponents() == 1, "cell1 should have 1 component" + assert output_mesh.GetCellData().GetArray( "cell3" ) is not None, "Should have cell3 array" + assert output_mesh.GetCellData().GetArray( "cell3" ).GetNumberOfComponents() == 3, "cell3 should have 3 components" + + assert output_mesh.GetPointData().GetArray( "point1" ) is not None, "Should have point1 array" + assert output_mesh.GetPointData().GetArray( + "point1" ).GetNumberOfComponents() == 1, "point1 should have 1 component" + assert output_mesh.GetPointData().GetArray( "point3" ) is not None, "Should have point3 array" + assert output_mesh.GetPointData().GetArray( + "point3" ).GetNumberOfComponents() == 3, "point3 should have 3 components" + + +def test_generate_rectilinear_grid_filter_no_global_ids(): + """Test the GenerateRectilinearGrid filter without global IDs.""" + filter_instance = GenerateRectilinearGrid( generateCellsGlobalIds=False, generatePointsGlobalIds=False ) + + filter_instance.setCoordinates( [ 0.0, 2.0 ], [ 0.0, 3.0 ], [ 0.0, 1.0 ] ) + filter_instance.setNumberElements( [ 2 ], [ 3 ], [ 1 ] ) + + success = filter_instance.applyFilter() + assert success, "Filter should succeed" + + output_mesh = filter_instance.getMesh() + + # Verify no global IDs + assert output_mesh.GetCellData().GetGlobalIds() is None, "Should not have cell global IDs" + assert output_mesh.GetPointData().GetGlobalIds() is None, "Should not have point global IDs" + + # Verify basic mesh properties + assert output_mesh.GetNumberOfCells() == 6, "Should have 6 cells (2x3x1)" + assert output_mesh.GetNumberOfPoints() == 24, "Should have 24 points (3x4x2)" + + +def test_generate_rectilinear_grid_filter_missing_parameters(): + """Test that filter fails gracefully when required parameters are missing.""" + filter_instance = GenerateRectilinearGrid() + + # Try to apply filter without setting coordinates + success = filter_instance.applyFilter() + assert not success, "Filter should fail when coordinates are not set" + + +def test_generate_rectilinear_grid_standalone(): + """Test the standalone generateRectilinearGrid function.""" + with tempfile.TemporaryDirectory() as temp_dir: + output_path = os.path.join( temp_dir, "test_grid.vtu" ) + + # Create a simple rectilinear grid + output_mesh = generateRectilinearGrid( coordsX=[ 0.0, 1.0, 2.0 ], + coordsY=[ 0.0, 1.0 ], + coordsZ=[ 0.0, 1.0 ], + numberElementsX=[ 2, 3 ], + numberElementsY=[ 2 ], + numberElementsZ=[ 2 ], + outputPath=output_path, + fields=[ FieldInfo( "test_field", 2, "CELLS" ) ], + generateCellsGlobalIds=True, + generatePointsGlobalIds=False ) + + # Verify mesh properties + assert output_mesh is not None, "Output mesh should not be None" + assert output_mesh.GetNumberOfCells() == 20, "Should have 20 cells (5x2x2)" + assert output_mesh.GetNumberOfPoints() == 54, "Should have 54 points (6x3x3)" + + # Verify field + assert output_mesh.GetCellData().GetArray( "test_field" ) is not None, "Should have test_field array" + test_field_array = output_mesh.GetCellData().GetArray( "test_field" ) + assert test_field_array.GetNumberOfComponents() == 2, "test_field should have 2 components" + + # Verify global IDs + assert output_mesh.GetCellData().GetGlobalIds() is not None, "Should have cell global IDs" + assert output_mesh.GetPointData().GetGlobalIds() is None, "Should not have point global IDs" + + # Verify output file was written + assert os.path.exists( output_path ), "Output file should exist" From 0894a8c4aac0e4ed46c6c24b2814166e86ce85c4 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Tue, 2 Sep 2025 18:02:34 -0700 Subject: [PATCH 36/52] Update test_generate_fractures to include GenerateFractures --- .../mesh/doctor/filters/GenerateFractures.py | 137 +++++++-------- geos-mesh/tests/test_generate_fractures.py | 156 ++++++++++++++++++ 2 files changed, 217 insertions(+), 76 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py index 87a111e6a..84c29ba12 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py @@ -4,8 +4,8 @@ from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorFilterBase from geos.mesh.doctor.parsing.generate_fractures_parsing import convert, convert_to_fracture_policy from geos.mesh.doctor.parsing.generate_fractures_parsing import ( __FIELD_NAME, __FIELD_VALUES, __FRACTURES_DATA_MODE, - __FRACTURES_OUTPUT_DIR, __FRACTURES_DATA_MODE_VALUES, - __POLICIES, __POLICY ) + __FRACTURES_OUTPUT_DIR, __POLICIES, __POLICY ) +from geos.mesh.doctor.parsing.vtk_output_parsing import __OUTPUT_BINARY_MODE_VALUES, __OUTPUT_FILE from geos.mesh.io.vtkIO import VtkOutput, write_mesh from geos.mesh.utils.arrayHelpers import has_array @@ -24,10 +24,12 @@ # instantiate the filter generateFracturesFilter = GenerateFractures( mesh, + policy=1, fieldName="fracture_field", fieldValues="1,2", fracturesOutputDir="./fractures/", - policy=1 + outputDataMode=0, + fracturesDataMode=1 ) # execute the filter @@ -51,10 +53,10 @@ splitMesh, fractureMeshes = generateFractures( mesh, outputPath="output/split_mesh.vtu", + policy=1, fieldName="fracture_field", fieldValues="1,2", fracturesOutputDir="./fractures/", - policy=1, outputDataMode=0, fracturesDataMode=1 ) @@ -63,10 +65,12 @@ FIELD_NAME = __FIELD_NAME FIELD_VALUES = __FIELD_VALUES FRACTURES_DATA_MODE = __FRACTURES_DATA_MODE -DATA_MODE = __FRACTURES_DATA_MODE_VALUES FRACTURES_OUTPUT_DIR = __FRACTURES_OUTPUT_DIR POLICIES = __POLICIES POLICY = __POLICY +OUTPUT_BINARY_MODE = "data_mode" +OUTPUT_BINARY_MODE_VALUES = __OUTPUT_BINARY_MODE_VALUES +OUTPUT_FILE = __OUTPUT_FILE loggerTitle: str = "Generate Fractures Filter" @@ -76,36 +80,48 @@ class GenerateFractures( MeshDoctorFilterBase ): def __init__( self: Self, mesh: vtkUnstructuredGrid, + policy: int = 1, fieldName: str = None, fieldValues: str = None, fracturesOutputDir: str = None, - policy: int = 1, outputDataMode: int = 0, - fracturesDataMode: int = 1, + fracturesDataMode: int = 0, useExternalLogger: bool = False, ) -> None: """Initialize the generate fractures filter. Args: mesh (vtkUnstructuredGrid): The input mesh to split. + policy (int): Fracture policy (0 for field, 1 for internal_surfaces). Defaults to 1. fieldName (str): Field name that defines fracture regions. Defaults to None. fieldValues (str): Comma-separated field values that identify fracture boundaries. Defaults to None. fracturesOutputDir (str): Output directory for fracture meshes. Defaults to None. - policy (int): Fracture policy (0 for internal, 1 for boundary). Defaults to 1. - outputDataMode (int): Data mode for main mesh (0 for ASCII, 1 for binary). Defaults to 0. - fracturesDataMode (int): Data mode for fracture meshes (0 for ASCII, 1 for binary). Defaults to 1. + outputDataMode (int): Data mode for main mesh (0 for binary, 1 for ASCII). Defaults to 0. + fracturesDataMode (int): Data mode for fracture meshes (0 for binary, 1 for ASCII). Defaults to 0. useExternalLogger (bool): Whether to use external logger. Defaults to False. """ super().__init__( mesh, loggerTitle, useExternalLogger ) - self.fieldName: str = fieldName - self.fieldValues: str = fieldValues - self.fracturesOutputDir: str = fracturesOutputDir - self.policy: str = POLICIES[ policy ] if 0 <= policy <= 1 else POLICIES[ 1 ] - self.outputDataMode: str = DATA_MODE[ outputDataMode ] if outputDataMode in [ 0, 1 ] else DATA_MODE[ 0 ] - self.fracturesDataMode: str = ( DATA_MODE[ fracturesDataMode ] - if fracturesDataMode in [ 0, 1 ] else DATA_MODE[ 1 ] ) + if outputDataMode not in [ 0, 1 ]: + self.logger.error( f"Invalid output data mode: {outputDataMode}. Must be 0 (binary) or 1 (ASCII)." + " Set to 0 (binary)." ) + outputDataMode = 0 + if fracturesDataMode not in [ 0, 1 ]: + self.logger.error( f"Invalid fractures data mode: {fracturesDataMode}. Must be 0 (binary) or 1 (ASCII)." + " Set to 0 (binary)." ) + fracturesDataMode = 0 + + self.allOptions: dict[ str, str ] = { + POLICY: convert_to_fracture_policy( POLICIES[ policy ] ), + FIELD_NAME: fieldName, + FIELD_VALUES: fieldValues, + OUTPUT_FILE: "./mesh.vtu", + OUTPUT_BINARY_MODE: OUTPUT_BINARY_MODE_VALUES[ outputDataMode ], + FRACTURES_OUTPUT_DIR: fracturesOutputDir, + FRACTURES_DATA_MODE: OUTPUT_BINARY_MODE_VALUES[ fracturesDataMode ] + } self.fractureMeshes: list[ vtkUnstructuredGrid ] = [] - self.allFracturesVtkOutput: list[ VtkOutput ] = [] + self.mesh: vtkUnstructuredGrid = mesh + self._options: Options = None def applyFilter( self: Self ) -> bool: """Apply the fracture generation. @@ -113,7 +129,7 @@ def applyFilter( self: Self ) -> bool: Returns: bool: True if fractures generated successfully, False otherwise. """ - self.logger.info( f"Apply filter {self.logger.name}" ) + self.logger.info( f"Apply filter {self.logger.name}." ) # Check for global IDs which are not allowed if has_array( self.mesh, [ "GLOBAL_IDS_POINTS", "GLOBAL_IDS_CELLS" ] ): @@ -122,23 +138,15 @@ def applyFilter( self: Self ) -> bool: " The correct procedure is to split the mesh and then generate global ids for new split meshes." ) return False - # Validate required parameters - parsedOptions = self._buildParsedOptions() - if len( parsedOptions ) < 5: - self.logger.error( "You must set all variables before trying to create fractures." ) + try: + self._options = convert( self.allOptions ) + except Exception as e: + self.logger.error( f"Failed to convert options: {e}.\nCannot generate fractures." + "You must set all variables before trying to create fractures." ) return False - self.logger.info( f"Parsed options: {parsedOptions}" ) - - # Convert options and split mesh - options: Options = convert( parsedOptions ) - self.allFracturesVtkOutput = options.all_fractures_VtkOutput - # Perform the fracture generation - output_mesh, self.fractureMeshes = split_mesh_on_fractures( self.mesh, options ) - - # Update the main mesh with the split result - self.mesh = output_mesh + self.mesh, self.fractureMeshes = split_mesh_on_fractures( self.mesh, self._options ) self.logger.info( f"Generated {len(self.fractureMeshes)} fracture meshes." ) self.logger.info( f"The filter {self.logger.name} succeeded." ) @@ -166,7 +174,7 @@ def setFieldName( self: Self, fieldName: str ) -> None: Args: fieldName (str): Name of the field. """ - self.fieldName = fieldName + self.allOptions[ FIELD_NAME ] = fieldName def setFieldValues( self: Self, fieldValues: str ) -> None: """Set the field values that identify fracture boundaries. @@ -174,7 +182,7 @@ def setFieldValues( self: Self, fieldValues: str ) -> None: Args: fieldValues (str): Comma-separated field values. """ - self.fieldValues = fieldValues + self.allOptions[ FIELD_VALUES ] = fieldValues def setFracturesDataMode( self: Self, choice: int ) -> None: """Set the data mode for fracture mesh outputs. @@ -183,10 +191,10 @@ def setFracturesDataMode( self: Self, choice: int ) -> None: choice (int): 0 for ASCII, 1 for binary. """ if choice not in [ 0, 1 ]: - self.logger.error( f"setFracturesDataMode: Please choose either 0 for {DATA_MODE[0]} " - f"or 1 for {DATA_MODE[1]}, not '{choice}'." ) + self.logger.error( f"setFracturesDataMode: Please choose either 0 for {OUTPUT_BINARY_MODE_VALUES[0]} " + f"or 1 for {OUTPUT_BINARY_MODE_VALUES[1]}, not '{choice}'." ) else: - self.fracturesDataMode = DATA_MODE[ choice ] + self.allOptions[ FRACTURES_DATA_MODE ] = OUTPUT_BINARY_MODE_VALUES[ choice ] def setFracturesOutputDirectory( self: Self, directory: str ) -> None: """Set the output directory for fracture meshes. @@ -194,7 +202,7 @@ def setFracturesOutputDirectory( self: Self, directory: str ) -> None: Args: directory (str): Directory path. """ - self.fracturesOutputDir = directory + self.allOptions[ FRACTURES_OUTPUT_DIR ] = directory def setOutputDataMode( self: Self, choice: int ) -> None: """Set the data mode for the main mesh output. @@ -204,10 +212,11 @@ def setOutputDataMode( self: Self, choice: int ) -> None: """ if choice not in [ 0, 1 ]: self.logger.error( - f"setOutputDataMode: Please choose either 0 for {DATA_MODE[0]} or 1 for {DATA_MODE[1]}, not '{choice}'." + f"setOutputDataMode: Please choose either 0 for {OUTPUT_BINARY_MODE_VALUES[0]} or 1 for" + f" {OUTPUT_BINARY_MODE_VALUES[1]}, not '{choice}'." ) else: - self.outputDataMode = DATA_MODE[ choice ] + self.allOptions[ OUTPUT_BINARY_MODE ] = OUTPUT_BINARY_MODE_VALUES[ choice ] def setPolicy( self: Self, choice: int ) -> None: """Set the fracture policy. @@ -221,29 +230,6 @@ def setPolicy( self: Self, choice: int ) -> None: else: self.policy = convert_to_fracture_policy( POLICIES[ choice ] ) - def _buildParsedOptions( self: Self ) -> dict[ str, str ]: - """Build parsed options to be used for an Options object.""" - parsedOptions: dict[ str, str ] = { "output": "./mesh.vtu", "data_mode": DATA_MODE[ 0 ] } - parsedOptions[ POLICY ] = self.policy - parsedOptions[ FRACTURES_DATA_MODE ] = self.fracturesDataMode - - if self.fieldName: - parsedOptions[ FIELD_NAME ] = self.fieldName - else: - self.logger.error( "No field name provided. Please use setFieldName." ) - - if self.fieldValues: - parsedOptions[ FIELD_VALUES ] = self.fieldValues - else: - self.logger.error( "No field values provided. Please use setFieldValues." ) - - if self.fracturesOutputDir: - parsedOptions[ FRACTURES_OUTPUT_DIR ] = self.fracturesOutputDir - else: - self.logger.error( "No fracture output directory provided. Please use setFracturesOutputDirectory." ) - - return parsedOptions - def writeMeshes( self: Self, filepath: str, isDataModeBinary: bool = True, canOverwrite: bool = False ) -> None: """Write both the split main mesh and all fracture meshes. @@ -258,38 +244,37 @@ def writeMeshes( self: Self, filepath: str, isDataModeBinary: bool = True, canOv self.logger.error( f"No output grid was built. Cannot output vtkUnstructuredGrid at {filepath}." ) for i, fractureMesh in enumerate( self.fractureMeshes ): - if i < len( self.allFracturesVtkOutput ): - write_mesh( fractureMesh, self.allFracturesVtkOutput[ i ] ) + write_mesh( fractureMesh, self._options.all_fractures_VtkOutput[ i ] ) # Main function for standalone use def generateFractures( mesh: vtkUnstructuredGrid, outputPath: str, - fieldName: str, - fieldValues: str, - fracturesOutputDir: str, policy: int = 1, + fieldName: str = None, + fieldValues: str = None, + fracturesOutputDir: str = None, outputDataMode: int = 0, - fracturesDataMode: int = 1 + fracturesDataMode: int = 0 ) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: """Apply fracture generation to a mesh. Args: mesh (vtkUnstructuredGrid): The input mesh. outputPath (str): Output file path if write_output is True. - fieldName (str): Field name that defines fracture regions. - fieldValues (str): Comma-separated field values that identify fracture boundaries. - fracturesOutputDir (str): Output directory for fracture meshes. policy (int): Fracture policy (0 for internal, 1 for boundary). Defaults to 1. - outputDataMode (int): Data mode for main mesh (0 for ASCII, 1 for binary). Defaults to 0. - fracturesDataMode (int): Data mode for fracture meshes (0 for ASCII, 1 for binary). Defaults to 1. + fieldName (str): Field name that defines fracture regions. Defaults to None. + fieldValues (str): Comma-separated field values that identify fracture boundaries. Defaults to None. + fracturesOutputDir (str): Output directory for fracture meshes. Defaults to None. + outputDataMode (int): Data mode for main mesh (0 for binary, 1 for ASCII). Defaults to 0. + fracturesDataMode (int): Data mode for fracture meshes (0 for binary, 1 for ASCII). Defaults to 0. Returns: tuple[vtkUnstructuredGrid, list[vtkUnstructuredGrid]]: Split mesh and fracture meshes. """ - filterInstance = GenerateFractures( mesh, fieldName, fieldValues, fracturesOutputDir, policy, outputDataMode, + filterInstance = GenerateFractures( mesh, policy, fieldName, fieldValues, fracturesOutputDir, outputDataMode, fracturesDataMode ) success = filterInstance.applyFilter() diff --git a/geos-mesh/tests/test_generate_fractures.py b/geos-mesh/tests/test_generate_fractures.py index 1e36be7e7..72c840af9 100644 --- a/geos-mesh/tests/test_generate_fractures.py +++ b/geos-mesh/tests/test_generate_fractures.py @@ -1,13 +1,19 @@ from dataclasses import dataclass +import os import numpy import pytest 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 +import tempfile 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.doctor.filters.GenerateFractures import GenerateFractures, generateFractures +from geos.mesh.doctor.filters.GenerateFractures import ( FIELD_NAME, FIELD_VALUES, FRACTURES_DATA_MODE, + FRACTURES_OUTPUT_DIR, OUTPUT_BINARY_MODE, + OUTPUT_BINARY_MODE_VALUES, POLICY ) from geos.mesh.utils.genericHelpers import to_vtk_id_list FaceNodesCoords = tuple[ tuple[ float ] ] @@ -358,3 +364,153 @@ def test_copy_fields_when_splitting_mesh(): with pytest.raises( ValueError ) as pytest_wrapped_e: main_mesh, fracture_meshes = split_mesh_on_fractures( mesh, options ) assert pytest_wrapped_e.type == ValueError + + +""" +Tests for GenerateFractures.py +""" + + +@pytest.mark.parametrize( "test_case", __generate_test_data() ) +def test_generate_fracture_filters_basic( test_case: TestCase ): + with tempfile.TemporaryDirectory() as temp_dir: + fracturesDir = os.path.join( temp_dir, "fractures" ) + os.makedirs( fracturesDir, exist_ok=True ) # Create the directory + policy = 1 if test_case.options.policy == FracturePolicy.INTERNAL_SURFACES else 0 + fieldValues = ','.join( map( str, test_case.options.field_values_combined ) ) + # Create filter instance + filterInstance = GenerateFractures( test_case.input_mesh, + policy=policy, + fieldName=test_case.options.field, + fieldValues=fieldValues, + fracturesOutputDir=fracturesDir ) + # Apply filter + success = filterInstance.applyFilter() + assert success, "Filter should succeed" + # Get results + splitMesh = filterInstance.getMesh() + fractureMeshes = filterInstance.getFractureMeshes() + allGrids = filterInstance.getAllGrids() + # Verify results + assert splitMesh is not None, "Split mesh should not be None" + assert isinstance( fractureMeshes, list ), "Fracture meshes should be a list" + assert allGrids[ 0 ] is splitMesh, "getAllGrids should return split mesh as first element" + assert allGrids[ 1 ] is fractureMeshes, "getAllGrids should return fracture meshes as second element" + + +def test_generate_fractures_filter_setters(): + """Test the setter methods of GenerateFractures filter.""" + # Create a simple test mesh + x = numpy.array( [ 0, 1 ] ) + y = numpy.array( [ 0, 1 ] ) + z = numpy.array( [ 0, 1 ] ) + xyz = XYZ( x, y, z ) + mesh = build_rectilinear_blocks_mesh( [ xyz ] ) + + # Add fracture field + add_simplified_field_for_cells( mesh, "test_field", 1 ) + + # Create filter instance + filterInstance = GenerateFractures( mesh ) + + # Test valid setters + filterInstance.setFieldName( "test_field" ) + assert filterInstance.allOptions[ FIELD_NAME ] == "test_field" + + filterInstance.setFieldValues( "1,2" ) + assert filterInstance.allOptions[ FIELD_VALUES ] == "1,2" + + filterInstance.setFracturesOutputDirectory( "./output/" ) + assert filterInstance.allOptions[ FRACTURES_OUTPUT_DIR ] == "./output/" + + filterInstance.setPolicy( 1 ) + assert filterInstance.allOptions[ POLICY ] == FracturePolicy.INTERNAL_SURFACES + + filterInstance.setOutputDataMode( 1 ) + assert filterInstance.allOptions[ OUTPUT_BINARY_MODE ] == OUTPUT_BINARY_MODE_VALUES[ 1 ] + + filterInstance.setFracturesDataMode( 0 ) + assert filterInstance.allOptions[ FRACTURES_DATA_MODE ] == OUTPUT_BINARY_MODE_VALUES[ 0 ] + + # Test invalid setters + original_policy = filterInstance.allOptions[ POLICY ] + filterInstance.setPolicy( 5 ) # Invalid value + assert filterInstance.allOptions[ POLICY ] == original_policy + + # Test invalid data modes - should not change the values + original_output_mode = filterInstance.allOptions[ OUTPUT_BINARY_MODE ] + filterInstance.setOutputDataMode( 5 ) # Invalid value + assert filterInstance.allOptions[ OUTPUT_BINARY_MODE ] == original_output_mode + + original_fractures_mode = filterInstance.allOptions[ FRACTURES_DATA_MODE ] + filterInstance.setFracturesDataMode( 5 ) # Invalid value + assert filterInstance.allOptions[ FRACTURES_DATA_MODE ] == original_fractures_mode + + +def test_generate_fractures_filter_with_global_ids(): + """Test that filter fails when mesh contains global IDs.""" + # Create a simple test mesh + x = numpy.array( [ 0, 1 ] ) + y = numpy.array( [ 0, 1 ] ) + z = numpy.array( [ 0, 1 ] ) + xyz = XYZ( x, y, z ) + mesh = build_rectilinear_blocks_mesh( [ xyz ] ) + + # Add global IDs (which should cause failure) + points_global_ids = numpy.arange( mesh.GetNumberOfPoints(), dtype=int ) + points_array = numpy_to_vtk( points_global_ids ) + points_array.SetName( "GLOBAL_IDS_POINTS" ) + mesh.GetPointData().AddArray( points_array ) + with tempfile.TemporaryDirectory() as temp_dir: + fractures_dir = os.path.join( temp_dir, "fractures" ) + os.makedirs( fractures_dir, exist_ok=True ) # Create the directory + # Create filter instance + filterInstance = GenerateFractures( mesh ) + # Should fail due to global IDs + success = filterInstance.applyFilter() + assert not success, "Filter should fail when mesh contains global IDs" + + +@pytest.mark.parametrize( "test_case", __generate_test_data() ) +def test_generate_fractures_standalone( test_case: TestCase ): + with tempfile.TemporaryDirectory() as temp_dir: + outputPath = os.path.join( temp_dir, "split_mesh.vtu" ) + fractures_dir = os.path.join( temp_dir, "fractures" ) + os.makedirs( fractures_dir, exist_ok=True ) # Create the directory + policy = 1 if test_case.options.policy == FracturePolicy.INTERNAL_SURFACES else 0 + fieldValues = ','.join( map( str, test_case.options.field_values_combined ) ) + # Create filter instance + splitMesh, fractureMeshes = generateFractures( test_case.input_mesh, + outputPath=outputPath, + policy=policy, + fieldName=test_case.options.field, + fieldValues=fieldValues, + fracturesOutputDir=fractures_dir ) + # Verify results + assert splitMesh is not None, "Split mesh should not be None" + assert isinstance( fractureMeshes, list ), "Fracture meshes should be a list" + # Verify output file was written + assert os.path.exists( outputPath ), "Output file should exist" + + +@pytest.mark.parametrize( "test_case", __generate_test_data() ) +def test_generate_fractures_write_meshes( test_case: TestCase ): + with tempfile.TemporaryDirectory() as temp_dir: + outputPath = os.path.join( temp_dir, "split_mesh.vtu" ) + fracturesDir = os.path.join( temp_dir, "fractures" ) + os.makedirs( fracturesDir, exist_ok=True ) # Create the directory + policy = 1 if test_case.options.policy == FracturePolicy.INTERNAL_SURFACES else 0 + fieldValues = ','.join( map( str, test_case.options.field_values_combined ) ) + # Create filter instance + filterInstance = GenerateFractures( test_case.input_mesh, + policy=policy, + fieldName=test_case.options.field, + fieldValues=fieldValues, + fracturesOutputDir=fracturesDir ) + # Apply filter + success = filterInstance.applyFilter() + assert success, "Filter should succeed" + # Test writing meshes + filterInstance.writeMeshes( outputPath, isDataModeBinary=False, canOverwrite=True ) + # Verify main mesh file was written + assert os.path.exists( outputPath ), "Main mesh file should exist" From f117435e30c0ba828855a3f698ea9125fa7cc3ac Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 3 Sep 2025 09:56:23 -0700 Subject: [PATCH 37/52] Remove condition for writing output grid in standalone functions --- geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py | 3 +-- geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py | 3 +-- geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py | 3 +-- .../src/geos/mesh/doctor/filters/SelfIntersectingElements.py | 3 +-- geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py | 3 +-- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py index 0afa5fdf3..332c01990 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py @@ -162,8 +162,7 @@ def collocatedNodes( """ filterInstance = CollocatedNodes( mesh, tolerance, writeWrongSupportElements ) filterInstance.applyFilter() - if writeWrongSupportElements: # If we are painting wrong support elements, we need to write the output - filterInstance.writeGrid( outputPath ) + filterInstance.writeGrid( outputPath ) return ( filterInstance.getMesh(), diff --git a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py index 6302c1fa5..9c019977d 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py @@ -176,8 +176,7 @@ def elementVolumes( if not success: raise RuntimeError( "Element volumes calculation failed." ) - if writeIsBelowVolume: - filterInstance.writeGrid( outputPath ) + filterInstance.writeGrid( outputPath ) return ( filterInstance.getMesh(), diff --git a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py index 1660a2e6c..3d2549611 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py @@ -216,8 +216,7 @@ def nonConformal( filterInstance = NonConformal( mesh, pointTolerance, faceTolerance, angleTolerance, writeNonConformalCells ) filterInstance.applyFilter() - if writeNonConformalCells: - filterInstance.writeGrid( outputPath ) + filterInstance.writeGrid( outputPath ) return ( filterInstance.getMesh(), diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py index bcd794a00..247230e96 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py @@ -168,8 +168,7 @@ def selfIntersectingElements( if not success: raise RuntimeError( "Self-intersecting elements detection failed" ) - if writeInvalidElements: - filter_instance.writeGrid( outputPath ) + filter_instance.writeGrid( outputPath ) return ( filter_instance.getMesh(), diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py index ac0833596..31fa960f7 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py @@ -233,8 +233,7 @@ def supportedElements( chunkSize ) filterInstance.applyFilter() - if writeUnsupportedElementTypes or writeUnsupportedPolyhedrons: - filterInstance.writeGrid( outputPath ) + filterInstance.writeGrid( outputPath ) return ( filterInstance.getMesh(), From 9edea276f6c4bb7ffde89d4c0e2fe7e0880a84d3 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 3 Sep 2025 11:02:58 -0700 Subject: [PATCH 38/52] Remove tempfile to use tmp_path functionality of pytest --- geos-mesh/tests/test_generate_cube.py | 66 +++++----- geos-mesh/tests/test_generate_fractures.py | 143 ++++++++++----------- 2 files changed, 101 insertions(+), 108 deletions(-) diff --git a/geos-mesh/tests/test_generate_cube.py b/geos-mesh/tests/test_generate_cube.py index 59b82d1d3..b690c6173 100644 --- a/geos-mesh/tests/test_generate_cube.py +++ b/geos-mesh/tests/test_generate_cube.py @@ -1,7 +1,6 @@ +import pytest from geos.mesh.doctor.actions.generate_cube import FieldInfo, Options, __build from geos.mesh.doctor.filters.GenerateRectilinearGrid import GenerateRectilinearGrid, generateRectilinearGrid -import tempfile -import os def test_generate_cube(): @@ -99,36 +98,35 @@ def test_generate_rectilinear_grid_filter_missing_parameters(): assert not success, "Filter should fail when coordinates are not set" -def test_generate_rectilinear_grid_standalone(): +def test_generate_rectilinear_grid_standalone(tmp_path): """Test the standalone generateRectilinearGrid function.""" - with tempfile.TemporaryDirectory() as temp_dir: - output_path = os.path.join( temp_dir, "test_grid.vtu" ) - - # Create a simple rectilinear grid - output_mesh = generateRectilinearGrid( coordsX=[ 0.0, 1.0, 2.0 ], - coordsY=[ 0.0, 1.0 ], - coordsZ=[ 0.0, 1.0 ], - numberElementsX=[ 2, 3 ], - numberElementsY=[ 2 ], - numberElementsZ=[ 2 ], - outputPath=output_path, - fields=[ FieldInfo( "test_field", 2, "CELLS" ) ], - generateCellsGlobalIds=True, - generatePointsGlobalIds=False ) - - # Verify mesh properties - assert output_mesh is not None, "Output mesh should not be None" - assert output_mesh.GetNumberOfCells() == 20, "Should have 20 cells (5x2x2)" - assert output_mesh.GetNumberOfPoints() == 54, "Should have 54 points (6x3x3)" - - # Verify field - assert output_mesh.GetCellData().GetArray( "test_field" ) is not None, "Should have test_field array" - test_field_array = output_mesh.GetCellData().GetArray( "test_field" ) - assert test_field_array.GetNumberOfComponents() == 2, "test_field should have 2 components" - - # Verify global IDs - assert output_mesh.GetCellData().GetGlobalIds() is not None, "Should have cell global IDs" - assert output_mesh.GetPointData().GetGlobalIds() is None, "Should not have point global IDs" - - # Verify output file was written - assert os.path.exists( output_path ), "Output file should exist" + output_path = tmp_path / "test_grid.vtu" + + # Create a simple rectilinear grid + output_mesh = generateRectilinearGrid( coordsX=[ 0.0, 1.0, 2.0 ], + coordsY=[ 0.0, 1.0 ], + coordsZ=[ 0.0, 1.0 ], + numberElementsX=[ 2, 3 ], + numberElementsY=[ 2 ], + numberElementsZ=[ 2 ], + outputPath=output_path, + fields=[ FieldInfo( "test_field", 2, "CELLS" ) ], + generateCellsGlobalIds=True, + generatePointsGlobalIds=False ) + + # Verify mesh properties + assert output_mesh is not None, "Output mesh should not be None" + assert output_mesh.GetNumberOfCells() == 20, "Should have 20 cells (5x2x2)" + assert output_mesh.GetNumberOfPoints() == 54, "Should have 54 points (6x3x3)" + + # Verify field + assert output_mesh.GetCellData().GetArray( "test_field" ) is not None, "Should have test_field array" + test_field_array = output_mesh.GetCellData().GetArray( "test_field" ) + assert test_field_array.GetNumberOfComponents() == 2, "test_field should have 2 components" + + # Verify global IDs + assert output_mesh.GetCellData().GetGlobalIds() is not None, "Should have cell global IDs" + assert output_mesh.GetPointData().GetGlobalIds() is None, "Should not have point global IDs" + + # Verify output file was written + assert output_path.exists(), "Output file should exist" diff --git a/geos-mesh/tests/test_generate_fractures.py b/geos-mesh/tests/test_generate_fractures.py index 72c840af9..69b86337b 100644 --- a/geos-mesh/tests/test_generate_fractures.py +++ b/geos-mesh/tests/test_generate_fractures.py @@ -1,11 +1,9 @@ from dataclasses import dataclass -import os import numpy import pytest 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 -import tempfile 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, @@ -372,30 +370,29 @@ def test_copy_fields_when_splitting_mesh(): @pytest.mark.parametrize( "test_case", __generate_test_data() ) -def test_generate_fracture_filters_basic( test_case: TestCase ): - with tempfile.TemporaryDirectory() as temp_dir: - fracturesDir = os.path.join( temp_dir, "fractures" ) - os.makedirs( fracturesDir, exist_ok=True ) # Create the directory - policy = 1 if test_case.options.policy == FracturePolicy.INTERNAL_SURFACES else 0 - fieldValues = ','.join( map( str, test_case.options.field_values_combined ) ) - # Create filter instance - filterInstance = GenerateFractures( test_case.input_mesh, - policy=policy, - fieldName=test_case.options.field, - fieldValues=fieldValues, - fracturesOutputDir=fracturesDir ) - # Apply filter - success = filterInstance.applyFilter() - assert success, "Filter should succeed" - # Get results - splitMesh = filterInstance.getMesh() - fractureMeshes = filterInstance.getFractureMeshes() - allGrids = filterInstance.getAllGrids() - # Verify results - assert splitMesh is not None, "Split mesh should not be None" - assert isinstance( fractureMeshes, list ), "Fracture meshes should be a list" - assert allGrids[ 0 ] is splitMesh, "getAllGrids should return split mesh as first element" - assert allGrids[ 1 ] is fractureMeshes, "getAllGrids should return fracture meshes as second element" +def test_generate_fracture_filters_basic( test_case: TestCase, tmp_path ): + fracturesDir = tmp_path / "fractures" + fracturesDir.mkdir( exist_ok=True ) # Create the directory + policy = 1 if test_case.options.policy == FracturePolicy.INTERNAL_SURFACES else 0 + fieldValues = ','.join( map( str, test_case.options.field_values_combined ) ) + # Create filter instance + filterInstance = GenerateFractures( test_case.input_mesh, + policy=policy, + fieldName=test_case.options.field, + fieldValues=fieldValues, + fracturesOutputDir=fracturesDir ) + # Apply filter + success = filterInstance.applyFilter() + assert success, "Filter should succeed" + # Get results + splitMesh = filterInstance.getMesh() + fractureMeshes = filterInstance.getFractureMeshes() + allGrids = filterInstance.getAllGrids() + # Verify results + assert splitMesh is not None, "Split mesh should not be None" + assert isinstance( fractureMeshes, list ), "Fracture meshes should be a list" + assert allGrids[ 0 ] is splitMesh, "getAllGrids should return split mesh as first element" + assert allGrids[ 1 ] is fractureMeshes, "getAllGrids should return fracture meshes as second element" def test_generate_fractures_filter_setters(): @@ -447,7 +444,7 @@ def test_generate_fractures_filter_setters(): assert filterInstance.allOptions[ FRACTURES_DATA_MODE ] == original_fractures_mode -def test_generate_fractures_filter_with_global_ids(): +def test_generate_fractures_filter_with_global_ids( tmp_path ): """Test that filter fails when mesh contains global IDs.""" # Create a simple test mesh x = numpy.array( [ 0, 1 ] ) @@ -461,56 +458,54 @@ def test_generate_fractures_filter_with_global_ids(): points_array = numpy_to_vtk( points_global_ids ) points_array.SetName( "GLOBAL_IDS_POINTS" ) mesh.GetPointData().AddArray( points_array ) - with tempfile.TemporaryDirectory() as temp_dir: - fractures_dir = os.path.join( temp_dir, "fractures" ) - os.makedirs( fractures_dir, exist_ok=True ) # Create the directory - # Create filter instance - filterInstance = GenerateFractures( mesh ) - # Should fail due to global IDs - success = filterInstance.applyFilter() - assert not success, "Filter should fail when mesh contains global IDs" + + fractures_dir = tmp_path / "fractures" + fractures_dir.mkdir( exist_ok=True ) # Create the directory + # Create filter instance + filterInstance = GenerateFractures( mesh ) + # Should fail due to global IDs + success = filterInstance.applyFilter() + assert not success, "Filter should fail when mesh contains global IDs" @pytest.mark.parametrize( "test_case", __generate_test_data() ) -def test_generate_fractures_standalone( test_case: TestCase ): - with tempfile.TemporaryDirectory() as temp_dir: - outputPath = os.path.join( temp_dir, "split_mesh.vtu" ) - fractures_dir = os.path.join( temp_dir, "fractures" ) - os.makedirs( fractures_dir, exist_ok=True ) # Create the directory - policy = 1 if test_case.options.policy == FracturePolicy.INTERNAL_SURFACES else 0 - fieldValues = ','.join( map( str, test_case.options.field_values_combined ) ) - # Create filter instance - splitMesh, fractureMeshes = generateFractures( test_case.input_mesh, - outputPath=outputPath, - policy=policy, - fieldName=test_case.options.field, - fieldValues=fieldValues, - fracturesOutputDir=fractures_dir ) - # Verify results - assert splitMesh is not None, "Split mesh should not be None" - assert isinstance( fractureMeshes, list ), "Fracture meshes should be a list" - # Verify output file was written - assert os.path.exists( outputPath ), "Output file should exist" +def test_generate_fractures_standalone( test_case: TestCase, tmp_path ): + outputPath = tmp_path / "split_mesh.vtu" + fractures_dir = tmp_path / "fractures" + fractures_dir.mkdir( exist_ok=True ) # Create the directory + policy = 1 if test_case.options.policy == FracturePolicy.INTERNAL_SURFACES else 0 + fieldValues = ','.join( map( str, test_case.options.field_values_combined ) ) + # Create filter instance + splitMesh, fractureMeshes = generateFractures( test_case.input_mesh, + outputPath=outputPath, + policy=policy, + fieldName=test_case.options.field, + fieldValues=fieldValues, + fracturesOutputDir=fractures_dir ) + # Verify results + assert splitMesh is not None, "Split mesh should not be None" + assert isinstance( fractureMeshes, list ), "Fracture meshes should be a list" + # Verify output file was written + assert outputPath.exists(), "Output file should exist" @pytest.mark.parametrize( "test_case", __generate_test_data() ) -def test_generate_fractures_write_meshes( test_case: TestCase ): - with tempfile.TemporaryDirectory() as temp_dir: - outputPath = os.path.join( temp_dir, "split_mesh.vtu" ) - fracturesDir = os.path.join( temp_dir, "fractures" ) - os.makedirs( fracturesDir, exist_ok=True ) # Create the directory - policy = 1 if test_case.options.policy == FracturePolicy.INTERNAL_SURFACES else 0 - fieldValues = ','.join( map( str, test_case.options.field_values_combined ) ) - # Create filter instance - filterInstance = GenerateFractures( test_case.input_mesh, - policy=policy, - fieldName=test_case.options.field, - fieldValues=fieldValues, - fracturesOutputDir=fracturesDir ) - # Apply filter - success = filterInstance.applyFilter() - assert success, "Filter should succeed" - # Test writing meshes - filterInstance.writeMeshes( outputPath, isDataModeBinary=False, canOverwrite=True ) - # Verify main mesh file was written - assert os.path.exists( outputPath ), "Main mesh file should exist" +def test_generate_fractures_write_meshes( test_case: TestCase, tmp_path ): + outputPath = tmp_path / "split_mesh.vtu" + fracturesDir = tmp_path / "fractures" + fracturesDir.mkdir( exist_ok=True ) # Create the directory + policy = 1 if test_case.options.policy == FracturePolicy.INTERNAL_SURFACES else 0 + fieldValues = ','.join( map( str, test_case.options.field_values_combined ) ) + # Create filter instance + filterInstance = GenerateFractures( test_case.input_mesh, + policy=policy, + fieldName=test_case.options.field, + fieldValues=fieldValues, + fracturesOutputDir=fracturesDir ) + # Apply filter + success = filterInstance.applyFilter() + assert success, "Filter should succeed" + # Test writing meshes + filterInstance.writeMeshes( outputPath, isDataModeBinary=False, canOverwrite=True ) + # Verify main mesh file was written + assert outputPath.exists(), "Main mesh file should exist" From c6911ae52666ac7bdfc3c1d433669d622e0cba5c Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 3 Sep 2025 11:04:31 -0700 Subject: [PATCH 39/52] Add test_CollocatedNodes --- geos-mesh/tests/test_CollocatedNodes.py | 170 ++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 geos-mesh/tests/test_CollocatedNodes.py diff --git a/geos-mesh/tests/test_CollocatedNodes.py b/geos-mesh/tests/test_CollocatedNodes.py new file mode 100644 index 000000000..6eed87f2a --- /dev/null +++ b/geos-mesh/tests/test_CollocatedNodes.py @@ -0,0 +1,170 @@ +import pytest +import numpy as np +import os +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, VTK_TRIANGLE +from vtkmodules.vtkCommonCore import vtkPoints +from geos.mesh.doctor.actions.generate_cube import XYZ, build_rectilinear_blocks_mesh +from geos.mesh.doctor.filters.CollocatedNodes import CollocatedNodes, collocatedNodes +from geos.mesh.utils.genericHelpers import to_vtk_id_list + +__doc__ = """ +Test module for CollocatedNodes filter. +Tests the functionality of detecting and handling collocated/duplicated nodes in meshes. +""" + + +@pytest.fixture( scope="module" ) +def mesh_with_collocated_nodes(): + """Fixture for a mesh with exactly duplicated and nearly collocated nodes.""" + x, y, z = np.array( [ 0, 1, 2 ] ), np.array( [ 0, 1 ] ), np.array( [ 0, 1 ] ) + mesh = build_rectilinear_blocks_mesh( [ XYZ( x, y, z ) ] ) + points = mesh.GetPoints() + + # Add nodes to create collocated situations: + # 1. Exact duplicate of point 0 + points.InsertNextPoint( 0.0, 0.0, 0.0 ) + # 2. Exact duplicate of point 1 + points.InsertNextPoint( 1.0, 0.0, 0.0 ) + # 3. A point very close to an existing point (2, 0, 0) + points.InsertNextPoint( 2.0, 0.0, 1e-8 ) + + return mesh + + +@pytest.fixture( scope="module" ) +def mesh_with_wrong_support(): + """Fixture for a mesh containing a cell with repeated node indices.""" + mesh = vtkUnstructuredGrid() + points = vtkPoints() + mesh.SetPoints( points ) + + points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 0 + points.InsertNextPoint( 1.0, 0.0, 0.0 ) # Point 1 + points.InsertNextPoint( 0.0, 1.0, 0.0 ) # Point 2 + points.InsertNextPoint( 1.0, 1.0, 0.0 ) # Point 3 + + # A degenerate triangle with a repeated node [0, 1, 1] + mesh.InsertNextCell( VTK_TRIANGLE, to_vtk_id_list( [ 0, 1, 1 ] ) ) + # A normal triangle for comparison + mesh.InsertNextCell( VTK_TRIANGLE, to_vtk_id_list( [ 1, 2, 3 ] ) ) + + return mesh + + +@pytest.fixture( scope="module" ) +def clean_mesh(): + """Fixture for a simple, valid mesh with no issues.""" + x, y, z = np.array( [ 0, 1, 2 ] ), np.array( [ 0, 1 ] ), np.array( [ 0, 1 ] ) + return build_rectilinear_blocks_mesh( [ XYZ( x, y, z ) ] ) + + +def test_filter_on_clean_mesh( clean_mesh ): + """Verify the filter runs correctly on a mesh with no issues.""" + filter_instance = CollocatedNodes( clean_mesh, writeWrongSupportElements=True ) + assert filter_instance.applyFilter() + + # Assert that no issues were found + assert not filter_instance.getCollocatedNodeBuckets() + assert not filter_instance.getWrongSupportElements() + + # Assert that no "WrongSupportElements" array was added if none were found + output_mesh = filter_instance.getMesh() + assert output_mesh.GetCellData().GetArray( "WrongSupportElements" ) is None + + +def test_filter_detection_and_bucket_structure( mesh_with_collocated_nodes ): + """Test basic detection and validate the structure of the result.""" + filter_instance = CollocatedNodes( mesh_with_collocated_nodes, tolerance=1e-7 ) + assert filter_instance.applyFilter() + + collocated_buckets = filter_instance.getCollocatedNodeBuckets() + + # Verify that collocated nodes were detected + assert len( collocated_buckets ) > 0 + + # Verify the structure of the output + for bucket in collocated_buckets: + assert isinstance( bucket, tuple ) + assert len( bucket ) >= 2 + for node_id in bucket: + assert isinstance( node_id, ( int, np.integer ) ) + assert 0 <= node_id < mesh_with_collocated_nodes.GetNumberOfPoints() + + +@pytest.mark.parametrize( + "tolerance, expected_min_buckets", + [ + ( 0.0, 2 ), # Zero tolerance should only find the 2 exact duplicates. + ( 1e-10, 2 ), # Strict tolerance should also only find exacts. + ( 1e-7, 3 ), # Looser tolerance should find the "nearby" node as well. + ( 10.0, 1 ) # Large tolerance should group many nodes together. + ] ) +def test_filter_tolerance_effects( mesh_with_collocated_nodes, tolerance, expected_min_buckets ): + """Test how different tolerance values affect detection.""" + filter_instance = CollocatedNodes( mesh_with_collocated_nodes, tolerance=tolerance ) + filter_instance.applyFilter() + collocated_buckets = filter_instance.getCollocatedNodeBuckets() + + # The number of buckets found should be consistent with the tolerance + assert len( collocated_buckets ) >= expected_min_buckets + + +def test_filter_wrong_support_elements( mesh_with_wrong_support ): + """Test the detection of cells with repeated nodes (wrong support).""" + filter_instance = CollocatedNodes( mesh_with_wrong_support, writeWrongSupportElements=True ) + assert filter_instance.applyFilter() + + # Verify the wrong support element was identified + wrong_support_elements = filter_instance.getWrongSupportElements() + assert len( wrong_support_elements ) == 1 + assert 0 in wrong_support_elements + + # Verify the corresponding data array was added to the mesh + output_mesh = filter_instance.getMesh() + wrong_support_array = output_mesh.GetCellData().GetArray( "WrongSupportElements" ) + assert wrong_support_array is not None + assert wrong_support_array.GetNumberOfTuples() == mesh_with_wrong_support.GetNumberOfCells() + + +def test_filter_mesh_integrity( mesh_with_collocated_nodes ): + """Ensure the filter only adds data arrays and doesn't alter mesh geometry/topology.""" + original_points = mesh_with_collocated_nodes.GetNumberOfPoints() + original_cells = mesh_with_collocated_nodes.GetNumberOfCells() + + filter_instance = CollocatedNodes( mesh_with_collocated_nodes ) + filter_instance.applyFilter() + output_mesh = filter_instance.getMesh() + + assert output_mesh.GetNumberOfPoints() == original_points + assert output_mesh.GetNumberOfCells() == original_cells + + +@pytest.mark.parametrize( + "mesh_fixture, write_support, check_collocated, check_wrong_support", + [ + # Scenario 1: Find collocated nodes + ( "mesh_with_collocated_nodes", False, True, False ), + # Scenario 2: Find wrong support elements + ( "mesh_with_wrong_support", True, False, True ), + ] ) +def test_standalone_function( tmpdir, request, mesh_fixture, write_support, check_collocated, check_wrong_support ): + """Test the standalone collocatedNodes wrapper function.""" + # Request the fixture by its name string passed from parametrize + input_mesh = request.getfixturevalue( mesh_fixture ) + output_path = os.path.join( str( tmpdir ), "output.vtu" ) + + output_mesh, buckets, wrong_support = collocatedNodes( input_mesh, + outputPath=output_path, + tolerance=1e-6, + writeWrongSupportElements=write_support ) + + # General assertions for all runs + assert output_mesh is not None + assert os.path.exists( output_path ) + + # Scenario-specific assertions + if check_collocated: + assert len( buckets ) > 0 + if check_wrong_support: + assert len( wrong_support ) > 0 + assert output_mesh.GetCellData().GetArray( "WrongSupportElements" ) is not None From 26fb43def756712cf2381ae1b7a7733fd7e2fc9a Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 3 Sep 2025 14:09:13 -0700 Subject: [PATCH 40/52] Add better input checking when init class --- .../src/geos/mesh/doctor/filters/Checks.py | 8 +++- .../mesh/doctor/filters/CollocatedNodes.py | 5 ++- .../mesh/doctor/filters/ElementVolumes.py | 1 - .../mesh/doctor/filters/GenerateFractures.py | 1 - .../doctor/filters/GenerateRectilinearGrid.py | 1 - .../doctor/filters/MeshDoctorFilterBase.py | 40 +++++++++++++------ .../geos/mesh/doctor/filters/NonConformal.py | 4 +- .../filters/SelfIntersectingElements.py | 1 - .../mesh/doctor/filters/SupportedElements.py | 4 +- 9 files changed, 43 insertions(+), 22 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py index b2a5e2bbf..f7b3e85cb 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py @@ -291,7 +291,9 @@ def allChecks( for param_name, value in params.items(): filterInstance.setCheckParameter( checkName, param_name, value ) - filterInstance.applyFilter() + success = filterInstance.applyFilter() + if not success: + raise RuntimeError( "allChecks calculation failed." ) return ( filterInstance.getMesh(), @@ -320,7 +322,9 @@ def mainChecks( for param_name, value in params.items(): filterInstance.setCheckParameter( checkName, param_name, value ) - filterInstance.applyFilter() + success = filterInstance.applyFilter() + if not success: + raise RuntimeError( "mainChecks calculation failed." ) return ( filterInstance.getMesh(), diff --git a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py index 332c01990..35d273426 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/CollocatedNodes.py @@ -161,7 +161,10 @@ def collocatedNodes( Processed mesh, collocated node buckets, wrong support elements. """ filterInstance = CollocatedNodes( mesh, tolerance, writeWrongSupportElements ) - filterInstance.applyFilter() + success = filterInstance.applyFilter() + if not success: + raise RuntimeError( "Element volumes calculation failed." ) + filterInstance.writeGrid( outputPath ) return ( diff --git a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py index 9c019977d..eac41495a 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/ElementVolumes.py @@ -172,7 +172,6 @@ def elementVolumes( """ filterInstance = ElementVolumes( mesh, minVolume, writeIsBelowVolume ) success = filterInstance.applyFilter() - if not success: raise RuntimeError( "Element volumes calculation failed." ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py index 84c29ba12..d9982a0cf 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py @@ -277,7 +277,6 @@ def generateFractures( filterInstance = GenerateFractures( mesh, policy, fieldName, fieldValues, fracturesOutputDir, outputDataMode, fracturesDataMode ) success = filterInstance.applyFilter() - if not success: raise RuntimeError( "Fracture generation failed." ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py index 70d82878b..008a0f99f 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateRectilinearGrid.py @@ -238,7 +238,6 @@ def generateRectilinearGrid( filterInstance.setFields( fields ) success = filterInstance.applyFilter() - if not success: raise RuntimeError( "Rectilinear grid generation failed." ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py index 00dcd4a83..1b88a596d 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py @@ -53,13 +53,7 @@ def applyFilter(self): class MeshDoctorFilterBase: - """Base class for all mesh doctor filters using direct mesh manipulation. - - This class provides common functionality shared across all mesh doctor filters, - including logger management, mesh access, and file writing capabilities. - Unlike MeshDoctorBase, this class works with direct mesh manipulation instead - of VTK pipeline patterns. - """ + """Base class for all mesh doctor filters using direct mesh manipulation.""" def __init__( self: Self, @@ -67,13 +61,23 @@ def __init__( filterName: str, useExternalLogger: bool = False, ) -> None: - """Initialize the base mesh doctor filter. + """Initialize the base mesh doctor filter.""" + # Check the 'mesh' input + if not isinstance(mesh, vtkUnstructuredGrid): + raise TypeError(f"Input 'mesh' must be a vtkUnstructuredGrid, but got {type(mesh).__name__}.") + if mesh.GetNumberOfCells() == 0: + raise ValueError("Input 'mesh' cannot be empty.") + + # Check the 'filterName' input + if not isinstance(filterName, str): + raise TypeError(f"Input 'filterName' must be a string, but got {type(filterName).__name__}.") + if not filterName.strip(): + raise ValueError("Input 'filterName' cannot be an empty or whitespace-only string.") + + # Check the 'useExternalLogger' input + if not isinstance(useExternalLogger, bool): + raise TypeError(f"Input 'useExternalLogger' must be a boolean, but got {type(useExternalLogger).__name__}.") - Args: - mesh (vtkUnstructuredGrid): The input mesh to process - filterName (str): Name of the filter for logging - useExternalLogger (bool): Whether to use external logger. Defaults to False. - """ self.mesh: vtkUnstructuredGrid = mesh self.filterName: str = filterName @@ -164,6 +168,16 @@ def __init__( filterName (str): Name of the filter for logging. useExternalLogger (bool): Whether to use external logger. Defaults to False. """ + # Check the 'filterName' input + if not isinstance(filterName, str): + raise TypeError(f"Input 'filterName' must be a string, but got {type(filterName).__name__}.") + if not filterName.strip(): + raise ValueError("Input 'filterName' cannot be an empty or whitespace-only string.") + + # Check the 'useExternalLogger' input + if not isinstance(useExternalLogger, bool): + raise TypeError(f"Input 'useExternalLogger' must be a boolean, but got {type(useExternalLogger).__name__}.") + self.mesh: Union[ vtkUnstructuredGrid, None ] = None self.filterName: str = filterName diff --git a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py index 3d2549611..a5ed270b8 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py @@ -214,7 +214,9 @@ def nonConformal( Processed mesh, non-conformal cell pairs. """ filterInstance = NonConformal( mesh, pointTolerance, faceTolerance, angleTolerance, writeNonConformalCells ) - filterInstance.applyFilter() + success = filterInstance.applyFilter() + if not success: + raise RuntimeError( "NonConformal detection failed." ) filterInstance.writeGrid( outputPath ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py index 247230e96..936878374 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py @@ -164,7 +164,6 @@ def selfIntersectingElements( """ filter_instance = SelfIntersectingElements( mesh, minDistance, writeInvalidElements ) success = filter_instance.applyFilter() - if not success: raise RuntimeError( "Self-intersecting elements detection failed" ) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py index 31fa960f7..03a875347 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py @@ -231,7 +231,9 @@ def supportedElements( """ filterInstance = SupportedElements( mesh, writeUnsupportedElementTypes, writeUnsupportedPolyhedrons, numProc, chunkSize ) - filterInstance.applyFilter() + success = filterInstance.applyFilter() + if not success: + raise RuntimeError( "Supported elements identification failed." ) filterInstance.writeGrid( outputPath ) From b718e8136f9da0315de5b45d7ff9b8634afd8789 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 3 Sep 2025 15:02:23 -0700 Subject: [PATCH 41/52] Add non-destructive behavior to input_mesh --- .../src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py index 1b88a596d..cf26ed9c3 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py @@ -78,7 +78,10 @@ def __init__( if not isinstance(useExternalLogger, bool): raise TypeError(f"Input 'useExternalLogger' must be a boolean, but got {type(useExternalLogger).__name__}.") - self.mesh: vtkUnstructuredGrid = mesh + # Non-destructive behavior. + # The filter should contain a COPY of the mesh, not the original object. + self.mesh: vtkUnstructuredGrid = vtkUnstructuredGrid() + self.mesh.DeepCopy( mesh ) self.filterName: str = filterName # Logger setup From b5278cadf933041fd4476d266b17dce24ea50eb2 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 3 Sep 2025 15:03:43 -0700 Subject: [PATCH 42/52] Add test_ElementVolumes --- geos-mesh/tests/test_ElementVolumes.py | 445 +++++++++++++++++++++++++ geos-mesh/tests/test_generate_cube.py | 2 +- 2 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 geos-mesh/tests/test_ElementVolumes.py diff --git a/geos-mesh/tests/test_ElementVolumes.py b/geos-mesh/tests/test_ElementVolumes.py new file mode 100644 index 000000000..deed2912b --- /dev/null +++ b/geos-mesh/tests/test_ElementVolumes.py @@ -0,0 +1,445 @@ +import pytest +import numpy as np +from vtkmodules.vtkCommonCore import vtkPoints +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, VTK_TETRA, VTK_TRIANGLE +from geos.mesh.utils.genericHelpers import to_vtk_id_list, createSingleCellMesh +from geos.mesh.doctor.actions.generate_cube import XYZ, build_rectilinear_blocks_mesh +from geos.mesh.doctor.filters.ElementVolumes import ElementVolumes, elementVolumes + +__doc__ = """ +Test module for ElementVolumes filter. +Tests the functionality of calculating element volumes and detecting problematic elements. +""" + + +@pytest.fixture( scope="module" ) +def simple_hex_mesh(): + """Fixture for a simple hexahedron mesh with known volumes.""" + x, y, z = np.array( [ 0, 1, 2 ] ), np.array( [ 0, 1 ] ), np.array( [ 0, 1 ] ) + mesh = build_rectilinear_blocks_mesh( [ XYZ( x, y, z ) ] ) + return mesh + + +@pytest.fixture( scope="module" ) +def mesh_with_negative_volume(): + """Fixture for a mesh containing an element with negative volume (inverted tetrahedron).""" + mesh = vtkUnstructuredGrid() + points = vtkPoints() + mesh.SetPoints( points ) + + # Create a normal tetrahedron + points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 0 + points.InsertNextPoint( 1.0, 0.0, 0.0 ) # Point 1 + points.InsertNextPoint( 0.0, 1.0, 0.0 ) # Point 2 + points.InsertNextPoint( 0.0, 0.0, 1.0 ) # Point 3 + + # Inverted tetrahedron with wrong node ordering (creates negative volume) + points.InsertNextPoint( 2.0, 0.0, 0.0 ) # Point 4 + points.InsertNextPoint( 3.0, 0.0, 0.0 ) # Point 5 + points.InsertNextPoint( 2.0, 1.0, 0.0 ) # Point 6 + points.InsertNextPoint( 2.0, 0.0, 1.0 ) # Point 7 + + # Normal tetrahedron [0, 1, 2, 3] + mesh.InsertNextCell( VTK_TETRA, to_vtk_id_list( [ 0, 1, 2, 3 ] ) ) + # Inverted tetrahedron [4, 6, 5, 7] - wrong ordering + mesh.InsertNextCell( VTK_TETRA, to_vtk_id_list( [ 4, 6, 5, 7 ] ) ) + + return mesh + + +@pytest.fixture( scope="module" ) +def mesh_with_zero_volume(): + """Fixture for a mesh containing degenerate elements with zero volume.""" + mesh = vtkUnstructuredGrid() + points = vtkPoints() + mesh.SetPoints( points ) + + # Degenerate tetrahedron - all points in the same plane + points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 0 + points.InsertNextPoint( 1.0, 0.0, 0.0 ) # Point 1 + points.InsertNextPoint( 0.5, 0.5, 0.0 ) # Point 2 + points.InsertNextPoint( 0.2, 0.3, 0.0 ) # Point 3 - all points are coplanar + + # Normal tetrahedron for comparison + points.InsertNextPoint( 2.0, 0.0, 0.0 ) # Point 4 + points.InsertNextPoint( 3.0, 0.0, 0.0 ) # Point 5 + points.InsertNextPoint( 2.0, 1.0, 0.0 ) # Point 6 + points.InsertNextPoint( 2.0, 0.0, 1.0 ) # Point 7 + + # Degenerate tetrahedron (should have zero or near-zero volume) + mesh.InsertNextCell( VTK_TETRA, to_vtk_id_list( [ 0, 1, 2, 3 ] ) ) + # Normal tetrahedron + mesh.InsertNextCell( VTK_TETRA, to_vtk_id_list( [ 4, 5, 6, 7 ] ) ) + + return mesh + + +@pytest.fixture( scope="module" ) +def mixed_cell_types_mesh(): + """Fixture for a mesh with different cell types.""" + mesh = vtkUnstructuredGrid() + points = vtkPoints() + mesh.SetPoints( points ) + + # Points for triangle + points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 0 + points.InsertNextPoint( 1.0, 0.0, 0.0 ) # Point 1 + points.InsertNextPoint( 0.5, 1.0, 0.0 ) # Point 2 + + # Points for tetrahedron + points.InsertNextPoint( 2.0, 0.0, 0.0 ) # Point 3 + points.InsertNextPoint( 3.0, 0.0, 0.0 ) # Point 4 + points.InsertNextPoint( 2.5, 1.0, 0.0 ) # Point 5 + points.InsertNextPoint( 2.5, 0.5, 1.0 ) # Point 6 + + # Add triangle (2D element) + mesh.InsertNextCell( VTK_TRIANGLE, to_vtk_id_list( [ 0, 1, 2 ] ) ) + # Add tetrahedron (3D element) + mesh.InsertNextCell( VTK_TETRA, to_vtk_id_list( [ 3, 4, 5, 6 ] ) ) + + return mesh + + +class TestElementVolumesFilter: + """Test class for ElementVolumes filter functionality.""" + + def test_filter_initialization( self, simple_hex_mesh ): + """Test basic filter initialization with different parameters.""" + # Test default initialization + filter_instance = ElementVolumes( simple_hex_mesh ) + assert filter_instance.minVolume == 0.0 + assert not filter_instance.writeIsBelowVolume + + processed_mesh = filter_instance.getMesh() + assert processed_mesh is not simple_hex_mesh + assert processed_mesh.GetNumberOfPoints() == simple_hex_mesh.GetNumberOfPoints() + assert processed_mesh.GetNumberOfCells() == simple_hex_mesh.GetNumberOfCells() + + # Test initialization with custom parameters + filter_instance = ElementVolumes( simple_hex_mesh, + minVolume=0.5, + writeIsBelowVolume=True, + useExternalLogger=True ) + assert filter_instance.minVolume == 0.5 + assert filter_instance.writeIsBelowVolume + + def test_apply_filter_success( self, simple_hex_mesh ): + """Test successful filter application on a clean mesh.""" + filter_instance = ElementVolumes( simple_hex_mesh, minVolume=0.0 ) + success = filter_instance.applyFilter() + + assert success + # Should not have any volumes below threshold of 0.0 for a normal mesh + below_volumes = filter_instance.getBelowVolumes() + assert isinstance( below_volumes, list ) + + def test_negative_volume_detection( self, mesh_with_negative_volume ): + """Test detection of elements with negative volumes.""" + filter_instance = ElementVolumes( mesh_with_negative_volume, minVolume=0.0 ) + success = filter_instance.applyFilter() + + assert success + below_volumes = filter_instance.getBelowVolumes() + + # Should detect at least one element with negative volume + assert len( below_volumes ) > 0 + + # Verify structure of results + for element_id, volume in below_volumes: + assert isinstance( element_id, int ) + assert isinstance( volume, float ) + assert element_id >= 0 + assert volume < 0.0 # Should be negative + + def test_zero_volume_detection( self, mesh_with_zero_volume ): + """Test detection of elements with zero or near-zero volumes.""" + filter_instance = ElementVolumes( mesh_with_zero_volume, minVolume=1e-10 ) + success = filter_instance.applyFilter() + + assert success + below_volumes = filter_instance.getBelowVolumes() + + # Should detect the degenerate element + assert len( below_volumes ) > 0 + + # Check that at least one volume is very small + volumes = [ vol for _, vol in below_volumes ] + assert any( abs( vol ) < 1e-10 for vol in volumes ) + + def test_threshold_filtering( self, simple_hex_mesh ): + """Test filtering with different volume thresholds.""" + # Test with very high threshold - should catch all elements + filter_instance = ElementVolumes( simple_hex_mesh, minVolume=100.0 ) + success = filter_instance.applyFilter() + + assert success + below_volumes = filter_instance.getBelowVolumes() + + # With a high threshold, normal unit cubes should be detected + assert len( below_volumes ) > 0 + + # Test with very low threshold - should catch nothing in a normal mesh + filter_instance = ElementVolumes( simple_hex_mesh, minVolume=-100.0 ) + success = filter_instance.applyFilter() + + assert success + below_volumes = filter_instance.getBelowVolumes() + assert len( below_volumes ) == 0 + + def test_write_below_volume_array( self, mesh_with_negative_volume ): + """Test addition of below volume threshold array to mesh.""" + filter_instance = ElementVolumes( mesh_with_negative_volume, minVolume=0.0, writeIsBelowVolume=True ) + success = filter_instance.applyFilter() + + assert success + + output_mesh = filter_instance.getMesh() + cell_data = output_mesh.GetCellData() + + # Should have added the below volume array + expected_array_name = "BelowVolumeThresholdOf0.0" + below_volume_array = cell_data.GetArray( expected_array_name ) + + assert below_volume_array is not None + assert below_volume_array.GetNumberOfTuples() == output_mesh.GetNumberOfCells() + + # Verify array contains 0s and 1s + for i in range( below_volume_array.GetNumberOfTuples() ): + value = below_volume_array.GetValue( i ) + assert value in [ 0, 1 ] + + def test_no_array_added_when_disabled( self, mesh_with_negative_volume ): + """Test that no array is added when writeIsBelowVolume is False.""" + # FIX: Create a deep copy to prevent tests from interfering with each other. + # This solves the state leakage issue from the module-scoped fixture. + mesh_copy = vtkUnstructuredGrid() + mesh_copy.DeepCopy( mesh_with_negative_volume ) + + filter_instance = ElementVolumes( mesh_copy, minVolume=0.0, writeIsBelowVolume=False ) + success = filter_instance.applyFilter() + assert success + + output_mesh = filter_instance.getMesh() + cell_data = output_mesh.GetCellData() + + expected_array_name = "BelowVolumeThresholdOf0.0" + below_volume_array = cell_data.GetArray( expected_array_name ) + assert below_volume_array is None + + def test_mixed_cell_types( self, mixed_cell_types_mesh ): + """Test filter behavior with mixed cell types.""" + filter_instance = ElementVolumes( mixed_cell_types_mesh, minVolume=0.0 ) + success = filter_instance.applyFilter() + + assert success + below_volumes = filter_instance.getBelowVolumes() + + # Should handle mixed cell types without crashing + assert isinstance( below_volumes, list ) + + def test_get_volumes_method( self, simple_hex_mesh ): + """Test the getVolumes method returns proper volume data.""" + filter_instance = ElementVolumes( simple_hex_mesh ) + + # Before applying filter, volumes should be None + assert filter_instance.getVolumes() is None + + success = filter_instance.applyFilter() + assert success + + # After applying filter, should still be None as this method + # is not implemented to return the actual volume array + volumes = filter_instance.getVolumes() + assert volumes is None # Current implementation returns None + + def test_set_write_below_volume( self, simple_hex_mesh ): + """Test the setWriteIsBelowVolume method.""" + filter_instance = ElementVolumes( simple_hex_mesh ) + + # Initially False + assert not filter_instance.writeIsBelowVolume + + # Set to True + filter_instance.setWriteIsBelowVolume( True ) + assert filter_instance.writeIsBelowVolume + + # Set back to False + filter_instance.setWriteIsBelowVolume( False ) + assert not filter_instance.writeIsBelowVolume + + def test_write_grid_functionality( self, simple_hex_mesh, tmp_path ): + """Test writing the output mesh to file.""" + filter_instance = ElementVolumes( simple_hex_mesh ) + success = filter_instance.applyFilter() + assert success + + # Write to temporary file + output_file = tmp_path / "test_output.vtu" + filter_instance.writeGrid( str( output_file ) ) + + # Verify file was created + assert output_file.exists() + assert output_file.stat().st_size > 0 + + +class TestElementVolumesStandaloneFunction: + """Test class for the standalone elementVolumes function.""" + + def test_standalone_function_basic( self, simple_hex_mesh, tmp_path ): + """Test basic functionality of the standalone elementVolumes function.""" + output_file = tmp_path / "standalone_output.vtu" + + mesh, volumes, below_volumes = elementVolumes( simple_hex_mesh, + str( output_file ), + minVolume=0.0, + writeIsBelowVolume=False ) + + # Verify return values + assert mesh is not None + assert isinstance( mesh, vtkUnstructuredGrid ) + assert volumes is None # Current implementation returns None + assert isinstance( below_volumes, list ) + + # Verify file was written + assert output_file.exists() + + def test_standalone_function_with_below_volume_writing( self, mesh_with_negative_volume, tmp_path ): + """Test standalone function with below volume writing enabled.""" + output_file = tmp_path / "standalone_with_array.vtu" + + mesh, volumes, below_volumes = elementVolumes( mesh_with_negative_volume, + str( output_file ), + minVolume=0.0, + writeIsBelowVolume=True ) + + assert mesh is not None + assert len( below_volumes ) > 0 + + # Check that the array was added + expected_array_name = "BelowVolumeThresholdOf0.0" + below_volume_array = mesh.GetCellData().GetArray( expected_array_name ) + assert below_volume_array is not None + + def test_standalone_function_error_handling( self, tmp_path ): + """Test error handling in the standalone function.""" + empty_mesh = vtkUnstructuredGrid() + output_file = tmp_path / "error_test.vtu" + + # FIX: The test should now expect a ValueError during initialization, + # which is better "fail-fast" behavior. + with pytest.raises( ValueError, match="Input 'mesh' cannot be empty." ): + elementVolumes( empty_mesh, str( output_file ), minVolume=0.0, writeIsBelowVolume=False ) + + def test_standalone_function_with_threshold( self, simple_hex_mesh, tmp_path ): + """Test standalone function with different volume thresholds.""" + output_file = tmp_path / "threshold_test.vtu" + + # Test with high threshold + mesh, volumes, below_volumes = elementVolumes( + simple_hex_mesh, + str( output_file ), + minVolume=10.0, # High threshold + writeIsBelowVolume=True ) + + # Should detect elements below the high threshold + assert len( below_volumes ) > 0 + + # Check that the array reflects the threshold + expected_array_name = "BelowVolumeThresholdOf10.0" + below_volume_array = mesh.GetCellData().GetArray( expected_array_name ) + assert below_volume_array is not None + + +class TestElementVolumesEdgeCases: + """Test class for edge cases and error conditions.""" + + def test_empty_mesh( self ): + """Test behavior with an empty mesh.""" + empty_mesh = vtkUnstructuredGrid() + with pytest.raises( ValueError, match="Input 'mesh' cannot be empty." ): + ElementVolumes( empty_mesh ) + + def test_single_cell_mesh( self ): + """Test with a mesh containing only one cell.""" + pts_coords = [ ( 0.0, 0.0, 0.0 ), ( 1.0, 0.0, 0.0 ), ( 0.0, 1.0, 0.0 ), ( 0.0, 0.0, 1.0 ) ] + mesh = createSingleCellMesh( VTK_TETRA, np.array( pts_coords ) ) + + filter_instance = ElementVolumes( mesh, minVolume=0.0 ) + success = filter_instance.applyFilter() + assert success + + below_volumes = filter_instance.getBelowVolumes() + assert isinstance( below_volumes, list ) + + def test_very_small_volumes( self, mesh_with_zero_volume ): + """Test with extremely small volume thresholds.""" + filter_instance = ElementVolumes( mesh_with_zero_volume, minVolume=1e-15 ) + success = filter_instance.applyFilter() + + assert success + below_volumes = filter_instance.getBelowVolumes() + + # Should still be able to detect volumes below extremely small thresholds + assert isinstance( below_volumes, list ) + + def test_very_large_volumes( self, simple_hex_mesh ): + """Test with very large volume thresholds.""" + filter_instance = ElementVolumes( simple_hex_mesh, minVolume=1e10 ) + success = filter_instance.applyFilter() + + assert success + below_volumes = filter_instance.getBelowVolumes() + + # All elements should be below this huge threshold + assert len( below_volumes ) == simple_hex_mesh.GetNumberOfCells() + + def test_negative_threshold( self, mesh_with_negative_volume ): + """Test with negative volume threshold.""" + filter_instance = ElementVolumes( mesh_with_negative_volume, minVolume=-0.5 ) + success = filter_instance.applyFilter() + + assert success + below_volumes = filter_instance.getBelowVolumes() + + # Only volumes below -0.5 should be detected + for _, volume in below_volumes: + assert volume < -0.5 + + +@pytest.mark.parametrize( "min_volume,expected_behavior", [ + ( 0.0, "detect_negative_and_zero" ), + ( 1.1, "detect_small_positive" ), + ( -1.0, "detect_very_negative" ), + ( 1e-10, "detect_near_zero" ), +] ) +def test_parametrized_volume_thresholds( simple_hex_mesh, min_volume, expected_behavior ): + """Parametrized test for different volume thresholds.""" + filter_instance = ElementVolumes( simple_hex_mesh, minVolume=min_volume ) + success = filter_instance.applyFilter() + + assert success + below_volumes = filter_instance.getBelowVolumes() + + if expected_behavior == "detect_small_positive": + # Unit cubes with volume 1.0 should be below a threshold of 1.1 + assert len( below_volumes ) > 0 + else: + # None of the other conditions should be met by the simple_hex_mesh + assert len( below_volumes ) == 0 + + +def test_logger_integration( simple_hex_mesh, caplog ): + """Test that the filter properly logs its operations.""" + filter_instance = ElementVolumes( simple_hex_mesh, useExternalLogger=False ) + + with caplog.at_level( "INFO" ): + success = filter_instance.applyFilter() + + assert success + # Check that some logging occurred + assert len( caplog.records ) > 0 + + # Check for expected log messages + log_messages = [ record.message for record in caplog.records ] + assert any( "Apply filter" in msg for msg in log_messages ) + assert any( "succeeded" in msg for msg in log_messages ) diff --git a/geos-mesh/tests/test_generate_cube.py b/geos-mesh/tests/test_generate_cube.py index b690c6173..5704e940a 100644 --- a/geos-mesh/tests/test_generate_cube.py +++ b/geos-mesh/tests/test_generate_cube.py @@ -98,7 +98,7 @@ def test_generate_rectilinear_grid_filter_missing_parameters(): assert not success, "Filter should fail when coordinates are not set" -def test_generate_rectilinear_grid_standalone(tmp_path): +def test_generate_rectilinear_grid_standalone( tmp_path ): """Test the standalone generateRectilinearGrid function.""" output_path = tmp_path / "test_grid.vtu" From 93ce02062aa40520ec7c2eb4f33d86aaeea8386b Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 3 Sep 2025 16:36:21 -0700 Subject: [PATCH 43/52] Add test_MeshDoctorFilterBase --- .../doctor/filters/MeshDoctorFilterBase.py | 4 +- geos-mesh/tests/test_MeshDoctorFilterBase.py | 504 ++++++++++++++++++ 2 files changed, 506 insertions(+), 2 deletions(-) create mode 100644 geos-mesh/tests/test_MeshDoctorFilterBase.py diff --git a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py index cf26ed9c3..0063c5770 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py @@ -99,7 +99,7 @@ def setLoggerHandler( self: Self, handler ) -> None: Args: handler: The logging handler to add. """ - if not self.logger.hasHandlers(): + if not self.logger.handlers: self.logger.addHandler( handler ) else: self.logger.warning( "The logger already has a handler, to use yours set 'useExternalLogger' " @@ -199,7 +199,7 @@ def setLoggerHandler( self: Self, handler ) -> None: Args: handler: The logging handler to add. """ - if not self.logger.hasHandlers(): + if not self.logger.handlers: self.logger.addHandler( handler ) else: self.logger.warning( "The logger already has a handler, to use yours set 'useExternalLogger' " diff --git a/geos-mesh/tests/test_MeshDoctorFilterBase.py b/geos-mesh/tests/test_MeshDoctorFilterBase.py new file mode 100644 index 000000000..41ee8a28f --- /dev/null +++ b/geos-mesh/tests/test_MeshDoctorFilterBase.py @@ -0,0 +1,504 @@ +import pytest +import logging +import numpy as np +from unittest.mock import Mock +from vtkmodules.vtkCommonCore import vtkPoints +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, VTK_TETRA +from geos.mesh.utils.genericHelpers import createSingleCellMesh +from geos.mesh.doctor.actions.generate_cube import XYZ, build_rectilinear_blocks_mesh +from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorFilterBase, MeshDoctorGeneratorBase + +__doc__ = """ +Test module for MeshDoctorFilterBase classes. +Tests the functionality of base classes for mesh doctor filters and generators. +""" + + +@pytest.fixture( scope="module" ) +def simple_test_mesh(): + """Fixture for a simple test mesh.""" + x, y, z = np.array( [ 0, 1 ] ), np.array( [ 0, 1 ] ), np.array( [ 0, 1 ] ) + mesh = build_rectilinear_blocks_mesh( [ XYZ( x, y, z ) ] ) + return mesh + + +@pytest.fixture( scope="module" ) +def single_cell_mesh(): + """Fixture for a single tetrahedron mesh.""" + return createSingleCellMesh( VTK_TETRA, np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ] ) ) + + +@pytest.fixture( scope="module" ) +def empty_mesh(): + """Fixture for an empty mesh.""" + mesh = vtkUnstructuredGrid() + points = vtkPoints() + mesh.SetPoints( points ) + return mesh + + +class ConcreteFilterForTesting( MeshDoctorFilterBase ): + """Concrete implementation of MeshDoctorFilterBase for testing purposes.""" + + def __init__( self, mesh, filterName="TestFilter", useExternalLogger=False, shouldSucceed=True ): + super().__init__( mesh, filterName, useExternalLogger ) + self.shouldSucceed = shouldSucceed + self.applyFilterCalled = False + + def applyFilter( self ): + """Test implementation that can be configured to succeed or fail.""" + self.applyFilterCalled = True + if self.shouldSucceed: + self.logger.info( "Test filter applied successfully" ) + return True + else: + self.logger.error( "Test filter failed" ) + return False + + +class ConcreteGeneratorForTesting( MeshDoctorGeneratorBase ): + """Concrete implementation of MeshDoctorGeneratorBase for testing purposes.""" + + def __init__( self, filterName="TestGenerator", useExternalLogger=False, shouldSucceed=True ): + super().__init__( filterName, useExternalLogger ) + self.shouldSucceed = shouldSucceed + self.applyFilterCalled = False + + def applyFilter( self ): + """Test implementation that generates a simple mesh or fails.""" + self.applyFilterCalled = True + if self.shouldSucceed: + # Generate a simple single-cell mesh + self.mesh = createSingleCellMesh( VTK_TETRA, + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ] ) ) + self.logger.info( "Test generator applied successfully" ) + return True + else: + self.logger.error( "Test generator failed" ) + return False + + +class TestMeshDoctorFilterBase: + """Test class for MeshDoctorFilterBase functionality.""" + + def test_initialization_valid_inputs( self, simple_test_mesh ): + """Test successful initialization with valid inputs.""" + filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter", False ) + + assert filter_instance.filterName == "TestFilter" + assert filter_instance.mesh is not None + assert filter_instance.mesh.GetNumberOfCells() > 0 + assert filter_instance.logger is not None + + # Verify that mesh is a copy, not the original + assert filter_instance.mesh is not simple_test_mesh + + def test_initialization_with_external_logger( self, simple_test_mesh ): + """Test initialization with external logger.""" + filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter", True ) + + assert filter_instance.filterName == "TestFilter" + assert isinstance( filter_instance.logger, logging.Logger ) + + def test_initialization_invalid_mesh_type( self ): + """Test initialization with invalid mesh type.""" + for error_obj in [ "not_a_mesh", 123, None ]: + with pytest.raises( TypeError, match="Input 'mesh' must be a vtkUnstructuredGrid" ): + ConcreteFilterForTesting( error_obj, "TestFilter" ) + + def test_initialization_empty_mesh( self, empty_mesh ): + """Test initialization with empty mesh.""" + with pytest.raises( ValueError, match="Input 'mesh' cannot be empty" ): + ConcreteFilterForTesting( empty_mesh, "TestFilter" ) + + def test_initialization_invalid_filter_name( self, simple_test_mesh ): + """Test initialization with invalid filter name.""" + for error_obj in [ 123, None ]: + with pytest.raises( TypeError, match="Input 'filterName' must be a string" ): + ConcreteFilterForTesting( simple_test_mesh, error_obj ) + + for error_obj in [ "", " " ]: + with pytest.raises( ValueError, match="Input 'filterName' cannot be an empty or whitespace-only string" ): + ConcreteFilterForTesting( simple_test_mesh, error_obj ) + + def test_initialization_invalid_external_logger_flag( self, simple_test_mesh ): + """Test initialization with invalid useExternalLogger flag.""" + for error_obj in [ "not_bool", 1 ]: + with pytest.raises( TypeError, match="Input 'useExternalLogger' must be a boolean" ): + ConcreteFilterForTesting( simple_test_mesh, "TestFilter", error_obj ) + + def test_get_mesh( self, simple_test_mesh ): + """Test getMesh method returns the correct mesh.""" + filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter" ) + returned_mesh = filter_instance.getMesh() + + assert returned_mesh is filter_instance.mesh + assert returned_mesh.GetNumberOfCells() == simple_test_mesh.GetNumberOfCells() + assert returned_mesh.GetNumberOfPoints() == simple_test_mesh.GetNumberOfPoints() + + def test_copy_mesh( self, simple_test_mesh ): + """Test copyMesh helper method.""" + filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter" ) + copied_mesh = filter_instance.copyMesh( simple_test_mesh ) + + assert copied_mesh is not simple_test_mesh + assert copied_mesh.GetNumberOfCells() == simple_test_mesh.GetNumberOfCells() + assert copied_mesh.GetNumberOfPoints() == simple_test_mesh.GetNumberOfPoints() + + def test_apply_filter_success( self, simple_test_mesh ): + """Test successful filter application.""" + filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter", shouldSucceed=True ) + result = filter_instance.applyFilter() + + assert result is True + assert filter_instance.applyFilterCalled + + def test_apply_filter_failure( self, simple_test_mesh ): + """Test filter application failure.""" + filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter", shouldSucceed=False ) + result = filter_instance.applyFilter() + + assert result is False + assert filter_instance.applyFilterCalled + + def test_write_grid_with_mesh( self, simple_test_mesh, tmp_path ): + """Test writing mesh to file when mesh is available.""" + filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter" ) + output_file = tmp_path / "test_output.vtu" + + filter_instance.writeGrid( str( output_file ) ) + + # Verify file was created + assert output_file.exists() + assert output_file.stat().st_size > 0 + + def test_write_grid_with_different_options( self, simple_test_mesh, tmp_path ): + """Test writing mesh with different file options.""" + filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter" ) + + # Test ASCII mode + output_file_ascii = tmp_path / "test_ascii.vtu" + filter_instance.writeGrid( str( output_file_ascii ), isDataModeBinary=False ) + assert output_file_ascii.exists() + + # Test with overwrite enabled + output_file_overwrite = tmp_path / "test_overwrite.vtu" + filter_instance.writeGrid( str( output_file_overwrite ), canOverwrite=True ) + assert output_file_overwrite.exists() + + # Write again with overwrite enabled (should not raise error) + filter_instance.writeGrid( str( output_file_overwrite ), canOverwrite=True ) + + def test_write_grid_without_mesh( self, simple_test_mesh, tmp_path, caplog ): + """Test writing when no mesh is available.""" + filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter" ) + filter_instance.mesh = None # Remove the mesh + + output_file = tmp_path / "should_not_exist.vtu" + + with caplog.at_level( logging.ERROR ): + filter_instance.writeGrid( str( output_file ) ) + + # Should log error and not create file + assert "No mesh available" in caplog.text + assert not output_file.exists() + + def test_set_logger_handler_without_existing_handlers( self, simple_test_mesh ): + """Test setting logger handler when no handlers exist.""" + filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter", useExternalLogger=True ) + + # Clear any existing handlers + filter_instance.logger.handlers.clear() + + # Create a mock handler + mock_handler = Mock() + filter_instance.setLoggerHandler( mock_handler ) + + # Verify handler was added + assert mock_handler in filter_instance.logger.handlers + + def test_set_logger_handler_with_existing_handlers( self, simple_test_mesh, caplog ): + """Test setting logger handler when handlers already exist.""" + filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter_with_handlers", + useExternalLogger=True ) + filter_instance.logger.addHandler( logging.NullHandler() ) + + mock_handler = Mock() + mock_handler.level = logging.WARNING + + with caplog.at_level( logging.WARNING ): + filter_instance.setLoggerHandler( mock_handler ) + + # Now caplog will capture the warning correctly + assert "already has a handler" in caplog.text + + def test_logger_functionality( self, simple_test_mesh, caplog ): + """Test that logging works correctly.""" + filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter_functionality" ) + + with caplog.at_level( logging.INFO ): + filter_instance.applyFilter() + + # Should have logged the success message + assert "Test filter applied successfully" in caplog.text + + def test_mesh_deep_copy_behavior( self, simple_test_mesh ): + """Test that the filter creates a deep copy of the input mesh.""" + filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter" ) + + # Modify the original mesh + original_cell_count = simple_test_mesh.GetNumberOfCells() + + # The filter's mesh should be independent of the original + filter_mesh = filter_instance.getMesh() + assert filter_mesh.GetNumberOfCells() == original_cell_count + assert filter_mesh is not simple_test_mesh + + +class TestMeshDoctorGeneratorBase: + """Test class for MeshDoctorGeneratorBase functionality.""" + + def test_initialization_valid_inputs( self ): + """Test successful initialization with valid inputs.""" + generator_instance = ConcreteGeneratorForTesting( "TestGenerator", False ) + + assert generator_instance.filterName == "TestGenerator" + assert generator_instance.mesh is None # Should start with no mesh + assert generator_instance.logger is not None + + def test_initialization_with_external_logger( self ): + """Test initialization with external logger.""" + generator_instance = ConcreteGeneratorForTesting( "TestGenerator", True ) + + assert generator_instance.filterName == "TestGenerator" + assert isinstance( generator_instance.logger, logging.Logger ) + + def test_initialization_invalid_filter_name( self ): + """Test initialization with invalid filter name.""" + for error_obj in [ 123, None ]: + with pytest.raises( TypeError, match="Input 'filterName' must be a string" ): + ConcreteGeneratorForTesting( error_obj ) + + for error_obj in [ "", " " ]: + with pytest.raises( ValueError, match="Input 'filterName' cannot be an empty or whitespace-only string" ): + ConcreteGeneratorForTesting( error_obj ) + + def test_initialization_invalid_external_logger_flag( self ): + """Test initialization with invalid useExternalLogger flag.""" + for error_obj in [ "not_bool", 1 ]: + with pytest.raises( TypeError, match="Input 'useExternalLogger' must be a boolean" ): + ConcreteGeneratorForTesting( "TestGenerator", error_obj ) + + def test_get_mesh_before_generation( self ): + """Test getMesh method before mesh generation.""" + generator_instance = ConcreteGeneratorForTesting( "TestGenerator" ) + returned_mesh = generator_instance.getMesh() + + assert returned_mesh is None + + def test_get_mesh_after_generation( self ): + """Test getMesh method after successful mesh generation.""" + generator_instance = ConcreteGeneratorForTesting( "TestGenerator", shouldSucceed=True ) + result = generator_instance.applyFilter() + + assert result is True + assert generator_instance.mesh is not None + + returned_mesh = generator_instance.getMesh() + assert returned_mesh is generator_instance.mesh + assert returned_mesh.GetNumberOfCells() > 0 + + def test_apply_filter_success( self ): + """Test successful mesh generation.""" + generator_instance = ConcreteGeneratorForTesting( "TestGenerator", shouldSucceed=True ) + result = generator_instance.applyFilter() + + assert result is True + assert generator_instance.applyFilterCalled + assert generator_instance.mesh is not None + + def test_apply_filter_failure( self ): + """Test mesh generation failure.""" + generator_instance = ConcreteGeneratorForTesting( "TestGenerator", shouldSucceed=False ) + result = generator_instance.applyFilter() + + assert result is False + assert generator_instance.applyFilterCalled + assert generator_instance.mesh is None + + def test_write_grid_with_generated_mesh( self, tmp_path ): + """Test writing generated mesh to file.""" + generator_instance = ConcreteGeneratorForTesting( "TestGenerator", shouldSucceed=True ) + generator_instance.applyFilter() + + output_file = tmp_path / "generated_mesh.vtu" + generator_instance.writeGrid( str( output_file ) ) + + # Verify file was created + assert output_file.exists() + assert output_file.stat().st_size > 0 + + def test_write_grid_without_generated_mesh( self, tmp_path, caplog ): + """Test writing when no mesh has been generated.""" + generator_instance = ConcreteGeneratorForTesting( "TestGenerator" ) + output_file = tmp_path / "should_not_exist.vtu" + + with caplog.at_level( logging.ERROR ): + generator_instance.writeGrid( str( output_file ) ) + + # Should log error and not create file + assert "No mesh generated" in caplog.text + assert not output_file.exists() + + def test_write_grid_with_different_options( self, tmp_path ): + """Test writing generated mesh with different file options.""" + generator_instance = ConcreteGeneratorForTesting( "TestGenerator", shouldSucceed=True ) + generator_instance.applyFilter() + + # Test ASCII mode + output_file_ascii = tmp_path / "generated_ascii.vtu" + generator_instance.writeGrid( str( output_file_ascii ), isDataModeBinary=False ) + assert output_file_ascii.exists() + + # Test with overwrite enabled + output_file_overwrite = tmp_path / "generated_overwrite.vtu" + generator_instance.writeGrid( str( output_file_overwrite ), canOverwrite=True ) + assert output_file_overwrite.exists() + + def test_set_logger_handler_without_existing_handlers( self ): + """Test setting logger handler when no handlers exist.""" + generator_instance = ConcreteGeneratorForTesting( "TestGenerator", useExternalLogger=True ) + + # Clear any existing handlers + generator_instance.logger.handlers.clear() + + # Create a mock handler + mock_handler = Mock() + generator_instance.setLoggerHandler( mock_handler ) + + # Verify handler was added + assert mock_handler in generator_instance.logger.handlers + + def test_set_logger_handler_with_existing_handlers( self, caplog ): + """Test setting logger handler when handlers already exist.""" + generator_instance = ConcreteGeneratorForTesting( "TestGenerator_with_handlers", useExternalLogger=True ) + generator_instance.logger.addHandler( logging.NullHandler() ) + + mock_handler = Mock() + mock_handler.level = logging.WARNING + + with caplog.at_level( logging.WARNING ): + generator_instance.setLoggerHandler( mock_handler ) + + # Now caplog will capture the warning correctly + assert "already has a handler" in caplog.text + + def test_logger_functionality( self, caplog ): + """Test that logging works correctly.""" + generator_instance = ConcreteGeneratorForTesting( "TestGenerator_functionality", shouldSucceed=True ) + + with caplog.at_level( logging.INFO ): + generator_instance.applyFilter() + + # Should have logged the success message + assert "Test generator applied successfully" in caplog.text + + +class TestMeshDoctorBaseEdgeCases: + """Test class for edge cases and integration scenarios.""" + + def test_filter_base_not_implemented_error( self, simple_test_mesh ): + """Test that base class raises NotImplementedError.""" + filter_instance = MeshDoctorFilterBase( simple_test_mesh, "BaseFilter" ) + + with pytest.raises( NotImplementedError, match="Subclasses must implement applyFilter method" ): + filter_instance.applyFilter() + + def test_generator_base_not_implemented_error( self ): + """Test that base generator class raises NotImplementedError.""" + generator_instance = MeshDoctorGeneratorBase( "BaseGenerator" ) + + with pytest.raises( NotImplementedError, match="Subclasses must implement applyFilter method" ): + generator_instance.applyFilter() + + def test_filter_with_single_cell_mesh( self, single_cell_mesh ): + """Test filter with a single cell mesh.""" + filter_instance = ConcreteFilterForTesting( single_cell_mesh, "SingleCellTest" ) + result = filter_instance.applyFilter() + + assert result is True + assert filter_instance.getMesh().GetNumberOfCells() == 1 + + def test_filter_mesh_independence( self, simple_test_mesh ): + """Test that multiple filters are independent.""" + filter1 = ConcreteFilterForTesting( simple_test_mesh, "Filter1" ) + filter2 = ConcreteFilterForTesting( simple_test_mesh, "Filter2" ) + + mesh1 = filter1.getMesh() + mesh2 = filter2.getMesh() + + # Meshes should be independent copies + assert mesh1 is not mesh2 + assert mesh1 is not simple_test_mesh + assert mesh2 is not simple_test_mesh + + def test_generator_multiple_instances( self ): + """Test that multiple generator instances are independent.""" + gen1 = ConcreteGeneratorForTesting( "Gen1", shouldSucceed=True ) + gen2 = ConcreteGeneratorForTesting( "Gen2", shouldSucceed=True ) + + gen1.applyFilter() + gen2.applyFilter() + + assert gen1.getMesh() is not gen2.getMesh() + assert gen1.getMesh() is not None + assert gen2.getMesh() is not None + + def test_filter_logger_names( self, simple_test_mesh ): + """Test that different filters get different logger names.""" + filter1 = ConcreteFilterForTesting( simple_test_mesh, "Filter1" ) + filter2 = ConcreteFilterForTesting( simple_test_mesh, "Filter2" ) + + assert filter1.logger.name != filter2.logger.name + + def test_generator_logger_names( self ): + """Test that different generators get different logger names.""" + gen1 = ConcreteGeneratorForTesting( "Gen1" ) + gen2 = ConcreteGeneratorForTesting( "Gen2" ) + + assert gen1.logger.name != gen2.logger.name + + +@pytest.mark.parametrize( "filter_name,should_succeed", [ + ( "ParametrizedFilter1", True ), + ( "ParametrizedFilter2", False ), + ( "LongFilterNameForTesting", True ), + ( "UnicodeFilter", True ), +] ) +def test_parametrized_filter_behavior( simple_test_mesh, filter_name, should_succeed ): + """Parametrized test for different filter configurations.""" + filter_instance = ConcreteFilterForTesting( simple_test_mesh, filter_name, shouldSucceed=should_succeed ) + + result = filter_instance.applyFilter() + assert result == should_succeed + assert filter_instance.filterName == filter_name + + +@pytest.mark.parametrize( "generator_name,should_succeed", [ + ( "ParametrizedGen1", True ), + ( "ParametrizedGen2", False ), + ( "LongGeneratorNameForTesting", True ), + ( "UnicodeGenerator", True ), +] ) +def test_parametrized_generator_behavior( generator_name, should_succeed ): + """Parametrized test for different generator configurations.""" + generator_instance = ConcreteGeneratorForTesting( generator_name, shouldSucceed=should_succeed ) + + result = generator_instance.applyFilter() + assert result == should_succeed + assert generator_instance.filterName == generator_name + + if should_succeed: + assert generator_instance.getMesh() is not None + else: + assert generator_instance.getMesh() is None From 8824b006b222d76525ecb069d74377b269276572 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Wed, 3 Sep 2025 16:47:26 -0700 Subject: [PATCH 44/52] Add NonCorformal tests --- .../geos/mesh/doctor/filters/NonConformal.py | 2 +- geos-mesh/tests/test_non_conformal.py | 456 ++++++++++++++++-- 2 files changed, 428 insertions(+), 30 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py index a5ed270b8..0d816861f 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py @@ -108,7 +108,7 @@ def applyFilter( self: Self ) -> bool: # Add marking arrays if requested if self.writeNonConformalCells and self.nonConformalCells: - self._addNonConformalCellsArray( self.nonConformalCells ) + self._addNonConformalCellsArray() self.logger.info( f"The filter {self.logger.name} succeeded." ) return True diff --git a/geos-mesh/tests/test_non_conformal.py b/geos-mesh/tests/test_non_conformal.py index e95c46972..4e574e1e4 100644 --- a/geos-mesh/tests/test_non_conformal.py +++ b/geos-mesh/tests/test_non_conformal.py @@ -1,63 +1,461 @@ import numpy +import pytest +import logging from geos.mesh.doctor.actions.generate_cube import build_rectilinear_blocks_mesh, XYZ from geos.mesh.doctor.actions.non_conformal import Options, mesh_action +from geos.mesh.doctor.filters.NonConformal import NonConformal, nonConformal +__doc__ = """ +Test module for non-conformal mesh detection. +Tests both the actions-based functions and the NonConformal filter class. +""" -def test_two_close_hexs(): + +@pytest.fixture( scope="module" ) +def two_close_hexs_mesh(): + """Fixture for two hexahedra that are very close (non-conformal).""" delta = 1.e-6 tmp = numpy.arange( 2, dtype=float ) xyz0 = XYZ( tmp, tmp, tmp ) xyz1 = XYZ( tmp + 1 + delta, tmp, tmp ) - mesh = build_rectilinear_blocks_mesh( ( xyz0, xyz1 ) ) + return build_rectilinear_blocks_mesh( ( xyz0, xyz1 ) ) + + +@pytest.fixture( scope="module" ) +def two_distant_hexs_mesh(): + """Fixture for two hexahedra that are far apart (conformal).""" + delta = 1 + tmp = numpy.arange( 2, dtype=float ) + xyz0 = XYZ( tmp, tmp, tmp ) + xyz1 = XYZ( tmp + 1 + delta, tmp, tmp ) + return build_rectilinear_blocks_mesh( ( xyz0, xyz1 ) ) + + +@pytest.fixture( scope="module" ) +def two_shifted_hexs_mesh(): + """Fixture for two hexahedra that are close but shifted (non-conformal).""" + delta_x, delta_y = 1.e-6, 0.5 + tmp = numpy.arange( 2, dtype=float ) + xyz0 = XYZ( tmp, tmp, tmp ) + xyz1 = XYZ( tmp + 1 + delta_x, tmp + delta_y, tmp + delta_y ) + return build_rectilinear_blocks_mesh( ( xyz0, xyz1 ) ) + + +@pytest.fixture( scope="module" ) +def big_small_elements_mesh(): + """Fixture for big element next to small element (non-conformal).""" + delta = 1.e-6 + tmp = numpy.arange( 2, dtype=float ) + xyz0 = XYZ( tmp, tmp + 1, tmp + 1 ) + xyz1 = XYZ( 3 * tmp + 1 + delta, 3 * tmp, 3 * tmp ) + return build_rectilinear_blocks_mesh( ( xyz0, xyz1 ) ) + +def test_two_close_hexs( two_close_hexs_mesh ): + delta = 1.e-6 # 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 = mesh_action( mesh, options ) + results = mesh_action( two_close_hexs_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 = mesh_action( mesh, options ) + results = mesh_action( two_close_hexs_mesh, options ) assert len( results.non_conformal_cells ) == 0 -def test_two_distant_hexs(): +def test_two_distant_hexs( two_distant_hexs_mesh ): delta = 1 - tmp = numpy.arange( 2, dtype=float ) - xyz0 = XYZ( tmp, tmp, tmp ) - xyz1 = XYZ( tmp + 1 + delta, tmp, tmp ) - mesh = build_rectilinear_blocks_mesh( ( xyz0, xyz1 ) ) - options = Options( angle_tolerance=1., point_tolerance=delta / 2., face_tolerance=delta / 2. ) - - results = mesh_action( mesh, options ) + results = mesh_action( two_distant_hexs_mesh, options ) assert len( results.non_conformal_cells ) == 0 -def test_two_close_shifted_hexs(): - delta_x, delta_y = 1.e-6, 0.5 - tmp = numpy.arange( 2, dtype=float ) - xyz0 = XYZ( tmp, tmp, tmp ) - xyz1 = XYZ( tmp + 1 + delta_x, tmp + delta_y, tmp + delta_y ) - mesh = build_rectilinear_blocks_mesh( ( xyz0, xyz1 ) ) - +def test_two_close_shifted_hexs( two_shifted_hexs_mesh ): + delta_x = 1.e-6 options = Options( angle_tolerance=1., point_tolerance=delta_x * 2, face_tolerance=delta_x * 2 ) - - results = mesh_action( mesh, options ) + results = mesh_action( two_shifted_hexs_mesh, options ) assert len( results.non_conformal_cells ) == 1 assert set( results.non_conformal_cells[ 0 ] ) == { 0, 1 } -def test_big_elem_next_to_small_elem(): +def test_big_elem_next_to_small_elem( big_small_elements_mesh ): delta = 1.e-6 - tmp = numpy.arange( 2, dtype=float ) - xyz0 = XYZ( tmp, tmp + 1, tmp + 1 ) - xyz1 = XYZ( 3 * tmp + 1 + delta, 3 * tmp, 3 * tmp ) - mesh = build_rectilinear_blocks_mesh( ( xyz0, xyz1 ) ) - options = Options( angle_tolerance=1., point_tolerance=delta * 2, face_tolerance=delta * 2 ) - - results = mesh_action( mesh, options ) + results = mesh_action( big_small_elements_mesh, options ) assert len( results.non_conformal_cells ) == 1 assert set( results.non_conformal_cells[ 0 ] ) == { 0, 1 } + + +class TestNonConformalFilter: + """Test class for NonConformal filter functionality.""" + + def test_filter_initialization_default( self, two_close_hexs_mesh ): + """Test filter initialization with default parameters.""" + filter_instance = NonConformal( two_close_hexs_mesh ) + + assert filter_instance.getPointTolerance() == 0.0 + assert filter_instance.getFaceTolerance() == 0.0 + assert filter_instance.getAngleTolerance() == 10.0 + assert not filter_instance.writeNonConformalCells + assert filter_instance.getMesh() is not None + + def test_filter_initialization_custom( self, two_close_hexs_mesh ): + """Test filter initialization with custom parameters.""" + filter_instance = NonConformal( two_close_hexs_mesh, + pointTolerance=1e-6, + faceTolerance=1e-5, + angleTolerance=5.0, + writeNonConformalCells=True, + useExternalLogger=True ) + + assert filter_instance.getPointTolerance() == 1e-6 + assert filter_instance.getFaceTolerance() == 1e-5 + assert filter_instance.getAngleTolerance() == 5.0 + assert filter_instance.writeNonConformalCells + + def test_filter_detect_non_conformal_strict_tolerance( self, two_close_hexs_mesh ): + """Test detection of non-conformal cells with strict tolerance.""" + delta = 1.e-6 + filter_instance = NonConformal( two_close_hexs_mesh, + pointTolerance=delta / 2, + faceTolerance=delta * 2, + angleTolerance=1.0 ) + + success = filter_instance.applyFilter() + assert success + + non_conformal_cells = filter_instance.getNonConformalCells() + assert len( non_conformal_cells ) == 1 + assert set( non_conformal_cells[ 0 ] ) == { 0, 1 } + + def test_filter_detect_conformal_loose_tolerance( self, two_close_hexs_mesh ): + """Test that close cells are considered conformal with loose tolerance.""" + delta = 1.e-6 + filter_instance = NonConformal( two_close_hexs_mesh, + pointTolerance=delta * 2, + faceTolerance=delta * 2, + angleTolerance=1.0 ) + + success = filter_instance.applyFilter() + assert success + + non_conformal_cells = filter_instance.getNonConformalCells() + assert len( non_conformal_cells ) == 0 + + def test_filter_distant_cells_conformal( self, two_distant_hexs_mesh ): + """Test that distant cells are considered conformal.""" + filter_instance = NonConformal( two_distant_hexs_mesh, + pointTolerance=0.5, + faceTolerance=0.5, + angleTolerance=1.0 ) + + success = filter_instance.applyFilter() + assert success + + non_conformal_cells = filter_instance.getNonConformalCells() + assert len( non_conformal_cells ) == 0 + + def test_filter_shifted_cells_non_conformal( self, two_shifted_hexs_mesh ): + """Test detection of shifted non-conformal cells.""" + delta_x = 1.e-6 + filter_instance = NonConformal( two_shifted_hexs_mesh, + pointTolerance=delta_x * 2, + faceTolerance=delta_x * 2, + angleTolerance=1.0 ) + + success = filter_instance.applyFilter() + assert success + + non_conformal_cells = filter_instance.getNonConformalCells() + assert len( non_conformal_cells ) == 1 + assert set( non_conformal_cells[ 0 ] ) == { 0, 1 } + + def test_filter_big_small_elements( self, big_small_elements_mesh ): + """Test detection of non-conformal interface between different sized elements.""" + delta = 1.e-6 + filter_instance = NonConformal( big_small_elements_mesh, + pointTolerance=delta * 2, + faceTolerance=delta * 2, + angleTolerance=1.0 ) + + success = filter_instance.applyFilter() + assert success + + non_conformal_cells = filter_instance.getNonConformalCells() + assert len( non_conformal_cells ) == 1 + assert set( non_conformal_cells[ 0 ] ) == { 0, 1 } + + def test_filter_write_non_conformal_array( self, two_close_hexs_mesh ): + """Test writing non-conformal cells array to mesh.""" + delta = 1.e-6 + filter_instance = NonConformal( two_close_hexs_mesh, + pointTolerance=delta / 2, + faceTolerance=delta * 2, + angleTolerance=1.0, + writeNonConformalCells=True ) + + success = filter_instance.applyFilter() + assert success + + # Check that the array was added to the mesh + output_mesh = filter_instance.getMesh() + cell_data = output_mesh.GetCellData() + + non_conformal_array = cell_data.GetArray( "IsNonConformal" ) + assert non_conformal_array is not None + assert non_conformal_array.GetNumberOfTuples() == output_mesh.GetNumberOfCells() + + # Check array values - should have 1s for non-conformal cells + has_non_conformal = False + for i in range( non_conformal_array.GetNumberOfTuples() ): + value = non_conformal_array.GetValue( i ) + assert value in [ 0, 1 ] + if value == 1: + has_non_conformal = True + + assert has_non_conformal # Should have detected some non-conformal cells + + def test_filter_no_array_when_disabled( self, two_close_hexs_mesh ): + """Test that no array is added when writeNonConformalCells is False.""" + filter_instance = NonConformal( two_close_hexs_mesh, + pointTolerance=1e-8, + faceTolerance=1e-6, + writeNonConformalCells=False ) + + success = filter_instance.applyFilter() + assert success + + output_mesh = filter_instance.getMesh() + cell_data = output_mesh.GetCellData() + + non_conformal_array = cell_data.GetArray( "IsNonConformal" ) + assert non_conformal_array is None + + def test_filter_no_array_when_no_non_conformal_cells( self, two_distant_hexs_mesh ): + """Test that no array is added when no non-conformal cells are found.""" + filter_instance = NonConformal( two_distant_hexs_mesh, + pointTolerance=0.1, + faceTolerance=0.1, + writeNonConformalCells=True ) + + success = filter_instance.applyFilter() + assert success + + non_conformal_cells = filter_instance.getNonConformalCells() + assert len( non_conformal_cells ) == 0 + + # Array should not be added when there are no non-conformal cells + output_mesh = filter_instance.getMesh() + cell_data = output_mesh.GetCellData() + non_conformal_array = cell_data.GetArray( "IsNonConformal" ) + assert non_conformal_array is None + + def test_filter_tolerance_setters_getters( self, two_close_hexs_mesh ): + """Test setter and getter methods for tolerances.""" + filter_instance = NonConformal( two_close_hexs_mesh ) + + # Test point tolerance + filter_instance.setPointTolerance( 1e-5 ) + assert filter_instance.getPointTolerance() == 1e-5 + + # Test face tolerance + filter_instance.setFaceTolerance( 2e-5 ) + assert filter_instance.getFaceTolerance() == 2e-5 + + # Test angle tolerance + filter_instance.setAngleTolerance( 15.0 ) + assert filter_instance.getAngleTolerance() == 15.0 + + # Test write flag + filter_instance.setWriteNonConformalCells( True ) + assert filter_instance.writeNonConformalCells + + def test_filter_write_grid( self, two_close_hexs_mesh, tmp_path ): + """Test writing the output mesh to file.""" + filter_instance = NonConformal( two_close_hexs_mesh ) + success = filter_instance.applyFilter() + assert success + + output_file = tmp_path / "non_conformal_output.vtu" + filter_instance.writeGrid( str( output_file ) ) + + assert output_file.exists() + assert output_file.stat().st_size > 0 + + +class TestNonConformalStandaloneFunction: + """Test class for the standalone nonConformal function.""" + + def test_standalone_function_basic( self, two_close_hexs_mesh, tmp_path ): + """Test basic functionality of the standalone function.""" + output_file = tmp_path / "standalone_output.vtu" + + mesh, non_conformal_cells = nonConformal( two_close_hexs_mesh, + str( output_file ), + pointTolerance=1e-8, + faceTolerance=1e-6, + angleTolerance=1.0, + writeNonConformalCells=False ) + + assert mesh is not None + assert isinstance( non_conformal_cells, list ) + assert len( non_conformal_cells ) > 0 # Should detect non-conformal cells + assert output_file.exists() + + def test_standalone_function_with_array_writing( self, two_close_hexs_mesh, tmp_path ): + """Test standalone function with array writing enabled.""" + output_file = tmp_path / "standalone_with_array.vtu" + + mesh, non_conformal_cells = nonConformal( two_close_hexs_mesh, + str( output_file ), + pointTolerance=1e-8, + faceTolerance=1e-6, + angleTolerance=1.0, + writeNonConformalCells=True ) + + assert mesh is not None + assert len( non_conformal_cells ) > 0 + + # Check that the array was added + non_conformal_array = mesh.GetCellData().GetArray( "IsNonConformal" ) + assert non_conformal_array is not None + + def test_standalone_function_no_non_conformal( self, two_distant_hexs_mesh, tmp_path ): + """Test standalone function when no non-conformal cells are found.""" + output_file = tmp_path / "no_non_conformal.vtu" + + mesh, non_conformal_cells = nonConformal( two_distant_hexs_mesh, + str( output_file ), + pointTolerance=0.1, + faceTolerance=0.1, + angleTolerance=1.0, + writeNonConformalCells=True ) + + assert mesh is not None + assert len( non_conformal_cells ) == 0 + assert output_file.exists() + + def test_standalone_function_different_tolerances( self, two_shifted_hexs_mesh, tmp_path ): + """Test standalone function with different tolerance settings.""" + output_file = tmp_path / "different_tolerances.vtu" + + mesh, non_conformal_cells = nonConformal( two_shifted_hexs_mesh, + str( output_file ), + pointTolerance=2e-6, + faceTolerance=2e-6, + angleTolerance=5.0, + writeNonConformalCells=True ) + + assert mesh is not None + assert len( non_conformal_cells ) == 1 + assert set( non_conformal_cells[ 0 ] ) == { 0, 1 } + + +class TestNonConformalEdgeCases: + """Test class for edge cases and specific scenarios.""" + + def test_filter_with_very_small_tolerances( self, two_close_hexs_mesh ): + """Test filter with extremely small tolerances.""" + filter_instance = NonConformal( two_close_hexs_mesh, + pointTolerance=1e-15, + faceTolerance=1e-15, + angleTolerance=0.001 ) + + success = filter_instance.applyFilter() + assert success + + # With extremely small tolerances, should detect non-conformal cells + non_conformal_cells = filter_instance.getNonConformalCells() + assert isinstance( non_conformal_cells, list ) + + def test_filter_with_large_tolerances( self, big_small_elements_mesh ): + """Test filter with very large tolerances.""" + filter_instance = NonConformal( big_small_elements_mesh, + pointTolerance=100.0, + faceTolerance=100.0, + angleTolerance=180.0 ) + + success = filter_instance.applyFilter() + assert success + + # With very large tolerances, should consider cells conformal + non_conformal_cells = filter_instance.getNonConformalCells() + # Depending on implementation, might still detect some non-conformal cases + assert isinstance( non_conformal_cells, list ) + + def test_filter_zero_tolerances( self, two_close_hexs_mesh ): + """Test filter with zero tolerances.""" + filter_instance = NonConformal( two_close_hexs_mesh, pointTolerance=0.0, faceTolerance=0.0, angleTolerance=0.0 ) + + success = filter_instance.applyFilter() + assert success + + non_conformal_cells = filter_instance.getNonConformalCells() + assert isinstance( non_conformal_cells, list ) + + def test_filter_result_structure( self, two_close_hexs_mesh ): + """Test the structure of non-conformal cell results.""" + filter_instance = NonConformal( two_close_hexs_mesh, pointTolerance=1e-8, faceTolerance=1e-6 ) + + success = filter_instance.applyFilter() + assert success + + non_conformal_cells = filter_instance.getNonConformalCells() + + # Check result structure + for pair in non_conformal_cells: + assert isinstance( pair, tuple ) + assert len( pair ) == 2 + cell_id1, cell_id2 = pair + assert isinstance( cell_id1, int ) + assert isinstance( cell_id2, int ) + assert cell_id1 >= 0 + assert cell_id2 >= 0 + assert cell_id1 != cell_id2 # Should be different cells + + +@pytest.mark.parametrize( "point_tol,face_tol,angle_tol,expected_behavior", [ + ( 1e-8, 1e-6, 1.0, "detect_non_conformal" ), + ( 1e-4, 1e-4, 1.0, "consider_conformal" ), + ( 0.0, 0.0, 0.0, "strict_detection" ), + ( 1.0, 1.0, 180.0, "loose_detection" ), +] ) +def test_parametrized_tolerance_combinations( two_close_hexs_mesh, point_tol, face_tol, angle_tol, expected_behavior ): + """Parametrized test for different tolerance combinations.""" + filter_instance = NonConformal( two_close_hexs_mesh, + pointTolerance=point_tol, + faceTolerance=face_tol, + angleTolerance=angle_tol ) + + success = filter_instance.applyFilter() + assert success + + non_conformal_cells = filter_instance.getNonConformalCells() + + if expected_behavior == "detect_non_conformal": + # Should detect non-conformal cells with strict tolerances + assert len( non_conformal_cells ) > 0 + elif expected_behavior == "consider_conformal": + # Should consider cells conformal with loose tolerances + assert len( non_conformal_cells ) == 0 + elif expected_behavior in [ "strict_detection", "loose_detection" ]: + # Just verify it runs without errors + assert isinstance( non_conformal_cells, list ) + + +def test_filter_logger_integration( two_close_hexs_mesh, caplog ): + """Test that the filter properly logs its operations.""" + filter_instance = NonConformal( two_close_hexs_mesh, useExternalLogger=False ) + + with caplog.at_level( logging.INFO ): + success = filter_instance.applyFilter() + + assert success + assert len( caplog.records ) > 0 + + # Check for expected log messages + log_messages = [ record.message for record in caplog.records ] + assert any( "Apply filter" in msg for msg in log_messages ) + assert any( "succeeded" in msg for msg in log_messages ) From 1bf5fd610cea91ca68b01a226ef7bcdc43b21b70 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Thu, 4 Sep 2025 19:19:48 -0700 Subject: [PATCH 45/52] Add test for SelfIntersectingElements --- .../filters/SelfIntersectingElements.py | 4 +- .../tests/test_SelfIntersectingElements.py | 457 ++++++++++++++++++ 2 files changed, 459 insertions(+), 2 deletions(-) create mode 100644 geos-mesh/tests/test_SelfIntersectingElements.py diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py index 936878374..962c5d860 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py @@ -88,8 +88,8 @@ def applyFilter( self: Self ) -> bool: """ self.logger.info( f"Apply filter {self.logger.name}" ) - self.invalidCells = get_invalid_cell_ids( self.mesh, self.minDistance ) - logger_results( self.logger, self.invalidCells ) + self.invalidCellIds = get_invalid_cell_ids( self.mesh, self.minDistance ) + logger_results( self.logger, self.invalidCellIds ) # Add marking arrays if requested if self.writeInvalidElements: diff --git a/geos-mesh/tests/test_SelfIntersectingElements.py b/geos-mesh/tests/test_SelfIntersectingElements.py new file mode 100644 index 000000000..078c70ffa --- /dev/null +++ b/geos-mesh/tests/test_SelfIntersectingElements.py @@ -0,0 +1,457 @@ +import pytest +import logging +import numpy as np +from vtkmodules.vtkCommonDataModel import VTK_HEXAHEDRON, VTK_PYRAMID, VTK_TETRA +from geos.mesh.utils.genericHelpers import createSingleCellMesh, createMultiCellMesh +from geos.mesh.doctor.filters.SelfIntersectingElements import SelfIntersectingElements, selfIntersectingElements + +__doc__ = """ +Test module for SelfIntersectingElements filter. +Tests the functionality of detecting various types of invalid or problematic elements. +""" + + +@pytest.fixture( scope="module" ) +def single_hex_mesh(): + """Fixture for a single valid hexahedron mesh with no invalid elements.""" + return createSingleCellMesh( + VTK_HEXAHEDRON, + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], + [ 0, 1, 1 ] ] ) ) + + +@pytest.fixture( scope="module" ) +def single_tetra_mesh(): + """Fixture for a single valid tetrahedron.""" + return createSingleCellMesh( VTK_TETRA, np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ] ) ) + + +@pytest.fixture( scope="module" ) +def degenerate_tetra_mesh(): + """Fixture for a tetrahedron with degenerate (coplanar) points.""" + return createSingleCellMesh( VTK_TETRA, + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0.5, 0.5, 0.0 ], [ 0.2, 0.3, 0.0 ] ] ) ) + + +@pytest.fixture( scope="module" ) +def inverted_pyramid_mesh(): + """Fixture for an inverted pyramid (wrong orientation).""" + return createSingleCellMesh( VTK_PYRAMID, + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], [ 0.5, 0.5, + -1.0 ] ] ) ) + + +@pytest.fixture( scope="module" ) +def wrong_point_count_mesh(): + """Fixture for elements with wrong number of points.""" + return createSingleCellMesh( VTK_TETRA, np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0.5, 1.0, 0.0 ] ] ) ) + + +class TestSelfIntersectingElementsFilter: + """Test class for SelfIntersectingElements filter functionality.""" + + def test_filter_initialization_default( self, single_hex_mesh ): + """Test filter initialization with default parameters.""" + filter_instance = SelfIntersectingElements( single_hex_mesh ) + + assert filter_instance.getMinDistance() == 0.0 + assert not filter_instance.writeInvalidElements + assert filter_instance.getMesh() is not None + assert isinstance( filter_instance.getInvalidCellIds(), dict ) + + def test_filter_initialization_custom( self, single_hex_mesh ): + """Test filter initialization with custom parameters.""" + filter_instance = SelfIntersectingElements( single_hex_mesh, + minDistance=1e-6, + writeInvalidElements=True, + useExternalLogger=True ) + + assert filter_instance.getMinDistance() == 1e-6 + assert filter_instance.writeInvalidElements + + def test_filter_on_clean_mesh( self, single_hex_mesh ): + """Test filter on a clean mesh with no invalid elements.""" + filter_instance = SelfIntersectingElements( single_hex_mesh, minDistance=1e-6 ) + success = filter_instance.applyFilter() + + assert success + invalid_cells = filter_instance.getInvalidCellIds() + + # Should be a dictionary with error type keys + assert isinstance( invalid_cells, dict ) + + # Check that all error types have empty lists (no invalid elements) + expected_error_types = [ + 'wrongNumberOfPointsElements', 'intersectingEdgesElements', 'intersectingFacesElements', + 'nonContiguousEdgesElements', 'nonConvexElements', 'facesOrientedIncorrectlyElements', + 'nonPlanarFacesElements', 'degenerateFacesElements' + ] + + for error_type in expected_error_types: + assert error_type in invalid_cells + assert isinstance( invalid_cells[ error_type ], list ) + + def test_filter_on_single_valid_element( self, single_tetra_mesh ): + """Test filter on a single valid element.""" + filter_instance = SelfIntersectingElements( single_tetra_mesh, minDistance=1e-8 ) + success = filter_instance.applyFilter() + + assert success + invalid_cells = filter_instance.getInvalidCellIds() + + # Should have no invalid elements for a properly constructed single tetrahedron + total_invalid = sum( len( cells ) for cells in invalid_cells.values() ) + # Note: This might detect some issues depending on how the single cell is constructed + assert total_invalid >= 0 # Just ensure it doesn't crash + + def test_filter_detect_degenerate_elements( self, degenerate_tetra_mesh ): + """Test detection of degenerate elements.""" + filter_instance = SelfIntersectingElements( degenerate_tetra_mesh, minDistance=1e-6 ) + success = filter_instance.applyFilter() + + assert success + invalid_cells = filter_instance.getInvalidCellIds() + + # Should detect some type of invalidity in the degenerate tetrahedron + total_invalid = sum( len( cells ) for cells in invalid_cells.values() ) + assert total_invalid > 0 + + def test_filter_detect_orientation_issues( self, inverted_pyramid_mesh ): + """Test detection of orientation issues.""" + filter_instance = SelfIntersectingElements( inverted_pyramid_mesh, minDistance=1e-6 ) + success = filter_instance.applyFilter() + + assert success + invalid_cells = filter_instance.getInvalidCellIds() + + # Should detect some type of invalidity in the inverted tetrahedron + total_invalid = sum( len( cells ) for cells in invalid_cells.values() ) + assert total_invalid > 0 + + def test_filter_detect_wrong_point_count( self, wrong_point_count_mesh ): + """Test detection of elements with wrong number of points.""" + filter_instance = SelfIntersectingElements( wrong_point_count_mesh, minDistance=1e-6 ) + success = filter_instance.applyFilter() + + assert success + invalid_cells = filter_instance.getInvalidCellIds() + + # Should detect wrong number of points + assert len( invalid_cells[ 'wrongNumberOfPointsElements' ] ) > 0 + + def test_filter_write_invalid_elements_arrays( self, degenerate_tetra_mesh ): + """Test writing invalid elements arrays to mesh.""" + filter_instance = SelfIntersectingElements( degenerate_tetra_mesh, minDistance=1e-6, writeInvalidElements=True ) + success = filter_instance.applyFilter() + + assert success + + output_mesh = filter_instance.getMesh() + cell_data = output_mesh.GetCellData() + invalid_cells = filter_instance.getInvalidCellIds() + + # Check that arrays were added for error types that have invalid elements + for error_type, invalid_ids in invalid_cells.items(): + if invalid_ids: # Only check if there are actually invalid elements of this type + array_name = f"Is{error_type}" + array = cell_data.GetArray( array_name ) + assert array is not None + assert array.GetNumberOfTuples() == output_mesh.GetNumberOfCells() + + # Verify array contains proper values + for i in range( array.GetNumberOfTuples() ): + value = array.GetValue( i ) + assert value in [ 0, 1 ] + + def test_filter_no_arrays_when_disabled( self, degenerate_tetra_mesh ): + """Test that no arrays are added when writeInvalidElements is False.""" + filter_instance = SelfIntersectingElements( degenerate_tetra_mesh, + minDistance=1e-6, + writeInvalidElements=False ) + success = filter_instance.applyFilter() + + assert success + + output_mesh = filter_instance.getMesh() + cell_data = output_mesh.GetCellData() + + # Should not have added any "Is*" arrays + error_types = [ + 'wrongNumberOfPointsElements', 'intersectingEdgesElements', 'intersectingFacesElements', + 'nonContiguousEdgesElements', 'nonConvexElements', 'facesOrientedIncorrectlyElements', + 'nonPlanarFacesElements', 'degenerateFacesElements' + ] + + for error_type in error_types: + array_name = f"Is{error_type}" + array = cell_data.GetArray( array_name ) + assert array is None + + def test_filter_tolerance_setter_getter( self, single_hex_mesh ): + """Test setter and getter methods for minimum distance.""" + filter_instance = SelfIntersectingElements( single_hex_mesh ) + + # Test default value + assert filter_instance.getMinDistance() == 0.0 + + # Test setting new value + filter_instance.setMinDistance( 1e-5 ) + assert filter_instance.getMinDistance() == 1e-5 + + # Test setting another value + filter_instance.setMinDistance( 1e-10 ) + assert filter_instance.getMinDistance() == 1e-10 + + def test_filter_write_invalid_elements_setter( self, single_hex_mesh ): + """Test setter method for writeInvalidElements flag.""" + filter_instance = SelfIntersectingElements( single_hex_mesh ) + + # Test default value + assert not filter_instance.writeInvalidElements + + # Test setting to True + filter_instance.setWriteInvalidElements( True ) + assert filter_instance.writeInvalidElements + + # Test setting back to False + filter_instance.setWriteInvalidElements( False ) + assert not filter_instance.writeInvalidElements + + def test_filter_write_grid( self, single_hex_mesh, tmp_path ): + """Test writing the output mesh to file.""" + filter_instance = SelfIntersectingElements( single_hex_mesh, minDistance=1e-6 ) + success = filter_instance.applyFilter() + assert success + + output_file = tmp_path / "self_intersecting_output.vtu" + filter_instance.writeGrid( str( output_file ) ) + + assert output_file.exists() + assert output_file.stat().st_size > 0 + + def test_filter_different_tolerance_values( self, degenerate_tetra_mesh ): + """Test filter behavior with different tolerance values.""" + # Test with very small tolerance + filter_small = SelfIntersectingElements( degenerate_tetra_mesh, minDistance=1e-15 ) + success_small = filter_small.applyFilter() + assert success_small + + # Test with larger tolerance + filter_large = SelfIntersectingElements( degenerate_tetra_mesh, minDistance=1e-3 ) + success_large = filter_large.applyFilter() + assert success_large + + # Both should succeed, but might detect different numbers of issues + invalid_small = filter_small.getInvalidCellIds() + invalid_large = filter_large.getInvalidCellIds() + + assert isinstance( invalid_small, dict ) + assert isinstance( invalid_large, dict ) + + +class TestSelfIntersectingElementsStandaloneFunction: + """Test class for the standalone selfIntersectingElements function.""" + + def test_standalone_function_basic( self, single_hex_mesh, tmp_path ): + """Test basic functionality of the standalone function.""" + output_file = tmp_path / "standalone_output.vtu" + + mesh, invalid_cells = selfIntersectingElements( single_hex_mesh, + str( output_file ), + minDistance=1e-6, + writeInvalidElements=False ) + + assert mesh is not None + assert isinstance( invalid_cells, dict ) + assert output_file.exists() + + def test_standalone_function_with_array_writing( self, degenerate_tetra_mesh, tmp_path ): + """Test standalone function with array writing enabled.""" + output_file = tmp_path / "standalone_with_arrays.vtu" + + mesh, invalid_cells = selfIntersectingElements( degenerate_tetra_mesh, + str( output_file ), + minDistance=1e-6, + writeInvalidElements=True ) + + assert mesh is not None + assert isinstance( invalid_cells, dict ) + + # Check if any arrays were added for detected invalid elements + cell_data = mesh.GetCellData() + arrays_found = False + for error_type, invalid_ids in invalid_cells.items(): + if invalid_ids: + array_name = f"Is{error_type}" + array = cell_data.GetArray( array_name ) + if array is not None: + arrays_found = True + break + + # If there were invalid elements detected, arrays should have been added + total_invalid = sum( len( cells ) for cells in invalid_cells.values() ) + if total_invalid > 0: + assert arrays_found + + def test_standalone_function_different_tolerances( self, degenerate_tetra_mesh, tmp_path ): + """Test standalone function with different tolerance settings.""" + output_file = tmp_path / "different_tolerance.vtu" + + mesh, invalid_cells = selfIntersectingElements( degenerate_tetra_mesh, + str( output_file ), + minDistance=1e-8, + writeInvalidElements=True ) + + assert mesh is not None + assert isinstance( invalid_cells, dict ) + assert output_file.exists() + + +class TestSelfIntersectingElementsEdgeCases: + """Test class for edge cases and specific scenarios.""" + + def test_filter_with_zero_tolerance( self, single_hex_mesh ): + """Test filter with zero tolerance.""" + filter_instance = SelfIntersectingElements( single_hex_mesh, minDistance=0.0 ) + success = filter_instance.applyFilter() + + assert success + invalid_cells = filter_instance.getInvalidCellIds() + assert isinstance( invalid_cells, dict ) + + def test_filter_with_negative_tolerance( self, single_hex_mesh ): + """Test filter with negative tolerance (should still work).""" + filter_instance = SelfIntersectingElements( single_hex_mesh, minDistance=-1e-6 ) + success = filter_instance.applyFilter() + + assert success + invalid_cells = filter_instance.getInvalidCellIds() + assert isinstance( invalid_cells, dict ) + + def test_filter_with_very_large_tolerance( self, single_hex_mesh ): + """Test filter with very large tolerance.""" + filter_instance = SelfIntersectingElements( single_hex_mesh, minDistance=1e10 ) + success = filter_instance.applyFilter() + + assert success + invalid_cells = filter_instance.getInvalidCellIds() + assert isinstance( invalid_cells, dict ) + + def test_filter_result_structure_validation( self, degenerate_tetra_mesh ): + """Test the structure of invalid cell results.""" + filter_instance = SelfIntersectingElements( degenerate_tetra_mesh, minDistance=1e-6 ) + success = filter_instance.applyFilter() + + assert success + invalid_cells = filter_instance.getInvalidCellIds() + + # Validate dictionary structure + assert isinstance( invalid_cells, dict ) + + expected_keys = [ + 'wrongNumberOfPointsElements', 'intersectingEdgesElements', 'intersectingFacesElements', + 'nonContiguousEdgesElements', 'nonConvexElements', 'facesOrientedIncorrectlyElements', + 'nonPlanarFacesElements', 'degenerateFacesElements' + ] + + for key in expected_keys: + assert key in invalid_cells + assert isinstance( invalid_cells[ key ], list ) + + # Validate cell IDs are non-negative integers + for cell_id in invalid_cells[ key ]: + assert isinstance( cell_id, ( int, np.integer ) ) + assert cell_id >= 0 + + +class TestSelfIntersectingElementsIntegration: + """Test class for integration scenarios and complex cases.""" + + def test_filter_multiple_error_types( self ): + """Test a mesh that might have multiple types of invalid elements.""" + mesh = createMultiCellMesh( [ VTK_TETRA, VTK_TETRA ], [ + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0.5, 1.0, 0.0 ], [ 0.5, 0.5, 1.0 ] ] ), + np.array( [ [ 2, 0, 0 ], [ 3, 0, 0 ], [ 2.5, 0.5, 0.0 ], [ 2.3, 0.3, 0.0 ] ] ) + ] ) + filter_instance = SelfIntersectingElements( mesh, minDistance=1e-6 ) + success = filter_instance.applyFilter() + + assert success + invalid_cells = filter_instance.getInvalidCellIds() + + # Should detect some invalid elements + total_invalid = sum( len( cells ) for cells in invalid_cells.values() ) + assert total_invalid >= 0 # At least should not crash + + def test_filter_logger_integration( self, degenerate_tetra_mesh, caplog ): + """Test that the filter properly logs its operations.""" + filter_instance = SelfIntersectingElements( degenerate_tetra_mesh, useExternalLogger=False ) + + with caplog.at_level( logging.INFO ): + success = filter_instance.applyFilter() + + assert success + assert len( caplog.records ) > 0 + + # Check for expected log messages + log_messages = [ record.message for record in caplog.records ] + assert any( "Apply filter" in msg for msg in log_messages ) + assert any( "succeeded" in msg for msg in log_messages ) + + +@pytest.mark.parametrize( "min_distance,expected_behavior", [ + ( 0.0, "zero_tolerance" ), + ( 1e-15, "very_small_tolerance" ), + ( 1e-6, "normal_tolerance" ), + ( 1e-3, "large_tolerance" ), + ( 1.0, "very_large_tolerance" ), +] ) +def test_parametrized_tolerance_values( single_hex_mesh, min_distance, expected_behavior ): + """Parametrized test for different tolerance values.""" + filter_instance = SelfIntersectingElements( single_hex_mesh, minDistance=min_distance ) + success = filter_instance.applyFilter() + + assert success + invalid_cells = filter_instance.getInvalidCellIds() + + # All should succeed and return proper structure + assert isinstance( invalid_cells, dict ) + + # Verify tolerance was set correctly + assert filter_instance.getMinDistance() == min_distance + + +@pytest.mark.parametrize( "write_arrays", [ True, False ] ) +def test_parametrized_array_writing( degenerate_tetra_mesh, write_arrays ): + """Parametrized test for array writing options.""" + filter_instance = SelfIntersectingElements( degenerate_tetra_mesh, + minDistance=1e-6, + writeInvalidElements=write_arrays ) + success = filter_instance.applyFilter() + + assert success + assert filter_instance.writeInvalidElements == write_arrays + + output_mesh = filter_instance.getMesh() + cell_data = output_mesh.GetCellData() + invalid_cells = filter_instance.getInvalidCellIds() + + if write_arrays: + # If arrays should be written and there are invalid elements, check for arrays + for error_type, invalid_ids in invalid_cells.items(): + if invalid_ids: + array_name = f"Is{error_type}" + array = cell_data.GetArray( array_name ) + assert array is not None + else: + # No arrays should be written + error_types = [ + 'wrongNumberOfPointsElements', 'intersectingEdgesElements', 'intersectingFacesElements', + 'nonContiguousEdgesElements', 'nonConvexElements', 'facesOrientedIncorrectlyElements', + 'nonPlanarFacesElements', 'degenerateFacesElements' + ] + + for error_type in error_types: + array_name = f"Is{error_type}" + array = cell_data.GetArray( array_name ) + assert array is None From d7d6e4c8938658112af49ccdd51367ee1e316861 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Fri, 5 Sep 2025 13:22:33 -0700 Subject: [PATCH 46/52] Add tests for SupportedElements --- .../mesh/doctor/filters/SupportedElements.py | 4 +- geos-mesh/tests/test_SupportedElements.py | 214 ++++++++++++++++++ 2 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 geos-mesh/tests/test_SupportedElements.py diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py index 03a875347..69e1b4816 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SupportedElements.py @@ -229,8 +229,8 @@ def supportedElements( tuple[vtkUnstructuredGrid, list[ str ], list[ int ]]: Processed mesh, list of unsupported element types, list of unsupported polyhedron indices. """ - filterInstance = SupportedElements( mesh, writeUnsupportedElementTypes, writeUnsupportedPolyhedrons, numProc, - chunkSize ) + filterInstance = SupportedElements( mesh, numProc, chunkSize, writeUnsupportedElementTypes, + writeUnsupportedPolyhedrons ) success = filterInstance.applyFilter() if not success: raise RuntimeError( "Supported elements identification failed." ) diff --git a/geos-mesh/tests/test_SupportedElements.py b/geos-mesh/tests/test_SupportedElements.py new file mode 100644 index 000000000..203afe768 --- /dev/null +++ b/geos-mesh/tests/test_SupportedElements.py @@ -0,0 +1,214 @@ +import pytest +import numpy as np +from pathlib import Path +from vtkmodules.vtkCommonCore import vtkIdList +from vtkmodules.vtkCommonDataModel import ( + vtkUnstructuredGrid, + VTK_HEXAHEDRON, + VTK_POLYHEDRON, + VTK_QUADRATIC_TETRA, # An example of an unsupported standard type + vtkCellTypes ) +from vtkmodules.util.numpy_support import vtk_to_numpy +from geos.mesh.doctor.actions.supported_elements import supported_cell_types +from geos.mesh.doctor.filters.SupportedElements import SupportedElements, supportedElements +from geos.mesh.utils.genericHelpers import createSingleCellMesh, createMultiCellMesh + + +@pytest.fixture +def good_mesh() -> vtkUnstructuredGrid: + """Creates a mesh with only supported element types.""" + return createSingleCellMesh( VTK_HEXAHEDRON, + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ], + [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ) ) + + +@pytest.fixture +def mesh_with_unsupported_std_type(): + """Creates a mesh containing an unsupported standard element type (VTK_QUADRATIC_TETRA).""" + # Check that our chosen unsupported type is actually not in the supported list + assert VTK_QUADRATIC_TETRA not in supported_cell_types + return createMultiCellMesh( [ VTK_HEXAHEDRON, VTK_QUADRATIC_TETRA ], + [ np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], + [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ), + np.array( [ [ 2, 0, 0 ], [ 3, 0, 0 ], [ 2.5, 1, 0 ], [ 2.5, 0.5, 1 ], [ 2.5, 0, 0 ], + [ 2.75, 0.5, 0 ], [ 2.25, 0.5, 0 ], [ 2.75, 0.25, 0.5 ], + [ 2.5, 0.75, 0.5 ], [ 2.25, 0.25, 0.5 ] ] ) ] ) + + +@pytest.fixture +def mesh_with_unsupported_polyhedron() -> vtkUnstructuredGrid: + """ + Creates a mesh with a convertible polyhedron (Hex) and a non-convertible one. + The non-convertible one is a triangular bipyramid, which has 6 faces like a hex, + but a different face-connectivity graph. + """ + points = [ [ 0, 0, 0 ], # points for the hex + [ 1, 0, 0 ], + [ 1, 1, 0 ], + [ 0, 1, 0 ], + [ 0, 0, 1 ], + [ 1, 0, 1 ], + [ 1, 1, 1 ], + [ 0, 1, 1 ], + [ 2, 0, 1 ], # other points to add for bipyramid + [ 3, -1, 0 ], + [ 3, 1, 0 ], + [ 1, 0, 0 ], + [ 2, 0, -1 ] ] + + mesh: vtkUnstructuredGrid = createSingleCellMesh( + VTK_HEXAHEDRON, np.array( [ points[ 0 ], points[ 1 ], points[ 2 ], points[ 3 ], points[ 4 ], points[ 5 ], + points[ 6 ], points[ 7 ] ] ) ) + + # Face stream for the triangular bipyramid (6 faces, 5 points) + # Format: [num_faces, num_pts_face1, p1, p2, ..., num_pts_face2, p1, p2, ...] + poly_faces = [ + 6, # Number of faces + 3, # Face 0 (top) + 8, + 9, + 10, + 3, # Face 1 (top) + 8, + 10, + 11, + 3, # Face 2 (top) + 8, + 11, + 9, + 3, # Face 3 (bottom) + 12, + 10, + 9, + 3, # Face 4 (bottom) + 12, + 11, + 10, + 3, # Face 5 (bottom) + 12, + 9, + 11 + ] + # Insert the polyhedron cell + face_stream = vtkIdList() + face_stream.SetNumberOfIds( len( poly_faces ) ) + for i, val in enumerate( poly_faces ): + face_stream.SetId( i, val ) + # Now, insert the polyhedron cell using the face stream + mesh.InsertNextCell( VTK_POLYHEDRON, face_stream ) + return mesh + + +class TestSupportedElements: + + def test_initialization( self, good_mesh ): + """Tests the constructor and default values.""" + filter_instance = SupportedElements( good_mesh ) + assert filter_instance.numProc == 1 + assert filter_instance.chunkSize == 1 + assert not filter_instance.writeUnsupportedElementTypes + assert not filter_instance.writeUnsupportedPolyhedrons + assert len( filter_instance.getUnsupportedElementTypes() ) == 0 + assert len( filter_instance.getUnsupportedPolyhedronElements() ) == 0 + + def test_setters( self, good_mesh ): + """Tests the various setter methods.""" + filt = SupportedElements( good_mesh ) + filt.setNumProc( 8 ) + assert filt.numProc == 8 + filt.setChunkSize( 2000 ) + assert filt.chunkSize == 2000 + filt.setWriteUnsupportedElementTypes( True ) + assert filt.writeUnsupportedElementTypes + filt.setWriteUnsupportedPolyhedrons( True ) + assert filt.writeUnsupportedPolyhedrons + + def test_apply_on_good_mesh( self, good_mesh ): + """Tests the filter on a mesh with no unsupported elements.""" + filt = SupportedElements( good_mesh ) + success = filt.applyFilter() + assert success + assert len( filt.getUnsupportedElementTypes() ) == 0 + assert len( filt.getUnsupportedPolyhedronElements() ) == 0 + + def test_find_unsupported_std_types( self, mesh_with_unsupported_std_type ): + """Tests detection of unsupported standard element types.""" + filt = SupportedElements( mesh_with_unsupported_std_type ) + success = filt.applyFilter() + assert success + + unsupported_types = filt.getUnsupportedElementTypes() + assert len( unsupported_types ) == 1 + + type_name = vtkCellTypes.GetClassNameFromTypeId( VTK_QUADRATIC_TETRA ) + expected_str = f"Type {VTK_QUADRATIC_TETRA}: {type_name}" + assert unsupported_types[ 0 ] == expected_str + + assert len( filt.getUnsupportedPolyhedronElements() ) == 0 + + def test_find_unsupported_polyhedrons( self, mesh_with_unsupported_polyhedron ): + """Tests detection of unsupported polyhedron elements.""" + filt = SupportedElements( mesh_with_unsupported_polyhedron, numProc=2, chunkSize=1 ) + success = filt.applyFilter() + assert success + + unsupported_polys = filt.getUnsupportedPolyhedronElements() + assert len( unsupported_polys ) == 1 + # The unsupported polyhedron is the second cell (index 1) + assert unsupported_polys[ 0 ] == 1 + + assert len( filt.getUnsupportedElementTypes() ) == 0 + + def test_array_writing( self, mesh_with_unsupported_std_type, mesh_with_unsupported_polyhedron ): + """Tests that CellData arrays are correctly added to the mesh.""" + # Test standard types array + filt_std = SupportedElements( mesh_with_unsupported_std_type, writeUnsupportedElementTypes=True ) + filt_std.applyFilter() + mesh_out_std = filt_std.getMesh() + + std_array = mesh_out_std.GetCellData().GetArray( "HasUnsupportedType" ) + assert std_array is not None + std_np = vtk_to_numpy( std_array ) + # Cell 0 (Hex) is supported, Cell 1 (QuadTetra) is not. + np.testing.assert_array_equal( std_np, [ 0, 1 ] ) + + # Test polyhedrons array + filt_poly = SupportedElements( mesh_with_unsupported_polyhedron, writeUnsupportedPolyhedrons=True ) + filt_poly.applyFilter() + mesh_out_poly = filt_poly.getMesh() + + poly_array = mesh_out_poly.GetCellData().GetArray( "IsUnsupportedPolyhedron" ) + assert poly_array is not None + poly_np = vtk_to_numpy( poly_array ) + # Cell 0 (Hex) is not an unsupported poly, Cell 1 is. + np.testing.assert_array_equal( poly_np, [ 0, 1 ] ) + + +def test_standalone_function( tmp_path, mesh_with_unsupported_polyhedron ): + """Tests the standalone `supportedElements` function.""" + output_dir = tmp_path / "output" + output_dir.mkdir() + output_path = output_dir / "test_output.vtu" + + # Run the standalone function + mesh, types, polys = supportedElements( + mesh_with_unsupported_polyhedron, + outputPath=str( output_path ), + numProc=1, + chunkSize=1, + writeUnsupportedPolyhedrons=True # Ensure array is written + ) + + # Verify returned values + assert len( types ) == 0 + assert len( polys ) == 1 + assert polys[ 0 ] == 1 + + # Verify file was written + assert Path( output_path ).is_file() + + # Verify the mesh object returned has the new array + array = mesh.GetCellData().GetArray( "IsUnsupportedPolyhedron" ) + assert array is not None + np_array = vtk_to_numpy( array ) + np.testing.assert_array_equal( np_array, [ 0, 1 ] ) From f5c5ec6a6b30e035209e8ca7145858f34de7d94b Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Fri, 5 Sep 2025 14:47:51 -0700 Subject: [PATCH 47/52] Add tests for Checks --- .../src/geos/mesh/doctor/filters/Checks.py | 2 +- geos-mesh/tests/test_Checks.py | 511 ++++++++++++++++++ 2 files changed, 512 insertions(+), 1 deletion(-) create mode 100644 geos-mesh/tests/test_Checks.py diff --git a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py index f7b3e85cb..f3a8682d2 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py @@ -224,7 +224,7 @@ def _buildOptions( self: Self ) -> Options: self.logger.error( f"Failed to create options for check '{checkName}': {e}. " f"This check will be skipped." ) - return Options( checksToPerform=list( individualCheckOptions.keys() ), + return Options( checks_to_perform=list( individualCheckOptions.keys() ), checks_options=individualCheckOptions, check_displays=individualCheckDisplay ) diff --git a/geos-mesh/tests/test_Checks.py b/geos-mesh/tests/test_Checks.py new file mode 100644 index 000000000..06cc25f54 --- /dev/null +++ b/geos-mesh/tests/test_Checks.py @@ -0,0 +1,511 @@ +import pytest +import numpy as np +from unittest.mock import patch, MagicMock +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, VTK_TETRA, VTK_HEXAHEDRON +from geos.mesh.utils.genericHelpers import createSingleCellMesh, createMultiCellMesh +from geos.mesh.doctor.filters.Checks import Checks, AllChecks, MainChecks, allChecks, mainChecks +from geos.mesh.doctor.actions.all_checks import Options + +__doc__ = """ +Test module for Checks filters. +Tests the functionality of AllChecks, MainChecks, and base Checks class for mesh validation. +""" + + +@pytest.fixture( scope="module" ) +def simple_hex_mesh() -> vtkUnstructuredGrid: + """Fixture for a simple hexahedron mesh.""" + return createSingleCellMesh( VTK_HEXAHEDRON, np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], + [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ) ) + + +@pytest.fixture( scope="module" ) +def simple_tetra_mesh() -> vtkUnstructuredGrid: + """Fixture for a simple tetrahedron mesh.""" + return createSingleCellMesh( VTK_TETRA, np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ] ) ) + + +@pytest.fixture( scope="module" ) +def mixed_quality_mesh() -> vtkUnstructuredGrid: + """Fixture for a mesh with elements of varying quality.""" + return createMultiCellMesh( + [ VTK_TETRA, VTK_TETRA ], + [ np.array( [ [ 0.0, 0.0, 0.0 ], [ 1.0, 0.0, 0.0 ], [ 0.5, 1.0, 0.0 ], [ 0.5, 0.5, 1.0 ] ] ), + np.array( [ [ 2.0, 0.0, 0.0 ], [ 5.0, 0.0, 0.0 ], [ 3.5, 0.1, 0.0 ], [ 3.5, 0.05, 0.05 ] ] ) ] + ) + + +@pytest.fixture( scope="module" ) +def mesh_with_collocated_nodes() -> vtkUnstructuredGrid: + """Fixture for a mesh with collocated (duplicate) nodes.""" + return createMultiCellMesh( + [ VTK_TETRA, VTK_TETRA ], + [ np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ] ), + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ] ) ] + ) + + +class TestChecksBase: + """Test class for base Checks filter functionality.""" + + def test_base_checks_initialization( self, simple_hex_mesh ): + """Test basic initialization of Checks base class.""" + # Create mock configurations for testing + mock_check_features = { "test_check": MagicMock( default_params={ "param1": 1.0, "param2": "value" } ) } + mock_ordered_names = [ "test_check" ] + + filter_instance = Checks( simple_hex_mesh, + checksToPerform=[ "test_check" ], + checkFeaturesConfig=mock_check_features, + orderedCheckNames=mock_ordered_names ) + + assert filter_instance.checksToPerform == [ "test_check" ] + assert filter_instance.checkParameters == {} + assert filter_instance.checkResults == {} + assert filter_instance.checkFeaturesConfig == mock_check_features + assert filter_instance.orderedCheckNames == mock_ordered_names + + # Test getMesh returns a copy + processed_mesh = filter_instance.getMesh() + assert processed_mesh is not simple_hex_mesh + assert processed_mesh.GetNumberOfPoints() == simple_hex_mesh.GetNumberOfPoints() + assert processed_mesh.GetNumberOfCells() == simple_hex_mesh.GetNumberOfCells() + + def test_parameter_setting( self, simple_hex_mesh ): + """Test setting parameters for checks.""" + mock_check_features = { + "check1": MagicMock( default_params={ + "tolerance": 1e-6, + "param2": "default" + } ), + "check2": MagicMock( default_params={ "minValue": 0.0 } ) + } + + filter_instance = Checks( simple_hex_mesh, + checksToPerform=[ "check1", "check2" ], + checkFeaturesConfig=mock_check_features, + orderedCheckNames=[ "check1", "check2" ] ) + + # Test setting individual check parameter + filter_instance.setCheckParameter( "check1", "tolerance", 1e-8 ) + assert filter_instance.checkParameters[ "check1" ][ "tolerance" ] == 1e-8 + + # Test setting parameter for all checks + filter_instance.setAllChecksParameter( "tolerance", 1e-10 ) + assert filter_instance.checkParameters[ "check1" ][ "tolerance" ] == 1e-10 + # check2 doesn't have tolerance parameter, so it shouldn't be set + assert "check2" not in filter_instance.checkParameters or \ + "tolerance" not in filter_instance.checkParameters.get("check2", {}) + + def test_get_available_checks( self, simple_hex_mesh ): + """Test getting available checks.""" + mock_check_features = { "check1": MagicMock(), "check2": MagicMock() } + ordered_names = [ "check1", "check2" ] + + filter_instance = Checks( simple_hex_mesh, + checksToPerform=[ "check1" ], + checkFeaturesConfig=mock_check_features, + orderedCheckNames=ordered_names ) + + available = filter_instance.getAvailableChecks() + assert available == ordered_names + + def test_get_default_parameters( self, simple_hex_mesh ): + """Test getting default parameters for a check.""" + default_params = { "tolerance": 1e-6, "minValue": 0.0 } + mock_check_features = { "test_check": MagicMock( default_params=default_params ) } + + filter_instance = Checks( simple_hex_mesh, + checksToPerform=[ "test_check" ], + checkFeaturesConfig=mock_check_features, + orderedCheckNames=[ "test_check" ] ) + + params = filter_instance.getDefaultParameters( "test_check" ) + assert params == default_params + + # Test non-existent check + params = filter_instance.getDefaultParameters( "nonexistent" ) + assert params == {} + + def test_set_checks_to_perform( self, simple_hex_mesh ): + """Test changing which checks to perform.""" + mock_check_features = { "check1": MagicMock(), "check2": MagicMock(), "check3": MagicMock() } + + filter_instance = Checks( simple_hex_mesh, + checksToPerform=[ "check1" ], + checkFeaturesConfig=mock_check_features, + orderedCheckNames=[ "check1", "check2", "check3" ] ) + + assert filter_instance.checksToPerform == [ "check1" ] + + filter_instance.setChecksToPerform( [ "check2", "check3" ] ) + assert filter_instance.checksToPerform == [ "check2", "check3" ] + + @patch( 'geos.mesh.doctor.filters.Checks.get_check_results' ) + @patch( 'geos.mesh.doctor.filters.Checks.display_results' ) + def test_apply_filter_success( self, mock_display, mock_get_results, simple_hex_mesh ): + """Test successful filter application.""" + # Mock the check results + mock_results = { "test_check": { "status": "passed", "details": "All good" } } + mock_get_results.return_value = mock_results + + mock_check_features = { + "test_check": + MagicMock( default_params={ "tolerance": 1e-6 }, options_cls=MagicMock( return_value=MagicMock() ) ) + } + + filter_instance = Checks( simple_hex_mesh, + checksToPerform=[ "test_check" ], + checkFeaturesConfig=mock_check_features, + orderedCheckNames=[ "test_check" ] ) + + success = filter_instance.applyFilter() + assert success + assert filter_instance.getCheckResults() == mock_results + + # Verify that get_check_results was called with proper arguments + mock_get_results.assert_called_once() + args = mock_get_results.call_args[ 0 ] + assert isinstance( args[ 0 ], vtkUnstructuredGrid ) + assert args[ 0 ] is not simple_hex_mesh # mesh argument + assert isinstance( args[ 1 ], Options ) # options argument + + +class TestAllChecks: + """Test class for AllChecks filter functionality.""" + + def test_all_checks_initialization( self, simple_hex_mesh ): + """Test AllChecks filter initialization.""" + filter_instance = AllChecks( simple_hex_mesh ) + + # AllChecks should have all available checks configured + assert len( filter_instance.checksToPerform ) > 0 + assert len( filter_instance.checkFeaturesConfig ) > 0 + assert len( filter_instance.orderedCheckNames ) > 0 + + # Check that checksToPerform matches orderedCheckNames for AllChecks + assert filter_instance.checksToPerform == filter_instance.orderedCheckNames + + def test_all_checks_with_external_logger( self, simple_hex_mesh ): + """Test AllChecks initialization with external logger.""" + filter_instance = AllChecks( simple_hex_mesh, useExternalLogger=True ) + assert filter_instance is not None + + @patch( 'geos.mesh.doctor.filters.Checks.get_check_results' ) + @patch( 'geos.mesh.doctor.filters.Checks.display_results' ) + def test_all_checks_apply_filter( self, mock_display, mock_get_results, simple_hex_mesh ): + """Test applying AllChecks filter.""" + # Mock successful check results + mock_results = { + "element_volumes": { + "status": "passed" + }, + "collocated_nodes": { + "status": "passed" + }, + } + mock_get_results.return_value = mock_results + + filter_instance = AllChecks( simple_hex_mesh ) + success = filter_instance.applyFilter() + + assert success + results = filter_instance.getCheckResults() + assert results == mock_results + + +class TestMainChecks: + """Test class for MainChecks filter functionality.""" + + def test_main_checks_initialization( self, simple_hex_mesh ): + """Test MainChecks filter initialization.""" + filter_instance = MainChecks( simple_hex_mesh ) + + # MainChecks should have a subset of checks + assert len( filter_instance.checksToPerform ) > 0 + assert len( filter_instance.checkFeaturesConfig ) > 0 + assert len( filter_instance.orderedCheckNames ) > 0 + + # Check that checksToPerform matches orderedCheckNames for MainChecks + assert filter_instance.checksToPerform == filter_instance.orderedCheckNames + + def test_main_checks_with_external_logger( self, simple_hex_mesh ): + """Test MainChecks initialization with external logger.""" + filter_instance = MainChecks( simple_hex_mesh, useExternalLogger=True ) + assert filter_instance is not None + + @patch( 'geos.mesh.doctor.filters.Checks.get_check_results' ) + @patch( 'geos.mesh.doctor.filters.Checks.display_results' ) + def test_main_checks_apply_filter( self, mock_display, mock_get_results, simple_hex_mesh ): + """Test applying MainChecks filter.""" + # Mock successful check results + mock_results = { + "element_volumes": { + "status": "passed" + }, + "collocated_nodes": { + "status": "passed" + }, + } + mock_get_results.return_value = mock_results + + filter_instance = MainChecks( simple_hex_mesh ) + success = filter_instance.applyFilter() + + assert success + results = filter_instance.getCheckResults() + assert results == mock_results + + def test_main_vs_all_checks_difference( self, simple_hex_mesh ): + """Test that MainChecks and AllChecks have different check sets.""" + all_checks_filter = AllChecks( simple_hex_mesh ) + main_checks_filter = MainChecks( simple_hex_mesh ) + + # MainChecks should typically be a subset of AllChecks + all_checks_set = set( all_checks_filter.checksToPerform ) + main_checks_set = set( main_checks_filter.checksToPerform ) + + # Main checks should be a subset (or equal) to all checks + assert main_checks_set.issubset( all_checks_set ) or main_checks_set == all_checks_set + + +class TestStandaloneFunctions: + """Test class for standalone allChecks and mainChecks functions.""" + + @patch( 'geos.mesh.doctor.filters.Checks.AllChecks' ) + def test_all_checks_function( self, mock_all_checks_class, simple_hex_mesh ): + """Test standalone allChecks function.""" + # Mock the filter instance + mock_filter = MagicMock() + mock_filter.applyFilter.return_value = True + mock_filter.getMesh.return_value = simple_hex_mesh + mock_filter.getCheckResults.return_value = { "test": "results" } + mock_all_checks_class.return_value = mock_filter + + # Test without custom parameters + result_mesh, results = allChecks( simple_hex_mesh ) + + assert result_mesh == simple_hex_mesh + assert results == { "test": "results" } + mock_all_checks_class.assert_called_once_with( simple_hex_mesh ) + mock_filter.applyFilter.assert_called_once() + + @patch( 'geos.mesh.doctor.filters.Checks.AllChecks' ) + def test_all_checks_function_with_parameters( self, mock_all_checks_class, simple_hex_mesh ): + """Test standalone allChecks function with custom parameters.""" + mock_filter = MagicMock() + mock_filter.applyFilter.return_value = True + mock_filter.getMesh.return_value = simple_hex_mesh + mock_filter.getCheckResults.return_value = { "test": "results" } + mock_all_checks_class.return_value = mock_filter + + custom_params = { "collocated_nodes": { "tolerance": 1e-8 }, "element_volumes": { "minVolume": 0.1 } } + + result_mesh, results = allChecks( simple_hex_mesh, custom_params ) + + assert result_mesh == simple_hex_mesh + assert results == { "test": "results" } + + # Verify custom parameters were set + mock_filter.setCheckParameter.assert_any_call( "collocated_nodes", "tolerance", 1e-8 ) + mock_filter.setCheckParameter.assert_any_call( "element_volumes", "minVolume", 0.1 ) + + @patch( 'geos.mesh.doctor.filters.Checks.AllChecks' ) + def test_all_checks_function_failure( self, mock_all_checks_class, simple_hex_mesh ): + """Test standalone allChecks function with filter failure.""" + mock_filter = MagicMock() + mock_filter.applyFilter.return_value = False + mock_all_checks_class.return_value = mock_filter + + with pytest.raises( RuntimeError, match="allChecks calculation failed" ): + allChecks( simple_hex_mesh ) + + @patch( 'geos.mesh.doctor.filters.Checks.MainChecks' ) + def test_main_checks_function( self, mock_main_checks_class, simple_hex_mesh ): + """Test standalone mainChecks function.""" + mock_filter = MagicMock() + mock_filter.applyFilter.return_value = True + mock_filter.getMesh.return_value = simple_hex_mesh + mock_filter.getCheckResults.return_value = { "test": "results" } + mock_main_checks_class.return_value = mock_filter + + # Test without custom parameters + result_mesh, results = mainChecks( simple_hex_mesh ) + + assert result_mesh == simple_hex_mesh + assert results == { "test": "results" } + mock_main_checks_class.assert_called_once_with( simple_hex_mesh ) + mock_filter.applyFilter.assert_called_once() + + @patch( 'geos.mesh.doctor.filters.Checks.MainChecks' ) + def test_main_checks_function_with_parameters( self, mock_main_checks_class, simple_hex_mesh ): + """Test standalone mainChecks function with custom parameters.""" + mock_filter = MagicMock() + mock_filter.applyFilter.return_value = True + mock_filter.getMesh.return_value = simple_hex_mesh + mock_filter.getCheckResults.return_value = { "test": "results" } + mock_main_checks_class.return_value = mock_filter + + custom_params = { "element_volumes": { "minVolume": 0.05 } } + + result_mesh, results = mainChecks( simple_hex_mesh, custom_params ) + + assert result_mesh == simple_hex_mesh + assert results == { "test": "results" } + + # Verify custom parameters were set + mock_filter.setCheckParameter.assert_called_with( "element_volumes", "minVolume", 0.05 ) + + @patch( 'geos.mesh.doctor.filters.Checks.MainChecks' ) + def test_main_checks_function_failure( self, mock_main_checks_class, simple_hex_mesh ): + """Test standalone mainChecks function with filter failure.""" + mock_filter = MagicMock() + mock_filter.applyFilter.return_value = False + mock_main_checks_class.return_value = mock_filter + + with pytest.raises( RuntimeError, match="mainChecks calculation failed" ): + mainChecks( simple_hex_mesh ) + + +class TestFileIO: + """Test class for file I/O operations with checks.""" + + def test_write_grid_functionality( self, simple_hex_mesh, tmp_path ): + """Test writing mesh to file using tmp_path.""" + filter_instance = AllChecks( simple_hex_mesh ) + + output_file = tmp_path / "test_output.vtu" + + # The writeGrid method should be inherited from MeshDoctorFilterBase + # We'll test that the method exists and can be called + assert hasattr( filter_instance, 'writeGrid' ) + + # Test calling writeGrid (it should not raise an exception) + try: + filter_instance.writeGrid( str( output_file ) ) + # If the file was created, verify it exists + if output_file.exists(): + assert output_file.stat().st_size > 0 + except Exception: + # Some implementations might require the filter to be applied first + filter_instance.applyFilter() + filter_instance.writeGrid( str( output_file ) ) + + +class TestOptionsBuilding: + """Test class for options building functionality.""" + + def test_build_options_with_defaults( self, simple_hex_mesh ): + """Test building options with default parameters.""" + mock_options_cls = MagicMock( return_value=MagicMock() ) + mock_check_features = { + "test_check": MagicMock( default_params={ "param1": 1.0, "param2": "default" }, + options_cls=mock_options_cls ) + } + + filter_instance = Checks( simple_hex_mesh, + checksToPerform=[ "test_check" ], + checkFeaturesConfig=mock_check_features, + orderedCheckNames=[ "test_check" ] ) + + options = filter_instance._buildOptions() + + assert isinstance( options, Options ) + assert "test_check" in options.checks_to_perform + # Verify options_cls was called with default parameters + mock_options_cls.assert_called_once_with( param1=1.0, param2="default" ) + + def test_build_options_with_failing_options_creation( self, simple_hex_mesh ): + """Test building options when options class instantiation fails.""" + mock_options_cls = MagicMock( side_effect=Exception( "Options creation failed" ) ) + mock_check_features = { + "failing_check": MagicMock( default_params={}, options_cls=mock_options_cls ), + "working_check": MagicMock( default_params={}, options_cls=MagicMock( return_value=MagicMock() ) ) + } + + filter_instance = Checks( simple_hex_mesh, + checksToPerform=[ "failing_check", "working_check" ], + checkFeaturesConfig=mock_check_features, + orderedCheckNames=[ "failing_check", "working_check" ] ) + + options = filter_instance._buildOptions() + + # Only working check should be in the options + assert options.checks_to_perform == [ "working_check" ] + assert "failing_check" not in options.checks_to_perform + + +class TestErrorHandling: + """Test class for error handling scenarios.""" + + def test_empty_checks_list( self, simple_hex_mesh ): + """Test handling of empty checks list.""" + filter_instance = Checks( simple_hex_mesh, checksToPerform=[], checkFeaturesConfig={}, orderedCheckNames=[] ) + + options = filter_instance._buildOptions() + assert options.checks_to_perform == [] + + def test_mesh_copy_integrity( self, simple_hex_mesh ): + """Test that mesh copy maintains integrity.""" + filter_instance = AllChecks( simple_hex_mesh ) + + copied_mesh = filter_instance.getMesh() + + # Verify the copy has the same structure + assert copied_mesh.GetNumberOfPoints() == simple_hex_mesh.GetNumberOfPoints() + assert copied_mesh.GetNumberOfCells() == simple_hex_mesh.GetNumberOfCells() + + # Verify it's actually a copy, not the same object + assert copied_mesh is not simple_hex_mesh + + # Verify point coordinates are the same + for i in range( simple_hex_mesh.GetNumberOfPoints() ): + original_point = simple_hex_mesh.GetPoint( i ) + copied_point = copied_mesh.GetPoint( i ) + np.testing.assert_array_equal( original_point, copied_point ) + + +class TestIntegrationScenarios: + """Test class for integration scenarios with different mesh types.""" + + @patch( 'geos.mesh.doctor.filters.Checks.get_check_results' ) + @patch( 'geos.mesh.doctor.filters.Checks.display_results' ) + def test_complex_parameter_workflow( self, mock_display, mock_get_results, mixed_quality_mesh ): + """Test complex parameter setting workflow.""" + mock_results = { "element_volumes": { "status": "warning", "issues": 1 } } + mock_get_results.return_value = mock_results + + filter_instance = AllChecks( mixed_quality_mesh ) + + # Set various parameters + filter_instance.setCheckParameter( "element_volumes", "minVolume", 0.01 ) + filter_instance.setAllChecksParameter( "tolerance", 1e-10 ) + filter_instance.setCheckParameter( "collocated_nodes", "tolerance", 1e-12 ) + + success = filter_instance.applyFilter() + assert success + + results = filter_instance.getCheckResults() + assert results == mock_results + + def test_multiple_filter_instances( self, simple_hex_mesh, simple_tetra_mesh ): + """Test using multiple filter instances simultaneously.""" + all_checks_filter = AllChecks( simple_hex_mesh ) + main_checks_filter = MainChecks( simple_tetra_mesh ) + + # Both should be independent + assert all_checks_filter.getMesh() is not main_checks_filter.getMesh() + assert all_checks_filter.checksToPerform != main_checks_filter.checksToPerform or \ + len(all_checks_filter.checksToPerform) >= len(main_checks_filter.checksToPerform) + + def test_parameter_isolation( self, simple_hex_mesh ): + """Test that parameters are isolated between filter instances.""" + filter1 = AllChecks( simple_hex_mesh ) + filter2 = AllChecks( simple_hex_mesh ) + + filter1.setCheckParameter( "element_volumes", "minVolume", 0.1 ) + filter2.setCheckParameter( "element_volumes", "minVolume", 0.2 ) + + # Parameters should be independent + assert filter1.checkParameters[ "element_volumes" ][ "minVolume" ] == 0.1 + assert filter2.checkParameters[ "element_volumes" ][ "minVolume" ] == 0.2 From dbe1e3d28afb4aafa654d3e89156db123224b6e0 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Fri, 5 Sep 2025 14:50:50 -0700 Subject: [PATCH 48/52] Better test mesh creation --- geos-mesh/tests/test_CollocatedNodes.py | 42 +++--- geos-mesh/tests/test_ElementVolumes.py | 17 ++- geos-mesh/tests/test_MeshDoctorFilterBase.py | 133 ++++++++----------- geos-mesh/tests/test_generate_fractures.py | 52 +++----- 4 files changed, 101 insertions(+), 143 deletions(-) diff --git a/geos-mesh/tests/test_CollocatedNodes.py b/geos-mesh/tests/test_CollocatedNodes.py index 6eed87f2a..decebe203 100644 --- a/geos-mesh/tests/test_CollocatedNodes.py +++ b/geos-mesh/tests/test_CollocatedNodes.py @@ -1,11 +1,9 @@ import pytest import numpy as np import os -from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, VTK_TRIANGLE -from vtkmodules.vtkCommonCore import vtkPoints -from geos.mesh.doctor.actions.generate_cube import XYZ, build_rectilinear_blocks_mesh +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, VTK_TRIANGLE, VTK_HEXAHEDRON from geos.mesh.doctor.filters.CollocatedNodes import CollocatedNodes, collocatedNodes -from geos.mesh.utils.genericHelpers import to_vtk_id_list +from geos.mesh.utils.genericHelpers import createMultiCellMesh __doc__ = """ Test module for CollocatedNodes filter. @@ -16,8 +14,11 @@ @pytest.fixture( scope="module" ) def mesh_with_collocated_nodes(): """Fixture for a mesh with exactly duplicated and nearly collocated nodes.""" - x, y, z = np.array( [ 0, 1, 2 ] ), np.array( [ 0, 1 ] ), np.array( [ 0, 1 ] ) - mesh = build_rectilinear_blocks_mesh( [ XYZ( x, y, z ) ] ) + mesh = createMultiCellMesh( [ VTK_HEXAHEDRON, VTK_HEXAHEDRON ], + [ np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], + [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ), + np.array( [ [ 1, 0, 0 ], [ 2, 0, 0 ], [ 2, 1, 0 ], [ 1, 1, 0 ], + [ 1, 0, 1 ], [ 2, 0, 1 ], [ 2, 1, 1 ], [ 1, 1, 1 ] ] ) ] ) points = mesh.GetPoints() # Add nodes to create collocated situations: @@ -32,30 +33,21 @@ def mesh_with_collocated_nodes(): @pytest.fixture( scope="module" ) -def mesh_with_wrong_support(): +def mesh_with_wrong_support() -> vtkUnstructuredGrid: """Fixture for a mesh containing a cell with repeated node indices.""" - mesh = vtkUnstructuredGrid() - points = vtkPoints() - mesh.SetPoints( points ) - - points.InsertNextPoint( 0.0, 0.0, 0.0 ) # Point 0 - points.InsertNextPoint( 1.0, 0.0, 0.0 ) # Point 1 - points.InsertNextPoint( 0.0, 1.0, 0.0 ) # Point 2 - points.InsertNextPoint( 1.0, 1.0, 0.0 ) # Point 3 - - # A degenerate triangle with a repeated node [0, 1, 1] - mesh.InsertNextCell( VTK_TRIANGLE, to_vtk_id_list( [ 0, 1, 1 ] ) ) - # A normal triangle for comparison - mesh.InsertNextCell( VTK_TRIANGLE, to_vtk_id_list( [ 1, 2, 3 ] ) ) - - return mesh + return createMultiCellMesh( [ VTK_TRIANGLE, VTK_TRIANGLE ], + [ np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 0, 0 ] ] ), + np.array( [ [ 1, 0, 0 ], [ 0, 1, 0 ], [ 1, 1, 0 ] ] ) ] ) @pytest.fixture( scope="module" ) -def clean_mesh(): +def clean_mesh() -> vtkUnstructuredGrid: """Fixture for a simple, valid mesh with no issues.""" - x, y, z = np.array( [ 0, 1, 2 ] ), np.array( [ 0, 1 ] ), np.array( [ 0, 1 ] ) - return build_rectilinear_blocks_mesh( [ XYZ( x, y, z ) ] ) + return createMultiCellMesh( [ VTK_HEXAHEDRON, VTK_HEXAHEDRON ], + [ np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], + [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ), + np.array( [ [ 1, 0, 0 ], [ 2, 0, 0 ], [ 2, 1, 0 ], [ 1, 1, 0 ], + [ 1, 0, 1 ], [ 2, 0, 1 ], [ 2, 1, 1 ], [ 1, 1, 1 ] ] ) ] ) def test_filter_on_clean_mesh( clean_mesh ): diff --git a/geos-mesh/tests/test_ElementVolumes.py b/geos-mesh/tests/test_ElementVolumes.py index deed2912b..f9636c24f 100644 --- a/geos-mesh/tests/test_ElementVolumes.py +++ b/geos-mesh/tests/test_ElementVolumes.py @@ -1,9 +1,8 @@ import pytest import numpy as np from vtkmodules.vtkCommonCore import vtkPoints -from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, VTK_TETRA, VTK_TRIANGLE -from geos.mesh.utils.genericHelpers import to_vtk_id_list, createSingleCellMesh -from geos.mesh.doctor.actions.generate_cube import XYZ, build_rectilinear_blocks_mesh +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, VTK_TETRA, VTK_TRIANGLE, VTK_HEXAHEDRON +from geos.mesh.utils.genericHelpers import to_vtk_id_list, createSingleCellMesh, createMultiCellMesh from geos.mesh.doctor.filters.ElementVolumes import ElementVolumes, elementVolumes __doc__ = """ @@ -15,9 +14,11 @@ @pytest.fixture( scope="module" ) def simple_hex_mesh(): """Fixture for a simple hexahedron mesh with known volumes.""" - x, y, z = np.array( [ 0, 1, 2 ] ), np.array( [ 0, 1 ] ), np.array( [ 0, 1 ] ) - mesh = build_rectilinear_blocks_mesh( [ XYZ( x, y, z ) ] ) - return mesh + return createMultiCellMesh( [ VTK_HEXAHEDRON, VTK_HEXAHEDRON ], + [ np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], + [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ), + np.array( [ [ 1, 0, 0 ], [ 2, 0, 0 ], [ 2, 1, 0 ], [ 1, 1, 0 ], + [ 1, 0, 1 ], [ 2, 0, 1 ], [ 2, 1, 1 ], [ 1, 1, 1 ] ] ) ] ) @pytest.fixture( scope="module" ) @@ -361,9 +362,7 @@ def test_empty_mesh( self ): def test_single_cell_mesh( self ): """Test with a mesh containing only one cell.""" - pts_coords = [ ( 0.0, 0.0, 0.0 ), ( 1.0, 0.0, 0.0 ), ( 0.0, 1.0, 0.0 ), ( 0.0, 0.0, 1.0 ) ] - mesh = createSingleCellMesh( VTK_TETRA, np.array( pts_coords ) ) - + mesh = createSingleCellMesh( VTK_TETRA, np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ] ) ) filter_instance = ElementVolumes( mesh, minVolume=0.0 ) success = filter_instance.applyFilter() assert success diff --git a/geos-mesh/tests/test_MeshDoctorFilterBase.py b/geos-mesh/tests/test_MeshDoctorFilterBase.py index 41ee8a28f..41aebafec 100644 --- a/geos-mesh/tests/test_MeshDoctorFilterBase.py +++ b/geos-mesh/tests/test_MeshDoctorFilterBase.py @@ -2,10 +2,8 @@ import logging import numpy as np from unittest.mock import Mock -from vtkmodules.vtkCommonCore import vtkPoints from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, VTK_TETRA from geos.mesh.utils.genericHelpers import createSingleCellMesh -from geos.mesh.doctor.actions.generate_cube import XYZ, build_rectilinear_blocks_mesh from geos.mesh.doctor.filters.MeshDoctorFilterBase import MeshDoctorFilterBase, MeshDoctorGeneratorBase __doc__ = """ @@ -15,28 +13,11 @@ @pytest.fixture( scope="module" ) -def simple_test_mesh(): - """Fixture for a simple test mesh.""" - x, y, z = np.array( [ 0, 1 ] ), np.array( [ 0, 1 ] ), np.array( [ 0, 1 ] ) - mesh = build_rectilinear_blocks_mesh( [ XYZ( x, y, z ) ] ) - return mesh - - -@pytest.fixture( scope="module" ) -def single_cell_mesh(): +def single_tetrahedron_mesh() -> vtkUnstructuredGrid: """Fixture for a single tetrahedron mesh.""" return createSingleCellMesh( VTK_TETRA, np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ] ) ) -@pytest.fixture( scope="module" ) -def empty_mesh(): - """Fixture for an empty mesh.""" - mesh = vtkUnstructuredGrid() - points = vtkPoints() - mesh.SetPoints( points ) - return mesh - - class ConcreteFilterForTesting( MeshDoctorFilterBase ): """Concrete implementation of MeshDoctorFilterBase for testing purposes.""" @@ -81,9 +62,9 @@ def applyFilter( self ): class TestMeshDoctorFilterBase: """Test class for MeshDoctorFilterBase functionality.""" - def test_initialization_valid_inputs( self, simple_test_mesh ): + def test_initialization_valid_inputs( self, single_tetrahedron_mesh ): """Test successful initialization with valid inputs.""" - filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter", False ) + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter", False ) assert filter_instance.filterName == "TestFilter" assert filter_instance.mesh is not None @@ -91,11 +72,11 @@ def test_initialization_valid_inputs( self, simple_test_mesh ): assert filter_instance.logger is not None # Verify that mesh is a copy, not the original - assert filter_instance.mesh is not simple_test_mesh + assert filter_instance.mesh is not single_tetrahedron_mesh - def test_initialization_with_external_logger( self, simple_test_mesh ): + def test_initialization_with_external_logger( self, single_tetrahedron_mesh ): """Test initialization with external logger.""" - filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter", True ) + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter", True ) assert filter_instance.filterName == "TestFilter" assert isinstance( filter_instance.logger, logging.Logger ) @@ -106,64 +87,64 @@ def test_initialization_invalid_mesh_type( self ): with pytest.raises( TypeError, match="Input 'mesh' must be a vtkUnstructuredGrid" ): ConcreteFilterForTesting( error_obj, "TestFilter" ) - def test_initialization_empty_mesh( self, empty_mesh ): + def test_initialization_empty_mesh( self ): """Test initialization with empty mesh.""" with pytest.raises( ValueError, match="Input 'mesh' cannot be empty" ): - ConcreteFilterForTesting( empty_mesh, "TestFilter" ) + ConcreteFilterForTesting( vtkUnstructuredGrid(), "TestFilter" ) - def test_initialization_invalid_filter_name( self, simple_test_mesh ): + def test_initialization_invalid_filter_name( self, single_tetrahedron_mesh ): """Test initialization with invalid filter name.""" for error_obj in [ 123, None ]: with pytest.raises( TypeError, match="Input 'filterName' must be a string" ): - ConcreteFilterForTesting( simple_test_mesh, error_obj ) + ConcreteFilterForTesting( single_tetrahedron_mesh, error_obj ) for error_obj in [ "", " " ]: with pytest.raises( ValueError, match="Input 'filterName' cannot be an empty or whitespace-only string" ): - ConcreteFilterForTesting( simple_test_mesh, error_obj ) + ConcreteFilterForTesting( single_tetrahedron_mesh, error_obj ) - def test_initialization_invalid_external_logger_flag( self, simple_test_mesh ): + def test_initialization_invalid_external_logger_flag( self, single_tetrahedron_mesh ): """Test initialization with invalid useExternalLogger flag.""" for error_obj in [ "not_bool", 1 ]: with pytest.raises( TypeError, match="Input 'useExternalLogger' must be a boolean" ): - ConcreteFilterForTesting( simple_test_mesh, "TestFilter", error_obj ) + ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter", error_obj ) - def test_get_mesh( self, simple_test_mesh ): + def test_get_mesh( self, single_tetrahedron_mesh ): """Test getMesh method returns the correct mesh.""" - filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter" ) + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter" ) returned_mesh = filter_instance.getMesh() assert returned_mesh is filter_instance.mesh - assert returned_mesh.GetNumberOfCells() == simple_test_mesh.GetNumberOfCells() - assert returned_mesh.GetNumberOfPoints() == simple_test_mesh.GetNumberOfPoints() + assert returned_mesh.GetNumberOfCells() == single_tetrahedron_mesh.GetNumberOfCells() + assert returned_mesh.GetNumberOfPoints() == single_tetrahedron_mesh.GetNumberOfPoints() - def test_copy_mesh( self, simple_test_mesh ): + def test_copy_mesh( self, single_tetrahedron_mesh ): """Test copyMesh helper method.""" - filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter" ) - copied_mesh = filter_instance.copyMesh( simple_test_mesh ) + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter" ) + copied_mesh = filter_instance.copyMesh( single_tetrahedron_mesh ) - assert copied_mesh is not simple_test_mesh - assert copied_mesh.GetNumberOfCells() == simple_test_mesh.GetNumberOfCells() - assert copied_mesh.GetNumberOfPoints() == simple_test_mesh.GetNumberOfPoints() + assert copied_mesh is not single_tetrahedron_mesh + assert copied_mesh.GetNumberOfCells() == single_tetrahedron_mesh.GetNumberOfCells() + assert copied_mesh.GetNumberOfPoints() == single_tetrahedron_mesh.GetNumberOfPoints() - def test_apply_filter_success( self, simple_test_mesh ): + def test_apply_filter_success( self, single_tetrahedron_mesh ): """Test successful filter application.""" - filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter", shouldSucceed=True ) + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter", shouldSucceed=True ) result = filter_instance.applyFilter() assert result is True assert filter_instance.applyFilterCalled - def test_apply_filter_failure( self, simple_test_mesh ): + def test_apply_filter_failure( self, single_tetrahedron_mesh ): """Test filter application failure.""" - filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter", shouldSucceed=False ) + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter", shouldSucceed=False ) result = filter_instance.applyFilter() assert result is False assert filter_instance.applyFilterCalled - def test_write_grid_with_mesh( self, simple_test_mesh, tmp_path ): + def test_write_grid_with_mesh( self, single_tetrahedron_mesh, tmp_path ): """Test writing mesh to file when mesh is available.""" - filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter" ) + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter" ) output_file = tmp_path / "test_output.vtu" filter_instance.writeGrid( str( output_file ) ) @@ -172,9 +153,9 @@ def test_write_grid_with_mesh( self, simple_test_mesh, tmp_path ): assert output_file.exists() assert output_file.stat().st_size > 0 - def test_write_grid_with_different_options( self, simple_test_mesh, tmp_path ): + def test_write_grid_with_different_options( self, single_tetrahedron_mesh, tmp_path ): """Test writing mesh with different file options.""" - filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter" ) + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter" ) # Test ASCII mode output_file_ascii = tmp_path / "test_ascii.vtu" @@ -189,9 +170,9 @@ def test_write_grid_with_different_options( self, simple_test_mesh, tmp_path ): # Write again with overwrite enabled (should not raise error) filter_instance.writeGrid( str( output_file_overwrite ), canOverwrite=True ) - def test_write_grid_without_mesh( self, simple_test_mesh, tmp_path, caplog ): + def test_write_grid_without_mesh( self, single_tetrahedron_mesh, tmp_path, caplog ): """Test writing when no mesh is available.""" - filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter" ) + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter" ) filter_instance.mesh = None # Remove the mesh output_file = tmp_path / "should_not_exist.vtu" @@ -203,9 +184,9 @@ def test_write_grid_without_mesh( self, simple_test_mesh, tmp_path, caplog ): assert "No mesh available" in caplog.text assert not output_file.exists() - def test_set_logger_handler_without_existing_handlers( self, simple_test_mesh ): + def test_set_logger_handler_without_existing_handlers( self, single_tetrahedron_mesh ): """Test setting logger handler when no handlers exist.""" - filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter", useExternalLogger=True ) + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter", useExternalLogger=True ) # Clear any existing handlers filter_instance.logger.handlers.clear() @@ -217,9 +198,9 @@ def test_set_logger_handler_without_existing_handlers( self, simple_test_mesh ): # Verify handler was added assert mock_handler in filter_instance.logger.handlers - def test_set_logger_handler_with_existing_handlers( self, simple_test_mesh, caplog ): + def test_set_logger_handler_with_existing_handlers( self, single_tetrahedron_mesh, caplog ): """Test setting logger handler when handlers already exist.""" - filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter_with_handlers", + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter_with_handlers", useExternalLogger=True ) filter_instance.logger.addHandler( logging.NullHandler() ) @@ -232,9 +213,9 @@ def test_set_logger_handler_with_existing_handlers( self, simple_test_mesh, capl # Now caplog will capture the warning correctly assert "already has a handler" in caplog.text - def test_logger_functionality( self, simple_test_mesh, caplog ): + def test_logger_functionality( self, single_tetrahedron_mesh, caplog ): """Test that logging works correctly.""" - filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter_functionality" ) + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter_functionality" ) with caplog.at_level( logging.INFO ): filter_instance.applyFilter() @@ -242,17 +223,17 @@ def test_logger_functionality( self, simple_test_mesh, caplog ): # Should have logged the success message assert "Test filter applied successfully" in caplog.text - def test_mesh_deep_copy_behavior( self, simple_test_mesh ): + def test_mesh_deep_copy_behavior( self, single_tetrahedron_mesh ): """Test that the filter creates a deep copy of the input mesh.""" - filter_instance = ConcreteFilterForTesting( simple_test_mesh, "TestFilter" ) + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter" ) # Modify the original mesh - original_cell_count = simple_test_mesh.GetNumberOfCells() + original_cell_count = single_tetrahedron_mesh.GetNumberOfCells() # The filter's mesh should be independent of the original filter_mesh = filter_instance.getMesh() assert filter_mesh.GetNumberOfCells() == original_cell_count - assert filter_mesh is not simple_test_mesh + assert filter_mesh is not single_tetrahedron_mesh class TestMeshDoctorGeneratorBase: @@ -407,9 +388,9 @@ def test_logger_functionality( self, caplog ): class TestMeshDoctorBaseEdgeCases: """Test class for edge cases and integration scenarios.""" - def test_filter_base_not_implemented_error( self, simple_test_mesh ): + def test_filter_base_not_implemented_error( self, single_tetrahedron_mesh ): """Test that base class raises NotImplementedError.""" - filter_instance = MeshDoctorFilterBase( simple_test_mesh, "BaseFilter" ) + filter_instance = MeshDoctorFilterBase( single_tetrahedron_mesh, "BaseFilter" ) with pytest.raises( NotImplementedError, match="Subclasses must implement applyFilter method" ): filter_instance.applyFilter() @@ -421,26 +402,26 @@ def test_generator_base_not_implemented_error( self ): with pytest.raises( NotImplementedError, match="Subclasses must implement applyFilter method" ): generator_instance.applyFilter() - def test_filter_with_single_cell_mesh( self, single_cell_mesh ): + def test_filter_with_single_cell_mesh( self, single_tetrahedron_mesh ): """Test filter with a single cell mesh.""" - filter_instance = ConcreteFilterForTesting( single_cell_mesh, "SingleCellTest" ) + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "SingleCellTest" ) result = filter_instance.applyFilter() assert result is True assert filter_instance.getMesh().GetNumberOfCells() == 1 - def test_filter_mesh_independence( self, simple_test_mesh ): + def test_filter_mesh_independence( self, single_tetrahedron_mesh ): """Test that multiple filters are independent.""" - filter1 = ConcreteFilterForTesting( simple_test_mesh, "Filter1" ) - filter2 = ConcreteFilterForTesting( simple_test_mesh, "Filter2" ) + filter1 = ConcreteFilterForTesting( single_tetrahedron_mesh, "Filter1" ) + filter2 = ConcreteFilterForTesting( single_tetrahedron_mesh, "Filter2" ) mesh1 = filter1.getMesh() mesh2 = filter2.getMesh() # Meshes should be independent copies assert mesh1 is not mesh2 - assert mesh1 is not simple_test_mesh - assert mesh2 is not simple_test_mesh + assert mesh1 is not single_tetrahedron_mesh + assert mesh2 is not single_tetrahedron_mesh def test_generator_multiple_instances( self ): """Test that multiple generator instances are independent.""" @@ -454,10 +435,10 @@ def test_generator_multiple_instances( self ): assert gen1.getMesh() is not None assert gen2.getMesh() is not None - def test_filter_logger_names( self, simple_test_mesh ): + def test_filter_logger_names( self, single_tetrahedron_mesh ): """Test that different filters get different logger names.""" - filter1 = ConcreteFilterForTesting( simple_test_mesh, "Filter1" ) - filter2 = ConcreteFilterForTesting( simple_test_mesh, "Filter2" ) + filter1 = ConcreteFilterForTesting( single_tetrahedron_mesh, "Filter1" ) + filter2 = ConcreteFilterForTesting( single_tetrahedron_mesh, "Filter2" ) assert filter1.logger.name != filter2.logger.name @@ -475,9 +456,9 @@ def test_generator_logger_names( self ): ( "LongFilterNameForTesting", True ), ( "UnicodeFilter", True ), ] ) -def test_parametrized_filter_behavior( simple_test_mesh, filter_name, should_succeed ): +def test_parametrized_filter_behavior( single_tetrahedron_mesh, filter_name, should_succeed ): """Parametrized test for different filter configurations.""" - filter_instance = ConcreteFilterForTesting( simple_test_mesh, filter_name, shouldSucceed=should_succeed ) + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, filter_name, shouldSucceed=should_succeed ) result = filter_instance.applyFilter() assert result == should_succeed diff --git a/geos-mesh/tests/test_generate_fractures.py b/geos-mesh/tests/test_generate_fractures.py index 69b86337b..7d26c5ef1 100644 --- a/geos-mesh/tests/test_generate_fractures.py +++ b/geos-mesh/tests/test_generate_fractures.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -import numpy +import numpy as np import pytest from typing import Iterable, Iterator, Sequence from vtkmodules.vtkCommonDataModel import ( vtkUnstructuredGrid, vtkQuad, VTK_HEXAHEDRON, VTK_POLYHEDRON, VTK_QUAD ) @@ -12,7 +12,8 @@ from geos.mesh.doctor.filters.GenerateFractures import ( FIELD_NAME, FIELD_VALUES, FRACTURES_DATA_MODE, FRACTURES_OUTPUT_DIR, OUTPUT_BINARY_MODE, OUTPUT_BINARY_MODE_VALUES, POLICY ) -from geos.mesh.utils.genericHelpers import to_vtk_id_list +from geos.mesh.utils.arrayModifiers import createConstantAttributeDataSet +from geos.mesh.utils.genericHelpers import to_vtk_id_list, createSingleCellMesh FaceNodesCoords = tuple[ tuple[ float ] ] IDMatrix = Sequence[ Sequence[ int ] ] @@ -36,7 +37,7 @@ class TestCase: result: TestResult -def __build_test_case( xs: tuple[ numpy.ndarray, numpy.ndarray, numpy.ndarray ], +def __build_test_case( xs: tuple[ np.ndarray, np.ndarray, np.ndarray ], attribute: Iterable[ int ], field_values: Iterable[ int ] = None, policy: FracturePolicy = FracturePolicy.FIELD ): @@ -44,7 +45,7 @@ def __build_test_case( xs: tuple[ numpy.ndarray, numpy.ndarray, numpy.ndarray ], mesh: vtkUnstructuredGrid = build_rectilinear_blocks_mesh( ( xyz, ) ) - ref = numpy.array( attribute, dtype=int ) + ref = np.array( attribute, dtype=int ) if policy == FracturePolicy.FIELD: assert len( ref ) == mesh.GetNumberOfCells() attr = numpy_to_vtk( ref ) @@ -77,9 +78,9 @@ def next( self, num: int ) -> Iterable[ int ]: def __generate_test_data() -> Iterator[ TestCase ]: - two_nodes = numpy.arange( 2, dtype=float ) - three_nodes = numpy.arange( 3, dtype=float ) - four_nodes = numpy.arange( 4, dtype=float ) + two_nodes = np.arange( 2, dtype=float ) + three_nodes = np.arange( 3, dtype=float ) + four_nodes = np.arange( 4, dtype=float ) # Split in 2 mesh, options = __build_test_case( ( three_nodes, three_nodes, three_nodes ), ( 0, 1, 0, 1, 0, 1, 0, 1 ) ) @@ -229,8 +230,8 @@ def add_simplified_field_for_cells( mesh: vtkUnstructuredGrid, field_name: str, """ data = mesh.GetCellData() n = mesh.GetNumberOfCells() - array = numpy.ones( ( n, field_dimension ), dtype=float ) - array = numpy.arange( 1, n * field_dimension + 1 ).reshape( n, field_dimension ) + array = np.ones( ( n, field_dimension ), dtype=float ) + array = np.arange( 1, n * field_dimension + 1 ).reshape( n, field_dimension ) vtk_array = numpy_to_vtk( array ) vtk_array.SetName( field_name ) data.AddArray( vtk_array ) @@ -306,9 +307,9 @@ def test_copy_fields_when_splitting_mesh(): that will be called when using split_mesh_on_fractures method from generate_fractures. """ # Generating the rectilinear grid and its quads on all borders - x: numpy.array = numpy.array( [ 0, 1, 2 ] ) - y: numpy.array = numpy.array( [ 0, 1 ] ) - z: numpy.array = numpy.array( [ 0, 1 ] ) + x: np.array = np.array( [ 0, 1, 2 ] ) + y: np.array = np.array( [ 0, 1 ] ) + z: np.array = np.array( [ 0, 1 ] ) xyzs: XYZ = XYZ( x, y, z ) mesh: vtkUnstructuredGrid = build_rectilinear_blocks_mesh( [ xyzs ] ) assert mesh.GetCells().GetNumberOfCells() == 2 @@ -397,16 +398,9 @@ def test_generate_fracture_filters_basic( test_case: TestCase, tmp_path ): def test_generate_fractures_filter_setters(): """Test the setter methods of GenerateFractures filter.""" - # Create a simple test mesh - x = numpy.array( [ 0, 1 ] ) - y = numpy.array( [ 0, 1 ] ) - z = numpy.array( [ 0, 1 ] ) - xyz = XYZ( x, y, z ) - mesh = build_rectilinear_blocks_mesh( [ xyz ] ) - - # Add fracture field - add_simplified_field_for_cells( mesh, "test_field", 1 ) - + mesh = createSingleCellMesh( VTK_HEXAHEDRON, np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], + [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ) ) + createConstantAttributeDataSet( dataSet=mesh, listValues=[ 1 ], attributeName="test_field", onPoints=False ) # Create filter instance filterInstance = GenerateFractures( mesh ) @@ -447,17 +441,9 @@ def test_generate_fractures_filter_setters(): def test_generate_fractures_filter_with_global_ids( tmp_path ): """Test that filter fails when mesh contains global IDs.""" # Create a simple test mesh - x = numpy.array( [ 0, 1 ] ) - y = numpy.array( [ 0, 1 ] ) - z = numpy.array( [ 0, 1 ] ) - xyz = XYZ( x, y, z ) - mesh = build_rectilinear_blocks_mesh( [ xyz ] ) - - # Add global IDs (which should cause failure) - points_global_ids = numpy.arange( mesh.GetNumberOfPoints(), dtype=int ) - points_array = numpy_to_vtk( points_global_ids ) - points_array.SetName( "GLOBAL_IDS_POINTS" ) - mesh.GetPointData().AddArray( points_array ) + mesh = createSingleCellMesh( VTK_HEXAHEDRON, np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], + [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ) ) + createConstantAttributeDataSet( dataSet=mesh, listValues=[ 1 ], attributeName="GLOBAL_IDS_POINTS", onPoints=False ) fractures_dir = tmp_path / "fractures" fractures_dir.mkdir( exist_ok=True ) # Create the directory From 1e7dd4e9561f61573be87ec2cdc1d4b1199ffd72 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Mon, 8 Sep 2025 11:18:02 -0700 Subject: [PATCH 49/52] Reimplement vtkIO and test it --- geos-mesh/src/geos/mesh/io/vtkIO.py | 163 +++++----- geos-mesh/tests/test_vtkIO.py | 443 ++++++++++++++++++++++++++++ 2 files changed, 540 insertions(+), 66 deletions(-) create mode 100644 geos-mesh/tests/test_vtkIO.py diff --git a/geos-mesh/src/geos/mesh/io/vtkIO.py b/geos-mesh/src/geos/mesh/io/vtkIO.py index dfc8951bf..9fdb626ad 100644 --- a/geos-mesh/src/geos/mesh/io/vtkIO.py +++ b/geos-mesh/src/geos/mesh/io/vtkIO.py @@ -3,8 +3,8 @@ # SPDX-FileContributor: Alexandre Benedicto from dataclasses import dataclass from enum import Enum -import os.path -from typing import Optional +from pathlib import Path +from typing import Optional, Type, TypeAlias from vtkmodules.vtkCommonDataModel import vtkPointSet, vtkUnstructuredGrid from vtkmodules.vtkIOCore import vtkWriter from vtkmodules.vtkIOLegacy import vtkDataReader, vtkUnstructuredGridWriter, vtkUnstructuredGridReader @@ -14,9 +14,9 @@ from geos.utils.Logger import getLogger __doc__ = """ -Input and Ouput methods for VTK meshes: - - VTK, VTU, VTS, PVTU, PVTS readers - - VTK, VTS, VTU writers +Input and Output methods for various VTK mesh formats. +Supports reading: .vtk, .vtu, .vts, .pvtu, .pvts +Supports writing: .vtk, .vtu, .vts """ io_logger = getLogger( "IO for geos-mesh" ) @@ -32,8 +32,12 @@ class VtkFormat( Enum ): PVTS = ".pvts" +# Improved: Use TypeAlias for cleaner and more readable type hints +VtkReaderClass: TypeAlias = Type[ vtkDataReader | vtkXMLDataReader ] +VtkWriterClass: TypeAlias = Type[ vtkWriter | vtkXMLWriter ] + # Centralized mapping of formats to their corresponding reader classes -READER_MAP: dict[ VtkFormat, vtkDataReader | vtkXMLDataReader ] = { +READER_MAP: dict[ VtkFormat, VtkReaderClass ] = { VtkFormat.VTK: vtkUnstructuredGridReader, VtkFormat.VTS: vtkXMLStructuredGridReader, VtkFormat.VTU: vtkXMLUnstructuredGridReader, @@ -42,7 +46,7 @@ class VtkFormat( Enum ): } # Centralized mapping of formats to their corresponding writer classes -WRITER_MAP: dict[ VtkFormat, vtkWriter | vtkXMLWriter ] = { +WRITER_MAP: dict[ VtkFormat, VtkWriterClass ] = { VtkFormat.VTK: vtkUnstructuredGridWriter, VtkFormat.VTS: vtkXMLStructuredGridWriter, VtkFormat.VTU: vtkXMLUnstructuredGridWriter, @@ -56,37 +60,64 @@ class VtkOutput: is_data_mode_binary: bool = True -def _read_data( filepath: str, reader_class: vtkDataReader | vtkXMLDataReader ) -> Optional[ vtkPointSet ]: - """Generic helper to read a VTK file using a specific reader class.""" - reader: vtkDataReader | vtkXMLDataReader = reader_class() +def _read_data( filepath: str, reader_class: VtkReaderClass ) -> Optional[ vtkPointSet ]: + """Generic helper to read a VTK file using a specific reader class. + + Args: + filepath (str): Path to the VTK file. + reader_class (VtkReaderClass): The VTK reader class to use. + + Returns: + Optional[ vtkPointSet ]: The read VTK point set, or None if reading failed. + """ + reader = reader_class() io_logger.info( f"Attempting to read '{filepath}' with {reader_class.__name__}..." ) - # VTK readers have different methods to check file compatibility - can_read: bool = False - if hasattr( reader, 'CanReadFile' ): - can_read = reader.CanReadFile( filepath ) - elif hasattr( reader, 'IsFileUnstructuredGrid' ): # Legacy reader - can_read = reader.IsFileUnstructuredGrid() + reader.SetFileName( str( filepath ) ) + + # For XML-based readers, CanReadFile is a reliable and fast pre-check. + if hasattr( reader, 'CanReadFile' ) and not reader.CanReadFile( filepath ): + io_logger.error( f"Reader {reader_class.__name__} reports it cannot read file '{filepath}'." ) + return None + + reader.Update() + + # FIX: Check the reader's error code. This is the most reliable way to + # detect a failed read, as GetOutput() can return a default empty object on failure. + if hasattr( reader, 'GetErrorCode' ) and reader.GetErrorCode() != 0: + io_logger.warning( + f"VTK reader {reader_class.__name__} reported an error code after attempting to read '{filepath}'." + ) + return None + + output = reader.GetOutput() + + if output is None: + return None - if can_read: - reader.SetFileName( filepath ) - reader.Update() - io_logger.info( "Read successful." ) - return reader.GetOutput() + io_logger.info( "Read successful." ) + return output - io_logger.info( "Reader did not match the file format." ) - return None +def _write_data( mesh: vtkPointSet, writer_class: VtkWriterClass, output: str, is_binary: bool ) -> int: + """Generic helper to write a VTK file using a specific writer class. -def _write_data( mesh: vtkPointSet, writer_class: vtkWriter | vtkXMLWriter, output_path: str, is_binary: bool ) -> int: - """Generic helper to write a VTK file using a specific writer class.""" - io_logger.info( f"Writing mesh to '{output_path}' using {writer_class.__name__}..." ) - writer: vtkWriter | vtkXMLWriter = writer_class() - writer.SetFileName( output_path ) + Args: + mesh (vtkPointSet): The grid data to write. + writer_class (VtkWriterClass): The VTK writer class to use. + output (str): The output file path. + is_binary (bool): Whether to write the file in binary mode. + + Returns: + int: The result of the write operation. + """ + io_logger.info( f"Writing mesh to '{output}' using {writer_class.__name__}..." ) + writer = writer_class() + writer.SetFileName( output ) writer.SetInputData( mesh ) # Set data mode only for XML writers that support it - if hasattr( writer, 'SetDataModeToBinary' ): + if isinstance( writer, vtkXMLWriter ): if is_binary: writer.SetDataModeToBinary() io_logger.info( "Data mode set to Binary." ) @@ -102,7 +133,7 @@ def read_mesh( filepath: str ) -> vtkPointSet: Reads a VTK file, automatically detecting the format. It first tries the reader associated with the file extension, then falls - back to trying all available readers if the first attempt fails. + back to trying all other available readers if the first attempt fails. Args: filepath (str): The path to the VTK file. @@ -114,32 +145,31 @@ def read_mesh( filepath: str ) -> vtkPointSet: Returns: vtkPointSet: The resulting mesh data. """ - if not os.path.exists( filepath ): + filepath_path: Path = Path( filepath ) + if not filepath_path.exists(): raise FileNotFoundError( f"Invalid file path: '{filepath}' does not exist." ) - _, extension = os.path.splitext( filepath ) - output_mesh: Optional[ vtkPointSet ] = None - - # 1. Try the reader associated with the file extension first + candidate_readers: list[ VtkReaderClass ] = [] + # 1. Prioritize the reader associated with the file extension try: - file_format = VtkFormat( extension ) + file_format = VtkFormat( filepath_path.suffix ) if file_format in READER_MAP: - reader_class = READER_MAP[ file_format ] - output_mesh = _read_data( filepath, reader_class ) + candidate_readers.append( READER_MAP[ file_format ] ) except ValueError: - io_logger.warning( f"Unknown file extension '{extension}'. Trying all readers." ) + io_logger.warning( f"Unknown file extension '{filepath_path.suffix}'. Trying all available readers." ) - # 2. If the first attempt failed or extension was unknown, try all readers - if not output_mesh: - for reader_class in set( READER_MAP.values() ): # Use set to avoid duplicates - output_mesh = _read_data( filepath, reader_class ) - if output_mesh: - break + # 2. Add all other unique readers as fallbacks + for reader_cls in READER_MAP.values(): + if reader_cls not in candidate_readers: + candidate_readers.append( reader_cls ) - if not output_mesh: - raise ValueError( f"Could not find a suitable reader for '{filepath}'." ) + # 3. Attempt to read with the candidates in order + for reader_class in candidate_readers: + output_mesh = _read_data( filepath, reader_class ) + if output_mesh: + return output_mesh - return output_mesh + raise ValueError( f"Could not find a suitable reader for '{filepath}'." ) def read_unstructured_grid( filepath: str ) -> vtkUnstructuredGrid: @@ -161,11 +191,8 @@ def read_unstructured_grid( filepath: str ) -> vtkUnstructuredGrid: vtkUnstructuredGrid: The resulting unstructured grid data. """ io_logger.info( f"Reading file '{filepath}' and expecting vtkUnstructuredGrid." ) - - # Reuse the generic mesh reader mesh = read_mesh( filepath ) - # Check the type of the resulting mesh if not isinstance( mesh, vtkUnstructuredGrid ): error_msg = ( f"File '{filepath}' was read successfully, but it is of type " f"'{type(mesh).__name__}', not the expected vtkUnstructuredGrid." ) @@ -196,25 +223,29 @@ def write_mesh( mesh: vtkPointSet, vtk_output: VtkOutput, can_overwrite: bool = Returns: int: Returns 1 on success, consistent with the VTK writer's return code. """ - if os.path.exists( vtk_output.output ) and not can_overwrite: - raise FileExistsError( f"File '{vtk_output.output}' already exists. Set can_overwrite=True to replace it." ) - - _, extension = os.path.splitext( vtk_output.output ) + output_path = Path( vtk_output.output ) + if output_path.exists() and not can_overwrite: + raise FileExistsError( f"File '{output_path}' already exists. Set can_overwrite=True to replace it." ) try: - file_format = VtkFormat( extension ) - if file_format not in WRITER_MAP: - raise ValueError( f"Writing to extension '{extension}' is not supported." ) - - writer_class = WRITER_MAP[ file_format ] - success_code = _write_data( mesh, writer_class, vtk_output.output, vtk_output.is_data_mode_binary ) - + # Catch the ValueError from an invalid enum to provide a consistent error message. + try: + file_format = VtkFormat( output_path.suffix ) + except ValueError: + # Re-raise with the message expected by the test. + raise ValueError( f"Writing to extension '{output_path.suffix}' is not supported." ) + + writer_class = WRITER_MAP.get( file_format ) + if not writer_class: + raise ValueError( f"Writing to extension '{output_path.suffix}' is not supported." ) + + success_code = _write_data( mesh, writer_class, str(output_path), vtk_output.is_data_mode_binary ) if not success_code: - raise RuntimeError( f"VTK writer failed to write file '{vtk_output.output}'." ) + raise RuntimeError( f"VTK writer failed to write file '{output_path}'." ) - io_logger.info( f"Successfully wrote mesh to '{vtk_output.output}'." ) - return success_code # VTK writers return 1 for success + io_logger.info( f"Successfully wrote mesh to '{output_path}'." ) + return success_code - except ValueError as e: + except ( ValueError, RuntimeError ) as e: io_logger.error( e ) raise diff --git a/geos-mesh/tests/test_vtkIO.py b/geos-mesh/tests/test_vtkIO.py new file mode 100644 index 000000000..5e824ad73 --- /dev/null +++ b/geos-mesh/tests/test_vtkIO.py @@ -0,0 +1,443 @@ +import pytest +import numpy as np +from vtkmodules.vtkCommonCore import vtkPoints +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, vtkStructuredGrid, VTK_TETRA, VTK_HEXAHEDRON +from geos.mesh.utils.genericHelpers import createSingleCellMesh +from geos.mesh.io.vtkIO import ( + VtkFormat, VtkOutput, read_mesh, read_unstructured_grid, write_mesh, + READER_MAP, WRITER_MAP +) + +__doc__ = """ +Test module for vtkIO module. +Tests the functionality of reading and writing various VTK file formats. +""" + + +@pytest.fixture( scope="module" ) +def simple_unstructured_mesh(): + """Fixture for a simple unstructured mesh with tetrahedron.""" + return createSingleCellMesh( VTK_TETRA, np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ] ) ) + + +@pytest.fixture( scope="module" ) +def simple_hex_mesh(): + """Fixture for a simple hexahedron mesh.""" + return createSingleCellMesh( + VTK_HEXAHEDRON, + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], + [ 0, 1, 1 ] ] ) ) + + +@pytest.fixture( scope="module" ) +def structured_mesh(): + """Fixture for a simple structured grid.""" + mesh = vtkStructuredGrid() + mesh.SetDimensions( 2, 2, 2 ) + + points = vtkPoints() + for k in range( 2 ): + for j in range( 2 ): + for i in range( 2 ): + points.InsertNextPoint( i, j, k ) + + mesh.SetPoints( points ) + return mesh + + +class TestVtkFormat: + """Test class for VtkFormat enumeration.""" + + def test_vtk_format_values( self ): + """Test that VtkFormat enum has correct values.""" + assert VtkFormat.VTK.value == ".vtk" + assert VtkFormat.VTS.value == ".vts" + assert VtkFormat.VTU.value == ".vtu" + assert VtkFormat.PVTU.value == ".pvtu" + assert VtkFormat.PVTS.value == ".pvts" + + def test_vtk_format_from_string( self ): + """Test creating VtkFormat from string values.""" + assert VtkFormat( ".vtk" ) == VtkFormat.VTK + assert VtkFormat( ".vtu" ) == VtkFormat.VTU + assert VtkFormat( ".vts" ) == VtkFormat.VTS + assert VtkFormat( ".pvtu" ) == VtkFormat.PVTU + assert VtkFormat( ".pvts" ) == VtkFormat.PVTS + + def test_invalid_format( self ): + """Test that invalid format raises ValueError.""" + with pytest.raises( ValueError ): + VtkFormat( ".invalid" ) + + +class TestVtkOutput: + """Test class for VtkOutput dataclass.""" + + def test_vtk_output_creation( self ): + """Test VtkOutput creation with default parameters.""" + output = VtkOutput( "test.vtu" ) + assert output.output == "test.vtu" + assert output.is_data_mode_binary is True + + def test_vtk_output_creation_custom( self ): + """Test VtkOutput creation with custom parameters.""" + output = VtkOutput( "test.vtu", is_data_mode_binary=False ) + assert output.output == "test.vtu" + assert output.is_data_mode_binary is False + + def test_vtk_output_immutable( self ): + """Test that VtkOutput is immutable (frozen dataclass).""" + output = VtkOutput( "test.vtu" ) + with pytest.raises( AttributeError ): + output.output = "new_test.vtu" + + +class TestMappings: + """Test class for reader and writer mappings.""" + + def test_reader_map_completeness( self ): + """Test that READER_MAP contains all readable formats.""" + expected_formats = { VtkFormat.VTK, VtkFormat.VTS, VtkFormat.VTU, VtkFormat.PVTU, VtkFormat.PVTS } + assert set( READER_MAP.keys() ) == expected_formats + + def test_writer_map_completeness( self ): + """Test that WRITER_MAP contains all writable formats.""" + expected_formats = { VtkFormat.VTK, VtkFormat.VTS, VtkFormat.VTU } + assert set( WRITER_MAP.keys() ) == expected_formats + + def test_reader_map_classes( self ): + """Test that READER_MAP contains valid reader classes.""" + for format_type, reader_class in READER_MAP.items(): + assert hasattr( reader_class, '__name__' ) + # All readers should be classes + assert isinstance( reader_class, type ) + + def test_writer_map_classes( self ): + """Test that WRITER_MAP contains valid writer classes.""" + for format_type, writer_class in WRITER_MAP.items(): + assert hasattr( writer_class, '__name__' ) + # All writers should be classes + assert isinstance( writer_class, type ) + + +class TestWriteMesh: + """Test class for write_mesh functionality.""" + + def test_write_vtu_binary( self, simple_unstructured_mesh, tmp_path ): + """Test writing VTU file in binary mode.""" + output_file = tmp_path / "test_mesh.vtu" + vtk_output = VtkOutput( str( output_file ), is_data_mode_binary=True ) + + result = write_mesh( simple_unstructured_mesh, vtk_output, can_overwrite=True ) + + assert result == 1 # VTK success code + assert output_file.exists() + assert output_file.stat().st_size > 0 + + def test_write_vtu_ascii( self, simple_unstructured_mesh, tmp_path ): + """Test writing VTU file in ASCII mode.""" + output_file = tmp_path / "test_mesh_ascii.vtu" + vtk_output = VtkOutput( str( output_file ), is_data_mode_binary=False ) + + result = write_mesh( simple_unstructured_mesh, vtk_output, can_overwrite=True ) + + assert result == 1 # VTK success code + assert output_file.exists() + assert output_file.stat().st_size > 0 + + def test_write_vtk_format( self, simple_unstructured_mesh, tmp_path ): + """Test writing VTK legacy format.""" + output_file = tmp_path / "test_mesh.vtk" + vtk_output = VtkOutput( str( output_file ) ) + + result = write_mesh( simple_unstructured_mesh, vtk_output, can_overwrite=True ) + + assert result == 1 # VTK success code + assert output_file.exists() + assert output_file.stat().st_size > 0 + + def test_write_vts_format( self, structured_mesh, tmp_path ): + """Test writing VTS (structured grid) format.""" + output_file = tmp_path / "test_mesh.vts" + vtk_output = VtkOutput( str( output_file ) ) + + result = write_mesh( structured_mesh, vtk_output, can_overwrite=True ) + + assert result == 1 # VTK success code + assert output_file.exists() + assert output_file.stat().st_size > 0 + + def test_write_file_exists_error( self, simple_unstructured_mesh, tmp_path ): + """Test that writing to existing file raises error when can_overwrite=False.""" + output_file = tmp_path / "existing_file.vtu" + output_file.write_text( "dummy content" ) # Create existing file + + vtk_output = VtkOutput( str( output_file ) ) + + with pytest.raises( FileExistsError, match="already exists" ): + write_mesh( simple_unstructured_mesh, vtk_output, can_overwrite=False ) + + def test_write_unsupported_format( self, simple_unstructured_mesh, tmp_path ): + """Test that writing unsupported format raises ValueError.""" + output_file = tmp_path / "test_mesh.unsupported" + vtk_output = VtkOutput( str( output_file ) ) + + with pytest.raises( ValueError, match="not supported" ): + write_mesh( simple_unstructured_mesh, vtk_output ) + + def test_write_overwrite_allowed( self, simple_unstructured_mesh, tmp_path ): + """Test that overwriting is allowed when can_overwrite=True.""" + output_file = tmp_path / "overwrite_test.vtu" + vtk_output = VtkOutput( str( output_file ) ) + + # First write + result1 = write_mesh( simple_unstructured_mesh, vtk_output, can_overwrite=True ) + assert result1 == 1 + assert output_file.exists() + + # Second write (overwrite) + result2 = write_mesh( simple_unstructured_mesh, vtk_output, can_overwrite=True ) + assert result2 == 1 + assert output_file.exists() + + +class TestReadMesh: + """Test class for read_mesh functionality.""" + + def test_read_nonexistent_file( self ): + """Test that reading nonexistent file raises FileNotFoundError.""" + with pytest.raises( FileNotFoundError, match="does not exist" ): + read_mesh( "nonexistent_file.vtu" ) + + def test_read_vtu_file( self, simple_unstructured_mesh, tmp_path ): + """Test reading VTU file.""" + output_file = tmp_path / "test_read.vtu" + vtk_output = VtkOutput( str( output_file ) ) + + # First write the file + write_mesh( simple_unstructured_mesh, vtk_output, can_overwrite=True ) + + # Then read it back + read_mesh_result = read_mesh( str( output_file ) ) + + assert read_mesh_result is not None + assert isinstance( read_mesh_result, vtkUnstructuredGrid ) + assert read_mesh_result.GetNumberOfPoints() == simple_unstructured_mesh.GetNumberOfPoints() + assert read_mesh_result.GetNumberOfCells() == simple_unstructured_mesh.GetNumberOfCells() + + def test_read_vtk_file( self, simple_unstructured_mesh, tmp_path ): + """Test reading VTK legacy file.""" + output_file = tmp_path / "test_read.vtk" + vtk_output = VtkOutput( str( output_file ) ) + + # First write the file + write_mesh( simple_unstructured_mesh, vtk_output, can_overwrite=True ) + + # Then read it back + read_mesh_result = read_mesh( str( output_file ) ) + + assert read_mesh_result is not None + assert isinstance( read_mesh_result, vtkUnstructuredGrid ) + assert read_mesh_result.GetNumberOfPoints() == simple_unstructured_mesh.GetNumberOfPoints() + assert read_mesh_result.GetNumberOfCells() == simple_unstructured_mesh.GetNumberOfCells() + + def test_read_vts_file( self, structured_mesh, tmp_path ): + """Test reading VTS (structured grid) file.""" + output_file = tmp_path / "test_read.vts" + vtk_output = VtkOutput( str( output_file ) ) + + # First write the file + write_mesh( structured_mesh, vtk_output, can_overwrite=True ) + + # Then read it back + read_mesh_result = read_mesh( str( output_file ) ) + + assert read_mesh_result is not None + assert isinstance( read_mesh_result, vtkStructuredGrid ) + assert read_mesh_result.GetNumberOfPoints() == structured_mesh.GetNumberOfPoints() + + def test_read_unknown_extension( self, simple_unstructured_mesh, tmp_path ): + """Test reading file with unknown extension falls back to trying all readers.""" + # Create a VTU file but with unknown extension + vtu_file = tmp_path / "test.vtu" + unknown_file = tmp_path / "test.unknown" + + # Write as VTU first + vtk_output = VtkOutput( str( vtu_file ) ) + write_mesh( simple_unstructured_mesh, vtk_output, can_overwrite=True ) + + # Copy to unknown extension + unknown_file.write_bytes( vtu_file.read_bytes() ) + + # Should still be able to read it + read_mesh_result = read_mesh( str( unknown_file ) ) + + assert read_mesh_result is not None + assert isinstance( read_mesh_result, vtkUnstructuredGrid ) + + def test_read_invalid_file_content( self, tmp_path ): + """Test that reading invalid file content raises ValueError.""" + invalid_file = tmp_path / "invalid.vtu" + invalid_file.write_text( "This is not a valid VTU file" ) + + with pytest.raises( ValueError, match="Could not find a suitable reader" ): + read_mesh( str( invalid_file ) ) + + +class TestReadUnstructuredGrid: + """Test class for read_unstructured_grid functionality.""" + + def test_read_unstructured_grid_success( self, simple_unstructured_mesh, tmp_path ): + """Test successfully reading an unstructured grid.""" + output_file = tmp_path / "test_ug.vtu" + vtk_output = VtkOutput( str( output_file ) ) + + # Write unstructured grid + write_mesh( simple_unstructured_mesh, vtk_output, can_overwrite=True ) + + # Read back as unstructured grid + result = read_unstructured_grid( str( output_file ) ) + + assert isinstance( result, vtkUnstructuredGrid ) + assert result.GetNumberOfPoints() == simple_unstructured_mesh.GetNumberOfPoints() + assert result.GetNumberOfCells() == simple_unstructured_mesh.GetNumberOfCells() + + def test_read_unstructured_grid_wrong_type( self, structured_mesh, tmp_path ): + """Test that reading non-unstructured grid raises TypeError.""" + output_file = tmp_path / "test_sg.vts" + vtk_output = VtkOutput( str( output_file ) ) + + # Write structured grid + write_mesh( structured_mesh, vtk_output, can_overwrite=True ) + + # Try to read as unstructured grid - should fail + with pytest.raises( TypeError, match="not the expected vtkUnstructuredGrid" ): + read_unstructured_grid( str( output_file ) ) + + def test_read_unstructured_grid_nonexistent( self ): + """Test that reading nonexistent file raises FileNotFoundError.""" + with pytest.raises( FileNotFoundError, match="does not exist" ): + read_unstructured_grid( "nonexistent.vtu" ) + + +class TestRoundTripReadWrite: + """Test class for round-trip read/write operations.""" + + def test_vtu_round_trip_binary( self, simple_unstructured_mesh, tmp_path ): + """Test round-trip write and read for VTU binary format.""" + output_file = tmp_path / "roundtrip_binary.vtu" + vtk_output = VtkOutput( str( output_file ), is_data_mode_binary=True ) + + # Write + write_result = write_mesh( simple_unstructured_mesh, vtk_output, can_overwrite=True ) + assert write_result == 1 + + # Read back + read_result = read_unstructured_grid( str( output_file ) ) + + # Compare + assert read_result.GetNumberOfPoints() == simple_unstructured_mesh.GetNumberOfPoints() + assert read_result.GetNumberOfCells() == simple_unstructured_mesh.GetNumberOfCells() + + # Check point coordinates are preserved + for i in range( read_result.GetNumberOfPoints() ): + orig_point = simple_unstructured_mesh.GetPoint( i ) + read_point = read_result.GetPoint( i ) + np.testing.assert_array_almost_equal( orig_point, read_point, decimal=6 ) + + def test_vtu_round_trip_ascii( self, simple_unstructured_mesh, tmp_path ): + """Test round-trip write and read for VTU ASCII format.""" + output_file = tmp_path / "roundtrip_ascii.vtu" + vtk_output = VtkOutput( str( output_file ), is_data_mode_binary=False ) + + # Write + write_result = write_mesh( simple_unstructured_mesh, vtk_output, can_overwrite=True ) + assert write_result == 1 + + # Read back + read_result = read_unstructured_grid( str( output_file ) ) + + # Compare + assert read_result.GetNumberOfPoints() == simple_unstructured_mesh.GetNumberOfPoints() + assert read_result.GetNumberOfCells() == simple_unstructured_mesh.GetNumberOfCells() + + def test_vtk_round_trip( self, simple_unstructured_mesh, tmp_path ): + """Test round-trip write and read for VTK legacy format.""" + output_file = tmp_path / "roundtrip.vtk" + vtk_output = VtkOutput( str( output_file ) ) + + # Write + write_result = write_mesh( simple_unstructured_mesh, vtk_output, can_overwrite=True ) + assert write_result == 1 + + # Read back + read_result = read_unstructured_grid( str( output_file ) ) + + # Compare + assert read_result.GetNumberOfPoints() == simple_unstructured_mesh.GetNumberOfPoints() + assert read_result.GetNumberOfCells() == simple_unstructured_mesh.GetNumberOfCells() + + def test_vts_round_trip( self, structured_mesh, tmp_path ): + """Test round-trip write and read for VTS format.""" + output_file = tmp_path / "roundtrip.vts" + vtk_output = VtkOutput( str( output_file ) ) + + # Write + write_result = write_mesh( structured_mesh, vtk_output, can_overwrite=True ) + assert write_result == 1 + + # Read back + read_result = read_mesh( str( output_file ) ) + + # Compare + assert isinstance( read_result, vtkStructuredGrid ) + assert read_result.GetNumberOfPoints() == structured_mesh.GetNumberOfPoints() + + +class TestEdgeCases: + """Test class for edge cases and error conditions.""" + + def test_empty_mesh_write( self, tmp_path ): + """Test writing an empty mesh.""" + empty_mesh = vtkUnstructuredGrid() + output_file = tmp_path / "empty.vtu" + vtk_output = VtkOutput( str( output_file ) ) + + result = write_mesh( empty_mesh, vtk_output, can_overwrite=True ) + assert result == 1 + assert output_file.exists() + + def test_empty_mesh_round_trip( self, tmp_path ): + """Test round-trip with empty mesh.""" + empty_mesh = vtkUnstructuredGrid() + output_file = tmp_path / "empty_roundtrip.vtu" + vtk_output = VtkOutput( str( output_file ) ) + + # Write + write_result = write_mesh( empty_mesh, vtk_output, can_overwrite=True ) + assert write_result == 1 + + # Read back + read_result = read_unstructured_grid( str( output_file ) ) + assert read_result.GetNumberOfPoints() == 0 + assert read_result.GetNumberOfCells() == 0 + + def test_large_path_names( self, simple_unstructured_mesh, tmp_path ): + """Test handling of long file paths.""" + # Create a deep directory structure + deep_dir = tmp_path + for i in range( 5 ): + deep_dir = deep_dir / f"very_long_directory_name_level_{i}" + deep_dir.mkdir( parents=True ) + + output_file = deep_dir / "mesh_with_very_long_filename_that_should_still_work.vtu" + vtk_output = VtkOutput( str( output_file ) ) + + # Should work fine + result = write_mesh( simple_unstructured_mesh, vtk_output, can_overwrite=True ) + assert result == 1 + assert output_file.exists() + + # And read back + read_result = read_unstructured_grid( str( output_file ) ) + assert read_result.GetNumberOfPoints() == simple_unstructured_mesh.GetNumberOfPoints() From f1a2410cdc842d834f43fc70adf195ebd16c41d8 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Mon, 15 Sep 2025 09:27:15 -0700 Subject: [PATCH 50/52] Correct some comments --- .../geos/mesh/doctor/actions/fix_elements_orderings.py | 9 ++++----- geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/actions/fix_elements_orderings.py b/geos-mesh/src/geos/mesh/doctor/actions/fix_elements_orderings.py index 3947ce51e..5ab0b8914 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/fix_elements_orderings.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/fix_elements_orderings.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Dict, FrozenSet, List, Set from vtkmodules.vtkCommonCore import vtkIdList from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid from geos.mesh.utils.genericHelpers import to_vtk_id_list @@ -9,20 +8,20 @@ @dataclass( frozen=True ) class Options: vtk_output: VtkOutput - cell_type_to_ordering: Dict[ int, List[ int ] ] + cell_type_to_ordering: dict[ int, list[ int ] ] @dataclass( frozen=True ) class Result: output: str - unchanged_cell_types: FrozenSet[ int ] + unchanged_cell_types: frozenset[ int ] def mesh_action( mesh: vtkUnstructuredGrid, 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 - unchanged_cell_types: Set[ int ] = set() # For logging purpose + cell_type_to_ordering: dict[ int, list[ int ] ] = options.cell_type_to_ordering + unchanged_cell_types: set[ int ] = set() # For logging purpose # Preparing the output mesh by first keeping the same instance type. output_mesh = mesh.NewInstance() 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 782f1a087..97bc254ff 100644 --- a/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py +++ b/geos-mesh/src/geos/mesh/doctor/actions/generate_cube.py @@ -159,7 +159,8 @@ def add_fields( mesh: vtkUnstructuredGrid, fields: Iterable[ FieldInfo ] ) -> vt success = createConstantAttributeDataSet( dataSet=mesh, listValues=listValues, attributeName=field_info.name, - onPoints=onPoints ) + onPoints=onPoints, + logger=setup_logger ) if not success: setup_logger.warning( f"Failed to create field {field_info.name}" ) return mesh From 10ec288b9fd7d24e6d85cb00c25c7249f91daf2e Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Mon, 15 Sep 2025 10:27:30 -0700 Subject: [PATCH 51/52] Add findUniqueCellCenterCellIds function --- .../src/geos/mesh/utils/genericHelpers.py | 78 ++++++++++++++++++- geos-mesh/tests/test_genericHelpers.py | 56 ++++++++++++- 2 files changed, 131 insertions(+), 3 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/genericHelpers.py b/geos-mesh/src/geos/mesh/utils/genericHelpers.py index de0624fd9..6684d1252 100644 --- a/geos-mesh/src/geos/mesh/utils/genericHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/genericHelpers.py @@ -6,8 +6,10 @@ from typing import Iterator, List, Sequence, Any, Union from vtkmodules.util.numpy_support import numpy_to_vtk from vtkmodules.vtkCommonCore import vtkIdList, vtkPoints, reference -from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, vtkMultiBlockDataSet, vtkPolyData, vtkDataSet, vtkDataObject, vtkPlane, vtkCellTypes, vtkIncrementalOctreePointLocator -from vtkmodules.vtkFiltersCore import vtk3DLinearGridPlaneCutter +from vtkmodules.vtkCommonDataModel import ( vtkUnstructuredGrid, vtkMultiBlockDataSet, vtkPolyData, vtkDataSet, + vtkDataObject, vtkPlane, vtkCellTypes, vtkIncrementalOctreePointLocator, + vtkStaticPointLocator ) +from vtkmodules.vtkFiltersCore import vtk3DLinearGridPlaneCutter, vtkCellCenters from geos.mesh.utils.multiblockHelpers import ( getBlockElementIndexesFlatten, getBlockFromFlatIndex ) __doc__ = """ @@ -85,6 +87,78 @@ def extractSurfaceFromElevation( mesh: vtkUnstructuredGrid, elevation: float ) - return cutter.GetOutputDataObject( 0 ) +def findUniqueCellCenterCellIds( grid1: vtkUnstructuredGrid, + grid2: vtkUnstructuredGrid, + tolerance: float = 1e-6 ) -> tuple[ list[ int ], list[ int ] ]: + """ + Compares two vtkUnstructuredGrids and finds the IDs of cells with unique centers. + + This function identifies cells whose centers exist in one grid but not the other, + within a specified floating-point tolerance. + + Args: + grid1 (vtk.vtkUnstructuredGrid): The first grid. + grid2 (vtk.vtkUnstructuredGrid): The second grid. + tolerance (float): The distance threshold to consider two points as the same. + + Returns: + tuple[list[int], list[int]]: A tuple containing two lists: + - The first list has the IDs of cells with centers unique to grid1. + - The second list has the IDs of cells with centers unique to grid2. + """ + if not grid1 or not grid2: + raise ValueError( "Input grids must be valid vtkUnstructuredGrid objects." ) + + # Generate cell centers for both grids using vtkCellCenters filter + centersFilter1 = vtkCellCenters() + centersFilter1.SetInputData( grid1 ) + centersFilter1.Update() + centers1 = centersFilter1.GetOutput().GetPoints() + + centersFilter2 = vtkCellCenters() + centersFilter2.SetInputData( grid2 ) + centersFilter2.Update() + centers2 = centersFilter2.GetOutput().GetPoints() + + # Find cells with centers that are unique to grid1 + uniqueIdsInGrid1: list[ int ] = [] + uniqueIdsCoordsInGrid1: list[ tuple[ float, float, float ] ] = [] + # Build a locator for the cell centers of grid2 for fast searching + locator2 = vtkStaticPointLocator() + locator2.SetDataSet( centersFilter2.GetOutput() ) + locator2.BuildLocator() + + for i in range( centers1.GetNumberOfPoints() ): + centerPt1 = centers1.GetPoint( i ) + # Find the closest point in grid2 to the current center from grid1 + result = vtkIdList() + locator2.FindPointsWithinRadius( tolerance, centerPt1, result ) + # If no point is found within the tolerance radius, the cell center is unique + if result.GetNumberOfIds() == 0: + uniqueIdsInGrid1.append( i ) + uniqueIdsCoordsInGrid1.append( centerPt1 ) + + # Find cells with centers that are unique to grid2 + uniqueIdsInGrid2: list[ int ] = [] + uniqueIdsCoordsInGrid2: list[ tuple[ float, float, float ] ] = [] + # Build a locator for the cell centers of grid1 for fast searching + locator1 = vtkStaticPointLocator() + locator1.SetDataSet( centersFilter1.GetOutput() ) + locator1.BuildLocator() + + for i in range( centers2.GetNumberOfPoints() ): + centerPt2 = centers2.GetPoint( i ) + # Find the closest point in grid1 to the current center from grid2 + result = vtkIdList() + locator1.FindPointsWithinRadius( tolerance, centerPt2, result ) + # If no point is found, it's unique to grid2 + if result.GetNumberOfIds() == 0: + uniqueIdsInGrid2.append( i ) + uniqueIdsCoordsInGrid2.append( centerPt2 ) + + return uniqueIdsInGrid1, uniqueIdsInGrid2, uniqueIdsCoordsInGrid1, uniqueIdsCoordsInGrid2 + + def getBounds( input: Union[ vtkUnstructuredGrid, vtkMultiBlockDataSet ] ) -> tuple[ float, float, float, float, float, float ]: diff --git a/geos-mesh/tests/test_genericHelpers.py b/geos-mesh/tests/test_genericHelpers.py index 85de45f5c..7132ca2c3 100644 --- a/geos-mesh/tests/test_genericHelpers.py +++ b/geos-mesh/tests/test_genericHelpers.py @@ -9,7 +9,8 @@ from typing import ( Iterator, ) -from geos.mesh.utils.genericHelpers import getBoundsFromPointCoords, createVertices, createMultiCellMesh +from geos.mesh.utils.genericHelpers import ( getBoundsFromPointCoords, createVertices, createMultiCellMesh, + findUniqueCellCenterCellIds ) from vtkmodules.util.numpy_support import vtk_to_numpy @@ -181,3 +182,56 @@ def test_getBoundsFromPointCoords() -> None: boundsExp: list[ float ] = [ 0., 5., 1., 8., 2., 9. ] boundsObs: list[ float ] = getBoundsFromPointCoords( cellPtsCoord ) assert boundsExp == boundsObs, f"Expected bounds are {boundsExp}." + + +def test_findUniqueCellCenterCellIds() -> None: + """Test of findUniqueCellCenterCellIds method.""" + # Create first mesh with two cells + cellTypes1: list[ int ] = [ VTK_TETRA, VTK_TETRA ] + cellPtsCoord1: list[ npt.NDArray[ np.float64 ] ] = [ + # First tetrahedron centered around (0.25, 0.25, 0.25) + np.array( [ [ 0.0, 0.0, 0.0 ], [ 1.0, 0.0, 0.0 ], [ 0.0, 1.0, 0.0 ], [ 0.0, 0.0, 1.0 ] ], dtype=float ), + # Second tetrahedron centered around (2.25, 2.25, 2.25) + np.array( [ [ 2.0, 2.0, 2.0 ], [ 3.0, 2.0, 2.0 ], [ 2.0, 3.0, 2.0 ], [ 2.0, 2.0, 3.0 ] ], dtype=float ), + ] + + # Create second mesh with different cells, one overlapping with first mesh + cellTypes2: list[ int ] = [ VTK_TETRA, VTK_TETRA ] + cellPtsCoord2: list[ npt.NDArray[ np.float64 ] ] = [ + # First tetrahedron with same center as first cell in mesh1 (should overlap) + np.array( [ [ 0.0, 0.0, 0.0 ], [ 1.0, 0.0, 0.0 ], [ 0.0, 1.0, 0.0 ], [ 0.0, 0.0, 1.0 ] ], dtype=float ), + # Second tetrahedron with different center (unique to mesh2) + np.array( [ [ 4.0, 4.0, 4.0 ], [ 5.0, 4.0, 4.0 ], [ 4.0, 5.0, 4.0 ], [ 4.0, 4.0, 5.0 ] ], dtype=float ), + ] + + # Create meshes + mesh1: vtkUnstructuredGrid = createMultiCellMesh( cellTypes1, cellPtsCoord1, sharePoints=True ) + mesh2: vtkUnstructuredGrid = createMultiCellMesh( cellTypes2, cellPtsCoord2, sharePoints=True ) + + # Test the function + uniqueIds1, uniqueIds2, uniqueCoords1, uniqueCoords2 = findUniqueCellCenterCellIds( mesh1, mesh2 ) + + # Expected results: + # - Cell 0 in both meshes have the same center, so should not be in unique lists + # - Cell 1 in mesh1 (centered at ~(2.25, 2.25, 2.25)) is unique to mesh1 + # - Cell 1 in mesh2 (centered at ~(4.25, 4.25, 4.25)) is unique to mesh2 + assert len( uniqueIds1 ) == 1, f"Expected 1 unique cell in mesh1, got {len(uniqueIds1)}" + assert len( uniqueIds2 ) == 1, f"Expected 1 unique cell in mesh2, got {len(uniqueIds2)}" + assert uniqueIds1 == [ 1 ], f"Expected unique cell 1 in mesh1, got {uniqueIds1}" + assert uniqueIds2 == [ 1 ], f"Expected unique cell 1 in mesh2, got {uniqueIds2}" + + # Test coordinate lists + assert len( uniqueCoords1 ) == 1, f"Expected 1 unique coordinate in mesh1, got {len(uniqueCoords1)}" + assert len( uniqueCoords2 ) == 1, f"Expected 1 unique coordinate in mesh2, got {len(uniqueCoords2)}" + + # Test with tolerance + uniqueIds1_tight, uniqueIds2_tight, _, _ = findUniqueCellCenterCellIds( mesh1, mesh2, tolerance=1e-12 ) + assert len( uniqueIds1_tight ) == 1, "Tight tolerance should still find 1 unique cell in mesh1" + assert len( uniqueIds2_tight ) == 1, "Tight tolerance should still find 1 unique cell in mesh2" + + # Test error handling + with pytest.raises( ValueError, match="Input grids must be valid vtkUnstructuredGrid objects" ): + findUniqueCellCenterCellIds( None, mesh2 ) # type: ignore[arg-type] + + with pytest.raises( ValueError, match="Input grids must be valid vtkUnstructuredGrid objects" ): + findUniqueCellCenterCellIds( mesh1, None ) # type: ignore[arg-type] From e141022c436d7176de71cb9a18d37d8a4cb5dc54 Mon Sep 17 00:00:00 2001 From: alexbenedicto Date: Mon, 15 Sep 2025 10:31:12 -0700 Subject: [PATCH 52/52] yapf --- .../src/geos/mesh/doctor/filters/Checks.py | 10 ++-- .../mesh/doctor/filters/GenerateFractures.py | 24 ++++----- .../doctor/filters/MeshDoctorFilterBase.py | 28 +++++----- .../geos/mesh/doctor/filters/NonConformal.py | 14 +++-- .../filters/SelfIntersectingElements.py | 9 ++-- .../parsing/collocated_nodes_parsing.py | 5 +- .../doctor/parsing/element_volumes_parsing.py | 2 +- .../doctor/parsing/non_conformal_parsing.py | 2 +- .../self_intersecting_elements_parsing.py | 24 ++++----- .../parsing/supported_elements_parsing.py | 7 ++- geos-mesh/src/geos/mesh/io/vtkIO.py | 5 +- geos-mesh/tests/test_Checks.py | 29 +++++----- geos-mesh/tests/test_CollocatedNodes.py | 29 +++++----- geos-mesh/tests/test_ElementVolumes.py | 11 ++-- geos-mesh/tests/test_MeshDoctorFilterBase.py | 3 +- geos-mesh/tests/test_SupportedElements.py | 53 ++++++++++--------- geos-mesh/tests/test_generate_fractures.py | 12 +++-- geos-mesh/tests/test_vtkIO.py | 6 +-- 18 files changed, 138 insertions(+), 135 deletions(-) diff --git a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py index f3a8682d2..63c13f5bc 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/Checks.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/Checks.py @@ -271,9 +271,8 @@ def __init__( # Main functions for backward compatibility and standalone use def allChecks( - mesh: vtkUnstructuredGrid, - customParameters: dict[ str, dict[ str, Any ] ] = None -) -> tuple[ vtkUnstructuredGrid, dict[ str, Any ] ]: + mesh: vtkUnstructuredGrid, + customParameters: dict[ str, dict[ str, Any ] ] = None ) -> tuple[ vtkUnstructuredGrid, dict[ str, Any ] ]: """Apply all available mesh checks to a mesh. Args: @@ -302,9 +301,8 @@ def allChecks( def mainChecks( - mesh: vtkUnstructuredGrid, - customParameters: dict[ str, dict[ str, Any ] ] = None -) -> tuple[ vtkUnstructuredGrid, dict[ str, Any ] ]: + mesh: vtkUnstructuredGrid, + customParameters: dict[ str, dict[ str, Any ] ] = None ) -> tuple[ vtkUnstructuredGrid, dict[ str, Any ] ]: """Apply main mesh checks to a mesh. Args: diff --git a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py index d9982a0cf..9c9c83384 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/GenerateFractures.py @@ -211,10 +211,8 @@ def setOutputDataMode( self: Self, choice: int ) -> None: choice (int): 0 for ASCII, 1 for binary. """ if choice not in [ 0, 1 ]: - self.logger.error( - f"setOutputDataMode: Please choose either 0 for {OUTPUT_BINARY_MODE_VALUES[0]} or 1 for" - f" {OUTPUT_BINARY_MODE_VALUES[1]}, not '{choice}'." - ) + self.logger.error( f"setOutputDataMode: Please choose either 0 for {OUTPUT_BINARY_MODE_VALUES[0]} or 1 for" + f" {OUTPUT_BINARY_MODE_VALUES[1]}, not '{choice}'." ) else: self.allOptions[ OUTPUT_BINARY_MODE ] = OUTPUT_BINARY_MODE_VALUES[ choice ] @@ -248,16 +246,14 @@ def writeMeshes( self: Self, filepath: str, isDataModeBinary: bool = True, canOv # Main function for standalone use -def generateFractures( - mesh: vtkUnstructuredGrid, - outputPath: str, - policy: int = 1, - fieldName: str = None, - fieldValues: str = None, - fracturesOutputDir: str = None, - outputDataMode: int = 0, - fracturesDataMode: int = 0 -) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: +def generateFractures( mesh: vtkUnstructuredGrid, + outputPath: str, + policy: int = 1, + fieldName: str = None, + fieldValues: str = None, + fracturesOutputDir: str = None, + outputDataMode: int = 0, + fracturesDataMode: int = 0 ) -> tuple[ vtkUnstructuredGrid, list[ vtkUnstructuredGrid ] ]: """Apply fracture generation to a mesh. Args: diff --git a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py index 0063c5770..e6bf682be 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/MeshDoctorFilterBase.py @@ -63,20 +63,21 @@ def __init__( ) -> None: """Initialize the base mesh doctor filter.""" # Check the 'mesh' input - if not isinstance(mesh, vtkUnstructuredGrid): - raise TypeError(f"Input 'mesh' must be a vtkUnstructuredGrid, but got {type(mesh).__name__}.") + if not isinstance( mesh, vtkUnstructuredGrid ): + raise TypeError( f"Input 'mesh' must be a vtkUnstructuredGrid, but got {type(mesh).__name__}." ) if mesh.GetNumberOfCells() == 0: - raise ValueError("Input 'mesh' cannot be empty.") + raise ValueError( "Input 'mesh' cannot be empty." ) # Check the 'filterName' input - if not isinstance(filterName, str): - raise TypeError(f"Input 'filterName' must be a string, but got {type(filterName).__name__}.") + if not isinstance( filterName, str ): + raise TypeError( f"Input 'filterName' must be a string, but got {type(filterName).__name__}." ) if not filterName.strip(): - raise ValueError("Input 'filterName' cannot be an empty or whitespace-only string.") + raise ValueError( "Input 'filterName' cannot be an empty or whitespace-only string." ) # Check the 'useExternalLogger' input - if not isinstance(useExternalLogger, bool): - raise TypeError(f"Input 'useExternalLogger' must be a boolean, but got {type(useExternalLogger).__name__}.") + if not isinstance( useExternalLogger, bool ): + raise TypeError( + f"Input 'useExternalLogger' must be a boolean, but got {type(useExternalLogger).__name__}." ) # Non-destructive behavior. # The filter should contain a COPY of the mesh, not the original object. @@ -172,14 +173,15 @@ def __init__( useExternalLogger (bool): Whether to use external logger. Defaults to False. """ # Check the 'filterName' input - if not isinstance(filterName, str): - raise TypeError(f"Input 'filterName' must be a string, but got {type(filterName).__name__}.") + if not isinstance( filterName, str ): + raise TypeError( f"Input 'filterName' must be a string, but got {type(filterName).__name__}." ) if not filterName.strip(): - raise ValueError("Input 'filterName' cannot be an empty or whitespace-only string.") + raise ValueError( "Input 'filterName' cannot be an empty or whitespace-only string." ) # Check the 'useExternalLogger' input - if not isinstance(useExternalLogger, bool): - raise TypeError(f"Input 'useExternalLogger' must be a boolean, but got {type(useExternalLogger).__name__}.") + if not isinstance( useExternalLogger, bool ): + raise TypeError( + f"Input 'useExternalLogger' must be a boolean, but got {type(useExternalLogger).__name__}." ) self.mesh: Union[ vtkUnstructuredGrid, None ] = None self.filterName: str = filterName diff --git a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py index 0d816861f..4ebedbc4f 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/NonConformal.py @@ -190,14 +190,12 @@ def _addNonConformalCellsArray( self: Self ) -> None: # Main function for standalone use -def nonConformal( - mesh: vtkUnstructuredGrid, - outputPath: str, - pointTolerance: float = 0.0, - faceTolerance: float = 0.0, - angleTolerance: float = 10.0, - writeNonConformalCells: bool = False -) -> tuple[ vtkUnstructuredGrid, list[ tuple[ int, int ] ] ]: +def nonConformal( mesh: vtkUnstructuredGrid, + outputPath: str, + pointTolerance: float = 0.0, + faceTolerance: float = 0.0, + angleTolerance: float = 10.0, + writeNonConformalCells: bool = False ) -> tuple[ vtkUnstructuredGrid, list[ tuple[ int, int ] ] ]: """Apply non-conformal detection to a mesh. Args: diff --git a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py index 962c5d860..46d1a348a 100644 --- a/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py +++ b/geos-mesh/src/geos/mesh/doctor/filters/SelfIntersectingElements.py @@ -145,11 +145,10 @@ def _addInvalidElementsArrays( self: Self ) -> None: # Main function for standalone use def selfIntersectingElements( - mesh: vtkUnstructuredGrid, - outputPath: str, - minDistance: float = 0.0, - writeInvalidElements: bool = False -) -> tuple[ vtkUnstructuredGrid, dict[ str, list[ int ] ] ]: + mesh: vtkUnstructuredGrid, + outputPath: str, + minDistance: float = 0.0, + writeInvalidElements: bool = False ) -> tuple[ vtkUnstructuredGrid, dict[ str, list[ int ] ] ]: """Apply self-intersecting elements detection to a mesh. Args: 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 a06ee0f75..958a91b38 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 @@ -38,7 +38,7 @@ def logger_results( logger, nodes_buckets: list[ tuple[ int ] ], wrong_support_e """ # Accounts for external logging object that would not contain 'results' attribute log_method = logger.info - if hasattr(logger, 'results'): + if hasattr( logger, 'results' ): log_method = logger.results all_collocated_nodes: list[ int ] = [] @@ -58,7 +58,6 @@ def logger_results( logger, nodes_buckets: list[ tuple[ int ] ], wrong_support_e if wrong_support_elements: tmp: str = ", ".join( map( str, wrong_support_elements ) ) - log_method( - f"You have {len(wrong_support_elements)} elements with duplicated support nodes.\n" + tmp ) + log_method( f"You have {len(wrong_support_elements)} elements with duplicated support nodes.\n" + tmp ) else: log_method( "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 db8bad93d..f52ee1f5a 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 @@ -43,7 +43,7 @@ def logger_results( logger, element_volumes: list[ tuple[ int, float ] ] ) -> No """ # Accounts for external logging object that would not contain 'results' attribute log_method = logger.info - if hasattr(logger, 'results'): + if hasattr( logger, 'results' ): log_method = logger.results log_method( "Elements index | Volumes calculated" ) 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 89d567eda..a2784ee4f 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 @@ -59,7 +59,7 @@ def logger_results( logger, non_conformal_cells: list[ tuple[ int, int ] ] ) -> """ # Accounts for external logging object that would not contain 'results' attribute log_method = logger.info - if hasattr(logger, 'results'): + if hasattr( logger, 'results' ): log_method = logger.results unique_cells: list[ int ] = [] 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 6e838c2cc..d7c55b704 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 @@ -24,15 +24,13 @@ def convert( parsed_options ) -> Options: 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( - '--' + __MIN_DISTANCE, - type=float, - required=False, - metavar=__MIN_DISTANCE_DEFAULT, - default=__MIN_DISTANCE_DEFAULT, - help=( "[float]: The minimum distance in the computation." - f" Defaults to your machine precision {__MIN_DISTANCE_DEFAULT}." ) - ) + p.add_argument( '--' + __MIN_DISTANCE, + type=float, + required=False, + metavar=__MIN_DISTANCE_DEFAULT, + default=__MIN_DISTANCE_DEFAULT, + help=( "[float]: The minimum distance in the computation." + f" Defaults to your machine precision {__MIN_DISTANCE_DEFAULT}." ) ) def display_results( options: Options, result: Result ): @@ -49,7 +47,7 @@ def logger_results( logger, invalid_cell_ids ) -> None: """ # Accounts for external logging object that would not contain 'results' attribute log_method = logger.info - if hasattr(logger, 'results'): + if hasattr( logger, 'results' ): log_method = logger.results # Human-readable descriptions for each error type @@ -67,6 +65,6 @@ def logger_results( logger, invalid_cell_ids ) -> None: # Log results for each error type that has invalid elements for error_type, invalid_ids in invalid_cell_ids.items(): if invalid_ids: - description = error_descriptions.get(error_type, f'elements with {error_type}') - log_method(f"You have {len(invalid_ids)} {description}.") - log_method("The elements indices are:\n" + ", ".join(map(str, invalid_ids))) + description = error_descriptions.get( error_type, f'elements with {error_type}' ) + log_method( f"You have {len(invalid_ids)} {description}." ) + log_method( "The elements indices are:\n" + ", ".join( map( str, invalid_ids ) ) ) 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 b24756b1a..567f8158a 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 @@ -41,8 +41,7 @@ def display_results( options: Options, result: Result ): logger_results( setup_logger, result.unsupported_polyhedron_elements, result.unsupported_std_elements_types ) -def logger_results( logger, - unsupported_polyhedron_elements: frozenset[ int ], +def logger_results( logger, unsupported_polyhedron_elements: frozenset[ int ], unsupported_std_elements_types: list[ str ] ) -> None: """Log the results of the supported elements check. @@ -53,7 +52,7 @@ def logger_results( logger, """ # Accounts for external logging object that would not contain 'results' attribute log_method = logger.info - if hasattr(logger, 'results'): + if hasattr( logger, 'results' ): log_method = logger.results if unsupported_polyhedron_elements: @@ -65,6 +64,6 @@ def logger_results( logger, log_method( "All the polyhedra (if any) can be converted to supported elements." ) if unsupported_std_elements_types: log_method( "There are unsupported vtk standard element types. The list of those vtk types is" - f" {tuple(sorted(unsupported_std_elements_types))}.") + f" {tuple(sorted(unsupported_std_elements_types))}." ) else: log_method( "All the standard vtk element types (if any) are supported." ) diff --git a/geos-mesh/src/geos/mesh/io/vtkIO.py b/geos-mesh/src/geos/mesh/io/vtkIO.py index 9fdb626ad..c325f110e 100644 --- a/geos-mesh/src/geos/mesh/io/vtkIO.py +++ b/geos-mesh/src/geos/mesh/io/vtkIO.py @@ -86,8 +86,7 @@ def _read_data( filepath: str, reader_class: VtkReaderClass ) -> Optional[ vtkPo # detect a failed read, as GetOutput() can return a default empty object on failure. if hasattr( reader, 'GetErrorCode' ) and reader.GetErrorCode() != 0: io_logger.warning( - f"VTK reader {reader_class.__name__} reported an error code after attempting to read '{filepath}'." - ) + f"VTK reader {reader_class.__name__} reported an error code after attempting to read '{filepath}'." ) return None output = reader.GetOutput() @@ -239,7 +238,7 @@ def write_mesh( mesh: vtkPointSet, vtk_output: VtkOutput, can_overwrite: bool = if not writer_class: raise ValueError( f"Writing to extension '{output_path.suffix}' is not supported." ) - success_code = _write_data( mesh, writer_class, str(output_path), vtk_output.is_data_mode_binary ) + success_code = _write_data( mesh, writer_class, str( output_path ), vtk_output.is_data_mode_binary ) if not success_code: raise RuntimeError( f"VTK writer failed to write file '{output_path}'." ) diff --git a/geos-mesh/tests/test_Checks.py b/geos-mesh/tests/test_Checks.py index 06cc25f54..458f496e2 100644 --- a/geos-mesh/tests/test_Checks.py +++ b/geos-mesh/tests/test_Checks.py @@ -15,8 +15,10 @@ @pytest.fixture( scope="module" ) def simple_hex_mesh() -> vtkUnstructuredGrid: """Fixture for a simple hexahedron mesh.""" - return createSingleCellMesh( VTK_HEXAHEDRON, np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], - [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ) ) + return createSingleCellMesh( + VTK_HEXAHEDRON, + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], + [ 0, 1, 1 ] ] ) ) @pytest.fixture( scope="module" ) @@ -28,21 +30,19 @@ def simple_tetra_mesh() -> vtkUnstructuredGrid: @pytest.fixture( scope="module" ) def mixed_quality_mesh() -> vtkUnstructuredGrid: """Fixture for a mesh with elements of varying quality.""" - return createMultiCellMesh( - [ VTK_TETRA, VTK_TETRA ], - [ np.array( [ [ 0.0, 0.0, 0.0 ], [ 1.0, 0.0, 0.0 ], [ 0.5, 1.0, 0.0 ], [ 0.5, 0.5, 1.0 ] ] ), - np.array( [ [ 2.0, 0.0, 0.0 ], [ 5.0, 0.0, 0.0 ], [ 3.5, 0.1, 0.0 ], [ 3.5, 0.05, 0.05 ] ] ) ] - ) + return createMultiCellMesh( [ VTK_TETRA, VTK_TETRA ], [ + np.array( [ [ 0.0, 0.0, 0.0 ], [ 1.0, 0.0, 0.0 ], [ 0.5, 1.0, 0.0 ], [ 0.5, 0.5, 1.0 ] ] ), + np.array( [ [ 2.0, 0.0, 0.0 ], [ 5.0, 0.0, 0.0 ], [ 3.5, 0.1, 0.0 ], [ 3.5, 0.05, 0.05 ] ] ) + ] ) @pytest.fixture( scope="module" ) def mesh_with_collocated_nodes() -> vtkUnstructuredGrid: """Fixture for a mesh with collocated (duplicate) nodes.""" - return createMultiCellMesh( - [ VTK_TETRA, VTK_TETRA ], - [ np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ] ), - np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ] ) ] - ) + return createMultiCellMesh( [ VTK_TETRA, VTK_TETRA ], [ + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ] ), + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ] ) + ] ) class TestChecksBase: @@ -399,7 +399,10 @@ def test_build_options_with_defaults( self, simple_hex_mesh ): """Test building options with default parameters.""" mock_options_cls = MagicMock( return_value=MagicMock() ) mock_check_features = { - "test_check": MagicMock( default_params={ "param1": 1.0, "param2": "default" }, + "test_check": MagicMock( default_params={ + "param1": 1.0, + "param2": "default" + }, options_cls=mock_options_cls ) } diff --git a/geos-mesh/tests/test_CollocatedNodes.py b/geos-mesh/tests/test_CollocatedNodes.py index decebe203..f19fdee91 100644 --- a/geos-mesh/tests/test_CollocatedNodes.py +++ b/geos-mesh/tests/test_CollocatedNodes.py @@ -14,11 +14,12 @@ @pytest.fixture( scope="module" ) def mesh_with_collocated_nodes(): """Fixture for a mesh with exactly duplicated and nearly collocated nodes.""" - mesh = createMultiCellMesh( [ VTK_HEXAHEDRON, VTK_HEXAHEDRON ], - [ np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], - [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ), - np.array( [ [ 1, 0, 0 ], [ 2, 0, 0 ], [ 2, 1, 0 ], [ 1, 1, 0 ], - [ 1, 0, 1 ], [ 2, 0, 1 ], [ 2, 1, 1 ], [ 1, 1, 1 ] ] ) ] ) + mesh = createMultiCellMesh( [ VTK_HEXAHEDRON, VTK_HEXAHEDRON ], [ + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], + [ 0, 1, 1 ] ] ), + np.array( [ [ 1, 0, 0 ], [ 2, 0, 0 ], [ 2, 1, 0 ], [ 1, 1, 0 ], [ 1, 0, 1 ], [ 2, 0, 1 ], [ 2, 1, 1 ], + [ 1, 1, 1 ] ] ) + ] ) points = mesh.GetPoints() # Add nodes to create collocated situations: @@ -35,19 +36,21 @@ def mesh_with_collocated_nodes(): @pytest.fixture( scope="module" ) def mesh_with_wrong_support() -> vtkUnstructuredGrid: """Fixture for a mesh containing a cell with repeated node indices.""" - return createMultiCellMesh( [ VTK_TRIANGLE, VTK_TRIANGLE ], - [ np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 0, 0 ] ] ), - np.array( [ [ 1, 0, 0 ], [ 0, 1, 0 ], [ 1, 1, 0 ] ] ) ] ) + return createMultiCellMesh( [ VTK_TRIANGLE, VTK_TRIANGLE ], [ + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 0, 0 ] ] ), + np.array( [ [ 1, 0, 0 ], [ 0, 1, 0 ], [ 1, 1, 0 ] ] ) + ] ) @pytest.fixture( scope="module" ) def clean_mesh() -> vtkUnstructuredGrid: """Fixture for a simple, valid mesh with no issues.""" - return createMultiCellMesh( [ VTK_HEXAHEDRON, VTK_HEXAHEDRON ], - [ np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], - [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ), - np.array( [ [ 1, 0, 0 ], [ 2, 0, 0 ], [ 2, 1, 0 ], [ 1, 1, 0 ], - [ 1, 0, 1 ], [ 2, 0, 1 ], [ 2, 1, 1 ], [ 1, 1, 1 ] ] ) ] ) + return createMultiCellMesh( [ VTK_HEXAHEDRON, VTK_HEXAHEDRON ], [ + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], + [ 0, 1, 1 ] ] ), + np.array( [ [ 1, 0, 0 ], [ 2, 0, 0 ], [ 2, 1, 0 ], [ 1, 1, 0 ], [ 1, 0, 1 ], [ 2, 0, 1 ], [ 2, 1, 1 ], + [ 1, 1, 1 ] ] ) + ] ) def test_filter_on_clean_mesh( clean_mesh ): diff --git a/geos-mesh/tests/test_ElementVolumes.py b/geos-mesh/tests/test_ElementVolumes.py index f9636c24f..279896711 100644 --- a/geos-mesh/tests/test_ElementVolumes.py +++ b/geos-mesh/tests/test_ElementVolumes.py @@ -14,11 +14,12 @@ @pytest.fixture( scope="module" ) def simple_hex_mesh(): """Fixture for a simple hexahedron mesh with known volumes.""" - return createMultiCellMesh( [ VTK_HEXAHEDRON, VTK_HEXAHEDRON ], - [ np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], - [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ), - np.array( [ [ 1, 0, 0 ], [ 2, 0, 0 ], [ 2, 1, 0 ], [ 1, 1, 0 ], - [ 1, 0, 1 ], [ 2, 0, 1 ], [ 2, 1, 1 ], [ 1, 1, 1 ] ] ) ] ) + return createMultiCellMesh( [ VTK_HEXAHEDRON, VTK_HEXAHEDRON ], [ + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], + [ 0, 1, 1 ] ] ), + np.array( [ [ 1, 0, 0 ], [ 2, 0, 0 ], [ 2, 1, 0 ], [ 1, 1, 0 ], [ 1, 0, 1 ], [ 2, 0, 1 ], [ 2, 1, 1 ], + [ 1, 1, 1 ] ] ) + ] ) @pytest.fixture( scope="module" ) diff --git a/geos-mesh/tests/test_MeshDoctorFilterBase.py b/geos-mesh/tests/test_MeshDoctorFilterBase.py index 41aebafec..9fb1f16c3 100644 --- a/geos-mesh/tests/test_MeshDoctorFilterBase.py +++ b/geos-mesh/tests/test_MeshDoctorFilterBase.py @@ -200,7 +200,8 @@ def test_set_logger_handler_without_existing_handlers( self, single_tetrahedron_ def test_set_logger_handler_with_existing_handlers( self, single_tetrahedron_mesh, caplog ): """Test setting logger handler when handlers already exist.""" - filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, "TestFilter_with_handlers", + filter_instance = ConcreteFilterForTesting( single_tetrahedron_mesh, + "TestFilter_with_handlers", useExternalLogger=True ) filter_instance.logger.addHandler( logging.NullHandler() ) diff --git a/geos-mesh/tests/test_SupportedElements.py b/geos-mesh/tests/test_SupportedElements.py index 203afe768..01013f7d7 100644 --- a/geos-mesh/tests/test_SupportedElements.py +++ b/geos-mesh/tests/test_SupportedElements.py @@ -17,9 +17,10 @@ @pytest.fixture def good_mesh() -> vtkUnstructuredGrid: """Creates a mesh with only supported element types.""" - return createSingleCellMesh( VTK_HEXAHEDRON, - np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ], - [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ) ) + return createSingleCellMesh( + VTK_HEXAHEDRON, + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], + [ 0, 1, 1 ] ] ) ) @pytest.fixture @@ -27,12 +28,12 @@ def mesh_with_unsupported_std_type(): """Creates a mesh containing an unsupported standard element type (VTK_QUADRATIC_TETRA).""" # Check that our chosen unsupported type is actually not in the supported list assert VTK_QUADRATIC_TETRA not in supported_cell_types - return createMultiCellMesh( [ VTK_HEXAHEDRON, VTK_QUADRATIC_TETRA ], - [ np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], - [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ), - np.array( [ [ 2, 0, 0 ], [ 3, 0, 0 ], [ 2.5, 1, 0 ], [ 2.5, 0.5, 1 ], [ 2.5, 0, 0 ], - [ 2.75, 0.5, 0 ], [ 2.25, 0.5, 0 ], [ 2.75, 0.25, 0.5 ], - [ 2.5, 0.75, 0.5 ], [ 2.25, 0.25, 0.5 ] ] ) ] ) + return createMultiCellMesh( [ VTK_HEXAHEDRON, VTK_QUADRATIC_TETRA ], [ + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], + [ 0, 1, 1 ] ] ), + np.array( [ [ 2, 0, 0 ], [ 3, 0, 0 ], [ 2.5, 1, 0 ], [ 2.5, 0.5, 1 ], [ 2.5, 0, 0 ], [ 2.75, 0.5, 0 ], + [ 2.25, 0.5, 0 ], [ 2.75, 0.25, 0.5 ], [ 2.5, 0.75, 0.5 ], [ 2.25, 0.25, 0.5 ] ] ) + ] ) @pytest.fixture @@ -42,23 +43,27 @@ def mesh_with_unsupported_polyhedron() -> vtkUnstructuredGrid: The non-convertible one is a triangular bipyramid, which has 6 faces like a hex, but a different face-connectivity graph. """ - points = [ [ 0, 0, 0 ], # points for the hex - [ 1, 0, 0 ], - [ 1, 1, 0 ], - [ 0, 1, 0 ], - [ 0, 0, 1 ], - [ 1, 0, 1 ], - [ 1, 1, 1 ], - [ 0, 1, 1 ], - [ 2, 0, 1 ], # other points to add for bipyramid - [ 3, -1, 0 ], - [ 3, 1, 0 ], - [ 1, 0, 0 ], - [ 2, 0, -1 ] ] + points = [ + [ 0, 0, 0 ], # points for the hex + [ 1, 0, 0 ], + [ 1, 1, 0 ], + [ 0, 1, 0 ], + [ 0, 0, 1 ], + [ 1, 0, 1 ], + [ 1, 1, 1 ], + [ 0, 1, 1 ], + [ 2, 0, 1 ], # other points to add for bipyramid + [ 3, -1, 0 ], + [ 3, 1, 0 ], + [ 1, 0, 0 ], + [ 2, 0, -1 ] + ] mesh: vtkUnstructuredGrid = createSingleCellMesh( - VTK_HEXAHEDRON, np.array( [ points[ 0 ], points[ 1 ], points[ 2 ], points[ 3 ], points[ 4 ], points[ 5 ], - points[ 6 ], points[ 7 ] ] ) ) + VTK_HEXAHEDRON, + np.array( + [ points[ 0 ], points[ 1 ], points[ 2 ], points[ 3 ], points[ 4 ], points[ 5 ], points[ 6 ], + points[ 7 ] ] ) ) # Face stream for the triangular bipyramid (6 faces, 5 points) # Format: [num_faces, num_pts_face1, p1, p2, ..., num_pts_face2, p1, p2, ...] diff --git a/geos-mesh/tests/test_generate_fractures.py b/geos-mesh/tests/test_generate_fractures.py index 7d26c5ef1..cd0516274 100644 --- a/geos-mesh/tests/test_generate_fractures.py +++ b/geos-mesh/tests/test_generate_fractures.py @@ -398,8 +398,10 @@ def test_generate_fracture_filters_basic( test_case: TestCase, tmp_path ): def test_generate_fractures_filter_setters(): """Test the setter methods of GenerateFractures filter.""" - mesh = createSingleCellMesh( VTK_HEXAHEDRON, np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], - [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ) ) + mesh = createSingleCellMesh( + VTK_HEXAHEDRON, + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], + [ 0, 1, 1 ] ] ) ) createConstantAttributeDataSet( dataSet=mesh, listValues=[ 1 ], attributeName="test_field", onPoints=False ) # Create filter instance filterInstance = GenerateFractures( mesh ) @@ -441,8 +443,10 @@ def test_generate_fractures_filter_setters(): def test_generate_fractures_filter_with_global_ids( tmp_path ): """Test that filter fails when mesh contains global IDs.""" # Create a simple test mesh - mesh = createSingleCellMesh( VTK_HEXAHEDRON, np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], - [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], [ 0, 1, 1 ] ] ) ) + mesh = createSingleCellMesh( + VTK_HEXAHEDRON, + np.array( [ [ 0, 0, 0 ], [ 1, 0, 0 ], [ 1, 1, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ], + [ 0, 1, 1 ] ] ) ) createConstantAttributeDataSet( dataSet=mesh, listValues=[ 1 ], attributeName="GLOBAL_IDS_POINTS", onPoints=False ) fractures_dir = tmp_path / "fractures" diff --git a/geos-mesh/tests/test_vtkIO.py b/geos-mesh/tests/test_vtkIO.py index 5e824ad73..56ab678a0 100644 --- a/geos-mesh/tests/test_vtkIO.py +++ b/geos-mesh/tests/test_vtkIO.py @@ -3,10 +3,8 @@ from vtkmodules.vtkCommonCore import vtkPoints from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, vtkStructuredGrid, VTK_TETRA, VTK_HEXAHEDRON from geos.mesh.utils.genericHelpers import createSingleCellMesh -from geos.mesh.io.vtkIO import ( - VtkFormat, VtkOutput, read_mesh, read_unstructured_grid, write_mesh, - READER_MAP, WRITER_MAP -) +from geos.mesh.io.vtkIO import ( VtkFormat, VtkOutput, read_mesh, read_unstructured_grid, write_mesh, READER_MAP, + WRITER_MAP ) __doc__ = """ Test module for vtkIO module.