-
Notifications
You must be signed in to change notification settings - Fork 0
feat: translate and rotate domains to clip the main frame #130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
4671570
First working version + some debug
jacquesn7 f70a9be
get rid of np.matrix
jacquesn7 faeb6a4
Using PV Transform filter
jacquesn7 dcea142
style
jacquesn7 0635ef1
more testing
jacquesn7 652dd7e
lint and style
jacquesn7 a801816
yapf-ing
jacquesn7 5b051e8
name conv
jafranc fa66f4b
change for full vtkTransform inheritance
jafranc 8db4114
loglevel changes and clean up
jafranc 29d549f
file's header
jafranc 0a2f1cb
using vtkLandmarkTransform and vtkOBBTree
jafranc 9be24e9
some doc and accessible tests
jafranc 29ba247
ruff and yapf
jafranc e46b174
docs
jafranc 9493451
fix doc and rename module
jafranc 43d7e25
Fix test and all quadrant
jafranc 6a5d546
Adressing Romain's comments
jafranc ce7b9b5
Some MaJ + conftest use
jafranc 8439b24
minors
jafranc e7afe9c
Merge branch 'main' into jafranc/feat/translateAndRotate
jafranc 20eec19
Logger
jafranc a75723f
yapf
jafranc 7f7fea6
stars not good for docs
jafranc af9f31f
apply Romain's comments
jafranc 4cdf4d9
fin comm test + mypy/ruff/yapf
jafranc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,310 @@ | ||
| # SPDX-License-Identifier: Apache 2.0 | ||
| # SPDX-FileCopyrightText: Copyright 2023-2025 TotalEnergies | ||
| # SPDX-FileContributor: Jacques Franc | ||
|
|
||
| from vtkmodules.numpy_interface import dataset_adapter as dsa | ||
| from vtkmodules.vtkCommonCore import vtkPoints | ||
| from vtkmodules.vtkCommonMath import vtkMatrix4x4 | ||
| from vtkmodules.vtkFiltersGeneral import vtkOBBTree | ||
| from vtkmodules.vtkFiltersGeometry import vtkDataSetSurfaceFilter | ||
| from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, vtkMultiBlockDataSet, vtkDataObjectTreeIterator, vtkPolyData | ||
| from vtkmodules.vtkCommonTransforms import vtkLandmarkTransform | ||
| from vtkmodules.vtkFiltersGeneral import vtkTransformFilter | ||
|
|
||
| from geos.utils.Logger import logging, Logger, getLogger | ||
|
|
||
| from geos.mesh.utils.genericHelpers import getMultiBlockBounds | ||
|
|
||
| import numpy as np | ||
| import numpy.typing as npt | ||
|
|
||
| from typing import Tuple | ||
|
|
||
| __doc__ = """ | ||
| Module to clip a mesh to the main frame using rigid body transformation. | ||
|
|
||
| Methods include: | ||
| - ClipToMainFrameElement class to compute the transformation | ||
| - ClipToMainFrame class to apply the transformation to a mesh | ||
|
|
||
| To use it: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| from geos.mesh.processing.ClipToMainFrame import ClipToMainFrame | ||
|
|
||
| # Filter inputs. | ||
| multiBlockDataSet: vtkMultiBlockDataSet | ||
| # Optional Inputs | ||
| speHandler : bool | ||
|
|
||
| # Instantiate the filter. | ||
| filter: ClipToMainFrame = ClipToMainFrame() | ||
| filter.SetInputData( multiBlockDataSet ) | ||
|
|
||
| # Set the handler of yours (only if speHandler is True). | ||
| yourHandler: logging.Handler | ||
| filter.setLoggerHandler( yourHandler ) | ||
|
|
||
| # Do calculations. | ||
| filter.ComputeTransform() | ||
| filter.Update() | ||
| output: vtkMultiBlockDataSet = filter.GetOutput() | ||
|
|
||
| """ | ||
|
|
||
|
|
||
| class ClipToMainFrameElement( vtkLandmarkTransform ): | ||
|
|
||
| sourcePts: vtkPoints | ||
| targetPts: vtkPoints | ||
| mesh: vtkUnstructuredGrid | ||
|
|
||
| def __init__( self, mesh: vtkUnstructuredGrid ) -> None: | ||
| """Clip mesh to main frame. | ||
|
|
||
| Args: | ||
| mesh (vtkUnstructuredGrid): Mesh to clip. | ||
| """ | ||
| super().__init__() | ||
| self.mesh = mesh | ||
|
|
||
| def Update( self ) -> None: | ||
| """Update the transformation.""" | ||
| self.sourcePts, self.targetPts = self.__getFramePoints( self.__getOBBTree( self.mesh ) ) | ||
| self.SetSourceLandmarks( self.sourcePts ) | ||
| self.SetTargetLandmarks( self.targetPts ) | ||
| self.SetModeToRigidBody() | ||
| super().Update() | ||
|
|
||
| def __str__( self ) -> str: | ||
| """String representation of the transformation.""" | ||
| return super().__str__() + f"\nSource points: {self.sourcePts}" \ | ||
| + f"\nTarget points: {self.targetPts}" \ | ||
| + f"\nAngle-Axis: {self.__getAngleAxis()}" \ | ||
| + f"\nTranslation: {self.__getTranslation()}" | ||
|
|
||
| def __getAngleAxis( self ) -> tuple[ float, npt.NDArray[ np.double ] ]: | ||
| """Get the angle and axis of the rotation. | ||
|
|
||
| tuple[float, npt.NDArray[np.double]]: Angle in degrees and axis of rotation. | ||
| """ | ||
| matrix: vtkMatrix4x4 = self.GetMatrix() | ||
| angle: np.double = np.arccos( | ||
| ( matrix.GetElement( 0, 0 ) + matrix.GetElement( 1, 1 ) + matrix.GetElement( 2, 2 ) - 1 ) / 2 ) | ||
| if angle == 0: | ||
| return 0.0, np.array( [ 1.0, 0.0, 0.0 ] ) | ||
| rx: float = matrix.GetElement( 2, 1 ) - matrix.GetElement( 1, 2 ) | ||
| ry: float = matrix.GetElement( 0, 2 ) - matrix.GetElement( 2, 0 ) | ||
| rz: float = matrix.GetElement( 1, 0 ) - matrix.GetElement( 0, 1 ) | ||
| r = np.array( [ rx, ry, rz ] ) | ||
jafranc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| r /= np.linalg.norm( r ) | ||
| return np.degrees( angle ), r | ||
|
|
||
| def __getTranslation( self ) -> npt.NDArray[ np.double ]: | ||
| """Get the translation vector. | ||
|
|
||
| Returns: | ||
| npt.NDArray[ np.double ]: The translation vector. | ||
| """ | ||
| matrix: vtkMatrix4x4 = self.GetMatrix() | ||
| return np.array( [ matrix.GetElement( 0, 3 ), matrix.GetElement( 1, 3 ), matrix.GetElement( 2, 3 ) ] ) | ||
|
|
||
| def __getOBBTree( self, mesh: vtkUnstructuredGrid ) -> vtkPoints: | ||
| """Get the OBB tree of the mesh. | ||
|
|
||
| Args: | ||
| mesh (vtkUnstructuredGrid): Mesh to get the OBB tree from. | ||
|
|
||
| Returns: | ||
| vtkPoints: Points from the 0-level OBB tree of the mesh. Fallback on Axis Aligned Bounding Box | ||
| """ | ||
| OBBTree = vtkOBBTree() | ||
| surfFilter = vtkDataSetSurfaceFilter() | ||
jafranc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| surfFilter.SetInputData( mesh ) | ||
| surfFilter.Update() | ||
| OBBTree.SetDataSet( surfFilter.GetOutput() ) | ||
| OBBTree.BuildLocator() | ||
| pdata = vtkPolyData() | ||
jafranc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| OBBTree.GenerateRepresentation( 0, pdata ) | ||
| # at level 0 this should return 8 corners of the bounding box or fallback on AABB | ||
| if pdata.GetNumberOfPoints() < 3: | ||
| return self.__allpoints( mesh.GetBounds() ) | ||
|
|
||
| return pdata.GetPoints() | ||
|
|
||
| def __allpoints( self, bounds: tuple[ float, float, float, float, float, float ] ) -> vtkPoints: | ||
| """Get the 8 corners of the bounding box. | ||
|
|
||
| Args: | ||
| bounds (tuple[float, float, float, float, float, float]): Bounding box. | ||
|
|
||
| Returns: | ||
| vtkPoints: 8 corners of the bounding box. | ||
| """ | ||
| pts = vtkPoints() | ||
jafranc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| pts.SetNumberOfPoints( 8 ) | ||
| pts.SetPoint( 0, [ bounds[ 0 ], bounds[ 2 ], bounds[ 4 ] ] ) | ||
| pts.SetPoint( 1, [ bounds[ 1 ], bounds[ 2 ], bounds[ 4 ] ] ) | ||
| pts.SetPoint( 2, [ bounds[ 1 ], bounds[ 3 ], bounds[ 4 ] ] ) | ||
| pts.SetPoint( 3, [ bounds[ 0 ], bounds[ 3 ], bounds[ 4 ] ] ) | ||
| pts.SetPoint( 4, [ bounds[ 0 ], bounds[ 2 ], bounds[ 5 ] ] ) | ||
| pts.SetPoint( 5, [ bounds[ 1 ], bounds[ 2 ], bounds[ 5 ] ] ) | ||
| pts.SetPoint( 6, [ bounds[ 1 ], bounds[ 3 ], bounds[ 5 ] ] ) | ||
| pts.SetPoint( 7, [ bounds[ 0 ], bounds[ 3 ], bounds[ 5 ] ] ) | ||
| return pts | ||
|
|
||
| def __getFramePoints( self, vpts: vtkPoints ) -> tuple[ vtkPoints, vtkPoints ]: | ||
jafranc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """Get the source and target points for the transformation. | ||
|
|
||
| Args: | ||
| vpts (vtkPoints): Points to transform. | ||
|
|
||
| Returns: | ||
| tuple[vtkPoints, vtkPoints]: Source and target points for the transformation. | ||
| """ | ||
| pts: npt.NDArray[ np.double ] = dsa.numpy_support.vtk_to_numpy( vpts.GetData() ) | ||
| #translate pts so they always lie on the -z,-y,-x quadrant | ||
| off: npt.NDArray[ np.double ] = np.asarray( [ | ||
| -2 * np.amax( np.abs( pts[ :, 0 ] ) ), -2 * np.amax( np.abs( pts[ :, 1 ] ) ), | ||
| -2 * np.amax( np.abs( pts[ :, 2 ] ) ) | ||
| ] ) | ||
| pts += off | ||
| further_ix: np.int_ = np.argmax( np.linalg.norm( | ||
| pts, axis=1 ) ) # by default take the min point furthest from origin | ||
| org: npt.NDArray = pts[ further_ix, : ] | ||
|
|
||
| # find 3 orthogonal vectors | ||
| # we assume points are on a box | ||
| dist_indexes: npt.NDArray[ np.int_ ] = np.argsort( np.linalg.norm( pts - org, axis=1 ) ) | ||
| # find u,v,w | ||
| v1: npt.NDArray[ np.double ] = pts[ dist_indexes[ 1 ], : ] - org | ||
| v2: npt.NDArray[ np.double ] = pts[ dist_indexes[ 2 ], : ] - org | ||
| v1 /= np.linalg.norm( v1 ) | ||
| v2 /= np.linalg.norm( v2 ) | ||
| if np.abs( v1[ 0 ] ) > np.abs( v2[ 0 ] ): | ||
| v1, v2 = v2, v1 | ||
|
|
||
| # ensure orthogonality | ||
| v2 -= np.dot( v2, v1 ) * v1 | ||
| v2 /= np.linalg.norm( v2 ) | ||
| v3: npt.NDArray[ np.double ] = np.cross( v1, v2 ) | ||
| v3 /= np.linalg.norm( v3 ) | ||
|
|
||
| #reorder axis if v3 points downward | ||
| if v3[ 2 ] < 0: | ||
| v3 = -v3 | ||
| v1, v2 = v2, v1 | ||
|
|
||
| sourcePts = vtkPoints() | ||
| sourcePts.SetNumberOfPoints( 4 ) | ||
| sourcePts.SetPoint( 0, list( org - off ) ) | ||
| sourcePts.SetPoint( 1, list( v1 + org - off ) ) | ||
| sourcePts.SetPoint( 2, list( v2 + org - off ) ) | ||
| sourcePts.SetPoint( 3, list( v3 + org - off ) ) | ||
|
|
||
| targetPts = vtkPoints() | ||
| targetPts.SetNumberOfPoints( 4 ) | ||
| targetPts.SetPoint( 0, [ 0., 0., 0. ] ) | ||
| targetPts.SetPoint( 1, [ 1., 0., 0. ] ) | ||
| targetPts.SetPoint( 2, [ 0., 1., 0. ] ) | ||
| targetPts.SetPoint( 3, [ 0., 0., 1. ] ) | ||
|
|
||
| return ( sourcePts, targetPts ) | ||
|
|
||
|
|
||
| loggerTitle: str = "Clip mesh to main frame." | ||
|
|
||
|
|
||
| class ClipToMainFrame( vtkTransformFilter ): | ||
| """Filter to clip a mesh to the main frame using ClipToMainFrame class.""" | ||
|
|
||
| def __init__( self, speHandler: bool = False, **properties: str ) -> None: | ||
| """Initialize the ClipToMainFrame Filter with optional speHandler args and forwarding properties to main class. | ||
|
|
||
| Args: | ||
| speHandler (bool, optional): True to use a specific handler, False to use the internal handler. | ||
| Defaults to False. | ||
| properties (kwargs): kwargs forwarded to vtkTransformFilter. | ||
| """ | ||
| super().__init__( **properties ) | ||
| # Logger. | ||
| self.logger: Logger | ||
| if not speHandler: | ||
| self.logger = getLogger( loggerTitle, True ) | ||
| else: | ||
| self.logger = logging.getLogger( loggerTitle ) | ||
| self.logger.setLevel( logging.INFO ) | ||
|
|
||
| def ComputeTransform( self ) -> None: | ||
| """Update the transformation.""" | ||
| # dispatch to ClipToMainFrame depending on input type | ||
| if isinstance( self.GetInput(), vtkMultiBlockDataSet ): | ||
| #locate reference point | ||
| try: | ||
| idBlock = self.__locate_reference_point( self.GetInput() ) | ||
| except IndexError: | ||
| self.logger.error( "Reference point is not in the domain" ) | ||
|
|
||
| clip = ClipToMainFrameElement( self.GetInput().GetDataSet( idBlock ) ) | ||
| else: | ||
| clip = ClipToMainFrameElement( self.GetInput() ) | ||
|
|
||
| clip.Update() | ||
| self.SetTransform( clip ) | ||
|
|
||
| def SetLoggerHandler( self, handler: logging.Handler ) -> None: | ||
| """Set a specific handler for the filter logger. In this filter 4 log levels are use, .info, .error, .warning and .critical, be sure to have at least the same 4 levels. | ||
|
|
||
| Args: | ||
| handler (logging.Handler): The handler to add. | ||
| """ | ||
| if not self.logger.hasHandlers(): | ||
| self.logger.addHandler( handler ) | ||
| else: | ||
| self.logger.warning( | ||
| "The logger already has an handler, to use yours set the argument 'speHandler' to True during the filter initialization." | ||
| ) | ||
|
|
||
| def __locate_reference_point( self, multiBlockDataSet: vtkMultiBlockDataSet ) -> int: | ||
| """Locate the block to use as reference for the transformation. | ||
|
|
||
| Args: | ||
| multiBlockDataSet (vtkMultiBlockDataSet): Input multiblock mesh. | ||
|
|
||
| Returns: | ||
| int: Index of the block to use as reference. | ||
| """ | ||
|
|
||
| def __inside( pt: npt.NDArray[ np.double ], bounds: tuple[ float, float, float, float, float, float ] ) -> bool: | ||
| """Check if a point is inside a box. | ||
|
|
||
| Args: | ||
| pt (npt.NDArray[np.double]): Point to check. | ||
| bounds (tuple[float, float, float, float, float, float]): Bounding box. | ||
|
|
||
| Returns: | ||
| bool: True if the point is inside the bounding box, False otherwise. | ||
| """ | ||
| self.logger.info( f"Checking if point {pt} is inside bounds {bounds}" ) | ||
jafranc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return ( pt[ 0 ] >= bounds[ 0 ] and pt[ 0 ] <= bounds[ 1 ] and pt[ 1 ] >= bounds[ 2 ] | ||
| and pt[ 1 ] <= bounds[ 3 ] and pt[ 2 ] >= bounds[ 4 ] and pt[ 2 ] <= bounds[ 5 ] ) | ||
|
|
||
| DOIterator: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() | ||
| DOIterator.SetDataSet( multiBlockDataSet ) | ||
| DOIterator.VisitOnlyLeavesOn() | ||
| DOIterator.GoToFirstItem() | ||
| xmin: float | ||
| ymin: float | ||
| zmin: float | ||
| xmin, _, ymin, _, zmin, _ = getMultiBlockBounds( multiBlockDataSet ) | ||
| while DOIterator.GetCurrentDataObject() is not None: | ||
| dataSet: vtkUnstructuredGrid = vtkUnstructuredGrid.SafeDownCast( DOIterator.GetCurrentDataObject() ) | ||
| bounds: Tuple[ float, float, float, float, float, float ] = dataSet.GetBounds() | ||
| #use the furthest bounds corner as reference point in the all negs quadrant | ||
| if __inside( np.asarray( [ xmin, ymin, zmin ] ), bounds ): | ||
| self.logger.info( f"Using block {DOIterator.GetCurrentFlatIndex()} as reference for transformation" ) | ||
| return DOIterator.GetCurrentFlatIndex() | ||
| DOIterator.GoToNextItem() | ||
|
|
||
| raise IndexError | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.