From 0ca3fe52c08a988494295ab331e0348e38199a47 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 24 Jun 2025 16:48:09 +0200 Subject: [PATCH 01/56] add a function to get the type of a vtk array --- geos-mesh/src/geos/mesh/utils/arrayHelpers.py | 27 +++++++++++++++---- geos-mesh/tests/test_arrayHelpers.py | 14 ++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index 3139d67f..fe3a8618 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -343,7 +343,7 @@ def isAttributeInObjectDataSet( object: vtkDataSet, attributeName: str, onPoints return bool( data.HasArray( attributeName ) ) -def getArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> npt.NDArray[ np.float64 ]: +def getArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> npt.NDArray[ any ]: """Return the numpy array corresponding to input attribute name in table. Args: @@ -355,12 +355,29 @@ def getArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) - Returns: ArrayLike[float]: the array corresponding to input attribute name. """ - array: vtkDoubleArray = getVtkArrayInObject( object, attributeName, onPoints ) - nparray: npt.NDArray[ np.float64 ] = vnp.vtk_to_numpy( array ) # type: ignore[no-untyped-call] + array: vtkDataArray = getVtkArrayInObject( object, attributeName, onPoints ) + nparray: npt.NDArray[ any ] = vnp.vtk_to_numpy( array ) # type: ignore[no-untyped-call] return nparray -def getVtkArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> vtkDoubleArray: +def getVtkArrayTypeInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> int: + """Return the type of the vtk array corrsponding to input attribute name in table. + + Args: + object (PointSet or UnstructuredGrid): input object. + attributeName (str): name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. + + Returns: + int: the type of the vtk array corrsponding to input attribute name. + """ + array: vtkDataArray = getVtkArrayInObject( object, attributeName, onPoints ) + vtkArrayType: int = array.GetDataType() + + return vtkArrayType + + +def getVtkArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> vtkDataArray: """Return the array corresponding to input attribute name in table. Args: @@ -370,7 +387,7 @@ def getVtkArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool on cells. Returns: - vtkDoubleArray: the vtk array corresponding to input attribute name. + vtkDataArray: the vtk array corresponding to input attribute name. """ assert isAttributeInObject( object, attributeName, onPoints ), f"{attributeName} is not in input object." return object.GetPointData().GetArray( attributeName ) if onPoints else object.GetCellData().GetArray( diff --git a/geos-mesh/tests/test_arrayHelpers.py b/geos-mesh/tests/test_arrayHelpers.py index 0a73ee99..b399b9a0 100644 --- a/geos-mesh/tests/test_arrayHelpers.py +++ b/geos-mesh/tests/test_arrayHelpers.py @@ -99,6 +99,20 @@ def test_getArrayInObject( request: pytest.FixtureRequest, arrayExpected: npt.ND assert ( obtained == expected ).all() +@pytest.mark.parametrize( "attributeName, onPoint", [ + ( "CellAttribute", False ), + ( "PointAttribute", True ), +] ) +def test_getVtkArrayTypeInObject( dataSetTest: vtkDataSet, attributeName: str, onPoint: bool ) -> None: + """Test getting the type of the vtk array of an attribute from dataset.""" + vtkDataSetTest: vtkDataSet = dataSetTest( "dataset" ) + + obtained: int = arrayHelpers.getVtkArrayTypeInObject( vtkDataSetTest, attributeName, onPoint ) + expected: int = 11 + + assert ( obtained == expected ) + + @pytest.mark.parametrize( "arrayExpected, onpoints", [ ( "PORO", False ), From a905450ce3817f07f43349e829ad893d98ceb1cc Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 24 Jun 2025 16:51:30 +0200 Subject: [PATCH 02/56] uptade the function createAttribute to preserve the type of the vtk array --- .../src/geos/mesh/utils/arrayModifiers.py | 97 ++++++----- geos-mesh/tests/conftest.py | 6 + geos-mesh/tests/data/displacedFaultempty.vtm | 7 + geos-mesh/tests/data/domain_res5_id_empty.vtu | 39 +++++ .../tests/data/fracture_res5_id_empty.vtu | 41 +++++ geos-mesh/tests/test_arrayModifiers.py | 150 ++++++++++-------- 6 files changed, 236 insertions(+), 104 deletions(-) create mode 100644 geos-mesh/tests/data/displacedFaultempty.vtm create mode 100644 geos-mesh/tests/data/domain_res5_id_empty.vtu create mode 100644 geos-mesh/tests/data/fracture_res5_id_empty.vtu diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index 6d9a738c..6f73df08 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -26,6 +26,7 @@ getAttributeSet, getArrayInObject, isAttributeInObject, + getVtkArrayTypeInObject, ) from geos.mesh.utils.multiblockHelpers import getBlockElementIndexesFlatten, getBlockFromFlatIndex @@ -39,56 +40,56 @@ """ -def fillPartialAttributes( - multiBlockMesh: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], - attributeName: str, - nbComponents: int, - onPoints: bool = False, -) -> bool: - """Fill input partial attribute of multiBlockMesh with nan values. +def fillPartialAttributes( multiBlockMesh: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], + attributeName: str, + nbComponents: int, + onPoints: bool = False, + value: float = np.nan, + ) -> bool: + """Fill input partial attribute of multiBlockMesh with values (defaults to nan). Args: multiBlockMesh (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): multiBlock - mesh where to fill the attribute - attributeName (str): attribute name - nbComponents (int): number of components - onPoints (bool, optional): Attribute is on Points (False) or - on Cells. - + mesh where to fill the attribute. + attributeName (str): attribute name. + nbComponents (int): number of components. + onPoints (bool, optional): Attribute is on Points (True) or on Cells (False). Defaults to False. + value (float, optional): value to fill in the partial atribute. + Defaults to nan. Returns: - bool: True if calculation successfully ended, False otherwise + bool: True if calculation successfully ended, False otherwise. """ componentNames: tuple[ str, ...] = () if nbComponents > 1: componentNames = getComponentNames( multiBlockMesh, attributeName, onPoints ) - values: list[ float ] = [ np.nan for _ in range( nbComponents ) ] + values: list[ float ] = [ value for _ in range( nbComponents ) ] createConstantAttribute( multiBlockMesh, values, attributeName, componentNames, onPoints ) multiBlockMesh.Modified() return True -def fillAllPartialAttributes( - multiBlockMesh: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], - onPoints: bool = False, -) -> bool: - """Fill all the partial attributes of multiBlockMesh with nan values. +def fillAllPartialAttributes( multiBlockMesh: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], + onPoints: bool = False, + value: float = np.nan, + ) -> bool: + """Fill all the partial attributes of multiBlockMesh with values (defaults to nan). Args: multiBlockMesh (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): multiBlockMesh where to fill the attribute - onPoints (bool, optional): Attribute is on Points (False) or - on Cells. - + onPoints (bool, optional): Attribute is on Points (True) or on Cells (False). Defaults to False. + value (float, optional): value to fill in all the partial atributes. + Defaults to nan. Returns: bool: True if calculation successfully ended, False otherwise """ attributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockMesh, onPoints ) for attributeName, nbComponents in attributes.items(): - fillPartialAttributes( multiBlockMesh, attributeName, nbComponents, onPoints ) + fillPartialAttributes( multiBlockMesh, attributeName, nbComponents, onPoints, value ) multiBlockMesh.Modified() return True @@ -233,28 +234,29 @@ def createConstantAttributeDataSet( def createAttribute( dataSet: vtkDataSet, - array: npt.NDArray[ np.float64 ], + array: npt.NDArray[ any ], attributeName: str, componentNames: tuple[ str, ...], onPoints: bool, + vtkArrayType: int = VTK_DOUBLE, ) -> bool: """Create an attribute from the given array. Args: - dataSet (vtkDataSet): dataSet where to create the attribute - array (npt.NDArray[np.float64]): array that contains the values - attributeName (str): name of the attribute - componentNames (tuple[str,...]): name of the components for vectorial - attributes - onPoints (bool): True if attributes are on points, False if they are - on cells. + dataSet (vtkDataSet): dataSet where to create the attribute. + array (npt.NDArray[np.float64]): array that contains the values. + attributeName (str): name of the attribute. + componentNames (tuple[str,...]): name of the components for vectorial attributes. + onPoints (bool): True if attributes are on points, False if they are on cells. + vtkArrayType (int): vtk type of the array of the attribute to create. + Defaults to VTK_DOUBLE Returns: - bool: True if the attribute was correctly created + bool: True if the attribute was correctly created. """ assert isinstance( dataSet, vtkDataSet ), "Attribute can only be created in vtkDataSet object." - newAttr: vtkDataArray = vnp.numpy_to_vtk( array, deep=True, array_type=VTK_DOUBLE ) + newAttr: vtkDataArray = vnp.numpy_to_vtk( array, deep=True, array_type=vtkArrayType ) newAttr.SetName( attributeName ) nbComponents: int = newAttr.GetNumberOfComponents() @@ -267,6 +269,7 @@ def createAttribute( else: dataSet.GetCellData().AddArray( newAttr ) dataSet.Modified() + return True @@ -275,17 +278,20 @@ def copyAttribute( objectTo: vtkMultiBlockDataSet, attributNameFrom: str, attributNameTo: str, + onPoint: bool = False, ) -> bool: - """Copy a cell attribute from objectFrom to objectTo. + """Copy an attribute from objectFrom to objectTo. Args: objectFrom (vtkMultiBlockDataSet): object from which to copy the attribute. objectTo (vtkMultiBlockDataSet): object where to copy the attribute. attributNameFrom (str): attribute name in objectFrom. attributNameTo (str): attribute name in objectTo. + onPoint (bool, optional): True if attributes are on points, False if they are on cells. + Defaults to False. Returns: - bool: True if copy successfully ended, False otherwise + bool: True if copy successfully ended, False otherwise. """ elementaryBlockIndexesTo: list[ int ] = getBlockElementIndexesFlatten( objectTo ) elementaryBlockIndexesFrom: list[ int ] = getBlockElementIndexesFlatten( objectFrom ) @@ -301,11 +307,13 @@ def copyAttribute( # get block from current time step object block: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( objectTo, index ) ) assert block is not None, "Block at current time step is null." + try: - copyAttributeDataSet( blockT0, block, attributNameFrom, attributNameTo ) + copyAttributeDataSet( blockT0, block, attributNameFrom, attributNameTo, onPoint ) except AssertionError: # skip attribute if not in block continue + return True @@ -314,25 +322,30 @@ def copyAttributeDataSet( objectTo: vtkDataSet, attributNameFrom: str, attributNameTo: str, + onPoint: bool = False, ) -> bool: - """Copy a cell attribute from objectFrom to objectTo. + """Copy an attribute from objectFrom to objectTo. Args: objectFrom (vtkDataSet): object from which to copy the attribute. objectTo (vtkDataSet): object where to copy the attribute. attributNameFrom (str): attribute name in objectFrom. attributNameTo (str): attribute name in objectTo. + onPoint (bool, optional): True if attributes are on points, False if they are on cells. + Defaults to False. Returns: - bool: True if copy successfully ended, False otherwise + bool: True if copy successfully ended, False otherwise. """ # get attribut from initial time step block - npArray: npt.NDArray[ np.float64 ] = getArrayInObject( objectFrom, attributNameFrom, False ) + npArray: npt.NDArray[ any ] = getArrayInObject( objectFrom, attributNameFrom, onPoint ) assert npArray is not None - componentNames: tuple[ str, ...] = getComponentNames( objectFrom, attributNameFrom, False ) + componentNames: tuple[ str, ...] = getComponentNames( objectFrom, attributNameFrom, onPoint ) + arrayType: int = getVtkArrayTypeInObject( objectFrom, attributNameFrom, onPoint ) # copy attribut to current time step block - createAttribute( objectTo, npArray, attributNameTo, componentNames, False ) + createAttribute( objectTo, npArray, attributNameTo, componentNames, onPoint, arrayType ) objectTo.Modified() + return True diff --git a/geos-mesh/tests/conftest.py b/geos-mesh/tests/conftest.py index 56a1de08..29cad120 100644 --- a/geos-mesh/tests/conftest.py +++ b/geos-mesh/tests/conftest.py @@ -39,9 +39,15 @@ def _get_dataset( datasetType: str ): if datasetType == "multiblock": reader = reader = vtkXMLMultiBlockDataReader() vtkFilename = "data/displacedFault.vtm" + elif datasetType == "emptymultiblock": + reader = reader = vtkXMLMultiBlockDataReader() + vtkFilename = "data/displacedFaultempty.vtm" elif datasetType == "dataset": reader: vtkXMLUnstructuredGridReader = vtkXMLUnstructuredGridReader() vtkFilename = "data/domain_res5_id.vtu" + elif datasetType == "emptydataset": + reader: vtkXMLUnstructuredGridReader = vtkXMLUnstructuredGridReader() + vtkFilename = "data/domain_res5_id_empty.vtu" elif datasetType == "polydata": reader: vtkXMLUnstructuredGridReader = vtkXMLUnstructuredGridReader() vtkFilename = "data/surface.vtu" diff --git a/geos-mesh/tests/data/displacedFaultempty.vtm b/geos-mesh/tests/data/displacedFaultempty.vtm new file mode 100644 index 00000000..20ff57fb --- /dev/null +++ b/geos-mesh/tests/data/displacedFaultempty.vtm @@ -0,0 +1,7 @@ + + + + + + + diff --git a/geos-mesh/tests/data/domain_res5_id_empty.vtu b/geos-mesh/tests/data/domain_res5_id_empty.vtu new file mode 100644 index 00000000..94b5c796 --- /dev/null +++ b/geos-mesh/tests/data/domain_res5_id_empty.vtu @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + 0 + + + 3221.0246817 + + + + + + + + + + + + + + + _AQAAAACAAADgfwAARhgAAA==eJw13dMWIMqSBcDbtm3btm3btm3btm3btm3b9ul5mOh6iU+olVWZO//3v/8/ARiQgRiYQRiUwRicIRiSoRiaYRiW4RieERiRkRiZURiV0RidMRiTsRibcRiX8RifCZiQiZiYSZiUyZicKZiSqZiaaZiW6ZieGZiRmZiZWZiV2ZidOZiTuZibeZiX+ZifBViQhViYRViUxVicJViSpViaZViW5VieFViRlViZVViV1VidNViTtVibdViX9VifDdiQjdiYTdiUzdicLdiSrdiabdiW7dieHdiRndiZXdiV3didPdiTvdibfdiX/difAziQgziYQziUwzicIziSoziaYziW4zieEziRkziZUziV0zidMziTszibcziX8zifC7iQi7iYS7iUy7icK7iSq7iaa7iW67ieG7iRm7iZW7iV27idO7iTu7ibe7iX+7ifB3iQh3iYR3iUx3icJ3iSp3iaZ3iW53ieF3iRl3iZV3iV13idN3iTt3ibd3iX93ifD/iQj/iYT/iUz/icL/iSr/iab/iW7/ieH/iRn/iZX/iV3/idP/iTv/ibf/gf//LfxR+AARmIgRmEQRmMwRmCIRmKoRmGYRmO4RmBERmJkRmFURmN0RmDMRmLsRmHcRmP8ZmACZmIiZmESZmMyZmCKZmKqZmGaZmO6ZmBGZmJmZmFWZmN2ZmDOZmLuZmHeZmP+VmABVmIhVmERVmMxVmCJVmKpVmGZVmO5VmBFVmJlVmFVVmN1VmDNVmLtVmHdVmP9dmADdmIjdmETdmMzdmCLdmKrdmGbdmO7dmBHdmJndmFXdmN3dmDPdmLvdmHfdmP/TmAAzmIgzmEQzmMwzmCIzmKozmGYzmO4zmBEzmJkzmFUzmN0zmDMzmLszmHczmP87mAC7mIi7mES7mMy7mCK7mKq7mGa7mO67mBG7mJm7mFW7mN27mDO7mLu7mHe7mP+3mAB3mIh3mER3mMx3mCJ3mKp3mGZ3mO53mBF3mJl3mFV3mN13mDN3mLt3mHd3mP9/mAD/mIj/mET/mMz/mCL/mKr/mGb/mO7/mBH/mJn/mFX/mN3/mDP/mLv/mH//Ev/xX8ARiQgRiYQRiUwRicIRiSoRiaYRiW4RieERiRkRiZURiV0RidMRiTsRibcRiX8RifCZiQiZiYSZiUyZicKZiSqZiaaZiW6ZieGZiRmZiZWZiV2ZidOZiTuZibeZiX+ZifBViQhViYRViUxVicJViSpViaZViW5VieFViRlViZVViV1VidNViTtVibdViX9VifDdiQjdiYTdiUzdicLdiSrdiabdiW7dieHdiRndiZXdiV3didPdiTvdibfdiX/difAziQgziYQziUwzicIziSoziaYziW4zieEziRkziZUziV0zidMziTszibcziX8zifC7iQi7iYS7iUy7icK7iSq7iaa7iW67ieG7iRm7iZW7iV27idO7iTu7ibe7iX+7ifB3iQh3iYR3iUx3icJ3iSp3iaZ3iW53ieF3iRl3iZV3iV13idN3iTt3ibd3iX93ifD/iQj/iYT/iUz/icL/iSr/iab/iW7/ieH/iRn/iZX/iV3/idP/iTv/ibf/gf//LfQ38ABmQgBmYQBmUwBmcIhmQohmYYhmU4hmcERmQkRmYURmU0RmcMxmQsxmYcxmU8xmcCJmQiJmYSJmUyJmcKpmQqpmYapmU6pmcGZmQmZmYWZmU2ZmcO5mQu5mYe5mU+5mcBFmQhFmYRFmUxFmcJlmQplmYZlmU5lmcFVmQlVmYVVmU1VmcN1mQt1mYd1mU91mcDNmQjNmYTNmUzNmcLtmQrtmYbtmU7tmcHdmQndmYXdmU3dmcP9mQv9mYf9mU/9ucADuQgDuYQDuUwDucIjuQojuYYjuU4jucETuQkTuYUTuU0TucMzuQszuYczuU8zucCLuQiLuYSLuUyLucKruQqruYaruU6rucGbuQmbuYWbuU2bucO7uQu7uYe7uU+7ucBHuQhHuYRHuUxHucJnuQpnuYZnuU5nucFXuQlXuYVXuU1XucN3uQt3uYd3uU93ucDPuQjPuYTPuUzPucLvuQrvuYbvuU7vucHfuQnfuYXfuU3fucP/uQv/uYf/se//PfBH4ABGYiBGYRBGYzBGYIhGYqhGYZhGY7hGYERGYmRGYVRGY3RGYMxGYuxGYdxGY/xmYAJmYiJmYRJmYzJmYIpmYqpmYZpmY7pmYEZmYmZmYVZmY3ZmYM5mYu5mYd5mY/5WYAFWYiFWYRFWYzFWYIlWYqlWYZlWY7lWYEVWYmVWYVVWY3VWYM1WYu1WYd1WY/12YAN2YiN2YRN2YzN2YIt2Yqt2YZt2Y7t2YEd2Ymd2YVd2Y3d2YM92Yu92Yd92Y/9OYADOYiDOYRDOYzDOYIjOYqjOYZjOY7jOYETOYmTOYVTOY3TOYMzOYuzOYdzOY/zuYALuYiLuYRLuYzLuYIruYqruYZruY7ruYEbuYmbuYVbuY3buYM7uYu7uYd7uY/7eYAHeYiHeYRHeYzHeYIneYqneYZneY7neYEXeYmXeYVXeY3XeYM3eYu3eYd3eY/3+YAP+YiP+YRP+YzP+YIv+Yqv+YZv+Y7v+YEf+Ymf+YVf+Y3f+YM/+Yu/+Yf/8S//NfYFYEAGYmAGYVAGY3CGYEiGYmiGYViGY3hGYERGYmRGYVRGY3TGYEzGYmzGYVzGY3wmYEImYmImYVImY3KmYEqmYmqmYVqmY3pmYEZmYmZmYVZmY3bmYE7mYm7mYV7mY34WYEEWYmEWYVEWY3GWYEmWYmmWYVmWY3lWYEVWYmVWYVVWY3XWYE3WYm3WYV3WY302YEM2YmM2YVM2Y3O2YEu2Ymu2YVu2Y3t2YEd2Ymd2YVd2Y3f2YE/2Ym/2YV/2Y38O4EAO4mAO4VAO43CO4EiO4miO4ViO43hO4ERO4mRO4VRO43TO4EzO4mzO4VzO43wu4EIu4mIu4VIu43Ku4Equ4mqu4Vqu43pu4EZu4mZu4VZu43bu4E7u4m7u4V7u434e4EEe4mEe4VEe43Ge4Eme4mme4Vme43le4EVe4mVe4VVe43Xe4E3e4m3e4V3e430+4EM+4mM+4VM+43O+4Eu+4mu+4Vu+43t+4Ed+4md+4Vd+43f+4E/+4m/+4X/8y38N/QEYkIEYmEEYlMEYnCEYkqEYmmEYluEYnhEYkZEYmVEYldEYnTEYk7EYm3EYl/EYnwmYkImYmEmYlMmYnCmYkqmYmmmYlumYnhmYkZmYmVmYldmYnTmYk7mYm3mYl/mYnwVYkIVYmEVYlMVYnCVYkqVYmmVYluVYnhVYkZVYmVVYldVYnTVYk7VYm3VYl/VYnw3YkI3YmE3YlM3YnC3Ykq3Ymm3Ylu3Ynh3YkZ3YmV3Yld3YnT3Yk73Ym33Yl/3YnwM4kIM4mEM4lMM4nCM4kqM4mmM4luM4nhM4kZM4mVM4ldM4nTM4k7M4m3M4l/M4nwu4kIu4mEu4lMu4nCu4kqu4mmu4luu4nhu4kZu4mVu4ldu4nTu4k7u4m3u4l/u4nwd4kId4mEd4lMd4nCd4kqd4mmd4lud4nhd4kZd4mVd4ldd4nTd4k7d4m3d4l/d4nw/4kI/4mE/4lM/4nC/4kq/4mm/4lu/4nh/4kZ/4mV/4ld/4nT/4k7/4m3/4H//y3yBfAAZkIAZmEAZlMAZnCIZkKIZmGIZlOIZnBEZkJEZmFEZlNEZnDMZkLMZmHMZlPMZnAiZkIiZmEiZlMiZnCqZkKqZmGqZlOqZnBmZkJmZmFmZlNmZnDuZkLuZmHuZlPuZnARZkIRZmERZlMRZnCZZkKZZmGZZlOZZnBVZkJVZmFVZlNVZnDdZkLdZmHdZlPdZnAzZkIzZmEzZlMzZnC7ZkK7ZmG7ZlO7ZnB3ZkJ3ZmF3ZlN3ZnD/ZkL/ZmH/ZlP/bnAA7kIA7mEA7lMA7nCI7kKI7mGI7lOI7nBE7kJE7mFE7lNE7nDM7kLM7mHM7lPM7nAi7kIi7mEi7lMi7nCq7kKq7mGq7lOq7nBm7kJm7mFm7lNm7nDu7kLu7mHu7lPu7nAR7kIR7mER7lMR7nCZ7kKZ7mGZ7lOZ7nBV7kJV7mFV7lNV7nDd7kLd7mHd7lPd7nAz7kIz7mEz7lMz7nC77kK77mG77lO77nB37kJ37mF37lN37nD/7kL/7mH/7Hv/w3wB+AARmIgRmEQRmMwRmCIRmKoRmGYRmO4RmBERmJkRmFURmN0RmDMRmLsRmHcRmP8ZmACZmIiZmESZmMyZmCKZmKqZmGaZmO6ZmBGZmJmZmFWZmN2ZmDOZmLuZmHeZmP+VmABVmIhVmERVmMxVmCJVmKpVmGZVmO5VmBFVmJlVmFVVmN1VmDNVmLtVmHdVmP9dmADdmIjdmETdmMzdmCLdmKrdmGbdmO7dmBHdmJndmFXdmN3dmDPdmLvdmHfdmP/TmAAzmIgzmEQzmMwzmCIzmKozmGYzmO4zmBEzmJkzmFUzmN0zmDMzmLszmHczmP87mAC7mIi7mES7mMy7mCK7mKq7mGa7mO67mBG7mJm7mFW7mN27mDO7mLu7mHe7mP+3mAB3mIh3mER3mMx3mCJ3mKp3mGZ3mO53mBF3mJl3mFV3mN13mDN3mLt3mHd3mP9/mAD/mIj/mET/mMz/mCL/mKr/mGb/mO7/mBH/mJn/mFX/mN3/mDP/mLv/mH//Ev/wX3BGBABmJgBmFQBmNwhmBIhmJohmFYhmN4RmBERmJkRmFURmN0xmBMxmJsxmFcxmN8JmBCJmJiJmFSJmNypmBKpmJqpmFapmN6ZmBGZmJmZmFWZmN25mBO5mJu5mFe5mN+FmBBFmJhFmFRFmNxlmBJlmJplmFZlmN5VmBFVmJlVmFVVmN11mBN1mJt1mFd1mN9NmBDNmJjNmFTNmNztmBLtmJrtmFbtmN7dmBHdmJndmFXdmN39mBP9mJv9mFf9mN/DuBADuJgDuFQDuNwjuBIjuJojuFYjuN4TuBETuJkTuFUTuN0zuBMzuJszuFczuN8LuBCLuJiLuFSLuNyruBKruJqruFaruN6buBGbuJmbuFWbuN27uBO7uJu7uFe7uN+HuBBHuJhHuFRHuNxnuBJnuJpnuFZnuN5XuBFXuJlXuFVXuN13uBN3uJt3uFd3uN9PuBDPuJjPuFTPuNzvuBLvuJrvuFbvuN7fuBHfuJnfuFXfuN3/uBP/uJv/uF//Mt/gX0BGJCBGJhBGJTBGJwhGJKhGJphGJbhGJ4RGJGRGJlRGJXRGJ0xGJOxGJtxGJfxGJ8JmJCJmJhJmJTJmJwpmJKpmJppmJbpmJ4ZmJGZmJlZmJXZmJ05mJO5mJt5mJf5mJ8FWJCFWJhFWJTFWJwlWJKlWJplWJblWJ4VWJGVWJlVWJXVWJ01WJO1WJt1WJf1WJ8N2JCN2JhN2JTN2Jwt2JKt2Jpt2Jbt2J4d2JGd2Jld2JXd2J092JO92Jt92Jf92J8DOJCDOJhDOJTDOJwjOJKjOJpjOJbjOJ4TOJGTOJlTOJXTOJ0zOJOzOJtzOJfzOJ8LuJCLuJhLuJTLuJwruJKruJpruJbruJ4buJGbuJlbuJXbuJ07uJO7uJt7uJf7uJ8HeJCHeJhHeJTHeJwneJKneJpneJbneJ4XeJGXeJlXeJXXeJ03eJO3eJt3eJf3eJ8P+JCP+JhP+JTP+Jwv+JKv+Jpv+Jbv+J4f+JGf+Jlf+JXf+J0/+JO/+Jt/+B//8l9QbwAGZCAGZhAGZTAGZwiGZCiGZhiGZTiGZwRGZCRGZhRGZTRGZwzGZCzGZhzGZTzGZwImZCImZhImZTImZwqmZCqmZhqmZTqmZwZmZCZmZhZmZTZmZw7mZC7mZh7mZT7mZwEWZCEWZhEWZTEWZwmWZCmWZhmWZTmWZwVWZCVWZhVWZTVWZw3WZC3WZh3WZT3WZwM2ZCM2ZhM2ZTM2Zwu2ZCu2Zhu2ZTu2Zwd2ZCd2Zhd2ZTd2Zw/2ZC/2Zh/2ZT/25wAO5CAO5hAO5TAO5wiO5CiO5hiO5TiO5wRO5CRO5hRO5TRO5wzO5CzO5hzO5TzO5wIu5CIu5hIu5TIu5wqu5Cqu5hqu5Tqu5wZu5CZu5hZu5TZu5w7u5C7u5h7u5T7u5wEe5CEe5hEe5TEe5wme5Cme5hme5Tme5wVe5CVe5hVe5TVe5w3e5C3e5h3e5T3e5wM+5CM+5hM+5TM+5wu+5Cu+5hu+5Tu+5wd+5Cd+5hd+5Td+5w/+5C/+5h/+x7/8F9AfgAEZiIEZhEEZjMEZgiEZiqEZhmEZjuEZgREZiZEZhVEZjdEZgzEZi7EZh3EZj/GZgAmZiImZhEmZjMmZgimZiqmZhmmZjumZgRmZiZmZhVmZjdmZgzmZi7mZh3mZj/lZgAVZiIVZhEVZjMVZgiVZiqVZhmVZjuVZgRVZiZVZhVVZjdVZgzVZi7VZh3VZj/XZgA3ZiI3ZhE3ZjM3Zgi3Ziq3Zhm3Zju3ZgR3ZiZ3ZhV3Zjd3Zgz3Zi73Zh33Zj/05gAM5iIM5hEM5jMM5giM5iqM5hmM5juM5gRM5iZM5hVM5jdM5gzM5i7M5h3M5j/O5gAu5iIu5hEu5jMu5giu5iqu5hmu5juu5gRu5iZu5hVu5jdu5gzu5i7u5h3u5j/t5gAd5iId5hEd5jMd5gid5iqd5hmd5jud5gRd5iZd5hVd5jdd5gzd5i7d5h3d5j/f5gA/5iI/5hE/5jM/5gi/5iq/5hm/5ju/5gR/5iZ/5hV/5jd/5gz/5i7/5h//xL/8t5gnAgAzEwAzCoAzG4AzBkAzF0AzDsAzH8IzAiIzEyIzCqIzG6IzBmIzF2IzDuIzH+EzAhEzExEzCpEzG5EzBlEzF1EzDtEzH9MzAjMzEzMzCrMzG7MzBnMzF3MzDvMzH/CzAgizEwizCoizG4izBkizF0izDsizH8qzAiqzEyqzCqqzG6qzBmqzF2qzDuqzH+mzAhmzExmzCpmzG5mzBlmzF1mzDtmzH9uzAjuzEzuzCruzG7uzBnuzF3uzDvuzH/hzAgRzEwRzCoRzG4RzBkRzF0RzDsRzH8ZzAiZzEyZzCqZzG6ZzBmZzF2ZzDuZzH+VzAhVzExVzCpVzG5VzBlVzF1VzDtVzH9dzAjdzEzdzCrdzG7dzBndzF3dzDvdzH/TzAgzzEwzzCozzG4zzBkzzF0zzDszzH87zAi7zEy7zCq7zG67zBm7zF27zDu7zH+3zAh3zEx3zCp3zG53zBl3zF13zDt3zH9/zAj/zEz/zCr/zG7/zBn/zF3/zD//iX/xbyBWBABmJgBmFQBmNwhmBIhmJohmFYhmN4RmBERmJkRmFURmN0xmBMxmJsxmFcxmN8JmBCJmJiJmFSJmNypmBKpmJqpmFapmN6ZmBGZmJmZmFWZmN25mBO5mJu5mFe5mN+FmBBFmJhFmFRFmNxlmBJlmJplmFZlmN5VmBFVmJlVmFVVmN11mBN1mJt1mFd1mN9NmBDNmJjNmFTNmNztmBLtmJrtmFbtmN7dmBHdmJndmFXdmN39mBP9mJv9mFf9mN/DuBADuJgDuFQDuNwjuBIjuJojuFYjuN4TuBETuJkTuFUTuN0zuBMzuJszuFczuN8LuBCLuJiLuFSLuNyruBKruJqruFaruN6buBGbuJmbuFWbuN27uBO7uJu7uFe7uN+HuBBHuJhHuFRHuNxnuBJnuJpnuFZnuN5XuBFXuJlXuFVXuN13uBN3uJt3uFd3uN9PuBDPuJjPuFTPuNzvuBLvuJrvuFbvuN7fuBHfuJnfuFXfuN3/uBP/uJv/uF//Mt/i3gDMCADMTCDMCiDMThDMCRDMTTDMCzDMTwjMCIjMTKjMCqjMTpjMCZjMTbjMC7jMT4TMCETMTGTMCmTMTlTMCVTMTXTMC3TMT0zMCMzMTOzMCuzMTtzMCdzMTfzMC/zMT8LsCALsTCLsCiLsThLsCRLsTTLsCzLsTwrsCIrsTKrsCqrsTprsCZrsTbrsC7rsT4bsCEbsTGbsCmbsTlbsCVbsTXbsC3bsT07sCM7sTO7sCu7sTt7sCd7sTf7sC/7sT8HcCAHcTCHcCiHcThHcCRHcTTHcCzHcTwncCIncTKncCqncTpncCZncTbncC7ncT4XcCEXcTGXcCmXcTlXcCVXcTXXcC3XcT03cCM3cTO3cCu3cTt3cCd3cTf3cC/3cT8P8CAP8TCP8CiP8ThP8CRP8TTP8CzP8Twv8CIv8TKv8Cqv8Tpv8CZv8Tbv8C7v8T4f8CEf8TGf8Cmf8Tlf8CVf8TXf8C3f8T0/8CM/8TO/8Cu/8Tt/8Cd/8f8AsyVsRw==AwAAAACAAACgfwAAdgAAAHcAAAB3AAAAeJztyDENACAMADC8EBJkTA1qmaeF2aA9m/fZcdqM0Vak995777333nvvvffee++9995777333nvvvffee++9995777333nvvvffee++9995777333nvvvffee++9995777333nvvvffee++99957/9EX/I+fp3ic7cgxDQAgDAAwvBASZEzN1DJPZNigPZvZZoxnRZ22I7333nvvvffee++9995777333nvvvffee++9995777333nvvvffee++9995777333nvvvffee++9995777333nvvvffee++9995777333vuP/gJF6p09eJztyDENACAMALB5ISTImBrUMk9kqOBoz0a0mXXayv2MDO+9995777333nvvvffee++9995777333nvvvffee++9995777333nvvvffee++9995777333nvvvffee++9995777333nvvvffee++99/6jvwaTeWc=AQAAAACAAAAwGwAABAEAAA==eJztlksOxDAIQ9PO/e88mq3lZ6JoVKURC5SGfADjUO4xxv1yuUA+BwjF9k95IkenYNX52CsfapNivWVddTTSWrXf+aO66p50ZsZvnV9wD9msbLk67HCajTt9u7WK34ShyythnHhA8VB8Dh/CM+FGOFexJRySn2rbYULcUl4QpitvKPHW2a14f4p0P9T/36ew6nzslY+Z+ubqp+popLVqv/NHddU96cyM3zrvfoh5QRgnHlA8FF/3Q4zpyhtKvHV2K96fIt0P9f/3Kaw6H3vlY6a+ufqpOhpprdrv/FFddU86M+O3zrsfYl4QxokHFA/F1/0QY7ryhhJvnd2K9z/5Ao84Duw=AQAAAACAAACQUQAAMQAAAA==eJztwzENAAAIA7B3SjCBx2lGCG3SbCeqqqqqqqqqqqqqqqqqqqqqqqqqqo8eaqCtmg==AQAAAACAAAAwGwAAIwAAAA==eJztwwENAAAIA6BmJjC67/QgwkZuJ6qqqqqqqvp0AWlKhrc=AQAAAACAAAAwGwAAPQAAAA==eJzt1rEJADAIRUGH/dl/hbSp0oiFcAci2Nm9VFUG5wxPnp3Pfet/AMC87b2ghwCAru29oIcAgK4L9At6fQ==AQAAAACAAABgNgAAawoAAA==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AgAAAACAAADQPwAAnzAAAG4eAAA=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4q6xxV1ji7umNcq6axpP665pjbXumtYi666xxV1ji7vGFneNLe4aW9w1trhrbHHX2OKuaX2z7prWSeuuscVdY4u7xhZ3TfPfumtpdNfY4q6xxV3T+mbdNV1T666xxV1ji7vGFneNLe4aW9w17ce6a7pe1l1ji7umdcO6a2xx19jirul+t+5aOvzOxu5aOpy37K6l0V2H28i8ZXdN65J119jirrHFXdM8se6a1g3rrrHFXdP9aN01trhrmhvWXWOLu8YWd23XZ3LX0mo9J3ctje463EbOhd01trhrbHHXNFetu6Z737prGnPrrrHFXdN6Zd01trhrbHHX/LnsrqXDY84O9inuWhrdNba4a2xx17QmWHdN18K6a1pPrLum9dm6a1pzrLvGFneNLe4aW9w1trhrbHHX2OKuscVdY4u7xhZ3jS3ums+L3bV0OA7srlXL78zkrrHFXWOLu8YWd40t7ppet+4aW9w1trhrbHHX2OKu6XOtu5YOjtO6a9WuNLprbHHXdC9Yd01zzLprbHHX2OKuscVdY4u7xhZ3jS3umseH3bW0eh6Ru5ZG8xNuEzyPyF1ji7vGFneNLe4aW9w1trhrbHHXPA7srqXDcWN3Ld1Xjse6a2l019jirmkNt+4aW9w1reHWXWOLu8YWd40t7pruI+uu+dzZXUuH48buWrpPMG6JatzYXWOLu8YWd03roXXXtO5Zd01rmnXXfL7srqXDsWJ3LT0iGKskNVbsrulzrbum9cG6a2xx13TvWHeNLe4aW9w1trhrbHHX2OKuscVdY4u7pnXAumtaf6y7xhZ3zePG7lpaPYPIXUujuw63CZ5B5K6xxV1ji7umNdm6a1rHrLvGFneNLe6a1gTrrrHFXdNnWXctHRynddfS6K7DbexxWndNa4t119jirrHFXWOLu8YWd033uHXX2OKuad227prWMeuuscVd8/myu+Z5wu5aOpxj7K6l0V2H28gcY3eNLe4aW9w1trhrbHHXdL7WXdN+rLuWDo7BumtpdNfhNvYYrLvGFneNLe4aW9w1trhrbHHX2OKuscVd87xidy2tvkuo51G6mofsrmm+WXdN64x119jirnk+sLsOOphLGeH+rbuWRndNa75119jirrHFXWOLu8YWd40t7pqvI7tr6XAOZKlnWZa67uyu6Vpbd40t7hpb3DW2uGua29Zd03yw7pqv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AQAAAACAAADMBgAAFgAAAA==eJzT0hoFo2AUjIJRMAqGNgAA9C4diA==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AQAAAACAAABgNgAAMAwAAA==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 + + diff --git a/geos-mesh/tests/data/fracture_res5_id_empty.vtu b/geos-mesh/tests/data/fracture_res5_id_empty.vtu new file mode 100644 index 00000000..a69adb95 --- /dev/null +++ b/geos-mesh/tests/data/fracture_res5_id_empty.vtu @@ -0,0 +1,41 @@ + + + + + + + AQAAAACAAACgBgAAbgEAAA==eJwtxdciEAAAAEBRUmlpK9q0aEpbey/tTUMb0d57T6VBO+2NSGjvoflDHrp7uYCA/6o40EGu6moOdnWHuIZrupZDXdt1XNf1XN9hbuCGbuTGbuKmbuZwN3cLRzjSLd3Krd3Gbd3O7R3laHdwR3dyZ3dxjGPd1d3c3T3c070c596Odx/3dT/39wAP9CAneLCHeKiHebhHeKRHebTHeKzHebwneKInebITPcVTPc3TPcMzPcuzPcdzPc/zvcBJTvZCL/JiL3GKl3qZl3uFV3qVVzvVaU73Gmc402u9zuu9wRu9yZu9xVu9zdu9wzu9y7u9x3u9z/t9wAd9yId9xEd9zMd9wid9ylk+7TPO9lmf83lfcI5zfdGXfNlXfNXXfN03nOebvuXbvuO7vuf7fuCHfuTHfuKnzneBC/3MRS72c5f4hUtd5nK/9Cu/9hu/9Tu/9wd/9Cd/9hd/9Td/9w9X+Kd/+bf/+K//uRLqf1df + + + + + AQAAAACAAADgBAAAEgEAAA==eJwtxddCCAAAAMAiozSkoaGh0NAeqGhrSNEg7SFkJKFhlIhoaCBUP9tDdy8XEHAo0Ed81EE+5uM+4ZMOdohPOdRhDneETzvSZxzlaMc41mcd53gnONHnnORkpzjV553mdF/wRV9yhjOd5Wxfdo5zned8F7jQRS52iUt9xVd9zWUud4Wv+4YrXeVq17jWda73TTe40U1u9i23+LZb3eY7vut2d7jTXb7n++72A/e4133u94AHPeRhj3jUDz3mR37sJx73Uz/zc7/whF960q885dd+47ee9oxnPed3fu8P/uh5L/iTF/3ZX7zkr/7mZX/3D6941Wte909veNNb3vYv//Yf7/iv//m/d73nfR8ARZMvOw== + + + + + AQAAAACAAADwCQAAXQIAAA==eJxtlaFOK1EQhheFQZCKq1E3+xQN6Zm+QR8ATdKER1iPQVVvVhxxa6rQtNy6DcFhGzwJkhzF9ITO/93kVn3Z7M78Z86XTtP4r7/YN6dfey7enInvvv4Gdx/ih3dx/ybejOKnrfj1UXxYiz97cbMSX96LrzrxzS3yLJBhir4tek1QvzwHXr+IuwG8FDe/wKtd89/faob3E+qAB/H1i7gp4sPEkF/cT8XdQnxzK77qxJf34mYl/uzFh7X49RF9t+LNiAxv4od35PkQ333h27N5cHsu7i/I+/Wf3+nnHbG7F+zuBbt7we5esLunb0exuxfs7gW7e8HuXrC7F+zuBbt7we6e8iyQYYq+LXpNUL88B7t7qjOAl2J3D9l24qbB8xneT6gDHhL6JuRJyGnIbziX4byGORjmY5ibIZthzob5i9093JfY3cP9it09+CB29+DPHF6J3T2wO5bhXoZ7Ge5luJfhXoZ7p29HcXUvw70M9zLcy3Avw70M9zLcO+VZIMMUfVv0mqD+0b0M9051BvBSXN2LbDvx0b14PsP7CXXAQ0LfhDwJOQ35DecynNcwB8N8DHMzZDPM2TB/cXUv7ktc3Yv7FVf3wgdxdS/8mcMrcXUveB//+3XnlnAvuO7cEu4F151bwj19O4rrzi3hXnDduSXc0+5ZievOLeFecN25pzwLZJiib4teE9Q/7twS7qnOAF6K686NbLt/dqSez/B+Qh3wkNA3IU9CTkN+w7kM5zXMwTAfw9wM2QxzFtedG/eCvltx3blxv+K6c8MHcd254c8cXonrzi3pG10/Tpw= + + + 0 + + + 2304.8861143 + + + + + + + AQAAAACAAACAEwAAWQMAAA==eJxd1Vf7CAQYhnEjpKiUWYmiREX29rf33nvvvffee29KoShbpE2plJXxhRy474Oe9+R3Xff1nL/1Cz2/BjgFJ2M9rB99EtaNnX0ifho7+wSsEzv7eKwdO/s4/CR29rH4cezsY/Cj2NlHY63Y2UdhzdjZR+KHsbOPwBqxsw/HD2JnH4bvx84+FKvHzj4Eq8XOPhjfi519EL4bO/tArBo7+wCsEjt7f3wndvZ+WDl29r74duzsffCt2Nl745uxs/fCSrGz98SKsbP3wAqxs3fH8rGzd8NysbN3xbKxs3fBN2Jn74yvx87eCcvEzt4RX4udvQO+Gjt7e3wldvZ2WDp29rZYKnb2Nvhy7Oyt8aXY2QuwZOzsrfDF2NlbYonY2Vtg8djZm2Ox2Nmb4QuxszfForGzN8EisbM3xsKxszcq9P8rHL0h+k/8O5/jZzgpdvbjODF29mM4IXb2ozg+dvYjOC529sM4Nnb2QzgmdvaDODp29gM4Knb2/TgydvZ9OCJ29r04PHb2PTgsdvbdODR29l04JHb2nTg4dvYdOCh29u04MHb2bTggdvat2D929i3YL3b2zdg3dvZN2Cd29o3YO3b2Ddgrdvb12DN29nXYI3b2tdg9dvY12C129tXYNXb2VdgldvaV2Dl29hXYKXb25dgxdvZl2CF29qXYPnb2JdgudvbF2DZ29kXYJnb2hdg6dvYFWBA7+3xsFTv7PGwZO/tcbBE7+xxsHjv7bGwWO/ssbBo7+0xsEjv7DGwcO/t0bBQ7+zRsGDv7VPSf+Hee4hM8Hjv7YzwWO/sjPBo7+394JHb2h3g4dvYHeCh29vt4MHb2e3ggdva7uD929n9xX+zs/+De2Nnv4J7Y2f/G3bGz/4W7Ymf/E3fGzn4bd8TO/gduj539d9wWO/st3Bo7+03cEjv7b7g5dvZfcVPs7L/gxtjZf8YNsbP/hOtjZ/8R18XO/gOujZ39Bq6Jnf17XB07+3VcFTv7NVwZO/t3uCJ29qu4PHb2K7gsdvbLuDR29ku4JHb2i7g4dvYLuCh29vO4MHb2c7ggdvZvcX7s7N/gvNjZz+Lc2NnP4JzY2b/G2bGzf4WzYmc/jTNjZz+FM2JnP4nTY2f/EqfFzv4FTo2d/QQ+A6EeATg= + + + AQAAAACAAADgBAAADgEAAA==eJwtxRFwAgAAAMC2C4IgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCAaDwSAIgiAIgiAIBkEQDPqXDwbeQg474qhjjjvhpFNOO+Osc8674KJLLrviqmuuu+GmW26746577nvgoUcee+KpZ5574aVXXnvjrb/87R//eue9Dz765LMvvvrmu//88NMvBz7eBR1y2BFHHXPcCSedctoZZ51z3gUXXXLZFVddc90NN91y2x133XPfAw898tgTTz3z3AsvvfLaG2/95W//+Nc7733w0SefffHVN9/954effjnw+S7okMOOOOqY40446ZTTzjjrnPMuuOiSy6646prrbrjpltvu+B9fwUXT + + + AQAAAACAAACcAAAADAAAAA==eJxjZx+8AABPhQRF + + + + + diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index 5f90bb13..cf0903aa 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -22,61 +22,59 @@ from geos.mesh.utils import arrayModifiers -@pytest.mark.parametrize( "attributeName, onpoints", [ ( "CellAttribute", False ), ( "PointAttribute", True ) ] ) +@pytest.mark.parametrize( "attributeName, nbComponents, onpoints, value_test", [ + ( "CellAttribute", 3, False, np.nan ), + ( "PointAttribute", 3, True, np.nan ), + ( "CELL_MARKERS", 1, False, np.nan ), + ( "PORO", 1, False, np.nan ), + ( "CellAttribute", 3, False, 2. ), + ( "PointAttribute", 3, True, 2. ), + ( "CELL_MARKERS", 1, False, 2. ), + ( "PORO", 1, False, 2. ), +] ) def test_fillPartialAttributes( dataSetTest: vtkMultiBlockDataSet, attributeName: str, + nbComponents: int, onpoints: bool, + value_test: float, ) -> None: - """Test filling a partial attribute from a multiblock with nan values.""" + """Test filling a partial attribute from a multiblock with values.""" + vtkMultiBlockDataSetTestRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) vtkMultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - arrayModifiers.fillPartialAttributes( vtkMultiBlockDataSetTest, attributeName, nbComponents=3, onPoints=onpoints ) - - iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() - iter.SetDataSet( vtkMultiBlockDataSetTest ) - iter.VisitOnlyLeavesOn() - iter.GoToFirstItem() - while iter.GetCurrentDataObject() is not None: - dataset: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) - data: Union[ vtkPointData, vtkCellData ] + arrayModifiers.fillPartialAttributes( vtkMultiBlockDataSetTest, + attributeName, + nbComponents, + onPoints=onpoints, + value=value_test ) + + nbBlock: int = vtkMultiBlockDataSetTestRef.GetNumberOfBlocks() + for block_id in range( nbBlock ): + datasetRef: vtkDataSet = cast( vtkDataSet, vtkMultiBlockDataSetTestRef.GetBlock( block_id ) ) + dataset: vtkDataSet = cast( vtkDataSet, vtkMultiBlockDataSetTest.GetBlock( block_id ) ) + expected_array: npt.NDArray[ np.float64 ] + array: npt.NDArray[ np.float64 ] if onpoints: - data = dataset.GetPointData() + array = vnp.vtk_to_numpy( dataset.GetPointData().GetArray( attributeName ) ) + if block_id == 0: + expected_array = vnp.vtk_to_numpy( datasetRef.GetPointData().GetArray( attributeName ) ) + else: + expected_array = np.array( [ [ value_test for i in range( nbComponents ) ] for _ in range( 212 ) ] ) else: - data = dataset.GetCellData() - assert data.HasArray( attributeName ) == 1 - - iter.GoToNextItem() - - -@pytest.mark.parametrize( "onpoints, expectedArrays", [ - ( True, ( "PointAttribute", "collocated_nodes" ) ), - ( False, ( "CELL_MARKERS", "CellAttribute", "FAULT", "PERM", "PORO" ) ), -] ) -def test_fillAllPartialAttributes( - dataSetTest: vtkMultiBlockDataSet, - onpoints: bool, - expectedArrays: tuple[ str, ...], -) -> None: - """Test filling all partial attributes from a multiblock with nan values.""" - vtkMultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - arrayModifiers.fillAllPartialAttributes( vtkMultiBlockDataSetTest, onpoints ) - - iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() - iter.SetDataSet( vtkMultiBlockDataSetTest ) - iter.VisitOnlyLeavesOn() - iter.GoToFirstItem() - while iter.GetCurrentDataObject() is not None: - dataset: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) - data: Union[ vtkPointData, vtkCellData ] - if onpoints: - data = dataset.GetPointData() + array = vnp.vtk_to_numpy( dataset.GetCellData().GetArray( attributeName ) ) + if block_id == 0: + expected_array = vnp.vtk_to_numpy( datasetRef.GetCellData().GetArray( attributeName ) ) + else: + expected_array = np.array( [ [ value_test for i in range( nbComponents ) ] for _ in range( 156 ) ] ) + + if block_id == 0: + assert ( array == expected_array ).all() else: - data = dataset.GetCellData() + if np.isnan( value_test ): + assert np.all( np.isnan( array ) == np.isnan( expected_array ) ) + else: + assert ( array == expected_array ).all() - for attribute in expectedArrays: - assert data.HasArray( attribute ) == 1 - - iter.GoToNextItem() @pytest.mark.parametrize( "attributeName, dataType, expectedDatatypeArray", [ @@ -200,40 +198,68 @@ def test_createAttribute( assert cnames == componentNames -def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet ) -> None: +@pytest.mark.parametrize( "attributeFrom, attributeTo, onPoint, idBlock", [ + ( "PORO", "POROTo", False, 0 ), + ( "CellAttribute", "CellAttributeTo", False, 0 ), + ( "FAULT", "FAULTTo", False, 0 ), + ( "PointAttribute", "PointAttributeTo", True, 0 ), + ( "collocated_nodes", "collocated_nodesTo", True, 1 ), +] ) +def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet, attributeFrom:str, attributeTo: str, onPoint: bool, idBlock: int ) -> None: """Test copy of cell attribute from one multiblock to another.""" objectFrom: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - objectTo: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + objectTo: vtkMultiBlockDataSet = dataSetTest( "emptymultiblock" ) - attributeFrom: str = "CellAttribute" - attributeTo: str = "CellAttributeTO" + arrayModifiers.copyAttribute( objectFrom, objectTo, attributeFrom, attributeTo, onPoint ) - arrayModifiers.copyAttribute( objectFrom, objectTo, attributeFrom, attributeTo ) - - blockIndex: int = 0 + blockIndex: int = idBlock blockFrom: vtkDataSet = cast( vtkDataSet, objectFrom.GetBlock( blockIndex ) ) blockTo: vtkDataSet = cast( vtkDataSet, objectTo.GetBlock( blockIndex ) ) - arrayFrom: npt.NDArray[ np.float64 ] = vnp.vtk_to_numpy( blockFrom.GetCellData().GetArray( attributeFrom ) ) - arrayTo: npt.NDArray[ np.float64 ] = vnp.vtk_to_numpy( blockTo.GetCellData().GetArray( attributeTo ) ) + if onPoint: + arrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( blockFrom.GetPointData().GetArray( attributeFrom ) ) + arrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( blockTo.GetPointData().GetArray( attributeTo ) ) + + typeArrayFrom: int = blockFrom.GetPointData().GetArray( attributeFrom ).GetDataType() + typeArrayTo: int = blockTo.GetPointData().GetArray( attributeTo ).GetDataType() + + else: + arrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( blockFrom.GetCellData().GetArray( attributeFrom ) ) + arrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( blockTo.GetCellData().GetArray( attributeTo ) ) + + typeArrayFrom: int = blockFrom.GetCellData().GetArray( attributeFrom ).GetDataType() + typeArrayTo: int = blockTo.GetCellData().GetArray( attributeTo ).GetDataType() assert ( arrayFrom == arrayTo ).all() + assert ( typeArrayFrom == typeArrayTo ) -def test_copyAttributeDataSet( dataSetTest: vtkDataSet, ) -> None: - """Test copy of cell attribute from one dataset to another.""" +@pytest.mark.parametrize( "attributeNameFrom, attributeNameTo, onPoint", [ + ( "CellAttribute", "CellAttributeTo", False ), + ( "PointAttribute", "PointAttributeTo", True ), +] ) +def test_copyAttributeDataSet( dataSetTest: vtkDataSet, attributeNameFrom:str, attributeNameTo: str, onPoint: bool ) -> None: + """Test copy of an attribute from one dataset to another.""" objectFrom: vtkDataSet = dataSetTest( "dataset" ) - objectTo: vtkDataSet = dataSetTest( "dataset" ) + objectTo: vtkDataSet = dataSetTest( "emptydataset" ) - attributNameFrom = "CellAttribute" - attributNameTo = "COPYATTRIBUTETO" + arrayModifiers.copyAttributeDataSet( objectFrom, objectTo, attributeNameFrom, attributeNameTo, onPoint ) - arrayModifiers.copyAttributeDataSet( objectFrom, objectTo, attributNameFrom, attributNameTo ) + if onPoint: + arrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( objectFrom.GetPointData().GetArray( attributeNameFrom ) ) + arrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( objectTo.GetPointData().GetArray( attributeNameTo ) ) + + typeArrayFrom: int = objectFrom.GetPointData().GetArray( attributeNameFrom ).GetDataType() + typeArrayTo: int = objectTo.GetPointData().GetArray( attributeNameTo ).GetDataType() + else: + arrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( objectFrom.GetCellData().GetArray( attributeNameFrom ) ) + arrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( objectTo.GetCellData().GetArray( attributeNameTo ) ) - arrayFrom: npt.NDArray[ np.float64 ] = vnp.vtk_to_numpy( objectFrom.GetCellData().GetArray( attributNameFrom ) ) - arrayTo: npt.NDArray[ np.float64 ] = vnp.vtk_to_numpy( objectTo.GetCellData().GetArray( attributNameTo ) ) + typeArrayFrom: int = objectFrom.GetCellData().GetArray( attributeNameFrom ).GetDataType() + typeArrayTo: int = objectTo.GetCellData().GetArray( attributeNameTo ).GetDataType() assert ( arrayFrom == arrayTo ).all() + assert ( typeArrayFrom == typeArrayTo ) @pytest.mark.parametrize( "attributeName, onpoints", [ From ec0302f863938c7e9a49522e5505adadb4de222d Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 24 Jun 2025 17:12:12 +0200 Subject: [PATCH 03/56] update the typing in the test of the function createAttribute --- geos-mesh/tests/test_arrayModifiers.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index cf0903aa..02666115 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -167,23 +167,24 @@ def test_createConstantAttributeDataSet( assert cnames == componentNames -@pytest.mark.parametrize( "onpoints, arrayTest, arrayExpected", [ - ( True, 4092, "random_4092" ), - ( False, 1740, "random_1740" ), +@pytest.mark.parametrize( "onpoints, arrayTest, arrayExpected, arrayTypeTest", [ + ( True, 4092, "random_4092", VTK_DOUBLE ), + ( False, 1740, "random_1740", VTK_DOUBLE ), ], indirect=[ "arrayTest", "arrayExpected" ] ) def test_createAttribute( dataSetTest: vtkDataSet, - arrayTest: npt.NDArray[ np.float64 ], - arrayExpected: npt.NDArray[ np.float64 ], + arrayTest: npt.NDArray[ any ], + arrayExpected: npt.NDArray[ any ], onpoints: bool, + arrayTypeTest: int, ) -> None: """Test creation of dataset in dataset from given array.""" vtkDataSetTest: vtkDataSet = dataSetTest( "dataset" ) componentNames: tuple[ str, str, str ] = ( "XX", "YY", "ZZ" ) attributeName: str = "AttributeName" - arrayModifiers.createAttribute( vtkDataSetTest, arrayTest, attributeName, componentNames, onpoints ) + arrayModifiers.createAttribute( vtkDataSetTest, arrayTest, attributeName, componentNames, onpoints, arrayTypeTest ) data: Union[ vtkPointData, vtkCellData ] if onpoints: @@ -191,11 +192,13 @@ def test_createAttribute( else: data = vtkDataSetTest.GetCellData() - createdAttribute: vtkDoubleArray = data.GetArray( attributeName ) + createdAttribute: vtkDataArray = data.GetArray( attributeName ) cnames: Tuple[ str, ...] = tuple( createdAttribute.GetComponentName( i ) for i in range( 3 ) ) + arrayTypeObtained: int = createdAttribute.GetDataType() assert ( vnp.vtk_to_numpy( createdAttribute ) == arrayExpected ).all() assert cnames == componentNames + assert arrayTypeTest == arrayTypeObtained @pytest.mark.parametrize( "attributeFrom, attributeTo, onPoint, idBlock", [ From 6c501c4191f4eca4b6a795970b182b704a6f03b4 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Thu, 26 Jun 2025 11:33:27 +0200 Subject: [PATCH 04/56] Update createAttribute and createConstantAttributeDataSet --- .../src/geos/mesh/utils/arrayModifiers.py | 135 ++++--- geos-mesh/tests/conftest.py | 30 ++ geos-mesh/tests/test_arrayModifiers.py | 357 ++++++++++++++---- 3 files changed, 402 insertions(+), 120 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index 6f73df08..fdac32c5 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -145,13 +145,11 @@ def createConstantAttribute( Args: object (vtkDataObject): object (vtkMultiBlockDataSet, vtkDataSet) - where to create the attribute - values ( list[float]): list of values of the attribute for each components - attributeName (str): name of the attribute - componentNames (tuple[str,...]): name of the components for vectorial - attributes - onPoints (bool): True if attributes are on points, False if they are - on cells. + where to create the attribute. + values ( list[float]): list of values of the attribute for each components. + attributeName (str): name of the attribute. + componentNames (tuple[str,...]): name of the components for vectorial attributes. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: bool: True if the attribute was correctly created @@ -168,25 +166,30 @@ def createConstantAttribute( def createConstantAttributeMultiBlock( multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet ], - values: list[ float ], + values: list[ any ], attributeName: str, componentNames: tuple[ str, ...], onPoints: bool, + vtkArrayType: Union[ int, any ] = None, ) -> bool: """Create an attribute with a constant value everywhere if absent. Args: multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet): vtkMultiBlockDataSet - where to create the attribute - values (list[float]): list of values of the attribute for each components - attributeName (str): name of the attribute - componentNames (tuple[str,...]): name of the components for vectorial - attributes - onPoints (bool): True if attributes are on points, False if they are - on cells. + where to create the attribute. + values (list[any]): list of values of the attribute for each components. + attributeName (str): name of the attribute. + componentNames (tuple[str,...]): name of the components for vectorial attributes. + onPoints (bool): True if attributes are on points, False if they are on cells. + vtkArrayType (Union(any, int), optional): vtk type of the array of the attribute to create. + Defaults to None, the type is given by the type of the array value. + Waring with int8, uint8 and int64 type of value, several vtk array type use it by default: + int8 -> VTK_SIGNED_CHAR + uint8 -> VTK_UNSIGNED_CHAR + int64 -> VTK_LONG_LONG Returns: - bool: True if the attribute was correctly created + bool: True if the attribute was correctly created. """ # initialize data object tree iterator iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() @@ -197,38 +200,50 @@ def createConstantAttributeMultiBlock( dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) listAttributes: set[ str ] = getAttributeSet( dataSet, onPoints ) if attributeName not in listAttributes: - createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints ) + createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkArrayType ) iter.GoToNextItem() return True def createConstantAttributeDataSet( dataSet: vtkDataSet, - values: list[ float ], + values: list[ any ], attributeName: str, - componentNames: tuple[ str, ...], - onPoints: bool, + componentNames: tuple[ str, ...] = (), + onPoints: bool = False, + vtkArrayType: Union[ int, any ] = None, ) -> bool: """Create an attribute with a constant value everywhere. Args: - dataSet (vtkDataSet): vtkDataSet where to create the attribute - values ( list[float]): list of values of the attribute for each components - attributeName (str): name of the attribute - componentNames (tuple[str,...]): name of the components for vectorial - attributes - onPoints (bool): True if attributes are on points, False if they are - on cells. + dataSet (vtkDataSet): vtkDataSet where to create the attribute. + values ( list[any]): list of values of the attribute for each components. + attributeName (str): name of the attribute. + componentNames (tuple[str,...], optional): name of the components for vectorial attributes. If one component, give an empty tuple. + Defaults to an empty tuple. + onPoints (bool): True if attributes are on points, False if they are on cells. + Defaults to False. + vtkArrayType (Union(any, int), optional): vtk type of the array of the attribute to create. + Defaults to None, the type is given by the type of the array value. + Waring with int8, uint8 and int64 type of value, several vtk array type use it by default: + int8 -> VTK_SIGNED_CHAR + uint8 -> VTK_UNSIGNED_CHAR + int64 -> VTK_LONG_LONG Returns: - bool: True if the attribute was correctly created + bool: True if the attribute was correctly created. """ nbElements: int = ( dataSet.GetNumberOfPoints() if onPoints else dataSet.GetNumberOfCells() ) + nbComponents: int = len( values ) - array: npt.NDArray[ np.float64 ] = np.ones( ( nbElements, nbComponents ) ) - for i, val in enumerate( values ): - array[ :, i ] *= val - createAttribute( dataSet, array, attributeName, componentNames, onPoints ) + array: npt.NDArray[ any ] + if nbComponents > 1: + array = np.array( [ [ val for val in values ] for _ in range( nbElements ) ] ) + else: + array = np.array( [ values[ 0 ] for _ in range( nbElements ) ] ) + + createAttribute( dataSet, array, attributeName, componentNames, onPoints, vtkArrayType ) + return True @@ -236,20 +251,26 @@ def createAttribute( dataSet: vtkDataSet, array: npt.NDArray[ any ], attributeName: str, - componentNames: tuple[ str, ...], - onPoints: bool, - vtkArrayType: int = VTK_DOUBLE, + componentNames: tuple[ str, ...] = (), + onPoints: bool = False, + vtkArrayType: Union[ int, any ] = None, ) -> bool: - """Create an attribute from the given array. + """Create an attribute and its VTK array from the given array. Args: dataSet (vtkDataSet): dataSet where to create the attribute. - array (npt.NDArray[np.float64]): array that contains the values. + array (npt.NDArray[any]): array that contains the values. attributeName (str): name of the attribute. - componentNames (tuple[str,...]): name of the components for vectorial attributes. + componentNames (tuple[str,...], optional): name of the components for vectorial attributes. If one component, give an empty tuple. + Defaults to an empty tuple. onPoints (bool): True if attributes are on points, False if they are on cells. - vtkArrayType (int): vtk type of the array of the attribute to create. - Defaults to VTK_DOUBLE + Defaults to False. + vtkArrayType (Union(any, int), optional): vtk type of the array of the attribute to create. + Defaults to None, the type is given by the type of the array value. + Waring with int8, uint8 and int64 type of value, several vtk array type use it. By default: + int8 -> VTK_SIGNED_CHAR + uint8 -> VTK_UNSIGNED_CHAR + int64 -> VTK_LONG_LONG Returns: bool: True if the attribute was correctly created. @@ -261,6 +282,14 @@ def createAttribute( nbComponents: int = newAttr.GetNumberOfComponents() if nbComponents > 1: + nbNames = len( componentNames ) + + if nbNames < nbComponents : + componentNames = tuple( [ "Component" + str( i ) for i in range( nbComponents ) ] ) + print( "Not enough component name enter, component names are seted to : Component0, Component1 ..." ) + elif nbNames > nbComponents: + print( "To many component names enter, the lastest will not be taken into account." ) + for i in range( nbComponents ): newAttr.SetComponentName( i, componentNames[ i ] ) @@ -276,8 +305,8 @@ def createAttribute( def copyAttribute( objectFrom: vtkMultiBlockDataSet, objectTo: vtkMultiBlockDataSet, - attributNameFrom: str, - attributNameTo: str, + attributeNameFrom: str, + attributeNameTo: str, onPoint: bool = False, ) -> bool: """Copy an attribute from objectFrom to objectTo. @@ -285,8 +314,8 @@ def copyAttribute( Args: objectFrom (vtkMultiBlockDataSet): object from which to copy the attribute. objectTo (vtkMultiBlockDataSet): object where to copy the attribute. - attributNameFrom (str): attribute name in objectFrom. - attributNameTo (str): attribute name in objectTo. + attributeNameFrom (str): attribute name in objectFrom. + attributeNameTo (str): attribute name in objectTo. onPoint (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. @@ -309,7 +338,7 @@ def copyAttribute( assert block is not None, "Block at current time step is null." try: - copyAttributeDataSet( blockT0, block, attributNameFrom, attributNameTo, onPoint ) + copyAttributeDataSet( blockT0, block, attributeNameFrom, attributeNameTo, onPoint ) except AssertionError: # skip attribute if not in block continue @@ -320,8 +349,8 @@ def copyAttribute( def copyAttributeDataSet( objectFrom: vtkDataSet, objectTo: vtkDataSet, - attributNameFrom: str, - attributNameTo: str, + attributeNameFrom: str, + attributeNameTo: str, onPoint: bool = False, ) -> bool: """Copy an attribute from objectFrom to objectTo. @@ -329,8 +358,8 @@ def copyAttributeDataSet( Args: objectFrom (vtkDataSet): object from which to copy the attribute. objectTo (vtkDataSet): object where to copy the attribute. - attributNameFrom (str): attribute name in objectFrom. - attributNameTo (str): attribute name in objectTo. + attributeNameFrom (str): attribute name in objectFrom. + attributeNameTo (str): attribute name in objectTo. onPoint (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. @@ -338,12 +367,12 @@ def copyAttributeDataSet( bool: True if copy successfully ended, False otherwise. """ # get attribut from initial time step block - npArray: npt.NDArray[ any ] = getArrayInObject( objectFrom, attributNameFrom, onPoint ) + npArray: npt.NDArray[ any ] = getArrayInObject( objectFrom, attributeNameFrom, onPoint ) assert npArray is not None - componentNames: tuple[ str, ...] = getComponentNames( objectFrom, attributNameFrom, onPoint ) - arrayType: int = getVtkArrayTypeInObject( objectFrom, attributNameFrom, onPoint ) + componentNames: tuple[ str, ...] = getComponentNames( objectFrom, attributeNameFrom, onPoint ) + vtkArrayType: int = getVtkArrayTypeInObject( objectFrom, attributeNameFrom, onPoint ) # copy attribut to current time step block - createAttribute( objectTo, npArray, attributNameTo, componentNames, onPoint, arrayType ) + createAttribute( objectTo, npArray, attributeNameTo, componentNames, onPoint, vtkArrayType ) objectTo.Modified() return True diff --git a/geos-mesh/tests/conftest.py b/geos-mesh/tests/conftest.py index 29cad120..50c9964f 100644 --- a/geos-mesh/tests/conftest.py +++ b/geos-mesh/tests/conftest.py @@ -31,6 +31,36 @@ def arrayTest( request: pytest.FixtureRequest ) -> npt.NDArray[ np.float64 ]: ) return array +@pytest.fixture +def getArrayWithSpeTypeValue() -> npt.NDArray[ any ]: + def _getarray( nb_component: int, nb_elements: int, valueType: str ) : + if valueType == "int32": + if nb_component == 1: + return np.array( [ np.int32( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) + else: + return np.array( [ [ np.int32( 1000 * np.random.random() ) for _ in range( nb_component ) ] for _ in range( nb_elements ) ] ) + + + elif valueType == "int64": + if nb_component == 1: + return np.array( [ np.int64( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) + else: + return np.array( [ [ np.int64( 1000 * np.random.random() ) for _ in range( nb_component ) ] for _ in range( nb_elements ) ] ) + + elif valueType == "float32": + if nb_component == 1: + return np.array( [ np.float32( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) + else: + return np.array( [ [ np.float32( 1000 * np.random.random() ) for _ in range( nb_component ) ] for _ in range( nb_elements ) ] ) + + elif valueType == "float64": + if nb_component == 1: + return np.array( [ np.float64( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) + else: + return np.array( [ [ np.float64( 1000 * np.random.random() ) for _ in range( nb_component ) ] for _ in range( nb_elements ) ] ) + + return _getarray + @pytest.fixture def dataSetTest() -> Union[ vtkMultiBlockDataSet, vtkPolyData, vtkDataSet ]: diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index 02666115..67d62645 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -15,10 +15,34 @@ from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet, vtkDataObjectTreeIterator, vtkPointData, vtkCellData ) +from vtkmodules.vtkIOXML import vtkXMLMultiBlockDataWriter, vtkXMLUnstructuredGridWriter + from vtk import ( # type: ignore[import-untyped] - VTK_CHAR, VTK_DOUBLE, VTK_FLOAT, VTK_INT, VTK_UNSIGNED_INT, + VTK_CHAR, VTK_DOUBLE, VTK_FLOAT, VTK_INT, VTK_UNSIGNED_INT, VTK_LONG_LONG, VTK_ID_TYPE, ) +# Information : +# vtk array type int numpy type +# VTK_CHAR = 2 = np.int8 +# VTK_SIGNED_CHAR = 15 = np.int8 +# VTK_SHORT = 4 = np.int16 +# VTK_INT = 6 = np.int32 +# VTK_BIT = 1 = np.uint8 +# VTK_UNSIGNED_CHAR = 3 = np.uint8 +# VTK_UNSIGNED_SHORT = 5 = np.uint16 +# VTK_UNSIGNED_INT = 7 = np.uint32 +# VTK_UNSIGNED_LONG_LONG = 17 = np.uint64 +# VTK_LONG = 8 = LONG_TYPE_CODE ( int32 | int64 ) +# VTK_UNSIGNED_LONG = 9 = ULONG_TYPE_CODE ( uint32 | uint64 ) +# VTK_FLOAT = 10 = np.float32 +# VTK_DOUBLE = 11 = np.float64 +# VTK_ID_TYPE = 12 = ID_TYPE_CODE ( int32 | int64 ) + +# vtk array type int IdType numpy type +# VTK_LONG_LONG = 16 = 2 = np.int64 + + + from geos.mesh.utils import arrayModifiers @@ -133,136 +157,335 @@ def test_createConstantAttributeMultiBlock( assert ( vnp.vtk_to_numpy( createdAttribute ) == np.full( ( elementSize[ iter.GetCurrentFlatIndex() - 1 ], 3 ), fill_value=values ) ).all() assert cnames == componentNames + assert ( vnp.vtk_to_numpy( createdAttribute ).dtype == "float64" ) iter.GoToNextItem() -@pytest.mark.parametrize( "values, onpoints, elementSize", [ - ( ( 42, 58, -103 ), True, 4092 ), - ( ( -42, -58, 103 ), False, 1740 ), +@pytest.mark.parametrize( "values, componentNames, componentNamesTest, onPoints, vtkArrayType, vtkArrayTypeTest, valueType", [ + ( [ np.float32( 42 ) ], (), (), True, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ) ], (), (), False, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ) ], (), (), True, None, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ) ], (), (), False, None, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], (), ( "Component0", "Component1" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], (), ( "Component0", "Component1" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], (), ( "Component0", "Component1" ), True, None, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], (), ( "Component0", "Component1" ), False, None, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_FLOAT, "float32" ), + ( [ np.float64( 42 ) ], (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ) ], (), (), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ) ], (), (), True, None, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ) ], (), (), False, None, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], (), ( "Component0", "Component1" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], (), ( "Component0", "Component1" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], (), ( "Component0", "Component1" ), True, None, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], (), ( "Component0", "Component1" ), False, None, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_DOUBLE, "float64" ), + ( [ np.int32( 42 ) ], (), (), True, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ) ], (), (), False, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ) ], (), (), True, None, VTK_INT, "int32" ), + ( [ np.int32( 42 ) ], (), (), False, None, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), True, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), False, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), True, None, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), False, None, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_INT, "int32" ), + ( [ np.int64( 42 ) ], (), (), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ) ], (), (), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ) ], (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ) ], (), (), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ) ], (), (), True, None, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ) ], (), (), False, None, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), True, None, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), False, None, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_LONG_LONG, "int64" ), ] ) def test_createConstantAttributeDataSet( dataSetTest: vtkDataSet, - values: list[ float ], - elementSize: int, - onpoints: bool, + values: list[ any ], + componentNames: Tuple[ str, ... ], + componentNamesTest: Tuple[ str, ... ], + onPoints: bool, + vtkArrayType: Union[ int, any ], + vtkArrayTypeTest: int, + valueType: str, ) -> None: """Test constant attribute creation in dataset.""" vtkDataSetTest: vtkDataSet = dataSetTest( "dataset" ) - componentNames: Tuple[ str, str, str ] = ( "XX", "YY", "ZZ" ) attributeName: str = "newAttributedataset" - arrayModifiers.createConstantAttributeDataSet( vtkDataSetTest, values, attributeName, componentNames, onpoints ) + arrayModifiers.createConstantAttributeDataSet( vtkDataSetTest, values, attributeName, componentNames, onPoints, vtkArrayType ) data: Union[ vtkPointData, vtkCellData ] - if onpoints: + nbElements: int + if onPoints: data = vtkDataSetTest.GetPointData() - + nbElements = vtkDataSetTest.GetNumberOfPoints() else: data = vtkDataSetTest.GetCellData() + nbElements = vtkDataSetTest.GetNumberOfCells() - createdAttribute: vtkDoubleArray = data.GetArray( attributeName ) - cnames: Tuple[ str, ...] = tuple( createdAttribute.GetComponentName( i ) for i in range( 3 ) ) - - assert ( vnp.vtk_to_numpy( createdAttribute ) == np.full( ( elementSize, 3 ), fill_value=values ) ).all() - assert cnames == componentNames + createdAttribute: vtkDataArray = data.GetArray( attributeName ) + nbComponents: int = len( values ) + nbComponentsCreated: int = createdAttribute.GetNumberOfComponents() + assert nbComponents == nbComponentsCreated -@pytest.mark.parametrize( "onpoints, arrayTest, arrayExpected, arrayTypeTest", [ - ( True, 4092, "random_4092", VTK_DOUBLE ), - ( False, 1740, "random_1740", VTK_DOUBLE ), -], - indirect=[ "arrayTest", "arrayExpected" ] ) + npArray: npt.NDArray[ any ] + if nbComponents > 1: + componentNamesCreated: Tuple[ str, ...] = tuple( createdAttribute.GetComponentName( i ) for i in range( nbComponents ) ) + assert componentNamesTest == componentNamesCreated + npArray = np.array( [ [ val for val in values ] for _ in range( nbElements ) ] ) + else: + npArray = np.array( [ values[ 0 ] for _ in range( nbElements ) ] ) + + npArraycreated: npt.NDArray[ any ] = vnp.vtk_to_numpy( createdAttribute ) + assert ( npArray == npArraycreated ).all() + assert valueType == npArraycreated.dtype + + vtkArrayTypeCreated: int = createdAttribute.GetDataType() + assert vtkArrayTypeTest == vtkArrayTypeCreated + + +@pytest.mark.parametrize( "componentNames, componentNamesTest, onPoints, vtkArrayType, vtkArrayTypeTest, valueType", [ + ( (), (), True, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( (), (), False, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( (), (), True, None, VTK_FLOAT, "float32" ), + ( (), (), False, None, VTK_FLOAT, "float32" ), + ( (), ( "Component0", "Component1" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( (), ( "Component0", "Component1" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( (), ( "Component0", "Component1" ), True, None, VTK_FLOAT, "float32" ), + ( (), ( "Component0", "Component1" ), False, None, VTK_FLOAT, "float32" ), + ( ( "X", "Y" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( ( "X", "Y" ), ( "X", "Y" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_FLOAT, "float32" ), + ( ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_FLOAT, "float32" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_FLOAT, "float32" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_FLOAT, "float32" ), + ( (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( (), (), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( (), (), True, None, VTK_DOUBLE, "float64" ), + ( (), (), False, None, VTK_DOUBLE, "float64" ), + ( (), ( "Component0", "Component1" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( (), ( "Component0", "Component1" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( (), ( "Component0", "Component1" ), True, None, VTK_DOUBLE, "float64" ), + ( (), ( "Component0", "Component1" ), False, None, VTK_DOUBLE, "float64" ), + ( ( "X", "Y" ), ( "X", "Y" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( ( "X", "Y" ), ( "X", "Y" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_DOUBLE, "float64" ), + ( ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_DOUBLE, "float64" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_DOUBLE, "float64" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_DOUBLE, "float64" ), + ( (), (), True, VTK_INT, VTK_INT, "int32" ), + ( (), (), False, VTK_INT, VTK_INT, "int32" ), + ( (), (), True, None, VTK_INT, "int32" ), + ( (), (), False, None, VTK_INT, "int32" ), + ( (), ( "Component0", "Component1" ), True, VTK_INT, VTK_INT, "int32" ), + ( (), ( "Component0", "Component1" ), False, VTK_INT, VTK_INT, "int32" ), + ( (), ( "Component0", "Component1" ), True, None, VTK_INT, "int32" ), + ( (), ( "Component0", "Component1" ), False, None, VTK_INT, "int32" ), + ( ( "X", "Y" ), ( "X", "Y" ), True, VTK_INT, VTK_INT, "int32" ), + ( ( "X", "Y" ), ( "X", "Y" ), False, VTK_INT, VTK_INT, "int32" ), + ( ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_INT, "int32" ), + ( ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_INT, "int32" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_INT, VTK_INT, "int32" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_INT, VTK_INT, "int32" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_INT, "int32" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_INT, "int32" ), + ( (), (), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( (), (), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( (), (), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( (), (), True, None, VTK_LONG_LONG, "int64" ), + ( (), (), False, None, VTK_LONG_LONG, "int64" ), + ( (), ( "Component0", "Component1" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( (), ( "Component0", "Component1" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( (), ( "Component0", "Component1" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( (), ( "Component0", "Component1" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( (), ( "Component0", "Component1" ), True, None, VTK_LONG_LONG, "int64" ), + ( (), ( "Component0", "Component1" ), False, None, VTK_LONG_LONG, "int64" ), + ( ( "X", "Y" ), ( "X", "Y" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( ( "X", "Y" ), ( "X", "Y" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( ( "X", "Y" ), ( "X", "Y" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( ( "X", "Y" ), ( "X", "Y" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_LONG_LONG, "int64" ), + ( ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_LONG_LONG, "int64" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_LONG_LONG, "int64" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_LONG_LONG, "int64" ), +] ) def test_createAttribute( dataSetTest: vtkDataSet, - arrayTest: npt.NDArray[ any ], - arrayExpected: npt.NDArray[ any ], - onpoints: bool, - arrayTypeTest: int, + getArrayWithSpeTypeValue: npt.NDArray[ any ], + componentNames: tuple[ str, ... ], + componentNamesTest: tuple[ str, ... ], + onPoints: bool, + vtkArrayType: int, + vtkArrayTypeTest: int, + valueType: str, ) -> None: """Test creation of dataset in dataset from given array.""" vtkDataSetTest: vtkDataSet = dataSetTest( "dataset" ) - componentNames: tuple[ str, str, str ] = ( "XX", "YY", "ZZ" ) attributeName: str = "AttributeName" - - arrayModifiers.createAttribute( vtkDataSetTest, arrayTest, attributeName, componentNames, onpoints, arrayTypeTest ) + nbComponents: int = ( 1 if len( componentNamesTest ) == 0 else len( componentNamesTest ) ) + nbElements: int = ( vtkDataSetTest.GetNumberOfPoints() if onPoints else vtkDataSetTest.GetNumberOfCells() ) + npArray: npt.NDArray[ any ] = getArrayWithSpeTypeValue( nbComponents, nbElements, valueType ) + arrayModifiers.createAttribute( vtkDataSetTest, npArray, attributeName, componentNames, onPoints, vtkArrayType ) data: Union[ vtkPointData, vtkCellData ] - if onpoints: + if onPoints: data = vtkDataSetTest.GetPointData() else: data = vtkDataSetTest.GetCellData() createdAttribute: vtkDataArray = data.GetArray( attributeName ) - cnames: Tuple[ str, ...] = tuple( createdAttribute.GetComponentName( i ) for i in range( 3 ) ) - arrayTypeObtained: int = createdAttribute.GetDataType() - assert ( vnp.vtk_to_numpy( createdAttribute ) == arrayExpected ).all() - assert cnames == componentNames - assert arrayTypeTest == arrayTypeObtained + nbComponentsCreated: int = createdAttribute.GetNumberOfComponents() + assert nbComponents == nbComponentsCreated + + if nbComponents > 1: + componentsNamesCreated: Tuple[ str, ...] = tuple( createdAttribute.GetComponentName( i ) for i in range( nbComponents ) ) + assert componentNamesTest == componentsNamesCreated + + npArraycreated: npt.NDArray[ any ] = vnp.vtk_to_numpy( createdAttribute ) + assert ( npArray == npArraycreated ).all() + assert valueType == npArraycreated.dtype + + vtkArrayTypeCreated: int = createdAttribute.GetDataType() + assert vtkArrayTypeTest == vtkArrayTypeCreated -@pytest.mark.parametrize( "attributeFrom, attributeTo, onPoint, idBlock", [ +@pytest.mark.parametrize( "attributeNameFrom, attributeNameTo, onPoints, idBlock", [ ( "PORO", "POROTo", False, 0 ), ( "CellAttribute", "CellAttributeTo", False, 0 ), ( "FAULT", "FAULTTo", False, 0 ), ( "PointAttribute", "PointAttributeTo", True, 0 ), ( "collocated_nodes", "collocated_nodesTo", True, 1 ), ] ) -def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet, attributeFrom:str, attributeTo: str, onPoint: bool, idBlock: int ) -> None: +def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet, attributeNameFrom:str, attributeNameTo: str, onPoints: bool, idBlock: int ) -> None: """Test copy of cell attribute from one multiblock to another.""" objectFrom: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) objectTo: vtkMultiBlockDataSet = dataSetTest( "emptymultiblock" ) - arrayModifiers.copyAttribute( objectFrom, objectTo, attributeFrom, attributeTo, onPoint ) + arrayModifiers.copyAttribute( objectFrom, objectTo, attributeNameFrom, attributeNameTo, onPoints ) blockIndex: int = idBlock blockFrom: vtkDataSet = cast( vtkDataSet, objectFrom.GetBlock( blockIndex ) ) blockTo: vtkDataSet = cast( vtkDataSet, objectTo.GetBlock( blockIndex ) ) - if onPoint: - arrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( blockFrom.GetPointData().GetArray( attributeFrom ) ) - arrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( blockTo.GetPointData().GetArray( attributeTo ) ) - - typeArrayFrom: int = blockFrom.GetPointData().GetArray( attributeFrom ).GetDataType() - typeArrayTo: int = blockTo.GetPointData().GetArray( attributeTo ).GetDataType() - + dataFrom: Union[ vtkPointData, vtkCellData ] + dataTo: Union[ vtkPointData, vtkCellData ] + if onPoints: + dataFrom = blockFrom.GetPointData() + dataTo = blockTo.GetPointData() else: - arrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( blockFrom.GetCellData().GetArray( attributeFrom ) ) - arrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( blockTo.GetCellData().GetArray( attributeTo ) ) + dataFrom = blockFrom.GetCellData() + dataTo = blockTo.GetCellData() + + attributeFrom: vtkDataArray = dataFrom.GetArray( attributeNameFrom ) + attributeTo: vtkDataArray = dataTo.GetArray( attributeNameTo ) - typeArrayFrom: int = blockFrom.GetCellData().GetArray( attributeFrom ).GetDataType() - typeArrayTo: int = blockTo.GetCellData().GetArray( attributeTo ).GetDataType() + nbComponentsFrom: int = attributeFrom.GetNumberOfComponents() + nbComponentsTo: int = attributeTo.GetNumberOfComponents() + assert nbComponentsFrom == nbComponentsTo - assert ( arrayFrom == arrayTo ).all() - assert ( typeArrayFrom == typeArrayTo ) + if nbComponentsFrom > 1: + componentsNamesFrom: Tuple[ str, ...] = tuple( attributeFrom.GetComponentName( i ) for i in range( nbComponentsFrom ) ) + componentsNamesTo: Tuple[ str, ...] = tuple( attributeTo.GetComponentName( i ) for i in range( nbComponentsTo ) ) + assert componentsNamesFrom == componentsNamesTo + npArrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( attributeFrom ) + npArrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( attributeTo ) + assert ( npArrayFrom == npArrayTo ).all() + assert npArrayFrom.dtype == npArrayTo.dtype -@pytest.mark.parametrize( "attributeNameFrom, attributeNameTo, onPoint", [ + vtkArrayTypeFrom: int = attributeFrom.GetDataType() + vtkArrayTypeTo: int = attributeTo.GetDataType() + assert vtkArrayTypeFrom == vtkArrayTypeTo + + +@pytest.mark.parametrize( "attributeNameFrom, attributeNameTo, onPoints", [ ( "CellAttribute", "CellAttributeTo", False ), ( "PointAttribute", "PointAttributeTo", True ), ] ) -def test_copyAttributeDataSet( dataSetTest: vtkDataSet, attributeNameFrom:str, attributeNameTo: str, onPoint: bool ) -> None: +def test_copyAttributeDataSet( dataSetTest: vtkDataSet, attributeNameFrom:str, attributeNameTo: str, onPoints: bool ) -> None: """Test copy of an attribute from one dataset to another.""" objectFrom: vtkDataSet = dataSetTest( "dataset" ) objectTo: vtkDataSet = dataSetTest( "emptydataset" ) - arrayModifiers.copyAttributeDataSet( objectFrom, objectTo, attributeNameFrom, attributeNameTo, onPoint ) - - if onPoint: - arrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( objectFrom.GetPointData().GetArray( attributeNameFrom ) ) - arrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( objectTo.GetPointData().GetArray( attributeNameTo ) ) + arrayModifiers.copyAttributeDataSet( objectFrom, objectTo, attributeNameFrom, attributeNameTo, onPoints ) - typeArrayFrom: int = objectFrom.GetPointData().GetArray( attributeNameFrom ).GetDataType() - typeArrayTo: int = objectTo.GetPointData().GetArray( attributeNameTo ).GetDataType() + dataFrom: Union[ vtkPointData, vtkCellData ] + dataTo: Union[ vtkPointData, vtkCellData ] + if onPoints: + dataFrom = objectFrom.GetPointData() + dataTo = objectTo.GetPointData() else: - arrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( objectFrom.GetCellData().GetArray( attributeNameFrom ) ) - arrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( objectTo.GetCellData().GetArray( attributeNameTo ) ) - - typeArrayFrom: int = objectFrom.GetCellData().GetArray( attributeNameFrom ).GetDataType() - typeArrayTo: int = objectTo.GetCellData().GetArray( attributeNameTo ).GetDataType() - - assert ( arrayFrom == arrayTo ).all() - assert ( typeArrayFrom == typeArrayTo ) + dataFrom = objectFrom.GetCellData() + dataTo = objectTo.GetCellData() + + attributeFrom: vtkDataArray = dataFrom.GetArray( attributeNameFrom ) + attributeTo: vtkDataArray = dataTo.GetArray( attributeNameTo ) + + nbComponentsFrom: int = attributeFrom.GetNumberOfComponents() + nbComponentsTo: int = attributeTo.GetNumberOfComponents() + assert nbComponentsFrom == nbComponentsTo + + if nbComponentsFrom > 1: + componentsNamesFrom: Tuple[ str, ...] = tuple( attributeFrom.GetComponentName( i ) for i in range( nbComponentsFrom ) ) + componentsNamesTo: Tuple[ str, ...] = tuple( attributeTo.GetComponentName( i ) for i in range( nbComponentsTo ) ) + assert componentsNamesFrom == componentsNamesTo + + vtkArrayTypeFrom: int = attributeFrom.GetDataType() + vtkArrayTypeTo: int = attributeTo.GetDataType() + assert vtkArrayTypeFrom == vtkArrayTypeTo + + npArrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( attributeFrom ) + npArrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( attributeTo ) + assert ( npArrayFrom == npArrayTo ).all() + assert npArrayFrom.dtype == npArrayTo.dtype @pytest.mark.parametrize( "attributeName, onpoints", [ From 490135c24973d7a1520d738e7abaa997fa6b39cc Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 27 Jun 2025 15:17:02 +0200 Subject: [PATCH 05/56] update fillPartialAttribute and fillAllPartialAttributes --- .../src/geos/mesh/utils/arrayModifiers.py | 275 +++++++++------- geos-mesh/tests/test_arrayModifiers.py | 299 +++++++++++------- 2 files changed, 336 insertions(+), 238 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index fdac32c5..52979738 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -1,24 +1,30 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. -# SPDX-FileContributor: Martin Lemay, Alexandre Benedicto, Paloma Martinez +# SPDX-FileContributor: Martin Lemay, Alexandre Benedicto, Paloma Martinez, Romain Baville import numpy as np import numpy.typing as npt import vtkmodules.util.numpy_support as vnp from typing import Union -from vtkmodules.vtkCommonDataModel import ( vtkMultiBlockDataSet, vtkDataSet, vtkPointSet, vtkCompositeDataSet, - vtkDataObject, vtkDataObjectTreeIterator ) -from vtkmodules.vtkFiltersCore import vtkArrayRename, vtkCellCenters, vtkPointDataToCellData from vtk import ( # type: ignore[import-untyped] - VTK_CHAR, VTK_DOUBLE, VTK_FLOAT, VTK_INT, VTK_UNSIGNED_INT, + VTK_DOUBLE, + VTK_FLOAT, +) +from vtkmodules.vtkCommonDataModel import ( + vtkMultiBlockDataSet, + vtkDataSet, + vtkPointSet, + vtkCompositeDataSet, + vtkDataObject, + vtkDataObjectTreeIterator, +) +from vtkmodules.vtkFiltersCore import ( + vtkArrayRename, + vtkCellCenters, + vtkPointDataToCellData, ) from vtkmodules.vtkCommonCore import ( - vtkCharArray, vtkDataArray, - vtkDoubleArray, - vtkFloatArray, - vtkIntArray, vtkPoints, - vtkUnsignedIntArray, ) from geos.mesh.utils.arrayHelpers import ( getComponentNames, @@ -27,8 +33,12 @@ getArrayInObject, isAttributeInObject, getVtkArrayTypeInObject, + getVtkArrayTypeInMultiBlock, +) +from geos.mesh.utils.multiblockHelpers import ( + getBlockElementIndexesFlatten, + getBlockFromFlatIndex, ) -from geos.mesh.utils.multiblockHelpers import getBlockElementIndexesFlatten, getBlockFromFlatIndex __doc__ = """ ArrayModifiers contains utilities to process VTK Arrays objects. @@ -40,127 +50,150 @@ """ -def fillPartialAttributes( multiBlockMesh: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], - attributeName: str, - nbComponents: int, - onPoints: bool = False, - value: float = np.nan, +def fillPartialAttributes( + multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], + attributeName: str, + onPoints: bool = False, + value: any = np.nan, ) -> bool: - """Fill input partial attribute of multiBlockMesh with values (defaults to nan). + """Fill input partial attribute of multiBlockDataSet with the same value for all the components. Args: - multiBlockMesh (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): multiBlock - mesh where to fill the attribute. + multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): multiBlockDataSet where to fill the attribute. attributeName (str): attribute name. - nbComponents (int): number of components. onPoints (bool, optional): Attribute is on Points (True) or on Cells (False). Defaults to False. - value (float, optional): value to fill in the partial atribute. - Defaults to nan. + value (any, optional): value to fill in the partial atribute. + Defaults to nan. For int vtk array, default value is automatically set to -1. Returns: - bool: True if calculation successfully ended, False otherwise. + bool: True if calculation successfully ended. """ + vtkArrayType: int = getVtkArrayTypeInMultiBlock( multiBlockDataSet, attributeName, onPoints ) + assert vtkArrayType != -1 + + infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints ) + nbComponents: int = infoAttributes[ attributeName ] + componentNames: tuple[ str, ...] = () if nbComponents > 1: - componentNames = getComponentNames( multiBlockMesh, attributeName, onPoints ) - values: list[ float ] = [ value for _ in range( nbComponents ) ] - createConstantAttribute( multiBlockMesh, values, attributeName, componentNames, onPoints ) - multiBlockMesh.Modified() + componentNames = getComponentNames( multiBlockDataSet, attributeName, onPoints ) + + valueType: str = type( value ) + typeMapping: dict[ int, any ] = vnp.get_vtk_to_numpy_typemap() + valueTypeExpected: any = typeMapping[ vtkArrayType ] + if valueTypeExpected != valueType: + if np.isnan( value ): + if vtkArrayType == VTK_DOUBLE or vtkArrayType == VTK_FLOAT: + value = valueTypeExpected( value ) + else: + print( attributeName + " vtk array type is " + str( valueTypeExpected ) + ", default value is automatically set to -1." ) + value = valueTypeExpected( -1 ) + + else: + print( "The value has the wrong type, it is update to " + str( valueTypeExpected ) + ", the type of the " + attributeName + " array to fill." ) + value = valueTypeExpected( value ) + + values: list[ any ] = [ value for _ in range( nbComponents ) ] + + createConstantAttribute( multiBlockDataSet, values, attributeName, componentNames, onPoints, vtkArrayType ) + multiBlockDataSet.Modified() + return True -def fillAllPartialAttributes( multiBlockMesh: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], - onPoints: bool = False, - value: float = np.nan, +def fillAllPartialAttributes( + multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], + value: any = np.nan, ) -> bool: - """Fill all the partial attributes of multiBlockMesh with values (defaults to nan). + """Fill all the partial attributes of multiBlockDataSet with same value for all attributes and they components. Args: - multiBlockMesh (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): - multiBlockMesh where to fill the attribute - onPoints (bool, optional): Attribute is on Points (True) or on Cells (False). - Defaults to False. - value (float, optional): value to fill in all the partial atributes. - Defaults to nan. + multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): multiBlockDataSet where to fill the attribute. + value (any, optional): value to fill in the partial atribute. + Defaults to nan. For int vtk array, default value is automatically set to -1. Returns: - bool: True if calculation successfully ended, False otherwise + bool: True if calculation successfully ended. """ - attributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockMesh, onPoints ) - for attributeName, nbComponents in attributes.items(): - fillPartialAttributes( multiBlockMesh, attributeName, nbComponents, onPoints, value ) - multiBlockMesh.Modified() + for onPoints in [ True, False ]: + infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints ) + for attributeName in infoAttributes.keys(): + fillPartialAttributes( multiBlockDataSet, attributeName, onPoints, value ) + + multiBlockDataSet.Modified() + return True def createEmptyAttribute( attributeName: str, componentNames: tuple[ str, ...], - dataType: int, + vtkDataType: int, ) -> vtkDataArray: """Create an empty attribute. Args: attributeName (str): name of the attribute - componentNames (tuple[str,...]): name of the components for vectorial - attributes - dataType (int): data type. + componentNames (tuple[str,...]): name of the components for vectorial attributes. + vtkDataType (int): data type. Returns: - bool: True if the attribute was correctly created + bool: True if the attribute was correctly created. """ - # create empty array - newAttr: vtkDataArray - if dataType == VTK_DOUBLE: - newAttr = vtkDoubleArray() - elif dataType == VTK_FLOAT: - newAttr = vtkFloatArray() - elif dataType == VTK_INT: - newAttr = vtkIntArray() - elif dataType == VTK_UNSIGNED_INT: - newAttr = vtkUnsignedIntArray() - elif dataType == VTK_CHAR: - newAttr = vtkCharArray() - else: + vtkDataTypeOk: dict = vnp.get_vtk_to_numpy_typemap() + if vtkDataType not in vtkDataTypeOk.keys(): raise ValueError( "Attribute type is unknown." ) + + nbComponents: int = len( componentNames ) - newAttr.SetName( attributeName ) - newAttr.SetNumberOfComponents( len( componentNames ) ) - if len( componentNames ) > 1: - for i in range( len( componentNames ) ): - newAttr.SetComponentName( i, componentNames[ i ] ) + createdAttribute: vtkDataArray = vtkDataArray.CreateDataArray( vtkDataType ) + createdAttribute.SetName( attributeName ) + createdAttribute.SetNumberOfComponents( nbComponents ) + if nbComponents > 1: + for i in range( nbComponents ): + createdAttribute.SetComponentName( i, componentNames[ i ] ) - return newAttr + return createdAttribute def createConstantAttribute( object: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], values: list[ float ], attributeName: str, - componentNames: tuple[ str, ...], - onPoints: bool, + componentNames: tuple[ str, ...] = (), + onPoints: bool = False, + vtkDataType: Union[ int, any ] = None, ) -> bool: """Create an attribute with a constant value everywhere if absent. Args: - object (vtkDataObject): object (vtkMultiBlockDataSet, vtkDataSet) - where to create the attribute. + object (vtkDataObject): object (vtkMultiBlockDataSet, vtkDataSet) where to create the attribute. values ( list[float]): list of values of the attribute for each components. attributeName (str): name of the attribute. - componentNames (tuple[str,...]): name of the components for vectorial attributes. + componentNames (tuple[str,...], optional): name of the components for vectorial attributes. If one component, give an empty tuple. + Defaults to an empty tuple. onPoints (bool): True if attributes are on points, False if they are on cells. + Defaults to False. + vtkDataType (Union(any, int), optional): vtk data type of the attribute to create. + Defaults to None, the type is given by the type of the array value. + Waring with int8, uint8 and int64 type of value, several vtk array type use it by default: + int8 -> VTK_SIGNED_CHAR + uint8 -> VTK_UNSIGNED_CHAR + int64 -> VTK_LONG_LONG Returns: - bool: True if the attribute was correctly created + bool: True if the attribute was correctly created False if the attribute was already present. """ if isinstance( object, ( vtkMultiBlockDataSet, vtkCompositeDataSet ) ): - return createConstantAttributeMultiBlock( object, values, attributeName, componentNames, onPoints ) + return createConstantAttributeMultiBlock( object, values, attributeName, componentNames, onPoints, vtkDataType ) + elif isinstance( object, vtkDataSet ): listAttributes: set[ str ] = getAttributeSet( object, onPoints ) if attributeName not in listAttributes: - return createConstantAttributeDataSet( object, values, attributeName, componentNames, onPoints ) - return True + return createConstantAttributeDataSet( object, values, attributeName, componentNames, onPoints, vtkDataType ) + print( "The attribute was already present in the vtkDataSet." ) + return False return False @@ -168,30 +201,33 @@ def createConstantAttributeMultiBlock( multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet ], values: list[ any ], attributeName: str, - componentNames: tuple[ str, ...], - onPoints: bool, - vtkArrayType: Union[ int, any ] = None, + componentNames: tuple[ str, ...] = (), + onPoints: bool = False, + vtkDataType: Union[ int, any ] = None, ) -> bool: """Create an attribute with a constant value everywhere if absent. Args: - multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet): vtkMultiBlockDataSet - where to create the attribute. + multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet): vtkMultiBlockDataSet where to create the attribute. values (list[any]): list of values of the attribute for each components. attributeName (str): name of the attribute. - componentNames (tuple[str,...]): name of the components for vectorial attributes. + componentNames (tuple[str,...], optional): name of the components for vectorial attributes. If one component, give an empty tuple. + Defaults to an empty tuple. onPoints (bool): True if attributes are on points, False if they are on cells. - vtkArrayType (Union(any, int), optional): vtk type of the array of the attribute to create. - Defaults to None, the type is given by the type of the array value. + Defaults to False. + vtkDataType (Union(any, int), optional): vtk data type of the attribute to create. + Defaults to None, the type is given by the type of the given value. Waring with int8, uint8 and int64 type of value, several vtk array type use it by default: int8 -> VTK_SIGNED_CHAR uint8 -> VTK_UNSIGNED_CHAR int64 -> VTK_LONG_LONG Returns: - bool: True if the attribute was correctly created. + bool: True if the attribute was correctly created, False if the attribute was already present. """ # initialize data object tree iterator + checkCreat: bool = False + iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() iter.SetDataSet( multiBlockDataSet ) iter.VisitOnlyLeavesOn() @@ -200,9 +236,15 @@ def createConstantAttributeMultiBlock( dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) listAttributes: set[ str ] = getAttributeSet( dataSet, onPoints ) if attributeName not in listAttributes: - createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkArrayType ) + checkCreat = createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkDataType ) + iter.GoToNextItem() - return True + + if checkCreat: + return True + else: + print( "The attribute was already present in the vtkMultiBlockDataSet." ) + return False def createConstantAttributeDataSet( @@ -211,7 +253,7 @@ def createConstantAttributeDataSet( attributeName: str, componentNames: tuple[ str, ...] = (), onPoints: bool = False, - vtkArrayType: Union[ int, any ] = None, + vtkDataType: Union[ int, any ] = None, ) -> bool: """Create an attribute with a constant value everywhere. @@ -223,8 +265,8 @@ def createConstantAttributeDataSet( Defaults to an empty tuple. onPoints (bool): True if attributes are on points, False if they are on cells. Defaults to False. - vtkArrayType (Union(any, int), optional): vtk type of the array of the attribute to create. - Defaults to None, the type is given by the type of the array value. + vtkDataType (Union(any, int), optional): vtk data type of the attribute to create. + Defaults to None, the type is given by the type of the given value. Waring with int8, uint8 and int64 type of value, several vtk array type use it by default: int8 -> VTK_SIGNED_CHAR uint8 -> VTK_UNSIGNED_CHAR @@ -242,9 +284,7 @@ def createConstantAttributeDataSet( else: array = np.array( [ values[ 0 ] for _ in range( nbElements ) ] ) - createAttribute( dataSet, array, attributeName, componentNames, onPoints, vtkArrayType ) - - return True + return createAttribute( dataSet, array, attributeName, componentNames, onPoints, vtkDataType ) def createAttribute( @@ -253,7 +293,7 @@ def createAttribute( attributeName: str, componentNames: tuple[ str, ...] = (), onPoints: bool = False, - vtkArrayType: Union[ int, any ] = None, + vtkDataType: Union[ int, any ] = None, ) -> bool: """Create an attribute and its VTK array from the given array. @@ -265,8 +305,8 @@ def createAttribute( Defaults to an empty tuple. onPoints (bool): True if attributes are on points, False if they are on cells. Defaults to False. - vtkArrayType (Union(any, int), optional): vtk type of the array of the attribute to create. - Defaults to None, the type is given by the type of the array value. + vtkDataType (Union(any, int), optional): vtk data type of the attribute to create. + Defaults to None, the type is given by the type of the given value in the array. Waring with int8, uint8 and int64 type of value, several vtk array type use it. By default: int8 -> VTK_SIGNED_CHAR uint8 -> VTK_UNSIGNED_CHAR @@ -277,10 +317,10 @@ def createAttribute( """ assert isinstance( dataSet, vtkDataSet ), "Attribute can only be created in vtkDataSet object." - newAttr: vtkDataArray = vnp.numpy_to_vtk( array, deep=True, array_type=vtkArrayType ) - newAttr.SetName( attributeName ) + createdAttribute: vtkDataArray = vnp.numpy_to_vtk( array, deep=True, array_type=vtkDataType ) + createdAttribute.SetName( attributeName ) - nbComponents: int = newAttr.GetNumberOfComponents() + nbComponents: int = createdAttribute.GetNumberOfComponents() if nbComponents > 1: nbNames = len( componentNames ) @@ -291,12 +331,13 @@ def createAttribute( print( "To many component names enter, the lastest will not be taken into account." ) for i in range( nbComponents ): - newAttr.SetComponentName( i, componentNames[ i ] ) + createdAttribute.SetComponentName( i, componentNames[ i ] ) if onPoints: - dataSet.GetPointData().AddArray( newAttr ) + dataSet.GetPointData().AddArray( createdAttribute ) else: - dataSet.GetCellData().AddArray( newAttr ) + dataSet.GetCellData().AddArray( createdAttribute ) + dataSet.Modified() return True @@ -307,7 +348,7 @@ def copyAttribute( objectTo: vtkMultiBlockDataSet, attributeNameFrom: str, attributeNameTo: str, - onPoint: bool = False, + onPoints: bool = False, ) -> bool: """Copy an attribute from objectFrom to objectTo. @@ -330,15 +371,15 @@ def copyAttribute( for index in elementaryBlockIndexesTo: # get block from initial time step object - blockT0: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( objectFrom, index ) ) - assert blockT0 is not None, "Block at initial time step is null." + blockFrom: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( objectFrom, index ) ) + assert blockFrom is not None, "Block at initial time step is null." # get block from current time step object - block: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( objectTo, index ) ) - assert block is not None, "Block at current time step is null." + blockTo: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( objectTo, index ) ) + assert blockTo is not None, "Block at current time step is null." try: - copyAttributeDataSet( blockT0, block, attributeNameFrom, attributeNameTo, onPoint ) + copyAttributeDataSet( blockFrom, blockTo, attributeNameFrom, attributeNameTo, onPoints ) except AssertionError: # skip attribute if not in block continue @@ -351,7 +392,7 @@ def copyAttributeDataSet( objectTo: vtkDataSet, attributeNameFrom: str, attributeNameTo: str, - onPoint: bool = False, + onPoints: bool = False, ) -> bool: """Copy an attribute from objectFrom to objectTo. @@ -360,19 +401,21 @@ def copyAttributeDataSet( objectTo (vtkDataSet): object where to copy the attribute. attributeNameFrom (str): attribute name in objectFrom. attributeNameTo (str): attribute name in objectTo. - onPoint (bool, optional): True if attributes are on points, False if they are on cells. + onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. Returns: bool: True if copy successfully ended, False otherwise. """ # get attribut from initial time step block - npArray: npt.NDArray[ any ] = getArrayInObject( objectFrom, attributeNameFrom, onPoint ) + npArray: npt.NDArray[ any ] = getArrayInObject( objectFrom, attributeNameFrom, onPoints ) assert npArray is not None - componentNames: tuple[ str, ...] = getComponentNames( objectFrom, attributeNameFrom, onPoint ) - vtkArrayType: int = getVtkArrayTypeInObject( objectFrom, attributeNameFrom, onPoint ) + + componentNames: tuple[ str, ...] = getComponentNames( objectFrom, attributeNameFrom, onPoints ) + vtkDataType: int = getVtkArrayTypeInObject( objectFrom, attributeNameFrom, onPoints ) + # copy attribut to current time step block - createAttribute( objectTo, npArray, attributeNameTo, componentNames, onPoint, vtkArrayType ) + createAttribute( objectTo, npArray, attributeNameTo, componentNames, onPoints, vtkDataType ) objectTo.Modified() return True @@ -387,9 +430,9 @@ def renameAttribute( """Rename an attribute. Args: - object (vtkMultiBlockDataSet): object where the attribute is - attributeName (str): name of the attribute - newAttributeName (str): new name of the attribute + object (vtkMultiBlockDataSet): object where the attribute is. + attributeName (str): name of the attribute. + newAttributeName (str): new name of the attribute. onPoints (bool): True if attributes are on points, False if they are on cells. Returns: diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index 67d62645..0ee7c569 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -1,27 +1,31 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. -# SPDX-FileContributor: Paloma Martinez +# SPDX-FileContributor: Paloma Martinez, Romain Baville # SPDX-License-Identifier: Apache 2.0 # ruff: noqa: E402 # disable Module level import not at top of file # mypy: disable-error-code="operator" import pytest -from typing import Union, Tuple, cast +from typing import Union, cast import numpy as np import numpy.typing as npt import vtkmodules.util.numpy_support as vnp -from vtkmodules.vtkCommonCore import vtkDataArray, vtkDoubleArray -from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet, vtkDataObjectTreeIterator, vtkPointData, - vtkCellData ) +from vtkmodules.vtkCommonCore import vtkDataArray +from vtkmodules.vtkCommonDataModel import ( + vtkDataSet, + vtkMultiBlockDataSet, + vtkPointData, + vtkCellData +) -from vtkmodules.vtkIOXML import vtkXMLMultiBlockDataWriter, vtkXMLUnstructuredGridWriter +from geos.mesh.utils.arrayHelpers import getAttributesWithNumberOfComponents from vtk import ( # type: ignore[import-untyped] VTK_CHAR, VTK_DOUBLE, VTK_FLOAT, VTK_INT, VTK_UNSIGNED_INT, VTK_LONG_LONG, VTK_ID_TYPE, ) -# Information : +# Informations : # vtk array type int numpy type # VTK_CHAR = 2 = np.int8 # VTK_SIGNED_CHAR = 15 = np.int8 @@ -46,59 +50,106 @@ from geos.mesh.utils import arrayModifiers -@pytest.mark.parametrize( "attributeName, nbComponents, onpoints, value_test", [ - ( "CellAttribute", 3, False, np.nan ), - ( "PointAttribute", 3, True, np.nan ), - ( "CELL_MARKERS", 1, False, np.nan ), - ( "PORO", 1, False, np.nan ), - ( "CellAttribute", 3, False, 2. ), - ( "PointAttribute", 3, True, 2. ), - ( "CELL_MARKERS", 1, False, 2. ), - ( "PORO", 1, False, 2. ), +@pytest.mark.parametrize( + "idBlockToFill, attributeName, nbComponentsRef, componentNamesRef, onPoints, value, valueRef, vtkDataTypeRef, valueTypeRef", [ + ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.nan, np.nan, VTK_DOUBLE, "float64" ), + ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.float64( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), + ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.int32( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), + ( 1, "PointAttribute", 3, ( "AX1", "AX2", "AX3" ), True, np.nan, np.nan, VTK_DOUBLE, "float64" ), + ( 1, "PointAttribute", 3, ( "AX1", "AX2", "AX3" ), True, np.float64( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), + ( 1, "PointAttribute", 3, ( "AX1", "AX2", "AX3" ), True, np.int32( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), + ( 1, "PORO", 1, (), False, np.nan, np.nan, VTK_FLOAT, "float32" ), + ( 1, "PORO", 1, (), False, np.float32( 4 ), np.float32( 4 ), VTK_FLOAT, "float32" ), + ( 1, "PORO", 1, (), False, np.int32( 4 ), np.float32( 4 ), VTK_FLOAT, "float32" ), + ( 1, "FAULT", 1, (), False, np.nan, np.int32( -1 ), VTK_INT, "int32" ), + ( 1, "FAULT", 1, (), False, np.int32( 4 ), np.int32( 4 ), VTK_INT, "int32" ), + ( 1, "FAULT", 1, (), False, np.float32( 4 ), np.int32( 4 ), VTK_INT, "int32" ), + ( 0, "collocated_nodes", 2, ( None, None ), True, np.nan, np.int64( -1 ), VTK_ID_TYPE, "int64" ), + ( 0, "collocated_nodes", 2, ( None, None ), True, np.int64( 4 ), np.int64( 4 ), VTK_ID_TYPE, "int64" ), + ( 0, "collocated_nodes", 2, ( None, None ), True, np.int32( 4 ), np.int64( 4 ), VTK_ID_TYPE, "int64" ), + ( 0, "collocated_nodes", 2, ( None, None ), True, np.float32( 4 ), np.int64( 4 ), VTK_ID_TYPE, "int64" ), ] ) def test_fillPartialAttributes( dataSetTest: vtkMultiBlockDataSet, + idBlockToFill: int, attributeName: str, - nbComponents: int, - onpoints: bool, - value_test: float, + nbComponentsRef: int, + componentNamesRef: tuple[ str, ... ], + onPoints: bool, + value: any, + valueRef: any, + vtkDataTypeRef: int, + valueTypeRef: str, ) -> None: """Test filling a partial attribute from a multiblock with values.""" - vtkMultiBlockDataSetTestRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - vtkMultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - arrayModifiers.fillPartialAttributes( vtkMultiBlockDataSetTest, - attributeName, - nbComponents, - onPoints=onpoints, - value=value_test ) - - nbBlock: int = vtkMultiBlockDataSetTestRef.GetNumberOfBlocks() - for block_id in range( nbBlock ): - datasetRef: vtkDataSet = cast( vtkDataSet, vtkMultiBlockDataSetTestRef.GetBlock( block_id ) ) - dataset: vtkDataSet = cast( vtkDataSet, vtkMultiBlockDataSetTest.GetBlock( block_id ) ) - expected_array: npt.NDArray[ np.float64 ] - array: npt.NDArray[ np.float64 ] - if onpoints: - array = vnp.vtk_to_numpy( dataset.GetPointData().GetArray( attributeName ) ) - if block_id == 0: - expected_array = vnp.vtk_to_numpy( datasetRef.GetPointData().GetArray( attributeName ) ) - else: - expected_array = np.array( [ [ value_test for i in range( nbComponents ) ] for _ in range( 212 ) ] ) - else: - array = vnp.vtk_to_numpy( dataset.GetCellData().GetArray( attributeName ) ) - if block_id == 0: - expected_array = vnp.vtk_to_numpy( datasetRef.GetCellData().GetArray( attributeName ) ) - else: - expected_array = np.array( [ [ value_test for i in range( nbComponents ) ] for _ in range( 156 ) ] ) + MultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + arrayModifiers.fillPartialAttributes( MultiBlockDataSetTest, attributeName, onPoints, value ) - if block_id == 0: - assert ( array == expected_array ).all() - else: - if np.isnan( value_test ): - assert np.all( np.isnan( array ) == np.isnan( expected_array ) ) - else: - assert ( array == expected_array ).all() + blockTest: vtkDataSet = cast( vtkDataSet, MultiBlockDataSetTest.GetBlock( idBlockToFill ) ) + dataTest: Union[ vtkPointData, vtkCellData ] + nbElements: int + if onPoints: + nbElements = blockTest.GetNumberOfPoints() + dataTest = blockTest.GetPointData() + else: + nbElements = blockTest.GetNumberOfCells() + dataTest = blockTest.GetCellData() + + attributeFillTest: vtkDataArray = dataTest.GetArray( attributeName ) + nbComponentsTest: int = attributeFillTest.GetNumberOfComponents() + assert nbComponentsRef == nbComponentsTest + + npArrayFillRef: npt.NDArray[ any ] + if nbComponentsRef > 1: + componentNamesTest: tuple[ str, ...] = tuple( attributeFillTest.GetComponentName( i ) for i in range( nbComponentsRef ) ) + assert componentNamesRef == componentNamesTest + + npArrayFillRef = np.array( [ [ valueRef for _ in range( nbComponentsRef ) ] for _ in range( nbElements ) ] ) + else: + npArrayFillRef = np.array( [ valueRef for _ in range( nbElements ) ] ) + npArrayFillTest: npt.NDArray[ any ] = vnp.vtk_to_numpy( attributeFillTest ) + assert valueTypeRef == npArrayFillTest.dtype + + + if np.isnan( valueRef ): + assert np.isnan( npArrayFillRef ).all() + else: + assert ( npArrayFillRef == npArrayFillTest ).all() + + vtkDataTypeTest: int = attributeFillTest.GetDataType() + assert vtkDataTypeRef == vtkDataTypeTest + +@pytest.mark.parametrize( "value", [ + ( np.nan ), + ( np.int32( 42 ) ), + ( np.int64( 42 ) ), + ( np.float32( 42 ) ), + ( np.float64( 42 ) ), +] ) +def test_FillAllPartialAttributes( + dataSetTest: vtkMultiBlockDataSet, + value: any, +) -> None: + """Test to fill all the partial attributes of a vtkMultiBlockDataSet with a value.""" + MultiBlockDataSetRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + MultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + arrayModifiers.fillAllPartialAttributes( MultiBlockDataSetTest, value ) + + nbBlock = MultiBlockDataSetRef.GetNumberOfBlocks() + for idBlock in range( nbBlock ): + datasetTest: vtkDataSet = cast( vtkDataSet, MultiBlockDataSetTest.GetBlock( idBlock ) ) + for onPoints in [True, False]: + infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( MultiBlockDataSetRef, onPoints ) + dataTest: Union[ vtkPointData, vtkCellData ] + if onPoints: + dataTest = datasetTest.GetPointData() + else: + dataTest = datasetTest.GetCellData() + + for attributeName in infoAttributes.keys(): + attributeTest: int = dataTest.HasArray( attributeName ) + assert attributeTest == 1 @pytest.mark.parametrize( "attributeName, dataType, expectedDatatypeArray", [ @@ -123,46 +174,50 @@ def test_createEmptyAttribute( assert newAttr.IsA( str( expectedDatatypeArray ) ) -@pytest.mark.parametrize( "onpoints, elementSize", [ - ( False, ( 1740, 156 ) ), - ( True, ( 4092, 212 ) ), +@pytest.mark.parametrize( "attributeName, isNewOnBlock, onPoints", [ + ( "newAttribute", ( True, True ), False ), + ( "newAttribute", ( True, True ), True ), + ( "PORO", ( True, True ), True ), + ( "PORO", ( False, True ), False ), + ( "PointAttribute", ( False, True ), True ), + ( "PointAttribute", ( True, True ), False ), + ( "collocated_nodes", ( True, False ), True ), + ( "collocated_nodes", ( True, True ), False ), ] ) def test_createConstantAttributeMultiBlock( dataSetTest: vtkMultiBlockDataSet, - onpoints: bool, - elementSize: Tuple[ int, ...], + attributeName: str, + isNewOnBlock: tuple[ bool, ... ], + onPoints: bool, ) -> None: """Test creation of constant attribute in multiblock dataset.""" - vtkMultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - attributeName: str = "testAttributemultiblock" - values: tuple[ float, float, float ] = ( 12.4, 10, 40.0 ) - componentNames: tuple[ str, str, str ] = ( "X", "Y", "Z" ) - arrayModifiers.createConstantAttributeMultiBlock( vtkMultiBlockDataSetTest, values, attributeName, componentNames, - onpoints ) - - iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() - iter.SetDataSet( vtkMultiBlockDataSetTest ) - iter.VisitOnlyLeavesOn() - iter.GoToFirstItem() - while iter.GetCurrentDataObject() is not None: - dataset: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) - data: Union[ vtkPointData, vtkCellData ] - if onpoints: - data = dataset.GetPointData() + MultiBlockDataSetRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + MultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + values: list[ float ] = [ np.nan ] + arrayModifiers.createConstantAttributeMultiBlock( MultiBlockDataSetTest, values, attributeName, onPoints=onPoints ) + + nbBlock = MultiBlockDataSetRef.GetNumberOfBlocks() + for idBlock in range( nbBlock ): + datasetRef: vtkDataSet = cast( vtkDataSet, MultiBlockDataSetRef.GetBlock( idBlock ) ) + datasetTest: vtkDataSet = cast( vtkDataSet, MultiBlockDataSetTest.GetBlock( idBlock ) ) + dataRef: Union[ vtkPointData, vtkCellData ] + dataTest: Union[ vtkPointData, vtkCellData ] + if onPoints: + dataRef = datasetRef.GetPointData() + dataTest = datasetTest.GetPointData() else: - data = dataset.GetCellData() - createdAttribute: vtkDoubleArray = data.GetArray( attributeName ) - cnames: Tuple[ str, ...] = tuple( createdAttribute.GetComponentName( i ) for i in range( 3 ) ) - - assert ( vnp.vtk_to_numpy( createdAttribute ) == np.full( ( elementSize[ iter.GetCurrentFlatIndex() - 1 ], 3 ), - fill_value=values ) ).all() - assert cnames == componentNames - assert ( vnp.vtk_to_numpy( createdAttribute ).dtype == "float64" ) + dataRef = datasetRef.GetCellData() + dataTest = datasetTest.GetCellData() - iter.GoToNextItem() + attributeRef: int = dataRef.HasArray( attributeName ) + attributeTest: int = dataTest.HasArray( attributeName ) + if isNewOnBlock[ idBlock ]: + assert attributeRef != attributeTest + else: + assert attributeRef == attributeTest -@pytest.mark.parametrize( "values, componentNames, componentNamesTest, onPoints, vtkArrayType, vtkArrayTypeTest, valueType", [ +@pytest.mark.parametrize( "values, componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, valueType", [ ( [ np.float32( 42 ) ], (), (), True, VTK_FLOAT, VTK_FLOAT, "float32" ), ( [ np.float32( 42 ) ], (), (), False, VTK_FLOAT, VTK_FLOAT, "float32" ), ( [ np.float32( 42 ) ], (), (), True, None, VTK_FLOAT, "float32" ), @@ -239,26 +294,26 @@ def test_createConstantAttributeMultiBlock( def test_createConstantAttributeDataSet( dataSetTest: vtkDataSet, values: list[ any ], - componentNames: Tuple[ str, ... ], - componentNamesTest: Tuple[ str, ... ], + componentNames: tuple[ str, ... ], + componentNamesTest: tuple[ str, ... ], onPoints: bool, - vtkArrayType: Union[ int, any ], - vtkArrayTypeTest: int, + vtkDataType: Union[ int, any ], + vtkDataTypeTest: int, valueType: str, ) -> None: """Test constant attribute creation in dataset.""" - vtkDataSetTest: vtkDataSet = dataSetTest( "dataset" ) + dataSet: vtkDataSet = dataSetTest( "dataset" ) attributeName: str = "newAttributedataset" - arrayModifiers.createConstantAttributeDataSet( vtkDataSetTest, values, attributeName, componentNames, onPoints, vtkArrayType ) + arrayModifiers.createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkDataType ) data: Union[ vtkPointData, vtkCellData ] nbElements: int if onPoints: - data = vtkDataSetTest.GetPointData() - nbElements = vtkDataSetTest.GetNumberOfPoints() + data = dataSet.GetPointData() + nbElements = dataSet.GetNumberOfPoints() else: - data = vtkDataSetTest.GetCellData() - nbElements = vtkDataSetTest.GetNumberOfCells() + data = dataSet.GetCellData() + nbElements = dataSet.GetNumberOfCells() createdAttribute: vtkDataArray = data.GetArray( attributeName ) @@ -268,8 +323,9 @@ def test_createConstantAttributeDataSet( npArray: npt.NDArray[ any ] if nbComponents > 1: - componentNamesCreated: Tuple[ str, ...] = tuple( createdAttribute.GetComponentName( i ) for i in range( nbComponents ) ) + componentNamesCreated: tuple[ str, ...] = tuple( createdAttribute.GetComponentName( i ) for i in range( nbComponents ) ) assert componentNamesTest == componentNamesCreated + npArray = np.array( [ [ val for val in values ] for _ in range( nbElements ) ] ) else: npArray = np.array( [ values[ 0 ] for _ in range( nbElements ) ] ) @@ -278,11 +334,11 @@ def test_createConstantAttributeDataSet( assert ( npArray == npArraycreated ).all() assert valueType == npArraycreated.dtype - vtkArrayTypeCreated: int = createdAttribute.GetDataType() - assert vtkArrayTypeTest == vtkArrayTypeCreated + vtkDataTypeCreated: int = createdAttribute.GetDataType() + assert vtkDataTypeTest == vtkDataTypeCreated -@pytest.mark.parametrize( "componentNames, componentNamesTest, onPoints, vtkArrayType, vtkArrayTypeTest, valueType", [ +@pytest.mark.parametrize( "componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, valueType", [ ( (), (), True, VTK_FLOAT, VTK_FLOAT, "float32" ), ( (), (), False, VTK_FLOAT, VTK_FLOAT, "float32" ), ( (), (), True, None, VTK_FLOAT, "float32" ), @@ -362,39 +418,39 @@ def test_createAttribute( componentNames: tuple[ str, ... ], componentNamesTest: tuple[ str, ... ], onPoints: bool, - vtkArrayType: int, - vtkArrayTypeTest: int, + vtkDataType: int, + vtkDataTypeTest: int, valueType: str, ) -> None: """Test creation of dataset in dataset from given array.""" - vtkDataSetTest: vtkDataSet = dataSetTest( "dataset" ) + dataSet: vtkDataSet = dataSetTest( "dataset" ) attributeName: str = "AttributeName" + nbComponents: int = ( 1 if len( componentNamesTest ) == 0 else len( componentNamesTest ) ) - nbElements: int = ( vtkDataSetTest.GetNumberOfPoints() if onPoints else vtkDataSetTest.GetNumberOfCells() ) + nbElements: int = ( dataSet.GetNumberOfPoints() if onPoints else dataSet.GetNumberOfCells() ) + npArray: npt.NDArray[ any ] = getArrayWithSpeTypeValue( nbComponents, nbElements, valueType ) - arrayModifiers.createAttribute( vtkDataSetTest, npArray, attributeName, componentNames, onPoints, vtkArrayType ) + arrayModifiers.createAttribute( dataSet, npArray, attributeName, componentNames, onPoints, vtkDataType ) data: Union[ vtkPointData, vtkCellData ] if onPoints: - data = vtkDataSetTest.GetPointData() + data = dataSet.GetPointData() else: - data = vtkDataSetTest.GetCellData() + data = dataSet.GetCellData() createdAttribute: vtkDataArray = data.GetArray( attributeName ) - nbComponentsCreated: int = createdAttribute.GetNumberOfComponents() assert nbComponents == nbComponentsCreated - if nbComponents > 1: - componentsNamesCreated: Tuple[ str, ...] = tuple( createdAttribute.GetComponentName( i ) for i in range( nbComponents ) ) + componentsNamesCreated: tuple[ str, ...] = tuple( createdAttribute.GetComponentName( i ) for i in range( nbComponents ) ) assert componentNamesTest == componentsNamesCreated npArraycreated: npt.NDArray[ any ] = vnp.vtk_to_numpy( createdAttribute ) assert ( npArray == npArraycreated ).all() assert valueType == npArraycreated.dtype - vtkArrayTypeCreated: int = createdAttribute.GetDataType() - assert vtkArrayTypeTest == vtkArrayTypeCreated + vtkDataTypeCreated: int = createdAttribute.GetDataType() + assert vtkDataTypeTest == vtkDataTypeCreated @pytest.mark.parametrize( "attributeNameFrom, attributeNameTo, onPoints, idBlock", [ @@ -411,9 +467,8 @@ def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet, attributeNameFrom:str arrayModifiers.copyAttribute( objectFrom, objectTo, attributeNameFrom, attributeNameTo, onPoints ) - blockIndex: int = idBlock - blockFrom: vtkDataSet = cast( vtkDataSet, objectFrom.GetBlock( blockIndex ) ) - blockTo: vtkDataSet = cast( vtkDataSet, objectTo.GetBlock( blockIndex ) ) + blockFrom: vtkDataSet = cast( vtkDataSet, objectFrom.GetBlock( idBlock ) ) + blockTo: vtkDataSet = cast( vtkDataSet, objectTo.GetBlock( idBlock ) ) dataFrom: Union[ vtkPointData, vtkCellData ] dataTo: Union[ vtkPointData, vtkCellData ] @@ -432,8 +487,8 @@ def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet, attributeNameFrom:str assert nbComponentsFrom == nbComponentsTo if nbComponentsFrom > 1: - componentsNamesFrom: Tuple[ str, ...] = tuple( attributeFrom.GetComponentName( i ) for i in range( nbComponentsFrom ) ) - componentsNamesTo: Tuple[ str, ...] = tuple( attributeTo.GetComponentName( i ) for i in range( nbComponentsTo ) ) + componentsNamesFrom: tuple[ str, ...] = tuple( attributeFrom.GetComponentName( i ) for i in range( nbComponentsFrom ) ) + componentsNamesTo: tuple[ str, ...] = tuple( attributeTo.GetComponentName( i ) for i in range( nbComponentsTo ) ) assert componentsNamesFrom == componentsNamesTo npArrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( attributeFrom ) @@ -441,9 +496,9 @@ def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet, attributeNameFrom:str assert ( npArrayFrom == npArrayTo ).all() assert npArrayFrom.dtype == npArrayTo.dtype - vtkArrayTypeFrom: int = attributeFrom.GetDataType() - vtkArrayTypeTo: int = attributeTo.GetDataType() - assert vtkArrayTypeFrom == vtkArrayTypeTo + vtkDataTypeFrom: int = attributeFrom.GetDataType() + vtkDataTypeTo: int = attributeTo.GetDataType() + assert vtkDataTypeFrom == vtkDataTypeTo @pytest.mark.parametrize( "attributeNameFrom, attributeNameTo, onPoints", [ @@ -474,13 +529,13 @@ def test_copyAttributeDataSet( dataSetTest: vtkDataSet, attributeNameFrom:str, a assert nbComponentsFrom == nbComponentsTo if nbComponentsFrom > 1: - componentsNamesFrom: Tuple[ str, ...] = tuple( attributeFrom.GetComponentName( i ) for i in range( nbComponentsFrom ) ) - componentsNamesTo: Tuple[ str, ...] = tuple( attributeTo.GetComponentName( i ) for i in range( nbComponentsTo ) ) + componentsNamesFrom: tuple[ str, ...] = tuple( attributeFrom.GetComponentName( i ) for i in range( nbComponentsFrom ) ) + componentsNamesTo: tuple[ str, ...] = tuple( attributeTo.GetComponentName( i ) for i in range( nbComponentsTo ) ) assert componentsNamesFrom == componentsNamesTo - vtkArrayTypeFrom: int = attributeFrom.GetDataType() - vtkArrayTypeTo: int = attributeTo.GetDataType() - assert vtkArrayTypeFrom == vtkArrayTypeTo + vtkDataTypeFrom: int = attributeFrom.GetDataType() + vtkDataTypeTo: int = attributeTo.GetDataType() + assert vtkDataTypeFrom == vtkDataTypeTo npArrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( attributeFrom ) npArrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( attributeTo ) From 15a67fa77c3d1eb5505c254c78a179ca41fd1472 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 27 Jun 2025 15:34:46 +0200 Subject: [PATCH 06/56] Add a function to get the vtk data type of an attribute of a multiblockdataset if it exist --- geos-mesh/src/geos/mesh/utils/arrayHelpers.py | 23 ++++++++++++++++++ geos-mesh/tests/test_arrayHelpers.py | 24 +++++++++++++++---- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index fe3a8618..6afe5e18 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -377,6 +377,29 @@ def getVtkArrayTypeInObject( object: vtkDataSet, attributeName: str, onPoints: return vtkArrayType +def getVtkArrayTypeInMultiBlock( multiBlockDataSet: vtkMultiBlockDataSet, attributeName: str, onPoints: bool ) -> int: + """Return the type of the vtk array corrsponding to input attribute name in the multiblock data set if it exist. + + Args: + object (PointSet or UnstructuredGrid): input object. + attributeName (str): name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. + + Returns: + int: type of the vtk array corrsponding to input attribute name, -1 if the multiblock has no attribute with given name. + """ + + nbBlocks = multiBlockDataSet.GetNumberOfBlocks() + for idBlock in range( nbBlocks ): + object: vtkDataSet = multiBlockDataSet.GetBlock( idBlock ) + listAttributes: set[ str ] = getAttributeSet( object, onPoints ) + if attributeName in listAttributes: + return getVtkArrayTypeInObject( object, attributeName, onPoints ) + + print( "The vtkMultiBlockDataSet has no attribute with the name " + attributeName + ".") + return -1 + + def getVtkArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> vtkDataArray: """Return the array corresponding to input attribute name in table. diff --git a/geos-mesh/tests/test_arrayHelpers.py b/geos-mesh/tests/test_arrayHelpers.py index b399b9a0..79182bcc 100644 --- a/geos-mesh/tests/test_arrayHelpers.py +++ b/geos-mesh/tests/test_arrayHelpers.py @@ -99,21 +99,37 @@ def test_getArrayInObject( request: pytest.FixtureRequest, arrayExpected: npt.ND assert ( obtained == expected ).all() -@pytest.mark.parametrize( "attributeName, onPoint", [ + +@pytest.mark.parametrize( "attributeName, vtkDataType, onPoints", [ + ( "CellAttribute", 11, False ), + ( "PointAttribute", 11, True ), + ( "collocated_nodes", 12, True ), + ( "collocated_nodes", -1, False ), + ( "newAttribute", -1, False ), +] ) +def test_getVtkArrayTypeInMultiBlock( dataSetTest: vtkMultiBlockDataSet, attributeName: str, + vtkDataType: int, onPoints: bool ) -> None: + """Test getting the type of the vtk array of an attribute from multiBlockDataSet.""" + multiBlockDataSet: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + + vtkDataTypeTest: int = arrayHelpers.getVtkArrayTypeInMultiBlock( multiBlockDataSet, attributeName, onPoints ) + + assert ( vtkDataType == vtkDataTypeTest ) + +@pytest.mark.parametrize( "attributeName, onPoints", [ ( "CellAttribute", False ), ( "PointAttribute", True ), ] ) -def test_getVtkArrayTypeInObject( dataSetTest: vtkDataSet, attributeName: str, onPoint: bool ) -> None: +def test_getVtkArrayTypeInObject( dataSetTest: vtkDataSet, attributeName: str, onPoints: bool ) -> None: """Test getting the type of the vtk array of an attribute from dataset.""" vtkDataSetTest: vtkDataSet = dataSetTest( "dataset" ) - obtained: int = arrayHelpers.getVtkArrayTypeInObject( vtkDataSetTest, attributeName, onPoint ) + obtained: int = arrayHelpers.getVtkArrayTypeInObject( vtkDataSetTest, attributeName, onPoints ) expected: int = 11 assert ( obtained == expected ) - @pytest.mark.parametrize( "arrayExpected, onpoints", [ ( "PORO", False ), ( "PointAttribute", True ), From 5b17644e49a8c0992d1ffe1012df70f28b7451dc Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 27 Jun 2025 17:21:12 +0200 Subject: [PATCH 07/56] Formating for the CI --- geos-mesh/src/geos/mesh/utils/arrayHelpers.py | 31 +- .../src/geos/mesh/utils/arrayModifiers.py | 117 +++--- geos-mesh/tests/conftest.py | 73 +++- geos-mesh/tests/test_arrayHelpers.py | 9 +- geos-mesh/tests/test_arrayModifiers.py | 337 ++++++++++-------- 5 files changed, 316 insertions(+), 251 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index 6afe5e18..4498203f 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -7,9 +7,9 @@ import numpy.typing as npt import pandas as pd # type: ignore[import-untyped] import vtkmodules.util.numpy_support as vnp -from typing import Optional, Union, cast +from typing import Optional, Union, Any, cast from vtkmodules.util.numpy_support import vtk_to_numpy -from vtkmodules.vtkCommonCore import vtkDataArray, vtkDoubleArray, vtkPoints +from vtkmodules.vtkCommonCore import vtkDataArray, vtkPoints from vtkmodules.vtkCommonDataModel import ( vtkUnstructuredGrid, vtkFieldData, vtkMultiBlockDataSet, vtkDataSet, vtkCompositeDataSet, vtkDataObject, vtkPointData, vtkCellData, vtkDataObjectTreeIterator, vtkPolyData ) @@ -343,7 +343,7 @@ def isAttributeInObjectDataSet( object: vtkDataSet, attributeName: str, onPoints return bool( data.HasArray( attributeName ) ) -def getArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> npt.NDArray[ any ]: +def getArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> npt.NDArray[ Any ]: """Return the numpy array corresponding to input attribute name in table. Args: @@ -356,18 +356,18 @@ def getArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) - ArrayLike[float]: the array corresponding to input attribute name. """ array: vtkDataArray = getVtkArrayInObject( object, attributeName, onPoints ) - nparray: npt.NDArray[ any ] = vnp.vtk_to_numpy( array ) # type: ignore[no-untyped-call] + nparray: npt.NDArray[ Any ] = vnp.vtk_to_numpy( array ) # type: ignore[no-untyped-call] return nparray -def getVtkArrayTypeInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> int: +def getVtkArrayTypeInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> int: """Return the type of the vtk array corrsponding to input attribute name in table. - + Args: object (PointSet or UnstructuredGrid): input object. attributeName (str): name of the attribute. onPoints (bool): True if attributes are on points, False if they are on cells. - + Returns: int: the type of the vtk array corrsponding to input attribute name. """ @@ -379,24 +379,23 @@ def getVtkArrayTypeInObject( object: vtkDataSet, attributeName: str, onPoints: def getVtkArrayTypeInMultiBlock( multiBlockDataSet: vtkMultiBlockDataSet, attributeName: str, onPoints: bool ) -> int: """Return the type of the vtk array corrsponding to input attribute name in the multiblock data set if it exist. - + Args: - object (PointSet or UnstructuredGrid): input object. + multiBlockDataSet (PointSet or UnstructuredGrid): input object. attributeName (str): name of the attribute. onPoints (bool): True if attributes are on points, False if they are on cells. - + Returns: int: type of the vtk array corrsponding to input attribute name, -1 if the multiblock has no attribute with given name. """ - nbBlocks = multiBlockDataSet.GetNumberOfBlocks() for idBlock in range( nbBlocks ): - object: vtkDataSet = multiBlockDataSet.GetBlock( idBlock ) + object: vtkDataSet = cast( vtkDataSet, multiBlockDataSet.GetBlock( idBlock ) ) listAttributes: set[ str ] = getAttributeSet( object, onPoints ) if attributeName in listAttributes: return getVtkArrayTypeInObject( object, attributeName, onPoints ) - print( "The vtkMultiBlockDataSet has no attribute with the name " + attributeName + ".") + print( "The vtkMultiBlockDataSet has no attribute with the name " + attributeName + "." ) return -1 @@ -454,7 +453,7 @@ def getNumberOfComponentsDataSet( dataSet: vtkDataSet, attributeName: str, onPoi Returns: int: number of components. """ - array: vtkDoubleArray = getVtkArrayInObject( dataSet, attributeName, onPoints ) + array: vtkDataArray = getVtkArrayInObject( dataSet, attributeName, onPoints ) return array.GetNumberOfComponents() @@ -478,7 +477,7 @@ def getNumberOfComponentsMultiBlock( for blockIndex in elementaryBlockIndexes: block: vtkDataSet = cast( vtkDataSet, getBlockFromFlatIndex( dataSet, blockIndex ) ) if isAttributeInObject( block, attributeName, onPoints ): - array: vtkDoubleArray = getVtkArrayInObject( block, attributeName, onPoints ) + array: vtkDataArray = getVtkArrayInObject( block, attributeName, onPoints ) return array.GetNumberOfComponents() return 0 @@ -522,7 +521,7 @@ def getComponentNamesDataSet( dataSet: vtkDataSet, attributeName: str, onPoints: tuple[str,...]: names of the components. """ - array: vtkDoubleArray = getVtkArrayInObject( dataSet, attributeName, onPoints ) + array: vtkDataArray = getVtkArrayInObject( dataSet, attributeName, onPoints ) componentNames: list[ str ] = [] if array.GetNumberOfComponents() > 1: diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index 52979738..40bfa06c 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -4,10 +4,9 @@ import numpy as np import numpy.typing as npt import vtkmodules.util.numpy_support as vnp -from typing import Union +from typing import Union, Any from vtk import ( # type: ignore[import-untyped] - VTK_DOUBLE, - VTK_FLOAT, + VTK_DOUBLE, VTK_FLOAT, ) from vtkmodules.vtkCommonDataModel import ( vtkMultiBlockDataSet, @@ -50,12 +49,12 @@ """ -def fillPartialAttributes( +def fillPartialAttributes( multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], attributeName: str, onPoints: bool = False, - value: any = np.nan, - ) -> bool: + value: Any = np.nan, +) -> bool: """Fill input partial attribute of multiBlockDataSet with the same value for all the components. Args: @@ -79,22 +78,24 @@ def fillPartialAttributes( if nbComponents > 1: componentNames = getComponentNames( multiBlockDataSet, attributeName, onPoints ) - valueType: str = type( value ) - typeMapping: dict[ int, any ] = vnp.get_vtk_to_numpy_typemap() - valueTypeExpected: any = typeMapping[ vtkArrayType ] + valueType: Any = type( value ) + typeMapping: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() + valueTypeExpected: Any = typeMapping[ vtkArrayType ] if valueTypeExpected != valueType: if np.isnan( value ): - if vtkArrayType == VTK_DOUBLE or vtkArrayType == VTK_FLOAT: + if vtkArrayType in ( VTK_DOUBLE, VTK_FLOAT ): value = valueTypeExpected( value ) else: - print( attributeName + " vtk array type is " + str( valueTypeExpected ) + ", default value is automatically set to -1." ) + print( attributeName + " vtk array type is " + str( valueTypeExpected ) + + ", default value is automatically set to -1." ) value = valueTypeExpected( -1 ) else: - print( "The value has the wrong type, it is update to " + str( valueTypeExpected ) + ", the type of the " + attributeName + " array to fill." ) - value = valueTypeExpected( value ) + print( "The value has the wrong type, it is update to " + str( valueTypeExpected ) + ", the type of the " + + attributeName + " array to fill." ) + value = valueTypeExpected( value ) - values: list[ any ] = [ value for _ in range( nbComponents ) ] + values: list[ Any ] = [ value for _ in range( nbComponents ) ] createConstantAttribute( multiBlockDataSet, values, attributeName, componentNames, onPoints, vtkArrayType ) multiBlockDataSet.Modified() @@ -102,10 +103,10 @@ def fillPartialAttributes( return True -def fillAllPartialAttributes( +def fillAllPartialAttributes( multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], - value: any = np.nan, - ) -> bool: + value: Any = np.nan, +) -> bool: """Fill all the partial attributes of multiBlockDataSet with same value for all attributes and they components. Args: @@ -118,7 +119,7 @@ def fillAllPartialAttributes( """ for onPoints in [ True, False ]: infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints ) - for attributeName in infoAttributes.keys(): + for attributeName in infoAttributes: fillPartialAttributes( multiBlockDataSet, attributeName, onPoints, value ) multiBlockDataSet.Modified() @@ -142,9 +143,9 @@ def createEmptyAttribute( bool: True if the attribute was correctly created. """ vtkDataTypeOk: dict = vnp.get_vtk_to_numpy_typemap() - if vtkDataType not in vtkDataTypeOk.keys(): + if vtkDataType not in vtkDataTypeOk: raise ValueError( "Attribute type is unknown." ) - + nbComponents: int = len( componentNames ) createdAttribute: vtkDataArray = vtkDataArray.CreateDataArray( vtkDataType ) @@ -158,12 +159,12 @@ def createEmptyAttribute( def createConstantAttribute( - object: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], - values: list[ float ], - attributeName: str, - componentNames: tuple[ str, ...] = (), - onPoints: bool = False, - vtkDataType: Union[ int, any ] = None, + object: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], + values: list[ float ], + attributeName: str, + componentNames: tuple[ str, ...] = (), + onPoints: bool = False, + vtkDataType: Union[ int, Any ] = None, ) -> bool: """Create an attribute with a constant value everywhere if absent. @@ -187,23 +188,24 @@ def createConstantAttribute( """ if isinstance( object, ( vtkMultiBlockDataSet, vtkCompositeDataSet ) ): return createConstantAttributeMultiBlock( object, values, attributeName, componentNames, onPoints, vtkDataType ) - + elif isinstance( object, vtkDataSet ): listAttributes: set[ str ] = getAttributeSet( object, onPoints ) if attributeName not in listAttributes: - return createConstantAttributeDataSet( object, values, attributeName, componentNames, onPoints, vtkDataType ) + return createConstantAttributeDataSet( object, values, attributeName, componentNames, onPoints, + vtkDataType ) print( "The attribute was already present in the vtkDataSet." ) return False return False def createConstantAttributeMultiBlock( - multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet ], - values: list[ any ], - attributeName: str, - componentNames: tuple[ str, ...] = (), - onPoints: bool = False, - vtkDataType: Union[ int, any ] = None, + multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet ], + values: list[ Any ], + attributeName: str, + componentNames: tuple[ str, ...] = (), + onPoints: bool = False, + vtkDataType: Union[ int, Any ] = None, ) -> bool: """Create an attribute with a constant value everywhere if absent. @@ -236,10 +238,11 @@ def createConstantAttributeMultiBlock( dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) listAttributes: set[ str ] = getAttributeSet( dataSet, onPoints ) if attributeName not in listAttributes: - checkCreat = createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkDataType ) - + checkCreat = createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, + vtkDataType ) + iter.GoToNextItem() - + if checkCreat: return True else: @@ -248,12 +251,12 @@ def createConstantAttributeMultiBlock( def createConstantAttributeDataSet( - dataSet: vtkDataSet, - values: list[ any ], - attributeName: str, - componentNames: tuple[ str, ...] = (), - onPoints: bool = False, - vtkDataType: Union[ int, any ] = None, + dataSet: vtkDataSet, + values: list[ Any ], + attributeName: str, + componentNames: tuple[ str, ...] = (), + onPoints: bool = False, + vtkDataType: Union[ int, Any ] = None, ) -> bool: """Create an attribute with a constant value everywhere. @@ -278,9 +281,9 @@ def createConstantAttributeDataSet( nbElements: int = ( dataSet.GetNumberOfPoints() if onPoints else dataSet.GetNumberOfCells() ) nbComponents: int = len( values ) - array: npt.NDArray[ any ] + array: npt.NDArray[ Any ] if nbComponents > 1: - array = np.array( [ [ val for val in values ] for _ in range( nbElements ) ] ) + array = np.array( [ values for _ in range( nbElements ) ] ) else: array = np.array( [ values[ 0 ] for _ in range( nbElements ) ] ) @@ -288,12 +291,12 @@ def createConstantAttributeDataSet( def createAttribute( - dataSet: vtkDataSet, - array: npt.NDArray[ any ], - attributeName: str, - componentNames: tuple[ str, ...] = (), - onPoints: bool = False, - vtkDataType: Union[ int, any ] = None, + dataSet: vtkDataSet, + array: npt.NDArray[ Any ], + attributeName: str, + componentNames: tuple[ str, ...] = (), + onPoints: bool = False, + vtkDataType: Union[ int, Any ] = None, ) -> bool: """Create an attribute and its VTK array from the given array. @@ -324,12 +327,12 @@ def createAttribute( if nbComponents > 1: nbNames = len( componentNames ) - if nbNames < nbComponents : + if nbNames < nbComponents: componentNames = tuple( [ "Component" + str( i ) for i in range( nbComponents ) ] ) print( "Not enough component name enter, component names are seted to : Component0, Component1 ..." ) elif nbNames > nbComponents: print( "To many component names enter, the lastest will not be taken into account." ) - + for i in range( nbComponents ): createdAttribute.SetComponentName( i, componentNames[ i ] ) @@ -337,7 +340,7 @@ def createAttribute( dataSet.GetPointData().AddArray( createdAttribute ) else: dataSet.GetCellData().AddArray( createdAttribute ) - + dataSet.Modified() return True @@ -357,7 +360,7 @@ def copyAttribute( objectTo (vtkMultiBlockDataSet): object where to copy the attribute. attributeNameFrom (str): attribute name in objectFrom. attributeNameTo (str): attribute name in objectTo. - onPoint (bool, optional): True if attributes are on points, False if they are on cells. + onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. Returns: @@ -377,7 +380,7 @@ def copyAttribute( # get block from current time step object blockTo: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( objectTo, index ) ) assert blockTo is not None, "Block at current time step is null." - + try: copyAttributeDataSet( blockFrom, blockTo, attributeNameFrom, attributeNameTo, onPoints ) except AssertionError: @@ -408,7 +411,7 @@ def copyAttributeDataSet( bool: True if copy successfully ended, False otherwise. """ # get attribut from initial time step block - npArray: npt.NDArray[ any ] = getArrayInObject( objectFrom, attributeNameFrom, onPoints ) + npArray: npt.NDArray[ Any ] = getArrayInObject( objectFrom, attributeNameFrom, onPoints ) assert npArray is not None componentNames: tuple[ str, ...] = getComponentNames( objectFrom, attributeNameFrom, onPoints ) diff --git a/geos-mesh/tests/conftest.py b/geos-mesh/tests/conftest.py index 50c9964f..3e26dced 100644 --- a/geos-mesh/tests/conftest.py +++ b/geos-mesh/tests/conftest.py @@ -5,7 +5,7 @@ # ruff: noqa: E402 # disable Module level import not at top of file import os import pytest -from typing import Union +from typing import Union, Any import numpy as np import numpy.typing as npt @@ -15,6 +15,7 @@ @pytest.fixture def arrayExpected( request: pytest.FixtureRequest ) -> npt.NDArray[ np.float64 ]: + """Get an array from a file.""" reference_data = "data/data.npz" reference_data_path = os.path.join( os.path.dirname( os.path.realpath( __file__ ) ), reference_data ) data = np.load( reference_data_path ) @@ -24,6 +25,7 @@ def arrayExpected( request: pytest.FixtureRequest ) -> npt.NDArray[ np.float64 ] @pytest.fixture def arrayTest( request: pytest.FixtureRequest ) -> npt.NDArray[ np.float64 ]: + """Get a random array of float64.""" np.random.seed( 42 ) array: npt.NDArray[ np.float64 ] = np.random.rand( request.param, @@ -31,60 +33,95 @@ def arrayTest( request: pytest.FixtureRequest ) -> npt.NDArray[ np.float64 ]: ) return array + @pytest.fixture -def getArrayWithSpeTypeValue() -> npt.NDArray[ any ]: - def _getarray( nb_component: int, nb_elements: int, valueType: str ) : +def getArrayWithSpeTypeValue() -> Any: + """Get a random array of input type with the function _getarray(). + + Returns: + npt.NDArray[Any]: random array of input type. + """ + + def _getarray( nb_component: int, nb_elements: int, valueType: str ) -> Any: + """Get a random array of input type. + + Args: + nb_component (int): nb of components. + nb_elements (int): nb of elements. + valueType (str): the type of the value. + + Returns: + npt.NDArray[Any]: random array of input type. + """ if valueType == "int32": if nb_component == 1: return np.array( [ np.int32( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) else: - return np.array( [ [ np.int32( 1000 * np.random.random() ) for _ in range( nb_component ) ] for _ in range( nb_elements ) ] ) - + return np.array( [ [ np.int32( 1000 * np.random.random() ) for _ in range( nb_component ) ] + for _ in range( nb_elements ) ] ) elif valueType == "int64": if nb_component == 1: return np.array( [ np.int64( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) else: - return np.array( [ [ np.int64( 1000 * np.random.random() ) for _ in range( nb_component ) ] for _ in range( nb_elements ) ] ) - + return np.array( [ [ np.int64( 1000 * np.random.random() ) for _ in range( nb_component ) ] + for _ in range( nb_elements ) ] ) + elif valueType == "float32": if nb_component == 1: return np.array( [ np.float32( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) else: - return np.array( [ [ np.float32( 1000 * np.random.random() ) for _ in range( nb_component ) ] for _ in range( nb_elements ) ] ) + return np.array( [ [ np.float32( 1000 * np.random.random() ) for _ in range( nb_component ) ] + for _ in range( nb_elements ) ] ) - elif valueType == "float64": + else: if nb_component == 1: return np.array( [ np.float64( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) else: - return np.array( [ [ np.float64( 1000 * np.random.random() ) for _ in range( nb_component ) ] for _ in range( nb_elements ) ] ) + return np.array( [ [ np.float64( 1000 * np.random.random() ) for _ in range( nb_component ) ] + for _ in range( nb_elements ) ] ) return _getarray @pytest.fixture -def dataSetTest() -> Union[ vtkMultiBlockDataSet, vtkPolyData, vtkDataSet ]: +def dataSetTest() -> Any: + """Get a vtkObject from a file with the function _get_dataset(). + + Returns: + (vtkMultiBlockDataSet, vtkPolyData, vtkDataSet): the vtk object. + """ + + def _get_dataset( datasetType: str ) -> Union[ vtkMultiBlockDataSet, vtkPolyData, vtkDataSet ]: + """Get a vtkObject from a file. - def _get_dataset( datasetType: str ): + Args: + datasetType (str): the type of vtk object wanted. + + Returns: + (vtkMultiBlockDataSet, vtkPolyData, vtkDataSet): the vtk object. + """ + reader: Union[ vtkXMLMultiBlockDataReader, vtkXMLUnstructuredGridReader ] if datasetType == "multiblock": - reader = reader = vtkXMLMultiBlockDataReader() + reader = vtkXMLMultiBlockDataReader() vtkFilename = "data/displacedFault.vtm" elif datasetType == "emptymultiblock": - reader = reader = vtkXMLMultiBlockDataReader() + reader = vtkXMLMultiBlockDataReader() vtkFilename = "data/displacedFaultempty.vtm" elif datasetType == "dataset": - reader: vtkXMLUnstructuredGridReader = vtkXMLUnstructuredGridReader() + reader = vtkXMLUnstructuredGridReader() vtkFilename = "data/domain_res5_id.vtu" elif datasetType == "emptydataset": - reader: vtkXMLUnstructuredGridReader = vtkXMLUnstructuredGridReader() + reader = vtkXMLUnstructuredGridReader() vtkFilename = "data/domain_res5_id_empty.vtu" elif datasetType == "polydata": - reader: vtkXMLUnstructuredGridReader = vtkXMLUnstructuredGridReader() + reader = vtkXMLUnstructuredGridReader() vtkFilename = "data/surface.vtu" + datapath: str = os.path.join( os.path.dirname( os.path.realpath( __file__ ) ), vtkFilename ) reader.SetFileName( datapath ) reader.Update() return reader.GetOutput() - return _get_dataset \ No newline at end of file + return _get_dataset diff --git a/geos-mesh/tests/test_arrayHelpers.py b/geos-mesh/tests/test_arrayHelpers.py index 79182bcc..eeebd177 100644 --- a/geos-mesh/tests/test_arrayHelpers.py +++ b/geos-mesh/tests/test_arrayHelpers.py @@ -107,15 +107,16 @@ def test_getArrayInObject( request: pytest.FixtureRequest, arrayExpected: npt.ND ( "collocated_nodes", -1, False ), ( "newAttribute", -1, False ), ] ) -def test_getVtkArrayTypeInMultiBlock( dataSetTest: vtkMultiBlockDataSet, attributeName: str, - vtkDataType: int, onPoints: bool ) -> None: +def test_getVtkArrayTypeInMultiBlock( dataSetTest: vtkMultiBlockDataSet, attributeName: str, vtkDataType: int, + onPoints: bool ) -> None: """Test getting the type of the vtk array of an attribute from multiBlockDataSet.""" multiBlockDataSet: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - vtkDataTypeTest: int = arrayHelpers.getVtkArrayTypeInMultiBlock( multiBlockDataSet, attributeName, onPoints ) + vtkDataTypeTest: int = arrayHelpers.getVtkArrayTypeInMultiBlock( multiBlockDataSet, attributeName, onPoints ) assert ( vtkDataType == vtkDataTypeTest ) + @pytest.mark.parametrize( "attributeName, onPoints", [ ( "CellAttribute", False ), ( "PointAttribute", True ), @@ -124,7 +125,7 @@ def test_getVtkArrayTypeInObject( dataSetTest: vtkDataSet, attributeName: str, o """Test getting the type of the vtk array of an attribute from dataset.""" vtkDataSetTest: vtkDataSet = dataSetTest( "dataset" ) - obtained: int = arrayHelpers.getVtkArrayTypeInObject( vtkDataSetTest, attributeName, onPoints ) + obtained: int = arrayHelpers.getVtkArrayTypeInObject( vtkDataSetTest, attributeName, onPoints ) expected: int = 11 assert ( obtained == expected ) diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index 0ee7c569..3aff05c4 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -5,19 +5,14 @@ # ruff: noqa: E402 # disable Module level import not at top of file # mypy: disable-error-code="operator" import pytest -from typing import Union, cast +from typing import Union, Any, cast import numpy as np import numpy.typing as npt import vtkmodules.util.numpy_support as vnp from vtkmodules.vtkCommonCore import vtkDataArray -from vtkmodules.vtkCommonDataModel import ( - vtkDataSet, - vtkMultiBlockDataSet, - vtkPointData, - vtkCellData -) +from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet, vtkPointData, vtkCellData ) from geos.mesh.utils.arrayHelpers import getAttributesWithNumberOfComponents @@ -45,39 +40,42 @@ # vtk array type int IdType numpy type # VTK_LONG_LONG = 16 = 2 = np.int64 - - from geos.mesh.utils import arrayModifiers @pytest.mark.parametrize( - "idBlockToFill, attributeName, nbComponentsRef, componentNamesRef, onPoints, value, valueRef, vtkDataTypeRef, valueTypeRef", [ - ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.nan, np.nan, VTK_DOUBLE, "float64" ), - ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.float64( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), - ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.int32( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), - ( 1, "PointAttribute", 3, ( "AX1", "AX2", "AX3" ), True, np.nan, np.nan, VTK_DOUBLE, "float64" ), - ( 1, "PointAttribute", 3, ( "AX1", "AX2", "AX3" ), True, np.float64( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), - ( 1, "PointAttribute", 3, ( "AX1", "AX2", "AX3" ), True, np.int32( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), - ( 1, "PORO", 1, (), False, np.nan, np.nan, VTK_FLOAT, "float32" ), - ( 1, "PORO", 1, (), False, np.float32( 4 ), np.float32( 4 ), VTK_FLOAT, "float32" ), - ( 1, "PORO", 1, (), False, np.int32( 4 ), np.float32( 4 ), VTK_FLOAT, "float32" ), - ( 1, "FAULT", 1, (), False, np.nan, np.int32( -1 ), VTK_INT, "int32" ), - ( 1, "FAULT", 1, (), False, np.int32( 4 ), np.int32( 4 ), VTK_INT, "int32" ), - ( 1, "FAULT", 1, (), False, np.float32( 4 ), np.int32( 4 ), VTK_INT, "int32" ), - ( 0, "collocated_nodes", 2, ( None, None ), True, np.nan, np.int64( -1 ), VTK_ID_TYPE, "int64" ), - ( 0, "collocated_nodes", 2, ( None, None ), True, np.int64( 4 ), np.int64( 4 ), VTK_ID_TYPE, "int64" ), - ( 0, "collocated_nodes", 2, ( None, None ), True, np.int32( 4 ), np.int64( 4 ), VTK_ID_TYPE, "int64" ), - ( 0, "collocated_nodes", 2, ( None, None ), True, np.float32( 4 ), np.int64( 4 ), VTK_ID_TYPE, "int64" ), -] ) + "idBlockToFill, attributeName, nbComponentsRef, componentNamesRef, onPoints, value, valueRef, vtkDataTypeRef, valueTypeRef", + [ + ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.nan, np.nan, VTK_DOUBLE, "float64" ), + ( 1, "CellAttribute", 3, + ( "AX1", "AX2", "AX3" ), False, np.float64( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), + ( 1, "CellAttribute", 3, + ( "AX1", "AX2", "AX3" ), False, np.int32( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), + ( 1, "PointAttribute", 3, ( "AX1", "AX2", "AX3" ), True, np.nan, np.nan, VTK_DOUBLE, "float64" ), + ( 1, "PointAttribute", 3, + ( "AX1", "AX2", "AX3" ), True, np.float64( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), + ( 1, "PointAttribute", 3, + ( "AX1", "AX2", "AX3" ), True, np.int32( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), + ( 1, "PORO", 1, (), False, np.nan, np.nan, VTK_FLOAT, "float32" ), + ( 1, "PORO", 1, (), False, np.float32( 4 ), np.float32( 4 ), VTK_FLOAT, "float32" ), + ( 1, "PORO", 1, (), False, np.int32( 4 ), np.float32( 4 ), VTK_FLOAT, "float32" ), + ( 1, "FAULT", 1, (), False, np.nan, np.int32( -1 ), VTK_INT, "int32" ), + ( 1, "FAULT", 1, (), False, np.int32( 4 ), np.int32( 4 ), VTK_INT, "int32" ), + ( 1, "FAULT", 1, (), False, np.float32( 4 ), np.int32( 4 ), VTK_INT, "int32" ), + ( 0, "collocated_nodes", 2, ( None, None ), True, np.nan, np.int64( -1 ), VTK_ID_TYPE, "int64" ), + ( 0, "collocated_nodes", 2, ( None, None ), True, np.int64( 4 ), np.int64( 4 ), VTK_ID_TYPE, "int64" ), + ( 0, "collocated_nodes", 2, ( None, None ), True, np.int32( 4 ), np.int64( 4 ), VTK_ID_TYPE, "int64" ), + ( 0, "collocated_nodes", 2, ( None, None ), True, np.float32( 4 ), np.int64( 4 ), VTK_ID_TYPE, "int64" ), + ] ) def test_fillPartialAttributes( dataSetTest: vtkMultiBlockDataSet, idBlockToFill: int, attributeName: str, nbComponentsRef: int, - componentNamesRef: tuple[ str, ... ], + componentNamesRef: tuple[ str, ...], onPoints: bool, - value: any, - valueRef: any, + value: Any, + valueRef: Any, vtkDataTypeRef: int, valueTypeRef: str, ) -> None: @@ -94,32 +92,33 @@ def test_fillPartialAttributes( else: nbElements = blockTest.GetNumberOfCells() dataTest = blockTest.GetCellData() - + attributeFillTest: vtkDataArray = dataTest.GetArray( attributeName ) nbComponentsTest: int = attributeFillTest.GetNumberOfComponents() assert nbComponentsRef == nbComponentsTest - - npArrayFillRef: npt.NDArray[ any ] + + npArrayFillRef: npt.NDArray[ Any ] if nbComponentsRef > 1: - componentNamesTest: tuple[ str, ...] = tuple( attributeFillTest.GetComponentName( i ) for i in range( nbComponentsRef ) ) + componentNamesTest: tuple[ str, ...] = tuple( + attributeFillTest.GetComponentName( i ) for i in range( nbComponentsRef ) ) assert componentNamesRef == componentNamesTest - + npArrayFillRef = np.array( [ [ valueRef for _ in range( nbComponentsRef ) ] for _ in range( nbElements ) ] ) else: npArrayFillRef = np.array( [ valueRef for _ in range( nbElements ) ] ) - npArrayFillTest: npt.NDArray[ any ] = vnp.vtk_to_numpy( attributeFillTest ) + npArrayFillTest: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeFillTest ) assert valueTypeRef == npArrayFillTest.dtype - if np.isnan( valueRef ): assert np.isnan( npArrayFillRef ).all() else: assert ( npArrayFillRef == npArrayFillTest ).all() - + vtkDataTypeTest: int = attributeFillTest.GetDataType() assert vtkDataTypeRef == vtkDataTypeTest + @pytest.mark.parametrize( "value", [ ( np.nan ), ( np.int32( 42 ) ), @@ -129,7 +128,7 @@ def test_fillPartialAttributes( ] ) def test_FillAllPartialAttributes( dataSetTest: vtkMultiBlockDataSet, - value: any, + value: Any, ) -> None: """Test to fill all the partial attributes of a vtkMultiBlockDataSet with a value.""" MultiBlockDataSetRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) @@ -139,17 +138,14 @@ def test_FillAllPartialAttributes( nbBlock = MultiBlockDataSetRef.GetNumberOfBlocks() for idBlock in range( nbBlock ): datasetTest: vtkDataSet = cast( vtkDataSet, MultiBlockDataSetTest.GetBlock( idBlock ) ) - for onPoints in [True, False]: + for onPoints in [ True, False ]: infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( MultiBlockDataSetRef, onPoints ) dataTest: Union[ vtkPointData, vtkCellData ] - if onPoints: - dataTest = datasetTest.GetPointData() - else: - dataTest = datasetTest.GetCellData() - - for attributeName in infoAttributes.keys(): + dataTest = datasetTest.GetPointData() if onPoints else datasetTest.GetCellData() + + for attributeName in infoAttributes: attributeTest: int = dataTest.HasArray( attributeName ) - assert attributeTest == 1 + assert attributeTest == 1 @pytest.mark.parametrize( "attributeName, dataType, expectedDatatypeArray", [ @@ -187,13 +183,13 @@ def test_createEmptyAttribute( def test_createConstantAttributeMultiBlock( dataSetTest: vtkMultiBlockDataSet, attributeName: str, - isNewOnBlock: tuple[ bool, ... ], + isNewOnBlock: tuple[ bool, ...], onPoints: bool, ) -> None: """Test creation of constant attribute in multiblock dataset.""" MultiBlockDataSetRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) MultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - values: list[ float ] = [ np.nan ] + values: list[ float ] = [ np.nan ] arrayModifiers.createConstantAttributeMultiBlock( MultiBlockDataSetTest, values, attributeName, onPoints=onPoints ) nbBlock = MultiBlockDataSetRef.GetNumberOfBlocks() @@ -217,94 +213,118 @@ def test_createConstantAttributeMultiBlock( assert attributeRef == attributeTest -@pytest.mark.parametrize( "values, componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, valueType", [ - ( [ np.float32( 42 ) ], (), (), True, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ) ], (), (), False, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ) ], (), (), True, None, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ) ], (), (), False, None, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], (), ( "Component0", "Component1" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], (), ( "Component0", "Component1" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], (), ( "Component0", "Component1" ), True, None, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], (), ( "Component0", "Component1" ), False, None, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_FLOAT, "float32" ), - ( [ np.float64( 42 ) ], (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ) ], (), (), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ) ], (), (), True, None, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ) ], (), (), False, None, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], (), ( "Component0", "Component1" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], (), ( "Component0", "Component1" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], (), ( "Component0", "Component1" ), True, None, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], (), ( "Component0", "Component1" ), False, None, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_DOUBLE, "float64" ), - ( [ np.int32( 42 ) ], (), (), True, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ) ], (), (), False, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ) ], (), (), True, None, VTK_INT, "int32" ), - ( [ np.int32( 42 ) ], (), (), False, None, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), True, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), False, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), True, None, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), False, None, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_INT, "int32" ), - ( [ np.int64( 42 ) ], (), (), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ) ], (), (), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ) ], (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ) ], (), (), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ) ], (), (), True, None, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ) ], (), (), False, None, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), True, None, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), False, None, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_LONG_LONG, "int64" ), -] ) +@pytest.mark.parametrize( + "values, componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, valueType", [ + ( [ np.float32( 42 ) ], (), (), True, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ) ], (), (), False, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ) ], (), (), True, None, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ) ], (), (), False, None, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], (), + ( "Component0", "Component1" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], (), + ( "Component0", "Component1" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], (), + ( "Component0", "Component1" ), True, None, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], (), + ( "Component0", "Component1" ), False, None, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), + ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), + ( "X", "Y" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_FLOAT, "float32" ), + ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_FLOAT, "float32" ), + ( [ np.float64( 42 ) ], (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ) ], (), (), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ) ], (), (), True, None, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ) ], (), (), False, None, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], (), + ( "Component0", "Component1" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], (), + ( "Component0", "Component1" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], (), + ( "Component0", "Component1" ), True, None, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], (), + ( "Component0", "Component1" ), False, None, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), + ( "X", "Y" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), + ( "X", "Y" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), + ( "X", "Y" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_DOUBLE, "float64" ), + ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_DOUBLE, "float64" ), + ( [ np.int32( 42 ) ], (), (), True, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ) ], (), (), False, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ) ], (), (), True, None, VTK_INT, "int32" ), + ( [ np.int32( 42 ) ], (), (), False, None, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), True, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), False, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), True, None, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), False, None, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_INT, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_INT, "int32" ), + ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_INT, "int32" ), + ( [ np.int64( 42 ) ], (), (), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ) ], (), (), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ) ], (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ) ], (), (), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ) ], (), (), True, None, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ) ], (), (), False, None, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], (), + ( "Component0", "Component1" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], (), + ( "Component0", "Component1" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], (), + ( "Component0", "Component1" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], (), + ( "Component0", "Component1" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), True, None, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), False, None, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), + ( "X", "Y" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), + ( "X", "Y" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), + ( "X", "Y" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), + ( "X", "Y" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), + ( "X", "Y" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_LONG_LONG, "int64" ), + ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_LONG_LONG, "int64" ), + ] ) def test_createConstantAttributeDataSet( dataSetTest: vtkDataSet, - values: list[ any ], - componentNames: tuple[ str, ... ], - componentNamesTest: tuple[ str, ... ], + values: list[ Any ], + componentNames: tuple[ str, ...], + componentNamesTest: tuple[ str, ...], onPoints: bool, - vtkDataType: Union[ int, any ], + vtkDataType: Union[ int, Any ], vtkDataTypeTest: int, valueType: str, ) -> None: """Test constant attribute creation in dataset.""" dataSet: vtkDataSet = dataSetTest( "dataset" ) attributeName: str = "newAttributedataset" - arrayModifiers.createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkDataType ) + arrayModifiers.createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, + vtkDataType ) data: Union[ vtkPointData, vtkCellData ] nbElements: int @@ -321,16 +341,17 @@ def test_createConstantAttributeDataSet( nbComponentsCreated: int = createdAttribute.GetNumberOfComponents() assert nbComponents == nbComponentsCreated - npArray: npt.NDArray[ any ] + npArray: npt.NDArray[ Any ] if nbComponents > 1: - componentNamesCreated: tuple[ str, ...] = tuple( createdAttribute.GetComponentName( i ) for i in range( nbComponents ) ) + componentNamesCreated: tuple[ str, ...] = tuple( + createdAttribute.GetComponentName( i ) for i in range( nbComponents ) ) assert componentNamesTest == componentNamesCreated - - npArray = np.array( [ [ val for val in values ] for _ in range( nbElements ) ] ) + + npArray = np.array( [ values for _ in range( nbElements ) ] ) else: npArray = np.array( [ values[ 0 ] for _ in range( nbElements ) ] ) - npArraycreated: npt.NDArray[ any ] = vnp.vtk_to_numpy( createdAttribute ) + npArraycreated: npt.NDArray[ Any ] = vnp.vtk_to_numpy( createdAttribute ) assert ( npArray == npArraycreated ).all() assert valueType == npArraycreated.dtype @@ -414,9 +435,9 @@ def test_createConstantAttributeDataSet( ] ) def test_createAttribute( dataSetTest: vtkDataSet, - getArrayWithSpeTypeValue: npt.NDArray[ any ], - componentNames: tuple[ str, ... ], - componentNamesTest: tuple[ str, ... ], + getArrayWithSpeTypeValue: npt.NDArray[ Any ], + componentNames: tuple[ str, ...], + componentNamesTest: tuple[ str, ...], onPoints: bool, vtkDataType: int, vtkDataTypeTest: int, @@ -429,23 +450,21 @@ def test_createAttribute( nbComponents: int = ( 1 if len( componentNamesTest ) == 0 else len( componentNamesTest ) ) nbElements: int = ( dataSet.GetNumberOfPoints() if onPoints else dataSet.GetNumberOfCells() ) - npArray: npt.NDArray[ any ] = getArrayWithSpeTypeValue( nbComponents, nbElements, valueType ) + npArray: npt.NDArray[ Any ] = getArrayWithSpeTypeValue( nbComponents, nbElements, valueType ) arrayModifiers.createAttribute( dataSet, npArray, attributeName, componentNames, onPoints, vtkDataType ) data: Union[ vtkPointData, vtkCellData ] - if onPoints: - data = dataSet.GetPointData() - else: - data = dataSet.GetCellData() + data = dataSet.GetPointData() if onPoints else dataSet.GetCellData() createdAttribute: vtkDataArray = data.GetArray( attributeName ) nbComponentsCreated: int = createdAttribute.GetNumberOfComponents() assert nbComponents == nbComponentsCreated if nbComponents > 1: - componentsNamesCreated: tuple[ str, ...] = tuple( createdAttribute.GetComponentName( i ) for i in range( nbComponents ) ) + componentsNamesCreated: tuple[ str, ...] = tuple( + createdAttribute.GetComponentName( i ) for i in range( nbComponents ) ) assert componentNamesTest == componentsNamesCreated - - npArraycreated: npt.NDArray[ any ] = vnp.vtk_to_numpy( createdAttribute ) + + npArraycreated: npt.NDArray[ Any ] = vnp.vtk_to_numpy( createdAttribute ) assert ( npArray == npArraycreated ).all() assert valueType == npArraycreated.dtype @@ -460,7 +479,8 @@ def test_createAttribute( ( "PointAttribute", "PointAttributeTo", True, 0 ), ( "collocated_nodes", "collocated_nodesTo", True, 1 ), ] ) -def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet, attributeNameFrom:str, attributeNameTo: str, onPoints: bool, idBlock: int ) -> None: +def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet, attributeNameFrom: str, attributeNameTo: str, onPoints: bool, + idBlock: int ) -> None: """Test copy of cell attribute from one multiblock to another.""" objectFrom: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) objectTo: vtkMultiBlockDataSet = dataSetTest( "emptymultiblock" ) @@ -478,7 +498,7 @@ def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet, attributeNameFrom:str else: dataFrom = blockFrom.GetCellData() dataTo = blockTo.GetCellData() - + attributeFrom: vtkDataArray = dataFrom.GetArray( attributeNameFrom ) attributeTo: vtkDataArray = dataTo.GetArray( attributeNameTo ) @@ -487,12 +507,14 @@ def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet, attributeNameFrom:str assert nbComponentsFrom == nbComponentsTo if nbComponentsFrom > 1: - componentsNamesFrom: tuple[ str, ...] = tuple( attributeFrom.GetComponentName( i ) for i in range( nbComponentsFrom ) ) - componentsNamesTo: tuple[ str, ...] = tuple( attributeTo.GetComponentName( i ) for i in range( nbComponentsTo ) ) + componentsNamesFrom: tuple[ str, ...] = tuple( + attributeFrom.GetComponentName( i ) for i in range( nbComponentsFrom ) ) + componentsNamesTo: tuple[ str, + ...] = tuple( attributeTo.GetComponentName( i ) for i in range( nbComponentsTo ) ) assert componentsNamesFrom == componentsNamesTo - npArrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( attributeFrom ) - npArrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( attributeTo ) + npArrayFrom: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeFrom ) + npArrayTo: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeTo ) assert ( npArrayFrom == npArrayTo ).all() assert npArrayFrom.dtype == npArrayTo.dtype @@ -505,7 +527,8 @@ def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet, attributeNameFrom:str ( "CellAttribute", "CellAttributeTo", False ), ( "PointAttribute", "PointAttributeTo", True ), ] ) -def test_copyAttributeDataSet( dataSetTest: vtkDataSet, attributeNameFrom:str, attributeNameTo: str, onPoints: bool ) -> None: +def test_copyAttributeDataSet( dataSetTest: vtkDataSet, attributeNameFrom: str, attributeNameTo: str, + onPoints: bool ) -> None: """Test copy of an attribute from one dataset to another.""" objectFrom: vtkDataSet = dataSetTest( "dataset" ) objectTo: vtkDataSet = dataSetTest( "emptydataset" ) @@ -520,7 +543,7 @@ def test_copyAttributeDataSet( dataSetTest: vtkDataSet, attributeNameFrom:str, a else: dataFrom = objectFrom.GetCellData() dataTo = objectTo.GetCellData() - + attributeFrom: vtkDataArray = dataFrom.GetArray( attributeNameFrom ) attributeTo: vtkDataArray = dataTo.GetArray( attributeNameTo ) @@ -529,16 +552,18 @@ def test_copyAttributeDataSet( dataSetTest: vtkDataSet, attributeNameFrom:str, a assert nbComponentsFrom == nbComponentsTo if nbComponentsFrom > 1: - componentsNamesFrom: tuple[ str, ...] = tuple( attributeFrom.GetComponentName( i ) for i in range( nbComponentsFrom ) ) - componentsNamesTo: tuple[ str, ...] = tuple( attributeTo.GetComponentName( i ) for i in range( nbComponentsTo ) ) + componentsNamesFrom: tuple[ str, ...] = tuple( + attributeFrom.GetComponentName( i ) for i in range( nbComponentsFrom ) ) + componentsNamesTo: tuple[ str, + ...] = tuple( attributeTo.GetComponentName( i ) for i in range( nbComponentsTo ) ) assert componentsNamesFrom == componentsNamesTo vtkDataTypeFrom: int = attributeFrom.GetDataType() vtkDataTypeTo: int = attributeTo.GetDataType() assert vtkDataTypeFrom == vtkDataTypeTo - npArrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( attributeFrom ) - npArrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( attributeTo ) + npArrayFrom: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeFrom ) + npArrayTo: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeTo ) assert ( npArrayFrom == npArrayTo ).all() assert npArrayFrom.dtype == npArrayTo.dtype From bd63003b4fcfbbb36da7dba3da74d98447b1cd71 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Mon, 30 Jun 2025 09:26:49 +0200 Subject: [PATCH 08/56] Uptade functions calling utils functions --- geos-mesh/src/geos/mesh/utils/multiblockModifiers.py | 3 +-- geos-posp/src/PVplugins/PVAttributeMapping.py | 7 ++----- geos-posp/src/geos_posp/filters/GeosBlockMerge.py | 11 +++-------- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/multiblockModifiers.py b/geos-mesh/src/geos/mesh/utils/multiblockModifiers.py index ebbf2100..5f00afb8 100644 --- a/geos-mesh/src/geos/mesh/utils/multiblockModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/multiblockModifiers.py @@ -29,8 +29,7 @@ def mergeBlocks( """ if keepPartialAttributes: - fillAllPartialAttributes( input, False ) - fillAllPartialAttributes( input, True ) + fillAllPartialAttributes( input ) af = vtkAppendDataSets() af.MergePointsOn() diff --git a/geos-posp/src/PVplugins/PVAttributeMapping.py b/geos-posp/src/PVplugins/PVAttributeMapping.py index a862b9a9..39b17b51 100644 --- a/geos-posp/src/PVplugins/PVAttributeMapping.py +++ b/geos-posp/src/PVplugins/PVAttributeMapping.py @@ -21,9 +21,7 @@ from geos.mesh.utils.arrayModifiers import fillPartialAttributes from geos.mesh.utils.multiblockModifiers import mergeBlocks from geos.mesh.utils.arrayHelpers import ( - getAttributeSet, - getNumberOfComponents, -) + getAttributeSet, ) from geos_posp.visu.PVUtils.checkboxFunction import ( # type: ignore[attr-defined] createModifiedCallback, ) from geos_posp.visu.PVUtils.paraviewTreatments import getArrayChoices @@ -192,8 +190,7 @@ def RequestData( outData.ShallowCopy( clientMesh ) attributeNames: set[ str ] = set( getArrayChoices( self.a02GetAttributeToTransfer() ) ) for attributeName in attributeNames: - nbComponents = getNumberOfComponents( serverMesh, attributeName, False ) - fillPartialAttributes( serverMesh, attributeName, nbComponents, False ) + fillPartialAttributes( serverMesh, attributeName, False ) mergedServerMesh: vtkUnstructuredGrid if isinstance( serverMesh, vtkUnstructuredGrid ): diff --git a/geos-posp/src/geos_posp/filters/GeosBlockMerge.py b/geos-posp/src/geos_posp/filters/GeosBlockMerge.py index 09b0a879..0844b1e8 100644 --- a/geos-posp/src/geos_posp/filters/GeosBlockMerge.py +++ b/geos-posp/src/geos_posp/filters/GeosBlockMerge.py @@ -365,14 +365,9 @@ def mergeChildBlocks( self: Self, compositeBlock: vtkMultiBlockDataSet ) -> vtkU Returns: vtkUnstructuredGrid: merged block """ - # fill partial cell attributes in all children blocks - if not fillAllPartialAttributes( compositeBlock, False ): - self.m_logger.warning( "Some partial cell attributes may not have been " + "propagated to the whole mesh." ) - - # # fill partial point attributes in all children blocks - if not fillAllPartialAttributes( compositeBlock, True ): - self.m_logger.warning( "Some partial point attributes may not have been " + - "propagated to the whole mesh." ) + # fill partial attributes in all children blocks + if not fillAllPartialAttributes( compositeBlock ): + self.m_logger.warning( "Some partial attributes may not have been " + "propagated to the whole mesh." ) # merge blocks return mergeBlocks( compositeBlock ) From 19ffa8d58bb900ba03624a03d435dd5a258c91d7 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Mon, 30 Jun 2025 11:42:11 +0200 Subject: [PATCH 09/56] Fix the doc issue --- .../src/geos/mesh/utils/arrayModifiers.py | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index 40bfa06c..df530189 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -159,12 +159,12 @@ def createEmptyAttribute( def createConstantAttribute( - object: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], - values: list[ float ], - attributeName: str, - componentNames: tuple[ str, ...] = (), - onPoints: bool = False, - vtkDataType: Union[ int, Any ] = None, + object: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], + values: list[ float ], + attributeName: str, + componentNames: tuple[ str, ...] = (), # noqa: C408 + onPoints: bool = False, + vtkDataType: Union[ int, Any ] = None, ) -> bool: """Create an attribute with a constant value everywhere if absent. @@ -179,9 +179,9 @@ def createConstantAttribute( vtkDataType (Union(any, int), optional): vtk data type of the attribute to create. Defaults to None, the type is given by the type of the array value. Waring with int8, uint8 and int64 type of value, several vtk array type use it by default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG + - int8 -> VTK_SIGNED_CHAR + - uint8 -> VTK_UNSIGNED_CHAR + - int64 -> VTK_LONG_LONG Returns: bool: True if the attribute was correctly created False if the attribute was already present. @@ -200,12 +200,12 @@ def createConstantAttribute( def createConstantAttributeMultiBlock( - multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet ], - values: list[ Any ], - attributeName: str, - componentNames: tuple[ str, ...] = (), - onPoints: bool = False, - vtkDataType: Union[ int, Any ] = None, + multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet ], + values: list[ Any ], + attributeName: str, + componentNames: tuple[ str, ...] = (), # noqa: C408 + onPoints: bool = False, + vtkDataType: Union[ int, Any ] = None, ) -> bool: """Create an attribute with a constant value everywhere if absent. @@ -220,9 +220,9 @@ def createConstantAttributeMultiBlock( vtkDataType (Union(any, int), optional): vtk data type of the attribute to create. Defaults to None, the type is given by the type of the given value. Waring with int8, uint8 and int64 type of value, several vtk array type use it by default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG + - int8 -> VTK_SIGNED_CHAR + - uint8 -> VTK_UNSIGNED_CHAR + - int64 -> VTK_LONG_LONG Returns: bool: True if the attribute was correctly created, False if the attribute was already present. @@ -251,12 +251,12 @@ def createConstantAttributeMultiBlock( def createConstantAttributeDataSet( - dataSet: vtkDataSet, - values: list[ Any ], - attributeName: str, - componentNames: tuple[ str, ...] = (), - onPoints: bool = False, - vtkDataType: Union[ int, Any ] = None, + dataSet: vtkDataSet, + values: list[ Any ], + attributeName: str, + componentNames: tuple[ str, ...] = (), # noqa: C408 + onPoints: bool = False, + vtkDataType: Union[ int, Any ] = None, ) -> bool: """Create an attribute with a constant value everywhere. @@ -271,9 +271,9 @@ def createConstantAttributeDataSet( vtkDataType (Union(any, int), optional): vtk data type of the attribute to create. Defaults to None, the type is given by the type of the given value. Waring with int8, uint8 and int64 type of value, several vtk array type use it by default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG + - int8 -> VTK_SIGNED_CHAR + - uint8 -> VTK_UNSIGNED_CHAR + - int64 -> VTK_LONG_LONG Returns: bool: True if the attribute was correctly created. @@ -291,12 +291,12 @@ def createConstantAttributeDataSet( def createAttribute( - dataSet: vtkDataSet, - array: npt.NDArray[ Any ], - attributeName: str, - componentNames: tuple[ str, ...] = (), - onPoints: bool = False, - vtkDataType: Union[ int, Any ] = None, + dataSet: vtkDataSet, + array: npt.NDArray[ Any ], + attributeName: str, + componentNames: tuple[ str, ...] = (), # noqa: C408 + onPoints: bool = False, + vtkDataType: Union[ int, Any ] = None, ) -> bool: """Create an attribute and its VTK array from the given array. @@ -311,9 +311,9 @@ def createAttribute( vtkDataType (Union(any, int), optional): vtk data type of the attribute to create. Defaults to None, the type is given by the type of the given value in the array. Waring with int8, uint8 and int64 type of value, several vtk array type use it. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG + - int8 -> VTK_SIGNED_CHAR + - uint8 -> VTK_UNSIGNED_CHAR + - int64 -> VTK_LONG_LONG Returns: bool: True if the attribute was correctly created. From 27b9c837ca348677f342f5d7445848a51c25fe9a Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Mon, 30 Jun 2025 15:32:12 +0200 Subject: [PATCH 10/56] Create the two files one for the plugin, one for the filter --- .../CreateConstantAttributePerRegion | 188 +++++++++++++ .../PVCreateConstantAttributePerRegion.py | 250 ++++++++++++++++++ 2 files changed, 438 insertions(+) create mode 100644 geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion create mode 100644 geos-pv/src/PVplugins/PVCreateConstantAttributePerRegion.py diff --git a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion new file mode 100644 index 00000000..d0148f83 --- /dev/null +++ b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion @@ -0,0 +1,188 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Romain Baville +from typing import Union + +import numpy as np +import numpy.typing as npt +from typing_extensions import Self + +import vtkmodules.util.numpy_support as vnp +from geos.utils.Logger import Logger, getLogger +from geos.mesh.utils.multiblockHelpers import ( + getBlockElementIndexesFlatten, + getBlockFromFlatIndex, +) +from geos.mesh.utils.arrayHelpers import isAttributeInObject +from geos.mesh.utils.arrayModifiers import createConstantAttribute + +from vtk import VTK_DOUBLE # type: ignore[import-untyped] +from vtkmodules.vtkCommonCore import ( + vtkDataArray, + vtkInformation, + vtkInformationVector, +) +from vtkmodules.vtkCommonDataModel import ( + vtkDataObject, + vtkMultiBlockDataSet, + vtkUnstructuredGrid, +) + +__doc__ = """ +TO DO + +""" + +SOURCE_NAME: str = "" +DEFAULT_REGION_ATTRIBUTE_NAME = "region" + + +class CreateConstantAttributePerRegion( VTKPythonAlgorithmBase ): + + def __init__( self: Self ) -> None: + """Create an attribute with constant value per region.""" + super().__init__( nInputPorts=1, nOutputPorts=1, outputType="vtkDataSet" ) + + self.m_table: list[ tuple[ int, float ] ] = [] + self.m_regionAttributeName: str = DEFAULT_REGION_ATTRIBUTE_NAME + self.m_attributeName: str = "attribute" + + # logger + self.m_logger: Logger = getLogger( "Create Constant Attribute Per Region Filter" ) + + def SetLogger( self: Self, logger: Logger ) -> None: + """Set filter logger. + + Args: + logger (Logger): logger + """ + self.m_logger = logger + + def RequestDataObject( + self: Self, + request: vtkInformation, + inInfoVec: list[ vtkInformationVector ], + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestDataObject. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + inData = self.GetInputData( inInfoVec, 0, 0 ) + outData = self.GetOutputData( outInfoVec, 0 ) + assert inData is not None + if outData is None or ( not outData.IsA( inData.GetClassName() ) ): + outData = inData.NewInstance() + outInfoVec.GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), outData ) + return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] + + def RequestData( + self: Self, + request: vtkInformation, # noqa: F841 + inInfoVec: list[ vtkInformationVector ], # noqa: F841 + outInfoVec: vtkInformationVector, # noqa: F841 + ) -> 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. + """ + self.m_logger.info( f"Apply filter {__name__}" ) + try: + input0: Union[ vtkUnstructuredGrid, vtkMultiBlockDataSet ] = ( self.GetInputData( inInfoVec, 0, 0 ) ) + output: Union[ vtkUnstructuredGrid, vtkMultiBlockDataSet ] = ( self.GetOutputData( outInfoVec, 0 ) ) + + assert input0 is not None, "Input Surface is null." + assert output is not None, "Output pipeline is null." + + output.ShallowCopy( input0 ) + + assert ( len( self.m_regionAttributeName ) + > 0 ), "Region attribute is undefined, please select an attribute." + if isinstance( output, vtkMultiBlockDataSet ): + self.createAttributesMultiBlock( output ) + else: + self.createAttributes( output ) + + mess: str = ( f"The new attribute {self.m_attributeName} was successfully added." ) + self.Modified() + self.m_logger.info( mess ) + except AssertionError as e: + mess1: str = "The new attribute was not added due to:" + self.m_logger.error( mess1 ) + self.m_logger.error( e, exc_info=True ) + return 0 + except Exception as e: + mess0: str = "The new attribute was not added due to:" + self.m_logger.critical( mess0 ) + self.m_logger.critical( e, exc_info=True ) + return 0 + self.m_compute = True + return 1 + + def createAttributesMultiBlock( self: Self, output: vtkMultiBlockDataSet ) -> None: + """Create attributes on vtkMultiBlockDataSet from input data. + + Args: + output (vtkMultiBlockDataSet): mesh where to create the attributes. + """ + # for each block + blockIndexes: list[ int ] = getBlockElementIndexesFlatten( output ) + for blockIndex in blockIndexes: + block0: vtkDataObject = getBlockFromFlatIndex( output, blockIndex ) + assert block0 is not None, "Block is undefined." + block: vtkUnstructuredGrid = vtkUnstructuredGrid.SafeDownCast( block0 ) + try: + self.createAttributes( block ) + except AssertionError as e: + self.m_logger.warning( f"Block {blockIndex}: {e}" ) + output.Modified() + + def createAttributes( self: Self, mesh: vtkUnstructuredGrid ) -> None: + """Create attributes on vtkUnstructuredGrid from input data. + + Args: + mesh (vtkUnstructuredGrid): mesh where to create the attributes. + """ + assert isAttributeInObject( mesh, self.m_regionAttributeName, + False ), f"{self.m_regionAttributeName} is not in the mesh." + regionAttr: vtkDataArray = mesh.GetCellData().GetArray( self.m_regionAttributeName ) + assert regionAttr is not None, "Region attribute is undefined" + npArray: npt.NDArray[ np.float64 ] = self.createNpArray( regionAttr ) + newAttr: vtkDataArray = vnp.numpy_to_vtk( npArray, True, VTK_DOUBLE ) + newAttr.SetName( self.m_attributeName ) + mesh.GetCellData().AddArray( newAttr ) + mesh.GetCellData().Modified() + mesh.Modified() + + def createNpArray( self: Self, regionAttr: vtkDataArray ) -> npt.NDArray[ np.float64 ]: + """Create numpy arrays from input data. + + Args: + regionAttr (vtkDataArray): Region attribute + + Returns: + npt.NDArray[np.float64]: numpy array of the new attribute. + """ + regionNpArray: npt.NDArray[ np.float64 ] = vnp.vtk_to_numpy( regionAttr ) + npArray: npt.NDArray[ np.float64 ] = np.full_like( regionNpArray, np.nan ) + # for each region + for regionIndex, value in self.m_table: + if regionIndex in np.unique( regionNpArray ): + mask: npt.NDArray[ np.bool_ ] = regionNpArray == regionIndex + npArray[ mask ] = value + else: + self.m_logger.warning( f"Index {regionIndex} is not in the values of the region" + + f" attribute '{regionAttr.GetName()}'" ) + return npArray diff --git a/geos-pv/src/PVplugins/PVCreateConstantAttributePerRegion.py b/geos-pv/src/PVplugins/PVCreateConstantAttributePerRegion.py new file mode 100644 index 00000000..d9d36260 --- /dev/null +++ b/geos-pv/src/PVplugins/PVCreateConstantAttributePerRegion.py @@ -0,0 +1,250 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Martin Lemay, Romain Baville +# ruff: noqa: E402 # disable Module level import not at top of file +import sys +from pathlib import Path +from typing import Union + +import numpy as np +import numpy.typing as npt +from typing_extensions import Self + +import vtkmodules.util.numpy_support as vnp +from geos.utils.Logger import Logger, getLogger +from geos.mesh.utils.multiblockHelpers import ( + getBlockElementIndexesFlatten, + getBlockFromFlatIndex, +) +from geos.mesh.utils.arrayHelpers import isAttributeInObject +from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] + VTKPythonAlgorithmBase, smdomain, smhint, smproperty, smproxy, +) +from vtk import VTK_DOUBLE # type: ignore[import-untyped] +from vtkmodules.vtkCommonCore import ( + vtkDataArray, + vtkInformation, + vtkInformationVector, +) +from vtkmodules.vtkCommonDataModel import ( + vtkDataObject, + vtkMultiBlockDataSet, + vtkUnstructuredGrid, +) + +# update sys.path to load all GEOS Python Package dependencies +geos_pv_path: Path = Path( __file__ ).parent.parent.parent +sys.path.insert( 0, str( geos_pv_path / "src" ) ) +from geos.pv.utils.config import update_paths + +update_paths() + +from geos.mesh.processing import CreateConstantAttributePerRegion + +__doc__ = """ +PVCreateConstantAttributePerRegion is a Paraview plugin that allows to +create 2 attributes whom values are constant for each region index. + +Input and output are either vtkMultiBlockDataSet or vtkUnstructuredGrid. + +To use it: + +* Load the module in Paraview: Tools>Manage Plugins...>Load new>PVCreateConstantAttributePerRegion. +* Select the mesh you want to create the attributes and containing a region attribute. +* Search and Apply Create Constant Attribute Per Region Filter. + +""" + +SOURCE_NAME: str = "" +DEFAULT_REGION_ATTRIBUTE_NAME = "region" + + +@smproxy.filter( + name="PVCreateConstantAttributePerRegion", + label="Create Constant Attribute Per Region", +) +@smhint.xml( """""" ) +@smproperty.input( name="Input", port_index=0 ) +@smdomain.datatype( + dataTypes=[ "vtkMultiBlockDataSet", "vtkUnstructuredGrid" ], + composite_data_supported=True, +) +class PVCreateConstantAttributePerRegion( VTKPythonAlgorithmBase ): + + def __init__( self: Self ) -> None: + """Create an attribute with constant value per region.""" + super().__init__( nInputPorts=1, nOutputPorts=1, outputType="vtkDataSet" ) + + self.m_table: list[ tuple[ int, float ] ] = [] + self.m_regionAttributeName: str = DEFAULT_REGION_ATTRIBUTE_NAME + self.m_attributeName: str = "attribute" + + # logger + self.m_logger: Logger = getLogger( "Create Constant Attribute Per Region Filter" ) + + def SetLogger( self: Self, logger: Logger ) -> None: + """Set filter logger. + + Args: + logger (Logger): logger + """ + self.m_logger = logger + + @smproperty.xml( """ + + + + + + + + Select an attribute containing the indexes of the regions + + + """ ) + def a01SetRegionAttributeName( self: Self, name: str ) -> None: + """Set region attribute name.""" + self.m_regionAttributeName = name + self.Modified() + + @smproperty.xml( """ + + + Name of the new attribute + + + """ ) + def a02SetAttributeName( self: Self, value: str ) -> None: + """Set attribute name. + + Args: + value (str): attribute name. + """ + self.m_attributeName = value + self.Modified() + + @smproperty.xml( """ + + + + + + + + + Set new attributes values for each region index. + + + """ ) + def b01SetAttributeValues( self: Self, regionIndex: int, value: float ) -> None: + """Set attribute values per region. + + Args: + regionIndex (int): region index. + + value (float): attribute value. + """ + self.m_table.append( ( regionIndex, value ) ) + self.Modified() + + @smproperty.xml( """ + + """ ) + def b02GroupFlow( self: Self ) -> None: + """Organize groups.""" + self.Modified() + + def RequestDataObject( + self: Self, + request: vtkInformation, + inInfoVec: list[ vtkInformationVector ], + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestDataObject. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + inData = self.GetInputData( inInfoVec, 0, 0 ) + outData = self.GetOutputData( outInfoVec, 0 ) + assert inData is not None + if outData is None or ( not outData.IsA( inData.GetClassName() ) ): + outData = inData.NewInstance() + outInfoVec.GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), outData ) + return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] + + def RequestData( + self: Self, + request: vtkInformation, # noqa: F841 + inInfoVec: list[ vtkInformationVector ], # noqa: F841 + outInfoVec: vtkInformationVector, # noqa: F841 + ) -> 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. + """ + self.m_logger.info( f"Apply filter {__name__}" ) + try: + input0: Union[ vtkUnstructuredGrid, vtkMultiBlockDataSet ] = ( self.GetInputData( inInfoVec, 0, 0 ) ) + output: Union[ vtkUnstructuredGrid, vtkMultiBlockDataSet ] = ( self.GetOutputData( outInfoVec, 0 ) ) + + assert input0 is not None, "Input Surface is null." + assert output is not None, "Output pipeline is null." + + output.ShallowCopy( input0 ) + + assert ( len( self.m_regionAttributeName ) + > 0 ), "Region attribute is undefined, please select an attribute." + if isinstance( output, vtkMultiBlockDataSet ): + self.createAttributesMultiBlock( output ) + else: + self.createAttributes( output ) + + mess: str = ( f"The new attribute {self.m_attributeName} was successfully added." ) + self.Modified() + self.m_logger.info( mess ) + except AssertionError as e: + mess1: str = "The new attribute was not added due to:" + self.m_logger.error( mess1 ) + self.m_logger.error( e, exc_info=True ) + return 0 + except Exception as e: + mess0: str = "The new attribute was not added due to:" + self.m_logger.critical( mess0 ) + self.m_logger.critical( e, exc_info=True ) + return 0 + self.m_compute = True + return 1 + From 69dca822b634bbf6645f13ede182c1393930c0dd Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Mon, 30 Jun 2025 18:45:55 +0200 Subject: [PATCH 11/56] Create the vtk filter from the paraview plugin --- .../CreateConstantAttributePerRegion | 145 ++++++++++-------- 1 file changed, 77 insertions(+), 68 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion index d0148f83..998a3b68 100644 --- a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion +++ b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion @@ -1,55 +1,71 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. # SPDX-FileContributor: Romain Baville -from typing import Union - import numpy as np import numpy.typing as npt +from typing import Union, Any from typing_extensions import Self import vtkmodules.util.numpy_support as vnp -from geos.utils.Logger import Logger, getLogger -from geos.mesh.utils.multiblockHelpers import ( - getBlockElementIndexesFlatten, - getBlockFromFlatIndex, -) -from geos.mesh.utils.arrayHelpers import isAttributeInObject -from geos.mesh.utils.arrayModifiers import createConstantAttribute -from vtk import VTK_DOUBLE # type: ignore[import-untyped] +from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] + VTKPythonAlgorithmBase, +) from vtkmodules.vtkCommonCore import ( - vtkDataArray, vtkInformation, vtkInformationVector, ) from vtkmodules.vtkCommonDataModel import ( - vtkDataObject, vtkMultiBlockDataSet, vtkUnstructuredGrid, + vtkDataSet, ) +from geos.utils.Logger import Logger, getLogger + +from geos.mesh.utils.arrayHelpers import isAttributeInObject +from geos.mesh.utils.arrayModifiers import createAttribute __doc__ = """ TO DO """ -SOURCE_NAME: str = "" -DEFAULT_REGION_ATTRIBUTE_NAME = "region" class CreateConstantAttributePerRegion( VTKPythonAlgorithmBase ): def __init__( self: Self ) -> None: """Create an attribute with constant value per region.""" - super().__init__( nInputPorts=1, nOutputPorts=1, outputType="vtkDataSet" ) + super().__init__( nInputPorts=1, nOutputPorts=1, outputType="vtkMultiBlockDataSet, vtkDataSet" ) + + self._SetRegionName() + self._SetAttributeName() + self._SetInfoRegion() - self.m_table: list[ tuple[ int, float ] ] = [] - self.m_regionAttributeName: str = DEFAULT_REGION_ATTRIBUTE_NAME - self.m_attributeName: str = "attribute" # logger self.m_logger: Logger = getLogger( "Create Constant Attribute Per Region Filter" ) + def _SetRegionName( self: Self, regionName: str = "Region" ) -> None: + self.regionName: str = regionName + + def _SetAttributeName( self: Self, attributeName: str = "Attribute" ) -> None: + self.attributeName: str = attributeName + + def _SetInfoRegion( self: Self, dictRegion: dict[ Any, Any ] = {}, valueType: int = 10, defaultValue: Any = np.nan ) -> None: + dictType: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() + self.valueType: type = dictType[ valueType ] + + self.dictRegion: dict[ Any, Any ] = dictRegion + for idRegion in self.dictRegion.keys(): + self.dictRegion[ idRegion ] = self.valueType( self.dictRegion[ idRegion ] ) + + if np.isnan( defaultValue ): + if valueType not in [ 10, 11 ]: + defaultValue = -1 + + self.defaultValue = self.valueType(defaultValue) + def SetLogger( self: Self, logger: Logger ) -> None: """Set filter logger. @@ -108,14 +124,42 @@ class CreateConstantAttributePerRegion( VTKPythonAlgorithmBase ): output.ShallowCopy( input0 ) - assert ( len( self.m_regionAttributeName ) - > 0 ), "Region attribute is undefined, please select an attribute." + assert ( len( self.regionName ) > 0 ), "Region attribute is undefined, please select an attribute." + + onPoints: bool + if isAttributeInObject( input0, self.regionName, False ): + onPoints = False + elif isAttributeInObject( input0, self.regionName, True ): + onPoints = True + else: + mess = f"{self.regionName} is not in the mesh." + self.m_logger.info( mess ) + return 0 + + regionNpArray: npt.NDArray[ Any ] if isinstance( output, vtkMultiBlockDataSet ): - self.createAttributesMultiBlock( output ) + nbBlock: int = output.GetNumberOfBlocks() + for idBlock in range( nbBlock ): + dataSetOutput: vtkDataSet = output.GetBlock( idBlock ) + dataSetInput0: vtkDataSet = input0.GetBlock( idBlock ) + if onPoints: + regionNpArray = vnp.vtk_to_numpy( dataSetInput0.GetPointData().GetArray( self.regionName ) ) + else: + regionNpArray = vnp.vtk_to_numpy( dataSetInput0.GetCellData().GetArray( self.regionName ) ) + + npArray: npt.NDArray[ Any ] = self.createNpArray( regionNpArray ) + createAttribute( dataSetOutput, npArray, self.attributeName, onPoints=onPoints ) + else: - self.createAttributes( output ) + if onPoints: + regionNpArray = vnp.vtk_to_numpy( input0.GetPointData().GetArray( self.regionName ) ) + else: + regionNpArray = vnp.vtk_to_numpy( input0.GetCellData().GetArray( self.regionName ) ) + + npArray: npt.NDArray[ Any ] = self.createNpArray( regionNpArray ) + createAttribute( output, npArray, self.attributeName, onPoints=onPoints ) - mess: str = ( f"The new attribute {self.m_attributeName} was successfully added." ) + mess: str = ( f"The new attribute {self.attributeName} was successfully added." ) self.Modified() self.m_logger.info( mess ) except AssertionError as e: @@ -131,58 +175,23 @@ class CreateConstantAttributePerRegion( VTKPythonAlgorithmBase ): self.m_compute = True return 1 - def createAttributesMultiBlock( self: Self, output: vtkMultiBlockDataSet ) -> None: - """Create attributes on vtkMultiBlockDataSet from input data. - Args: - output (vtkMultiBlockDataSet): mesh where to create the attributes. - """ - # for each block - blockIndexes: list[ int ] = getBlockElementIndexesFlatten( output ) - for blockIndex in blockIndexes: - block0: vtkDataObject = getBlockFromFlatIndex( output, blockIndex ) - assert block0 is not None, "Block is undefined." - block: vtkUnstructuredGrid = vtkUnstructuredGrid.SafeDownCast( block0 ) - try: - self.createAttributes( block ) - except AssertionError as e: - self.m_logger.warning( f"Block {blockIndex}: {e}" ) - output.Modified() - - def createAttributes( self: Self, mesh: vtkUnstructuredGrid ) -> None: - """Create attributes on vtkUnstructuredGrid from input data. - - Args: - mesh (vtkUnstructuredGrid): mesh where to create the attributes. - """ - assert isAttributeInObject( mesh, self.m_regionAttributeName, - False ), f"{self.m_regionAttributeName} is not in the mesh." - regionAttr: vtkDataArray = mesh.GetCellData().GetArray( self.m_regionAttributeName ) - assert regionAttr is not None, "Region attribute is undefined" - npArray: npt.NDArray[ np.float64 ] = self.createNpArray( regionAttr ) - newAttr: vtkDataArray = vnp.numpy_to_vtk( npArray, True, VTK_DOUBLE ) - newAttr.SetName( self.m_attributeName ) - mesh.GetCellData().AddArray( newAttr ) - mesh.GetCellData().Modified() - mesh.Modified() - - def createNpArray( self: Self, regionAttr: vtkDataArray ) -> npt.NDArray[ np.float64 ]: + def createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArray[ Any ]: """Create numpy arrays from input data. Args: - regionAttr (vtkDataArray): Region attribute + regionNpArray (npt.NDArray[ Any ]): Region attribute Returns: npt.NDArray[np.float64]: numpy array of the new attribute. """ - regionNpArray: npt.NDArray[ np.float64 ] = vnp.vtk_to_numpy( regionAttr ) - npArray: npt.NDArray[ np.float64 ] = np.full_like( regionNpArray, np.nan ) - # for each region - for regionIndex, value in self.m_table: - if regionIndex in np.unique( regionNpArray ): - mask: npt.NDArray[ np.bool_ ] = regionNpArray == regionIndex - npArray[ mask ] = value + nbElements: int = len ( regionNpArray ) + npArray: npt.NDArray[ Any ] = np.ones( nbElements, self.valueType ) + for elem in range( nbElements ): + idRegion: Any = regionNpArray[elem] + if idRegion in self.dictRegion.keys(): + npArray[elem] = self.dictRegion[idRegion] else: - self.m_logger.warning( f"Index {regionIndex} is not in the values of the region" + - f" attribute '{regionAttr.GetName()}'" ) + npArray[elem] = self.defaultValue + return npArray From 27b6abfe1c50bbf1279c29856d8d235044acde9c Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 1 Jul 2025 10:15:32 +0200 Subject: [PATCH 12/56] Fix build issue --- ...on => CreateConstantAttributePerRegion.py} | 51 ++++++++++--------- .../PVCreateConstantAttributePerRegion.py | 2 +- 2 files changed, 28 insertions(+), 25 deletions(-) rename geos-mesh/src/geos/mesh/processing/{CreateConstantAttributePerRegion => CreateConstantAttributePerRegion.py} (96%) diff --git a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py similarity index 96% rename from geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion rename to geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py index 998a3b68..01a1dcb1 100644 --- a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion +++ b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py @@ -42,30 +42,9 @@ def __init__( self: Self ) -> None: self._SetAttributeName() self._SetInfoRegion() - # logger self.m_logger: Logger = getLogger( "Create Constant Attribute Per Region Filter" ) - def _SetRegionName( self: Self, regionName: str = "Region" ) -> None: - self.regionName: str = regionName - - def _SetAttributeName( self: Self, attributeName: str = "Attribute" ) -> None: - self.attributeName: str = attributeName - - def _SetInfoRegion( self: Self, dictRegion: dict[ Any, Any ] = {}, valueType: int = 10, defaultValue: Any = np.nan ) -> None: - dictType: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() - self.valueType: type = dictType[ valueType ] - - self.dictRegion: dict[ Any, Any ] = dictRegion - for idRegion in self.dictRegion.keys(): - self.dictRegion[ idRegion ] = self.valueType( self.dictRegion[ idRegion ] ) - - if np.isnan( defaultValue ): - if valueType not in [ 10, 11 ]: - defaultValue = -1 - - self.defaultValue = self.valueType(defaultValue) - def SetLogger( self: Self, logger: Logger ) -> None: """Set filter logger. @@ -137,6 +116,7 @@ def RequestData( return 0 regionNpArray: npt.NDArray[ Any ] + npArray: npt.NDArray[ Any ] if isinstance( output, vtkMultiBlockDataSet ): nbBlock: int = output.GetNumberOfBlocks() for idBlock in range( nbBlock ): @@ -147,7 +127,7 @@ def RequestData( else: regionNpArray = vnp.vtk_to_numpy( dataSetInput0.GetCellData().GetArray( self.regionName ) ) - npArray: npt.NDArray[ Any ] = self.createNpArray( regionNpArray ) + npArray = self.createNpArray( regionNpArray ) createAttribute( dataSetOutput, npArray, self.attributeName, onPoints=onPoints ) else: @@ -156,7 +136,7 @@ def RequestData( else: regionNpArray = vnp.vtk_to_numpy( input0.GetCellData().GetArray( self.regionName ) ) - npArray: npt.NDArray[ Any ] = self.createNpArray( regionNpArray ) + npArray = self.createNpArray( regionNpArray ) createAttribute( output, npArray, self.attributeName, onPoints=onPoints ) mess: str = ( f"The new attribute {self.attributeName} was successfully added." ) @@ -172,10 +152,33 @@ def RequestData( self.m_logger.critical( mess0 ) self.m_logger.critical( e, exc_info=True ) return 0 - self.m_compute = True + return 1 + def _SetRegionName( self: Self, regionName: str = "" ) -> None: + self.regionName: str = regionName + + + def _SetAttributeName( self: Self, attributeName: str = "Attribute" ) -> None: + self.attributeName: str = attributeName + + + def _SetInfoRegion( self: Self, dictRegion: dict[ Any, Any ] = {}, valueType: int = 10, defaultValue: Any = np.nan ) -> None: + dictType: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() + self.valueType: type = dictType[ valueType ] + + self.dictRegion: dict[ Any, Any ] = dictRegion + for idRegion in self.dictRegion.keys(): + self.dictRegion[ idRegion ] = self.valueType( self.dictRegion[ idRegion ] ) + + if np.isnan( defaultValue ): + if valueType not in [ 10, 11 ]: + defaultValue = -1 + + self.defaultValue = self.valueType(defaultValue) + + def createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArray[ Any ]: """Create numpy arrays from input data. diff --git a/geos-pv/src/PVplugins/PVCreateConstantAttributePerRegion.py b/geos-pv/src/PVplugins/PVCreateConstantAttributePerRegion.py index d9d36260..7001576f 100644 --- a/geos-pv/src/PVplugins/PVCreateConstantAttributePerRegion.py +++ b/geos-pv/src/PVplugins/PVCreateConstantAttributePerRegion.py @@ -39,7 +39,7 @@ update_paths() -from geos.mesh.processing import CreateConstantAttributePerRegion +from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion __doc__ = """ PVCreateConstantAttributePerRegion is a Paraview plugin that allows to From bcdf4bdfeda7d0de11077499a432f42e6066fbd4 Mon Sep 17 00:00:00 2001 From: Romain Baville <126683264+RomainBaville@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:04:43 +0200 Subject: [PATCH 13/56] Apply suggestions from code review Co-authored-by: paloma-martinez <104762252+paloma-martinez@users.noreply.github.com> --- geos-mesh/src/geos/mesh/utils/arrayHelpers.py | 14 ++-- .../src/geos/mesh/utils/arrayModifiers.py | 82 +++++++++---------- geos-mesh/tests/test_arrayHelpers.py | 2 +- geos-mesh/tests/test_arrayModifiers.py | 8 +- .../src/geos_posp/filters/GeosBlockMerge.py | 2 +- 5 files changed, 54 insertions(+), 54 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index 4498203f..01b81edb 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -361,15 +361,15 @@ def getArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) - def getVtkArrayTypeInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> int: - """Return the type of the vtk array corrsponding to input attribute name in table. + """Return VTK type of requested array from dataset input. Args: - object (PointSet or UnstructuredGrid): input object. - attributeName (str): name of the attribute. + object (PointSet or UnstructuredGrid): Input object. + attributeName (str): Name of the attribute. onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - int: the type of the vtk array corrsponding to input attribute name. + int: the type of the vtk array corresponding to input attribute name. """ array: vtkDataArray = getVtkArrayInObject( object, attributeName, onPoints ) vtkArrayType: int = array.GetDataType() @@ -378,15 +378,15 @@ def getVtkArrayTypeInObject( object: vtkDataSet, attributeName: str, onPoints: b def getVtkArrayTypeInMultiBlock( multiBlockDataSet: vtkMultiBlockDataSet, attributeName: str, onPoints: bool ) -> int: - """Return the type of the vtk array corrsponding to input attribute name in the multiblock data set if it exist. + """Return VTK type of requested array from multiblock dataset input, if existing. Args: multiBlockDataSet (PointSet or UnstructuredGrid): input object. - attributeName (str): name of the attribute. + attributeName (str): Name of the attribute. onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - int: type of the vtk array corrsponding to input attribute name, -1 if the multiblock has no attribute with given name. + int: Type of the requested vtk array if existing in input multiblock dataset, otherwise -1. """ nbBlocks = multiBlockDataSet.GetNumberOfBlocks() for idBlock in range( nbBlocks ): diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index df530189..15d2ba01 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -58,12 +58,12 @@ def fillPartialAttributes( """Fill input partial attribute of multiBlockDataSet with the same value for all the components. Args: - multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): multiBlockDataSet where to fill the attribute. - attributeName (str): attribute name. + multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): MultiBlockDataSet where to fill the attribute. + attributeName (str): Attribute name. onPoints (bool, optional): Attribute is on Points (True) or on Cells (False). Defaults to False. - value (any, optional): value to fill in the partial atribute. - Defaults to nan. For int vtk array, default value is automatically set to -1. + value (any, optional): Filling value. + Defaults to -1 for int VTK arrays, nan otherwise. Returns: bool: True if calculation successfully ended. @@ -107,12 +107,12 @@ def fillAllPartialAttributes( multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], value: Any = np.nan, ) -> bool: - """Fill all the partial attributes of multiBlockDataSet with same value for all attributes and they components. + """Fill all the partial attributes of a multiBlockDataSet with a same value. All components of each attribute are filled with the same value. Args: - multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): multiBlockDataSet where to fill the attribute. - value (any, optional): value to fill in the partial atribute. - Defaults to nan. For int vtk array, default value is automatically set to -1. + multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): MultiBlockDataSet where to fill the attribute. + value (any, optional): Filling value. + Defaults to -1 for int VTK arrays, nan otherwise. Returns: bool: True if calculation successfully ended. @@ -136,8 +136,8 @@ def createEmptyAttribute( Args: attributeName (str): name of the attribute - componentNames (tuple[str,...]): name of the components for vectorial attributes. - vtkDataType (int): data type. + componentNames (tuple[str,...]): Name of the components for vectorial attributes. + vtkDataType (int): Data type. Returns: bool: True if the attribute was correctly created. @@ -169,22 +169,22 @@ def createConstantAttribute( """Create an attribute with a constant value everywhere if absent. Args: - object (vtkDataObject): object (vtkMultiBlockDataSet, vtkDataSet) where to create the attribute. - values ( list[float]): list of values of the attribute for each components. - attributeName (str): name of the attribute. - componentNames (tuple[str,...], optional): name of the components for vectorial attributes. If one component, give an empty tuple. + object (vtkDataObject): Object (vtkMultiBlockDataSet, vtkDataSet) where to create the attribute. + values (list[float]): List of values of the attribute for each components. + attributeName (str): Name of the attribute. + componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. onPoints (bool): True if attributes are on points, False if they are on cells. Defaults to False. - vtkDataType (Union(any, int), optional): vtk data type of the attribute to create. + vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. Defaults to None, the type is given by the type of the array value. - Waring with int8, uint8 and int64 type of value, several vtk array type use it by default: + Warning with int8, uint8 and int64 type of value, several vtk array type use it by default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG Returns: - bool: True if the attribute was correctly created False if the attribute was already present. + bool: True if the attribute was correctly created, False otherwise. """ if isinstance( object, ( vtkMultiBlockDataSet, vtkCompositeDataSet ) ): return createConstantAttributeMultiBlock( object, values, attributeName, componentNames, onPoints, vtkDataType ) @@ -211,15 +211,15 @@ def createConstantAttributeMultiBlock( Args: multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet): vtkMultiBlockDataSet where to create the attribute. - values (list[any]): list of values of the attribute for each components. - attributeName (str): name of the attribute. - componentNames (tuple[str,...], optional): name of the components for vectorial attributes. If one component, give an empty tuple. + values (list[any]): List of values of the attribute for each components. + attributeName (str): Name of the attribute. + componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. onPoints (bool): True if attributes are on points, False if they are on cells. Defaults to False. - vtkDataType (Union(any, int), optional): vtk data type of the attribute to create. + vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. Defaults to None, the type is given by the type of the given value. - Waring with int8, uint8 and int64 type of value, several vtk array type use it by default: + Warning with int8, uint8 and int64 type of value, several vtk array type use it by default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG @@ -261,16 +261,16 @@ def createConstantAttributeDataSet( """Create an attribute with a constant value everywhere. Args: - dataSet (vtkDataSet): vtkDataSet where to create the attribute. - values ( list[any]): list of values of the attribute for each components. - attributeName (str): name of the attribute. - componentNames (tuple[str,...], optional): name of the components for vectorial attributes. If one component, give an empty tuple. + dataSet (vtkDataSet): VtkDataSet where to create the attribute. + values ( list[any]): List of values of the attribute for each components. + attributeName (str): Name of the attribute. + componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. onPoints (bool): True if attributes are on points, False if they are on cells. Defaults to False. - vtkDataType (Union(any, int), optional): vtk data type of the attribute to create. + vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. Defaults to None, the type is given by the type of the given value. - Waring with int8, uint8 and int64 type of value, several vtk array type use it by default: + Warning with int8, uint8 and int64 type of value, several vtk array type use it by default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG @@ -301,16 +301,16 @@ def createAttribute( """Create an attribute and its VTK array from the given array. Args: - dataSet (vtkDataSet): dataSet where to create the attribute. - array (npt.NDArray[any]): array that contains the values. - attributeName (str): name of the attribute. - componentNames (tuple[str,...], optional): name of the components for vectorial attributes. If one component, give an empty tuple. + dataSet (vtkDataSet): DataSet where to create the attribute. + array (npt.NDArray[any]): Array that contains the values. + attributeName (str): Name of the attribute. + componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. onPoints (bool): True if attributes are on points, False if they are on cells. Defaults to False. - vtkDataType (Union(any, int), optional): vtk data type of the attribute to create. + vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. Defaults to None, the type is given by the type of the given value in the array. - Waring with int8, uint8 and int64 type of value, several vtk array type use it. By default: + Warning with int8, uint8 and int64 type of value, several vtk array type use it. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG @@ -329,9 +329,9 @@ def createAttribute( if nbNames < nbComponents: componentNames = tuple( [ "Component" + str( i ) for i in range( nbComponents ) ] ) - print( "Not enough component name enter, component names are seted to : Component0, Component1 ..." ) + print( "Insufficient number of input component names. Component names will be set to : Component0, Component1 ..." ) elif nbNames > nbComponents: - print( "To many component names enter, the lastest will not be taken into account." ) + print( f"Excessive number of input component names, only the {len(nbComponents)} first ones will be used." ) for i in range( nbComponents ): createdAttribute.SetComponentName( i, componentNames[ i ] ) @@ -402,8 +402,8 @@ def copyAttributeDataSet( Args: objectFrom (vtkDataSet): object from which to copy the attribute. objectTo (vtkDataSet): object where to copy the attribute. - attributeNameFrom (str): attribute name in objectFrom. - attributeNameTo (str): attribute name in objectTo. + attributeNameFrom (str): Attribute name in objectFrom. + attributeNameTo (str): Attribute name in objectTo. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. @@ -433,9 +433,9 @@ def renameAttribute( """Rename an attribute. Args: - object (vtkMultiBlockDataSet): object where the attribute is. - attributeName (str): name of the attribute. - newAttributeName (str): new name of the attribute. + object (vtkMultiBlockDataSet): Object where the attribute is. + attributeName (str): Name of the attribute. + newAttributeName (str): New name of the attribute. onPoints (bool): True if attributes are on points, False if they are on cells. Returns: diff --git a/geos-mesh/tests/test_arrayHelpers.py b/geos-mesh/tests/test_arrayHelpers.py index eeebd177..d3d411d7 100644 --- a/geos-mesh/tests/test_arrayHelpers.py +++ b/geos-mesh/tests/test_arrayHelpers.py @@ -114,7 +114,7 @@ def test_getVtkArrayTypeInMultiBlock( dataSetTest: vtkMultiBlockDataSet, attribu vtkDataTypeTest: int = arrayHelpers.getVtkArrayTypeInMultiBlock( multiBlockDataSet, attributeName, onPoints ) - assert ( vtkDataType == vtkDataTypeTest ) + assert ( vtkDataTypeTest == vtkDataType ) @pytest.mark.parametrize( "attributeName, onPoints", [ diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index 3aff05c4..bf406a1f 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -80,10 +80,10 @@ def test_fillPartialAttributes( valueTypeRef: str, ) -> None: """Test filling a partial attribute from a multiblock with values.""" - MultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - arrayModifiers.fillPartialAttributes( MultiBlockDataSetTest, attributeName, onPoints, value ) + multiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + arrayModifiers.fillPartialAttributes( multiBlockDataSetTest, attributeName, onPoints, value ) - blockTest: vtkDataSet = cast( vtkDataSet, MultiBlockDataSetTest.GetBlock( idBlockToFill ) ) + blockTest: vtkDataSet = cast( vtkDataSet, multiBlockDataSetTest.GetBlock( idBlockToFill ) ) dataTest: Union[ vtkPointData, vtkCellData ] nbElements: int if onPoints: @@ -95,7 +95,7 @@ def test_fillPartialAttributes( attributeFillTest: vtkDataArray = dataTest.GetArray( attributeName ) nbComponentsTest: int = attributeFillTest.GetNumberOfComponents() - assert nbComponentsRef == nbComponentsTest + assert nbComponentsTest == nbComponentsRef npArrayFillRef: npt.NDArray[ Any ] if nbComponentsRef > 1: diff --git a/geos-posp/src/geos_posp/filters/GeosBlockMerge.py b/geos-posp/src/geos_posp/filters/GeosBlockMerge.py index 0844b1e8..8d24b593 100644 --- a/geos-posp/src/geos_posp/filters/GeosBlockMerge.py +++ b/geos-posp/src/geos_posp/filters/GeosBlockMerge.py @@ -367,7 +367,7 @@ def mergeChildBlocks( self: Self, compositeBlock: vtkMultiBlockDataSet ) -> vtkU """ # fill partial attributes in all children blocks if not fillAllPartialAttributes( compositeBlock ): - self.m_logger.warning( "Some partial attributes may not have been " + "propagated to the whole mesh." ) + self.m_logger.warning( "Some partial attributes may not have been propagated to the whole mesh." ) # merge blocks return mergeBlocks( compositeBlock ) From e79f5abed7373455cf851df6003d78cac67fc67e Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 15 Jul 2025 11:55:01 +0200 Subject: [PATCH 14/56] Generalize error message of copyAttribute --- geos-mesh/src/geos/mesh/utils/arrayModifiers.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index 15d2ba01..6cfb9525 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -356,10 +356,10 @@ def copyAttribute( """Copy an attribute from objectFrom to objectTo. Args: - objectFrom (vtkMultiBlockDataSet): object from which to copy the attribute. - objectTo (vtkMultiBlockDataSet): object where to copy the attribute. - attributeNameFrom (str): attribute name in objectFrom. - attributeNameTo (str): attribute name in objectTo. + objectFrom (vtkMultiBlockDataSet): Object from which to copy the attribute. + objectTo (vtkMultiBlockDataSet): Object where to copy the attribute. + attributeNameFrom (str): Attribute name in objectFrom. + attributeNameTo (str): Attribute name in objectTo. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. @@ -370,16 +370,14 @@ def copyAttribute( elementaryBlockIndexesFrom: list[ int ] = getBlockElementIndexesFlatten( objectFrom ) assert elementaryBlockIndexesTo == elementaryBlockIndexesFrom, ( - "ObjectFrom " + "and objectTo do not have the same block indexes." ) + "ObjectFrom and objectTo do not have the same block indexes." ) for index in elementaryBlockIndexesTo: - # get block from initial time step object blockFrom: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( objectFrom, index ) ) - assert blockFrom is not None, "Block at initial time step is null." + assert blockFrom is not None, f"Block { str( index ) } of objectFrom is null." - # get block from current time step object blockTo: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( objectTo, index ) ) - assert blockTo is not None, "Block at current time step is null." + assert blockTo is not None, f"Block { str( index ) } of objectTo is null." try: copyAttributeDataSet( blockFrom, blockTo, attributeNameFrom, attributeNameTo, onPoints ) From b17e2e51f8a58e9fa6127ff3d42c0f94dac8e7c6 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 15 Jul 2025 11:57:18 +0200 Subject: [PATCH 15/56] Add a raise assertion error in case of the mesh doen't have the attribute --- geos-mesh/src/geos/mesh/utils/arrayHelpers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index 01b81edb..d466ef62 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -381,12 +381,12 @@ def getVtkArrayTypeInMultiBlock( multiBlockDataSet: vtkMultiBlockDataSet, attrib """Return VTK type of requested array from multiblock dataset input, if existing. Args: - multiBlockDataSet (PointSet or UnstructuredGrid): input object. + multiBlockDataSet (vtkMultiBlockDataSet): Input object. attributeName (str): Name of the attribute. onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - int: Type of the requested vtk array if existing in input multiblock dataset, otherwise -1. + int: Type of the requested vtk array if existing in input multiblock dataset. """ nbBlocks = multiBlockDataSet.GetNumberOfBlocks() for idBlock in range( nbBlocks ): @@ -395,8 +395,7 @@ def getVtkArrayTypeInMultiBlock( multiBlockDataSet: vtkMultiBlockDataSet, attrib if attributeName in listAttributes: return getVtkArrayTypeInObject( object, attributeName, onPoints ) - print( "The vtkMultiBlockDataSet has no attribute with the name " + attributeName + "." ) - return -1 + raise AssertionError( "The vtkMultiBlockDataSet has no attribute with the name " + attributeName + "." ) def getVtkArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> vtkDataArray: From 5941980e213cc7258765417d5e59465b425d66a9 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 15 Jul 2025 13:37:34 +0200 Subject: [PATCH 16/56] Update the default value for uint case for fillpartialattribute --- .../src/geos/mesh/utils/arrayModifiers.py | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index 6cfb9525..6f823bf0 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -6,7 +6,7 @@ import vtkmodules.util.numpy_support as vnp from typing import Union, Any from vtk import ( # type: ignore[import-untyped] - VTK_DOUBLE, VTK_FLOAT, + VTK_DOUBLE, VTK_FLOAT, VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG, ) from vtkmodules.vtkCommonDataModel import ( vtkMultiBlockDataSet, @@ -63,13 +63,12 @@ def fillPartialAttributes( onPoints (bool, optional): Attribute is on Points (True) or on Cells (False). Defaults to False. value (any, optional): Filling value. - Defaults to -1 for int VTK arrays, nan otherwise. + Defaults to -1 for int VTK arrays, 0 for uint VTK arrays and nan otherwise. Returns: bool: True if calculation successfully ended. """ vtkArrayType: int = getVtkArrayTypeInMultiBlock( multiBlockDataSet, attributeName, onPoints ) - assert vtkArrayType != -1 infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints ) nbComponents: int = infoAttributes[ attributeName ] @@ -78,22 +77,22 @@ def fillPartialAttributes( if nbComponents > 1: componentNames = getComponentNames( multiBlockDataSet, attributeName, onPoints ) - valueType: Any = type( value ) typeMapping: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() - valueTypeExpected: Any = typeMapping[ vtkArrayType ] - if valueTypeExpected != valueType: - if np.isnan( value ): - if vtkArrayType in ( VTK_DOUBLE, VTK_FLOAT ): - value = valueTypeExpected( value ) - else: - print( attributeName + " vtk array type is " + str( valueTypeExpected ) + - ", default value is automatically set to -1." ) - value = valueTypeExpected( -1 ) - + valueType: Any = typeMapping[ vtkArrayType ] + if np.isnan( value ): + if vtkArrayType in ( VTK_DOUBLE, VTK_FLOAT ): + value = valueType( value ) + elif vtkArrayType in ( VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG ): + print( attributeName + " vtk array type is " + str( valueType ) + + ", default value is automatically set to 0." ) + value = valueType( 0 ) else: - print( "The value has the wrong type, it is update to " + str( valueTypeExpected ) + ", the type of the " + - attributeName + " array to fill." ) - value = valueTypeExpected( value ) + print( attributeName + " vtk array type is " + str( valueType ) + + ", default value is automatically set to -1." ) + value = valueType( -1 ) + + else: + value = valueType( value ) values: list[ Any ] = [ value for _ in range( nbComponents ) ] @@ -112,7 +111,7 @@ def fillAllPartialAttributes( Args: multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): MultiBlockDataSet where to fill the attribute. value (any, optional): Filling value. - Defaults to -1 for int VTK arrays, nan otherwise. + Defaults to -1 for int VTK arrays, 0 for uint VTK arrays and nan otherwise. Returns: bool: True if calculation successfully ended. @@ -135,7 +134,7 @@ def createEmptyAttribute( """Create an empty attribute. Args: - attributeName (str): name of the attribute + attributeName (str): Name of the attribute componentNames (tuple[str,...]): Name of the components for vectorial attributes. vtkDataType (int): Data type. @@ -178,7 +177,7 @@ def createConstantAttribute( Defaults to False. vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. Defaults to None, the type is given by the type of the array value. - Warning with int8, uint8 and int64 type of value, several vtk array type use it by default: + Warning with int8, uint8 and int64 type of value, the vtk array type associated are multiple. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG @@ -219,7 +218,7 @@ def createConstantAttributeMultiBlock( Defaults to False. vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. Defaults to None, the type is given by the type of the given value. - Warning with int8, uint8 and int64 type of value, several vtk array type use it by default: + Warning with int8, uint8 and int64 type of value, the vtk array type associated are multiple. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG @@ -270,7 +269,7 @@ def createConstantAttributeDataSet( Defaults to False. vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. Defaults to None, the type is given by the type of the given value. - Warning with int8, uint8 and int64 type of value, several vtk array type use it by default: + Warning with int8, uint8 and int64 type of value, the vtk array type associated are multiple. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG @@ -310,7 +309,7 @@ def createAttribute( Defaults to False. vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. Defaults to None, the type is given by the type of the given value in the array. - Warning with int8, uint8 and int64 type of value, several vtk array type use it. By default: + Warning with int8, uint8 and int64 type of value, the vtk array type associated are multiple. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG @@ -331,7 +330,7 @@ def createAttribute( componentNames = tuple( [ "Component" + str( i ) for i in range( nbComponents ) ] ) print( "Insufficient number of input component names. Component names will be set to : Component0, Component1 ..." ) elif nbNames > nbComponents: - print( f"Excessive number of input component names, only the {len(nbComponents)} first ones will be used." ) + print( f"Excessive number of input component names, only the { len( nbComponents ) } first ones will be used." ) for i in range( nbComponents ): createdAttribute.SetComponentName( i, componentNames[ i ] ) @@ -398,8 +397,8 @@ def copyAttributeDataSet( """Copy an attribute from objectFrom to objectTo. Args: - objectFrom (vtkDataSet): object from which to copy the attribute. - objectTo (vtkDataSet): object where to copy the attribute. + objectFrom (vtkDataSet): Object from which to copy the attribute. + objectTo (vtkDataSet): Object where to copy the attribute. attributeNameFrom (str): Attribute name in objectFrom. attributeNameTo (str): Attribute name in objectTo. onPoints (bool, optional): True if attributes are on points, False if they are on cells. @@ -408,14 +407,12 @@ def copyAttributeDataSet( Returns: bool: True if copy successfully ended, False otherwise. """ - # get attribut from initial time step block npArray: npt.NDArray[ Any ] = getArrayInObject( objectFrom, attributeNameFrom, onPoints ) assert npArray is not None componentNames: tuple[ str, ...] = getComponentNames( objectFrom, attributeNameFrom, onPoints ) vtkDataType: int = getVtkArrayTypeInObject( objectFrom, attributeNameFrom, onPoints ) - # copy attribut to current time step block createAttribute( objectTo, npArray, attributeNameTo, componentNames, onPoints, vtkDataType ) objectTo.Modified() @@ -487,7 +484,7 @@ def doCreateCellCenterAttribute( block: vtkDataSet, cellCenterAttributeName: str """Create elementCenter attribute in a vtkDataSet if it does not exist. Args: - block (vtkDataSet): input mesh that must be a vtkDataSet + block (vtkDataSet): Input mesh that must be a vtkDataSet cellCenterAttributeName (str): Name of the attribute Returns: @@ -500,7 +497,7 @@ def doCreateCellCenterAttribute( block: vtkDataSet, cellCenterAttributeName: str filter.Update() output: vtkPointSet = filter.GetOutputDataObject( 0 ) assert output is not None, "vtkCellCenters output is null." - # transfer output to ouput arrays + # transfer output to output arrays centers: vtkPoints = output.GetPoints() assert centers is not None, "Center are undefined." centerCoords: vtkDataArray = centers.GetData() From f46fde5a2413360d2735bedc4a61a2460cf01ee3 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 16 Jul 2025 18:39:11 +0200 Subject: [PATCH 17/56] Cleen and add logger to manadge output messages --- .../src/geos/mesh/utils/arrayModifiers.py | 299 ++++++++++++------ 1 file changed, 206 insertions(+), 93 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index 6f823bf0..ba783031 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -5,6 +5,8 @@ import numpy.typing as npt import vtkmodules.util.numpy_support as vnp from typing import Union, Any +from geos.utils.Logger import getLogger, Logger + from vtk import ( # type: ignore[import-untyped] VTK_DOUBLE, VTK_FLOAT, VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG, ) @@ -15,6 +17,8 @@ vtkCompositeDataSet, vtkDataObject, vtkDataObjectTreeIterator, + vtkPointData, + vtkCellData, ) from vtkmodules.vtkFiltersCore import ( vtkArrayRename, @@ -28,9 +32,11 @@ from geos.mesh.utils.arrayHelpers import ( getComponentNames, getAttributesWithNumberOfComponents, - getAttributeSet, getArrayInObject, isAttributeInObject, + isAttributeInObjectDataSet, + isAttributeInObjectMultiBlockDataSet, + isAttributeGlobal, getVtkArrayTypeInObject, getVtkArrayTypeInMultiBlock, ) @@ -43,7 +49,7 @@ ArrayModifiers contains utilities to process VTK Arrays objects. These methods include: - - filling partial VTK arrays with nan values (useful for block merge) + - filling partial VTK arrays with values (useful for block merge) - creation of new VTK array, empty or with a given data array - transfer from VTK point data to VTK cell data """ @@ -54,6 +60,7 @@ def fillPartialAttributes( attributeName: str, onPoints: bool = False, value: Any = np.nan, + logger: Logger = getLogger( "fillPartialAttributes", True ), ) -> bool: """Fill input partial attribute of multiBlockDataSet with the same value for all the components. @@ -64,12 +71,23 @@ def fillPartialAttributes( Defaults to False. value (any, optional): Filling value. Defaults to -1 for int VTK arrays, 0 for uint VTK arrays and nan otherwise. + logger (Logger, optional): A logger to manage the output messages. + Defaults to an internal logger. Returns: - bool: True if calculation successfully ended. + bool: True if the attribute was correctly created and filled, False if not. """ - vtkArrayType: int = getVtkArrayTypeInMultiBlock( multiBlockDataSet, attributeName, onPoints ) + #assert isinstance( multiBlockDataSet, vtkMultiBlockDataSet ), "Input mesh has to be inherited from vtkMultiBlockDataSet." + if not isinstance( multiBlockDataSet, vtkMultiBlockDataSet ): + logger.error( f"Input mesh has to be inherited from vtkMultiBlockDataSet." ) + return False + + #assert not isAttributeGlobal( multiBlockDataSet, attributeName, onPoints ), f"The attribute { attributeName } is already global." + if isAttributeGlobal( multiBlockDataSet, attributeName, onPoints ): + logger.error( f"The attribute { attributeName } is already global." ) + return False + vtkArrayType: int = getVtkArrayTypeInMultiBlock( multiBlockDataSet, attributeName, onPoints ) infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints ) nbComponents: int = infoAttributes[ attributeName ] @@ -83,12 +101,10 @@ def fillPartialAttributes( if vtkArrayType in ( VTK_DOUBLE, VTK_FLOAT ): value = valueType( value ) elif vtkArrayType in ( VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG ): - print( attributeName + " vtk array type is " + str( valueType ) + - ", default value is automatically set to 0." ) + logger.warning( f"{ attributeName } vtk array type is { valueType }, default value is automatically set to 0." ) value = valueType( 0 ) else: - print( attributeName + " vtk array type is " + str( valueType ) + - ", default value is automatically set to -1." ) + logger.warning( f"{ attributeName } vtk array type is { valueType }, default value is automatically set to -1." ) value = valueType( -1 ) else: @@ -96,8 +112,19 @@ def fillPartialAttributes( values: list[ Any ] = [ value for _ in range( nbComponents ) ] - createConstantAttribute( multiBlockDataSet, values, attributeName, componentNames, onPoints, vtkArrayType ) - multiBlockDataSet.Modified() + # Parse the multiBlockDataSet to create and fill the attribute on blocks where the attribute is not. + iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() + iter.SetDataSet( multiBlockDataSet ) + iter.VisitOnlyLeavesOn() + iter.GoToFirstItem() + while iter.GetCurrentDataObject() is not None: + dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) + if not isAttributeInObjectDataSet( dataSet, attributeName, onPoints ): + created: bool = createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkArrayType, logger ) + if not created: + return False + + iter.GoToNextItem() return True @@ -105,6 +132,7 @@ def fillPartialAttributes( def fillAllPartialAttributes( multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], value: Any = np.nan, + logger: Logger = getLogger( "fillAllPartialAttributes", True ), ) -> bool: """Fill all the partial attributes of a multiBlockDataSet with a same value. All components of each attribute are filled with the same value. @@ -112,16 +140,20 @@ def fillAllPartialAttributes( multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): MultiBlockDataSet where to fill the attribute. value (any, optional): Filling value. Defaults to -1 for int VTK arrays, 0 for uint VTK arrays and nan otherwise. + logger (Logger, optional): A logger to manage the output messages. + Defaults to an internal logger. Returns: - bool: True if calculation successfully ended. - """ + bool: True if attributes were correctly created and filled, False if not. + """ + # Parse all attributes, onPoints and onCells for onPoints in [ True, False ]: infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints ) for attributeName in infoAttributes: - fillPartialAttributes( multiBlockDataSet, attributeName, onPoints, value ) - - multiBlockDataSet.Modified() + if not isAttributeGlobal( multiBlockDataSet, attributeName, onPoints ): + filled: bool = fillPartialAttributes( multiBlockDataSet, attributeName, onPoints, value, logger ) + if not filled: + return False return True @@ -142,8 +174,7 @@ def createEmptyAttribute( bool: True if the attribute was correctly created. """ vtkDataTypeOk: dict = vnp.get_vtk_to_numpy_typemap() - if vtkDataType not in vtkDataTypeOk: - raise ValueError( "Attribute type is unknown." ) + assert vtkDataType in vtkDataTypeOk, f"Attribute type { vtkDataType } is unknown. The empty attribute { attributeName } has not been created into the mesh." nbComponents: int = len( componentNames ) @@ -164,8 +195,9 @@ def createConstantAttribute( componentNames: tuple[ str, ...] = (), # noqa: C408 onPoints: bool = False, vtkDataType: Union[ int, Any ] = None, + logger: Logger = getLogger( "createConstantAttribute", True ), ) -> bool: - """Create an attribute with a constant value everywhere if absent. + """Create a new attribute with a constant value in the object. Args: object (vtkDataObject): Object (vtkMultiBlockDataSet, vtkDataSet) where to create the attribute. @@ -173,29 +205,36 @@ def createConstantAttribute( attributeName (str): Name of the attribute. componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. - onPoints (bool): True if attributes are on points, False if they are on cells. + onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. Defaults to None, the type is given by the type of the array value. - Warning with int8, uint8 and int64 type of value, the vtk array type associated are multiple. By default: + Warning with int8, uint8 and int64 type of value, the vtk array type corresponding are multiple. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG + logger (Logger, optional): A logger to manage the output messages. + Defaults to an internal logger. Returns: - bool: True if the attribute was correctly created, False otherwise. + bool: True if the attribute was correctly created, False if it was not created. """ + # assert not isAttributeInObject( object, attributeName, onPoints ), f"The attribute { attributeName } is already present in the mesh" + if isAttributeInObject( object, attributeName, onPoints ): + logger.error( f"The attribute { attributeName } is already present in the mesh." ) + logger.error( f"The attribute { attributeName } has not been created into the mesh." ) + return False + if isinstance( object, ( vtkMultiBlockDataSet, vtkCompositeDataSet ) ): - return createConstantAttributeMultiBlock( object, values, attributeName, componentNames, onPoints, vtkDataType ) + return createConstantAttributeMultiBlock( object, values, attributeName, componentNames, onPoints, vtkDataType, logger ) elif isinstance( object, vtkDataSet ): - listAttributes: set[ str ] = getAttributeSet( object, onPoints ) - if attributeName not in listAttributes: - return createConstantAttributeDataSet( object, values, attributeName, componentNames, onPoints, - vtkDataType ) - print( "The attribute was already present in the vtkDataSet." ) + return createConstantAttributeDataSet( object, values, attributeName, componentNames, onPoints, vtkDataType, logger ) + + else: + logger.error( f"The mesh has to be inherited from a vtkMultiBlockDataSet or a vtkDataSet" ) + logger.error( f"The attribute { attributeName } has not been created into the mesh." ) return False - return False def createConstantAttributeMultiBlock( @@ -205,16 +244,17 @@ def createConstantAttributeMultiBlock( componentNames: tuple[ str, ...] = (), # noqa: C408 onPoints: bool = False, vtkDataType: Union[ int, Any ] = None, + logger: Logger = getLogger( "createConstantAttributeMultiBlock", True ), ) -> bool: - """Create an attribute with a constant value everywhere if absent. + """Create a new attribute with a constant value on every blocks of the multiBlockDataSet. Args: - multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet): vtkMultiBlockDataSet where to create the attribute. + multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet): MultiBlockDataSet where to create the attribute. values (list[any]): List of values of the attribute for each components. attributeName (str): Name of the attribute. componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. - onPoints (bool): True if attributes are on points, False if they are on cells. + onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. Defaults to None, the type is given by the type of the given value. @@ -222,31 +262,38 @@ def createConstantAttributeMultiBlock( - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG + logger (Logger, optional): A logger to manage the output messages. + Defaults to an internal logger. Returns: - bool: True if the attribute was correctly created, False if the attribute was already present. + bool: True if the attribute was correctly created, False if it was not created. """ - # initialize data object tree iterator - checkCreat: bool = False + #assert isinstance( multiBlockDataSet, vtkMultiBlockDataSet ), "Input mesh has to be inherited from vtkMultiBlockDataSet." + if not isinstance( multiBlockDataSet, vtkMultiBlockDataSet ): + logger.error( f"Input mesh has to be inherited from vtkMultiBlockDataSet." ) + logger.error( f"The attribute { attributeName } has not been created into the mesh." ) + return False + + #assert not isAttributeInObjectMultiBlockDataSet( multiBlockDataSet, attributeName, onPoints ), f"The attribute { attributeName } is already present in the multiBlockDataSet." + if isAttributeInObjectMultiBlockDataSet( multiBlockDataSet, attributeName, onPoints ): + logger.error( f"The attribute { attributeName } is already present in the multiBlockDataSet." ) + logger.error( f"The attribute { attributeName } has not been created into the mesh." ) + return False + # Initialize data object tree iterator iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() iter.SetDataSet( multiBlockDataSet ) iter.VisitOnlyLeavesOn() iter.GoToFirstItem() while iter.GetCurrentDataObject() is not None: dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) - listAttributes: set[ str ] = getAttributeSet( dataSet, onPoints ) - if attributeName not in listAttributes: - checkCreat = createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, - vtkDataType ) - + created: bool = createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkDataType, logger ) + if not created: + return False + iter.GoToNextItem() - if checkCreat: - return True - else: - print( "The attribute was already present in the vtkMultiBlockDataSet." ) - return False + return True def createConstantAttributeDataSet( @@ -256,16 +303,17 @@ def createConstantAttributeDataSet( componentNames: tuple[ str, ...] = (), # noqa: C408 onPoints: bool = False, vtkDataType: Union[ int, Any ] = None, + logger: Logger = getLogger( "createConstantAttributeDataSet", True ), ) -> bool: - """Create an attribute with a constant value everywhere. + """Create an attribute with a constant value in the dataSet. Args: - dataSet (vtkDataSet): VtkDataSet where to create the attribute. + dataSet (vtkDataSet): DataSet where to create the attribute. values ( list[any]): List of values of the attribute for each components. attributeName (str): Name of the attribute. componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. - onPoints (bool): True if attributes are on points, False if they are on cells. + onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. Defaults to None, the type is given by the type of the given value. @@ -273,39 +321,41 @@ def createConstantAttributeDataSet( - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG + logger (Logger, optional): A logger to manage the output messages. + Defaults to an internal logger. Returns: - bool: True if the attribute was correctly created. - """ + bool: True if the attribute was correctly created, False if it was not created. + """ nbElements: int = ( dataSet.GetNumberOfPoints() if onPoints else dataSet.GetNumberOfCells() ) - nbComponents: int = len( values ) - array: npt.NDArray[ Any ] + npArray: npt.NDArray[ Any ] if nbComponents > 1: - array = np.array( [ values for _ in range( nbElements ) ] ) + npArray = np.array( [ values for _ in range( nbElements ) ] ) else: - array = np.array( [ values[ 0 ] for _ in range( nbElements ) ] ) + npArray = np.array( [ values[ 0 ] for _ in range( nbElements ) ] ) - return createAttribute( dataSet, array, attributeName, componentNames, onPoints, vtkDataType ) + return createAttribute( dataSet, npArray, attributeName, componentNames, onPoints, vtkDataType, logger ) def createAttribute( dataSet: vtkDataSet, - array: npt.NDArray[ Any ], + npArray: npt.NDArray[ Any ], attributeName: str, componentNames: tuple[ str, ...] = (), # noqa: C408 onPoints: bool = False, vtkDataType: Union[ int, Any ] = None, + logger: Logger = getLogger( "createAttribute", True ), ) -> bool: """Create an attribute and its VTK array from the given array. Args: dataSet (vtkDataSet): DataSet where to create the attribute. - array (npt.NDArray[any]): Array that contains the values. + npArray (npt.NDArray[any]): Array that contains the values. attributeName (str): Name of the attribute. componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. - onPoints (bool): True if attributes are on points, False if they are on cells. + onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. Defaults to None, the type is given by the type of the given value in the array. @@ -313,34 +363,59 @@ def createAttribute( - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG + logger (Logger, optional): A logger to manage the output messages. + Defaults to an internal logger. Returns: - bool: True if the attribute was correctly created. + bool: True if the attribute was correctly created, False if it was not created. """ - assert isinstance( dataSet, vtkDataSet ), "Attribute can only be created in vtkDataSet object." - - createdAttribute: vtkDataArray = vnp.numpy_to_vtk( array, deep=True, array_type=vtkDataType ) + #assert isinstance( dataSet, vtkDataSet ), "Input mesh has to be inherited from vtkDataSet." + if not isinstance( dataSet, vtkDataSet ): + logger.error( f"Input mesh has to be inherited from vtkDataSet." ) + logger.error( f"The attribute { attributeName } has not been created into the mesh." ) + return False + + #assert not isAttributeInObjectDataSet( dataSet, attributeName, onPoints ), f"The attribute { attributeName } is already present in the dataSet." + if isAttributeInObjectDataSet( dataSet, attributeName, onPoints ): + logger.error( f"The attribute { attributeName } is already present in the dataSet." ) + logger.error( f"The attribute { attributeName } has not been created into the mesh." ) + return False + + data: Union[ vtkPointData, vtkCellData] + nbElements: int + if onPoints: + data = dataSet.GetPointData() + nbElements = dataSet.GetNumberOfPoints() + else: + data = dataSet.GetCellData() + nbElements = dataSet.GetNumberOfCells() + + #assert len( array ) == nbElements, f"The array has to have { nbElements } elements, but have only { len( array ) } elements" + if len( npArray ) != nbElements: + logger.error( f"The array has to have { nbElements } elements, but have only { len( npArray ) } elements" ) + logger.error( f"The attribute { attributeName } has not been created into the mesh." ) + return False + + createdAttribute: vtkDataArray = vnp.numpy_to_vtk( npArray, deep=True, array_type=vtkDataType ) createdAttribute.SetName( attributeName ) nbComponents: int = createdAttribute.GetNumberOfComponents() + nbNames: int = len( componentNames ) + if nbComponents == 1 and nbNames > 0: + logger.warning( f"The array has one component, its name is the name of the attribute: { attributeName }, the components names you have enter will not be taking into account." ) + if nbComponents > 1: - nbNames = len( componentNames ) - if nbNames < nbComponents: componentNames = tuple( [ "Component" + str( i ) for i in range( nbComponents ) ] ) - print( "Insufficient number of input component names. Component names will be set to : Component0, Component1 ..." ) + logger.warning( f"Insufficient number of input component names. { attributeName } component names will be set to : Component0, Component1 ..." ) elif nbNames > nbComponents: - print( f"Excessive number of input component names, only the { len( nbComponents ) } first ones will be used." ) + logger.warning( f"Excessive number of input component names, only the first { nbComponents } names will be used." ) for i in range( nbComponents ): createdAttribute.SetComponentName( i, componentNames[ i ] ) - if onPoints: - dataSet.GetPointData().AddArray( createdAttribute ) - else: - dataSet.GetCellData().AddArray( createdAttribute ) - - dataSet.Modified() + data.AddArray( createdAttribute ) + data.Modified() return True @@ -351,38 +426,63 @@ def copyAttribute( attributeNameFrom: str, attributeNameTo: str, onPoints: bool = False, + logger: Logger = getLogger( "copyAttribute", True ), ) -> bool: - """Copy an attribute from objectFrom to objectTo. + """Copy an attribute from a multiBlockDataSet to another. Args: - objectFrom (vtkMultiBlockDataSet): Object from which to copy the attribute. - objectTo (vtkMultiBlockDataSet): Object where to copy the attribute. + objectFrom (vtkMultiBlockDataSet): MultiBlockDataSet from which to copy the attribute. + objectTo (vtkMultiBlockDataSet): MultiBlockDataSet where to copy the attribute. attributeNameFrom (str): Attribute name in objectFrom. attributeNameTo (str): Attribute name in objectTo. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. + logger (Logger, optional): A logger to manage the output messages. + Defaults to an internal logger. Returns: bool: True if copy successfully ended, False otherwise. """ + if not isinstance( objectFrom, vtkMultiBlockDataSet ): + logger.error( f"ObjectFrom has to be inherited from vtkMultiBlockDataSet." ) + logger.error( f"The attribute { attributeNameFrom } has not been copied." ) + return False + + if not isinstance( objectTo, vtkMultiBlockDataSet ): + logger.error( f"ObjectTo has to be inherited from vtkMultiBlockDataSet." ) + logger.error( f"The attribute { attributeNameFrom } has not been copied." ) + return False + + if not isAttributeInObjectMultiBlockDataSet( objectFrom, attributeNameFrom, onPoints ): + logger.error( f"The attribute { attributeNameFrom } is not in the objectFrom." ) + logger.error( f"The attribute { attributeNameFrom } has not been copied." ) + return False + elementaryBlockIndexesTo: list[ int ] = getBlockElementIndexesFlatten( objectTo ) elementaryBlockIndexesFrom: list[ int ] = getBlockElementIndexesFlatten( objectFrom ) - assert elementaryBlockIndexesTo == elementaryBlockIndexesFrom, ( - "ObjectFrom and objectTo do not have the same block indexes." ) - + if elementaryBlockIndexesTo != elementaryBlockIndexesFrom: + logger.error( f"ObjectFrom and objectTo do not have the same block indexes." ) + logger.error( f"The attribute { attributeNameFrom } has not been copied." ) + return False + for index in elementaryBlockIndexesTo: blockFrom: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( objectFrom, index ) ) - assert blockFrom is not None, f"Block { str( index ) } of objectFrom is null." + if blockFrom is None: + logger.error( f"Block { str( index ) } of objectFrom is null." ) + logger.error( f"The attribute { attributeNameFrom } has not been copied." ) + return False blockTo: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( objectTo, index ) ) - assert blockTo is not None, f"Block { str( index ) } of objectTo is null." + if blockTo is None: + logger.error( f"Block { str( index ) } of objectTo is null." ) + logger.error( f"The attribute { attributeNameFrom } has not been copied." ) + return False - try: - copyAttributeDataSet( blockFrom, blockTo, attributeNameFrom, attributeNameTo, onPoints ) - except AssertionError: - # skip attribute if not in block - continue + if isAttributeInObjectDataSet( blockFrom, attributeNameFrom, onPoints ): + copied: bool = copyAttributeDataSet( blockFrom, blockTo, attributeNameFrom, attributeNameTo, onPoints, logger ) + if not copied: + return False return True @@ -393,30 +493,43 @@ def copyAttributeDataSet( attributeNameFrom: str, attributeNameTo: str, onPoints: bool = False, + logger: Logger = getLogger( "copyAttributeDataSet", True ), ) -> bool: - """Copy an attribute from objectFrom to objectTo. + """Copy an attribute from a dataSet to another. Args: - objectFrom (vtkDataSet): Object from which to copy the attribute. - objectTo (vtkDataSet): Object where to copy the attribute. + objectFrom (vtkDataSet): DataSet from which to copy the attribute. + objectTo (vtkDataSet): DataSet where to copy the attribute. attributeNameFrom (str): Attribute name in objectFrom. attributeNameTo (str): Attribute name in objectTo. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. + logger (Logger, optional): A logger to manage the output messages. + Defaults to an internal logger. Returns: bool: True if copy successfully ended, False otherwise. """ + if not isinstance( objectFrom, vtkDataSet ): + logger.error( f"ObjectFrom has to be inherited from vtkDataSet." ) + logger.error( f"The attribute { attributeNameFrom } has not been copied." ) + return False + + if not isinstance( objectTo, vtkDataSet ): + logger.error( f"ObjectTo has to be inherited from vtkDataSet." ) + logger.error( f"The attribute { attributeNameFrom } has not been copied." ) + return False + + if not isAttributeInObjectDataSet( objectFrom, attributeNameFrom, onPoints ): + logger.error( f"The attribute { attributeNameFrom } is not in the objectFrom." ) + logger.error( f"The attribute { attributeNameFrom } has not been copied." ) + return False + npArray: npt.NDArray[ Any ] = getArrayInObject( objectFrom, attributeNameFrom, onPoints ) - assert npArray is not None - componentNames: tuple[ str, ...] = getComponentNames( objectFrom, attributeNameFrom, onPoints ) vtkDataType: int = getVtkArrayTypeInObject( objectFrom, attributeNameFrom, onPoints ) - createAttribute( objectTo, npArray, attributeNameTo, componentNames, onPoints, vtkDataType ) - objectTo.Modified() - - return True + return createAttribute( objectTo, npArray, attributeNameTo, componentNames, onPoints, vtkDataType, logger ) def renameAttribute( From 614cafa5fa8c4a79807cbcd6a3a876951dc8dfe5 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 18 Jul 2025 15:54:39 +0200 Subject: [PATCH 18/56] clear the tests and functions of arrayModifiers --- .../src/geos/mesh/utils/arrayModifiers.py | 178 +++++--- geos-mesh/tests/test_arrayModifiers.py | 424 +++++++----------- 2 files changed, 291 insertions(+), 311 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index ba783031..36131048 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -77,17 +77,15 @@ def fillPartialAttributes( Returns: bool: True if the attribute was correctly created and filled, False if not. """ - #assert isinstance( multiBlockDataSet, vtkMultiBlockDataSet ), "Input mesh has to be inherited from vtkMultiBlockDataSet." if not isinstance( multiBlockDataSet, vtkMultiBlockDataSet ): logger.error( f"Input mesh has to be inherited from vtkMultiBlockDataSet." ) return False - #assert not isAttributeGlobal( multiBlockDataSet, attributeName, onPoints ), f"The attribute { attributeName } is already global." if isAttributeGlobal( multiBlockDataSet, attributeName, onPoints ): logger.error( f"The attribute { attributeName } is already global." ) return False - vtkArrayType: int = getVtkArrayTypeInMultiBlock( multiBlockDataSet, attributeName, onPoints ) + vtkDataType: int = getVtkArrayTypeInMultiBlock( multiBlockDataSet, attributeName, onPoints ) infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints ) nbComponents: int = infoAttributes[ attributeName ] @@ -96,11 +94,11 @@ def fillPartialAttributes( componentNames = getComponentNames( multiBlockDataSet, attributeName, onPoints ) typeMapping: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() - valueType: Any = typeMapping[ vtkArrayType ] + valueType: Any = typeMapping[ vtkDataType ] if np.isnan( value ): - if vtkArrayType in ( VTK_DOUBLE, VTK_FLOAT ): + if vtkDataType in ( VTK_DOUBLE, VTK_FLOAT ): value = valueType( value ) - elif vtkArrayType in ( VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG ): + elif vtkDataType in ( VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG ): logger.warning( f"{ attributeName } vtk array type is { valueType }, default value is automatically set to 0." ) value = valueType( 0 ) else: @@ -120,7 +118,7 @@ def fillPartialAttributes( while iter.GetCurrentDataObject() is not None: dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) if not isAttributeInObjectDataSet( dataSet, attributeName, onPoints ): - created: bool = createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkArrayType, logger ) + created: bool = createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkDataType, logger ) if not created: return False @@ -146,13 +144,12 @@ def fillAllPartialAttributes( Returns: bool: True if attributes were correctly created and filled, False if not. """ - # Parse all attributes, onPoints and onCells + # Parse all partial attributes, onPoints and onCells to fill them. for onPoints in [ True, False ]: infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints ) for attributeName in infoAttributes: if not isAttributeGlobal( multiBlockDataSet, attributeName, onPoints ): - filled: bool = fillPartialAttributes( multiBlockDataSet, attributeName, onPoints, value, logger ) - if not filled: + if not fillPartialAttributes( multiBlockDataSet, attributeName, onPoints, value, logger ): return False return True @@ -173,8 +170,9 @@ def createEmptyAttribute( Returns: bool: True if the attribute was correctly created. """ - vtkDataTypeOk: dict = vnp.get_vtk_to_numpy_typemap() - assert vtkDataType in vtkDataTypeOk, f"Attribute type { vtkDataType } is unknown. The empty attribute { attributeName } has not been created into the mesh." + # Check if the vtk data type is correct. + vtkNumpyTypeMap: dict[ int, type ] = vnp.get_vtk_to_numpy_typemap() + assert vtkDataType in vtkNumpyTypeMap, f"Attribute type { vtkDataType } is unknown. The empty attribute { attributeName } has not been created into the mesh." nbComponents: int = len( componentNames ) @@ -190,7 +188,7 @@ def createEmptyAttribute( def createConstantAttribute( object: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], - values: list[ float ], + listValues: list[ Any ], attributeName: str, componentNames: tuple[ str, ...] = (), # noqa: C408 onPoints: bool = False, @@ -201,15 +199,17 @@ def createConstantAttribute( Args: object (vtkDataObject): Object (vtkMultiBlockDataSet, vtkDataSet) where to create the attribute. - values (list[float]): List of values of the attribute for each components. + listValues (list[any]): List of values of the attribute for each components. It is better to use numpy scalar type for the values. attributeName (str): Name of the attribute. componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. - Defaults to None, the type is given by the type of the array value. - Warning with int8, uint8 and int64 type of value, the vtk array type corresponding are multiple. By default: + If None the vtk data type is given by the type of the values. + Else, the values are converted to the corresponding numpy type. + Defaults to None. + Warning with int8, uint8 and int64 type of value, the vtk data type corresponding are multiples. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG @@ -218,47 +218,43 @@ def createConstantAttribute( Returns: bool: True if the attribute was correctly created, False if it was not created. - """ - # assert not isAttributeInObject( object, attributeName, onPoints ), f"The attribute { attributeName } is already present in the mesh" - if isAttributeInObject( object, attributeName, onPoints ): - logger.error( f"The attribute { attributeName } is already present in the mesh." ) - logger.error( f"The attribute { attributeName } has not been created into the mesh." ) - return False - + """ if isinstance( object, ( vtkMultiBlockDataSet, vtkCompositeDataSet ) ): - return createConstantAttributeMultiBlock( object, values, attributeName, componentNames, onPoints, vtkDataType, logger ) + return createConstantAttributeMultiBlock( object, listValues, attributeName, componentNames, onPoints, vtkDataType, logger ) elif isinstance( object, vtkDataSet ): - return createConstantAttributeDataSet( object, values, attributeName, componentNames, onPoints, vtkDataType, logger ) + return createConstantAttributeDataSet( object, listValues, attributeName, componentNames, onPoints, vtkDataType, logger ) else: logger.error( f"The mesh has to be inherited from a vtkMultiBlockDataSet or a vtkDataSet" ) - logger.error( f"The attribute { attributeName } has not been created into the mesh." ) + logger.error( f"The constant attribute { attributeName } has not been created into the mesh." ) return False def createConstantAttributeMultiBlock( multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet ], - values: list[ Any ], + listValues: list[ Any ], attributeName: str, componentNames: tuple[ str, ...] = (), # noqa: C408 onPoints: bool = False, vtkDataType: Union[ int, Any ] = None, logger: Logger = getLogger( "createConstantAttributeMultiBlock", True ), ) -> bool: - """Create a new attribute with a constant value on every blocks of the multiBlockDataSet. + """Create a new attribute with a constant value per component on every blocks of the multiBlockDataSet. Args: multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet): MultiBlockDataSet where to create the attribute. - values (list[any]): List of values of the attribute for each components. + listValues (list[any]): List of values of the attribute for each components. It is better to use numpy scalar type for the values. attributeName (str): Name of the attribute. componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. - Defaults to None, the type is given by the type of the given value. - Warning with int8, uint8 and int64 type of value, the vtk array type associated are multiple. By default: + If None the vtk data type is given by the type of the values. + Else, values type have to correspond to the type of the vtk data, check https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/numpy_support.py for more information. + Defaults to None. + Warning with int8, uint8 and int64 type of value, the vtk data type corresponding are multiples. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG @@ -268,27 +264,33 @@ def createConstantAttributeMultiBlock( Returns: bool: True if the attribute was correctly created, False if it was not created. """ - #assert isinstance( multiBlockDataSet, vtkMultiBlockDataSet ), "Input mesh has to be inherited from vtkMultiBlockDataSet." + # Check if the input mesh is inherited from vtkMultiBlockDataSet. if not isinstance( multiBlockDataSet, vtkMultiBlockDataSet ): logger.error( f"Input mesh has to be inherited from vtkMultiBlockDataSet." ) - logger.error( f"The attribute { attributeName } has not been created into the mesh." ) + logger.error( f"The constant attribute { attributeName } has not been created into the mesh." ) return False - #assert not isAttributeInObjectMultiBlockDataSet( multiBlockDataSet, attributeName, onPoints ), f"The attribute { attributeName } is already present in the multiBlockDataSet." + # Check if the attribute already exist in the input mesh. if isAttributeInObjectMultiBlockDataSet( multiBlockDataSet, attributeName, onPoints ): logger.error( f"The attribute { attributeName } is already present in the multiBlockDataSet." ) - logger.error( f"The attribute { attributeName } has not been created into the mesh." ) + logger.error( f"The constant attribute { attributeName } has not been created into the mesh." ) return False - # Initialize data object tree iterator + # Check if an attribute with the same name exist on the opposite piece (points or cells) on the input mesh. + oppositePiece: bool = not onPoints + oppositePieceName: str = "points" if oppositePiece else "cells" + if isAttributeInObjectMultiBlockDataSet( multiBlockDataSet, attributeName, oppositePiece ): + oppositePieceState: str = "global" if isAttributeGlobal( multiBlockDataSet, attributeName, oppositePiece ) else "partial" + logger.warning( f"A { oppositePieceState } attribute with the same name ({ attributeName }) is already present in the multiBlockDataSet but on { oppositePieceName }." ) + + # Parse the multiBlockDataSet to create the constant attribute on each blocks. iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() iter.SetDataSet( multiBlockDataSet ) iter.VisitOnlyLeavesOn() iter.GoToFirstItem() while iter.GetCurrentDataObject() is not None: dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) - created: bool = createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkDataType, logger ) - if not created: + if not createConstantAttributeDataSet( dataSet, listValues, attributeName, componentNames, onPoints, vtkDataType, logger ): return False iter.GoToNextItem() @@ -298,26 +300,28 @@ def createConstantAttributeMultiBlock( def createConstantAttributeDataSet( dataSet: vtkDataSet, - values: list[ Any ], + listValues: list[ Any ], attributeName: str, componentNames: tuple[ str, ...] = (), # noqa: C408 onPoints: bool = False, vtkDataType: Union[ int, Any ] = None, logger: Logger = getLogger( "createConstantAttributeDataSet", True ), ) -> bool: - """Create an attribute with a constant value in the dataSet. + """Create an attribute with a constant value per component in the dataSet. Args: dataSet (vtkDataSet): DataSet where to create the attribute. - values ( list[any]): List of values of the attribute for each components. + listValues (list[any]): List of values of the attribute for each components. It is better to use numpy scalar type for the values. attributeName (str): Name of the attribute. componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. - Defaults to None, the type is given by the type of the given value. - Warning with int8, uint8 and int64 type of value, the vtk array type associated are multiple. By default: + If None the vtk data type is given by the type of the values of listValues. + Else, values type have to correspond to the type of the vtk data, check https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/numpy_support.py for more information. + Defaults to None. + Warning with int8, uint8 and int64 type of value, the vtk data type corresponding are multiples. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG @@ -326,14 +330,51 @@ def createConstantAttributeDataSet( Returns: bool: True if the attribute was correctly created, False if it was not created. - """ + """ + # Check if listValues have at least one value. + if len( listValues ) == 0: + logger.error( f"To create a constant attribute, you have to give at least one value in the listValues." ) + logger.error( f"The constant attribute { attributeName } has not been created into the mesh." ) + return False + + # Check if all the values of listValues have the same type. + valueType: type = type( listValues[ 0 ] ) + for value in listValues: + valueTypeTest: type = type( value ) + if valueType != valueTypeTest: + logger.error( f"All values in the list of values have not the same type." ) + logger.error( f"The constant attribute { attributeName } has not been created into the mesh." ) + return False + + # Convert int and float type into numpy scalar type. + if valueType in ( int, float ): + npType: type = type( np.array( listValues )[ 0 ] ) + logger.warning( f"During the creation of the constant attribute { attributeName }, values will be converted from { valueType } to { npType }." ) + logger.warning( f"To avoid any issue with the conversion use directly numpy scalar type for the values" ) + valueType = npType + + # Check the coherency between the given value type and the vtk array type if it exist. + valueType = valueType().dtype + if vtkDataType is not None: + vtkNumpyTypeMap: dict[ int, type ] = vnp.get_vtk_to_numpy_typemap() + if vtkDataType not in vtkNumpyTypeMap: + logger.error( f"The vtk data type { vtkDataType } is unknown." ) + logger.error( f"The constant attribute { attributeName } has not been created into the mesh." ) + return False + npArrayTypeFromVtk: type = vtkNumpyTypeMap[ vtkDataType ]().dtype + if npArrayTypeFromVtk != valueType: + logger.error( f"Values type { valueType } is not coherent with the type of array created ({ npArrayTypeFromVtk }) from the given vtkDataType." ) + logger.error( f"The constant attribute { attributeName } has not been created into the mesh." ) + return False + + # Create the numpy array constant per component. + nbComponents: int = len( listValues ) nbElements: int = ( dataSet.GetNumberOfPoints() if onPoints else dataSet.GetNumberOfCells() ) - nbComponents: int = len( values ) npArray: npt.NDArray[ Any ] if nbComponents > 1: - npArray = np.array( [ values for _ in range( nbElements ) ] ) + npArray = np.array( [ listValues for _ in range( nbElements ) ], valueType ) else: - npArray = np.array( [ values[ 0 ] for _ in range( nbElements ) ] ) + npArray = np.array( [ listValues[ 0 ] for _ in range( nbElements ) ], valueType ) return createAttribute( dataSet, npArray, attributeName, componentNames, onPoints, vtkDataType, logger ) @@ -347,7 +388,7 @@ def createAttribute( vtkDataType: Union[ int, Any ] = None, logger: Logger = getLogger( "createAttribute", True ), ) -> bool: - """Create an attribute and its VTK array from the given array. + """Create an attribute from the given numpy array. Args: dataSet (vtkDataSet): DataSet where to create the attribute. @@ -358,8 +399,10 @@ def createAttribute( onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. - Defaults to None, the type is given by the type of the given value in the array. - Warning with int8, uint8 and int64 type of value, the vtk array type associated are multiple. By default: + If None the vtk data type is given by the type of the numpy array. + Else, numpy array type have to correspond to the type of the vtk data, check https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/numpy_support.py for more information. + Defaults to None. + Warning with int8, uint8 and int64 type of value, the vtk data type corresponding are multiples. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG @@ -369,40 +412,63 @@ def createAttribute( Returns: bool: True if the attribute was correctly created, False if it was not created. """ - #assert isinstance( dataSet, vtkDataSet ), "Input mesh has to be inherited from vtkDataSet." + # Check if the input mesh is inherited from vtkDataSet. if not isinstance( dataSet, vtkDataSet ): logger.error( f"Input mesh has to be inherited from vtkDataSet." ) logger.error( f"The attribute { attributeName } has not been created into the mesh." ) return False - #assert not isAttributeInObjectDataSet( dataSet, attributeName, onPoints ), f"The attribute { attributeName } is already present in the dataSet." + # Check if the attribute already exist in the input mesh. if isAttributeInObjectDataSet( dataSet, attributeName, onPoints ): logger.error( f"The attribute { attributeName } is already present in the dataSet." ) logger.error( f"The attribute { attributeName } has not been created into the mesh." ) return False + # Check the coherency between the given array type and the vtk array type if it exist. + if vtkDataType is not None: + vtkNumpyTypeMap: dict[ int, type ] = vnp.get_vtk_to_numpy_typemap() + if vtkDataType not in vtkNumpyTypeMap: + logger.error( f"The vtk data type { vtkDataType } is unknown." ) + logger.error( f"The attribute { attributeName } has not been created into the mesh." ) + return False + npArrayTypeFromVtk: type = vtkNumpyTypeMap[ vtkDataType ]().dtype + npArrayTypeFromInput: type = npArray.dtype + if npArrayTypeFromVtk != npArrayTypeFromInput: + logger.error( f"The numpy array type { npArrayTypeFromInput } is not coherent with the type of array created ({ npArrayTypeFromVtk }) from the given vtkDataType." ) + logger.error( f"The attribute { attributeName } has not been created into the mesh." ) + return False + data: Union[ vtkPointData, vtkCellData] nbElements: int + oppositePieceName: str if onPoints: data = dataSet.GetPointData() nbElements = dataSet.GetNumberOfPoints() + oppositePieceName = "cells" else: data = dataSet.GetCellData() nbElements = dataSet.GetNumberOfCells() + oppositePieceName = "points" - #assert len( array ) == nbElements, f"The array has to have { nbElements } elements, but have only { len( array ) } elements" + # Check if the input array has the good size. if len( npArray ) != nbElements: logger.error( f"The array has to have { nbElements } elements, but have only { len( npArray ) } elements" ) logger.error( f"The attribute { attributeName } has not been created into the mesh." ) return False + # Check if an attribute with the same name exist on the opposite piece (points or cells). + oppositePiece: bool = not onPoints + if isAttributeInObjectDataSet( dataSet, attributeName, oppositePiece ): + logger.warning( f"An attribute with the same name ({ attributeName }) is already present in the dataSet but on { oppositePieceName }." ) + + # Convert the numpy array int a vtkDataArray. createdAttribute: vtkDataArray = vnp.numpy_to_vtk( npArray, deep=True, array_type=vtkDataType ) createdAttribute.SetName( attributeName ) nbComponents: int = createdAttribute.GetNumberOfComponents() nbNames: int = len( componentNames ) if nbComponents == 1 and nbNames > 0: - logger.warning( f"The array has one component, its name is the name of the attribute: { attributeName }, the components names you have enter will not be taking into account." ) + logger.warning( f"The array has one component and no name, the components names you have enter will not be taking into account." ) if nbComponents > 1: if nbNames < nbComponents: @@ -527,9 +593,9 @@ def copyAttributeDataSet( npArray: npt.NDArray[ Any ] = getArrayInObject( objectFrom, attributeNameFrom, onPoints ) componentNames: tuple[ str, ...] = getComponentNames( objectFrom, attributeNameFrom, onPoints ) - vtkDataType: int = getVtkArrayTypeInObject( objectFrom, attributeNameFrom, onPoints ) + vtkArrayType: int = getVtkArrayTypeInObject( objectFrom, attributeNameFrom, onPoints ) - return createAttribute( objectTo, npArray, attributeNameTo, componentNames, onPoints, vtkDataType, logger ) + return createAttribute( objectTo, npArray, attributeNameTo, componentNames, onPoints, vtkArrayType, logger ) def renameAttribute( diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index bf406a1f..b8c22d31 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -17,10 +17,23 @@ from geos.mesh.utils.arrayHelpers import getAttributesWithNumberOfComponents from vtk import ( # type: ignore[import-untyped] - VTK_CHAR, VTK_DOUBLE, VTK_FLOAT, VTK_INT, VTK_UNSIGNED_INT, VTK_LONG_LONG, VTK_ID_TYPE, + VTK_UNSIGNED_CHAR, + VTK_UNSIGNED_SHORT, + VTK_UNSIGNED_INT, + VTK_UNSIGNED_LONG_LONG, + VTK_SIGNED_CHAR, + VTK_SHORT, + VTK_INT, + VTK_LONG_LONG, + VTK_FLOAT, + VTK_DOUBLE, + VTK_ID_TYPE, + VTK_CHAR, ) -# Informations : +# Information : +# https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/numpy_support.py +# https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/vtkConstants.py # vtk array type int numpy type # VTK_CHAR = 2 = np.int8 # VTK_SIGNED_CHAR = 15 = np.int8 @@ -101,22 +114,20 @@ def test_fillPartialAttributes( if nbComponentsRef > 1: componentNamesTest: tuple[ str, ...] = tuple( attributeFillTest.GetComponentName( i ) for i in range( nbComponentsRef ) ) - assert componentNamesRef == componentNamesTest + assert componentNamesTest == componentNamesRef - npArrayFillRef = np.array( [ [ valueRef for _ in range( nbComponentsRef ) ] for _ in range( nbElements ) ] ) - else: - npArrayFillRef = np.array( [ valueRef for _ in range( nbElements ) ] ) + npArrayFillRef = np.full( ( nbElements, nbComponentsRef ), valueRef ) npArrayFillTest: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeFillTest ) - assert valueTypeRef == npArrayFillTest.dtype + assert npArrayFillTest.dtype == valueTypeRef if np.isnan( valueRef ): assert np.isnan( npArrayFillRef ).all() else: - assert ( npArrayFillRef == npArrayFillTest ).all() + assert ( npArrayFillTest == npArrayFillRef ).all() vtkDataTypeTest: int = attributeFillTest.GetDataType() - assert vtkDataTypeRef == vtkDataTypeTest + assert vtkDataTypeTest == vtkDataTypeRef @pytest.mark.parametrize( "value", [ @@ -131,15 +142,15 @@ def test_FillAllPartialAttributes( value: Any, ) -> None: """Test to fill all the partial attributes of a vtkMultiBlockDataSet with a value.""" - MultiBlockDataSetRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - MultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - arrayModifiers.fillAllPartialAttributes( MultiBlockDataSetTest, value ) + multiBlockDataSetRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + multiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + arrayModifiers.fillAllPartialAttributes( multiBlockDataSetTest, value ) - nbBlock = MultiBlockDataSetRef.GetNumberOfBlocks() + nbBlock = multiBlockDataSetRef.GetNumberOfBlocks() for idBlock in range( nbBlock ): - datasetTest: vtkDataSet = cast( vtkDataSet, MultiBlockDataSetTest.GetBlock( idBlock ) ) + datasetTest: vtkDataSet = cast( vtkDataSet, multiBlockDataSetTest.GetBlock( idBlock ) ) for onPoints in [ True, False ]: - infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( MultiBlockDataSetRef, onPoints ) + infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSetRef, onPoints ) dataTest: Union[ vtkPointData, vtkCellData ] dataTest = datasetTest.GetPointData() if onPoints else datasetTest.GetCellData() @@ -170,162 +181,92 @@ def test_createEmptyAttribute( assert newAttr.IsA( str( expectedDatatypeArray ) ) -@pytest.mark.parametrize( "attributeName, isNewOnBlock, onPoints", [ - ( "newAttribute", ( True, True ), False ), - ( "newAttribute", ( True, True ), True ), - ( "PORO", ( True, True ), True ), - ( "PORO", ( False, True ), False ), - ( "PointAttribute", ( False, True ), True ), - ( "PointAttribute", ( True, True ), False ), - ( "collocated_nodes", ( True, False ), True ), - ( "collocated_nodes", ( True, True ), False ), +@pytest.mark.parametrize( "attributeName, onPoints", [ + ( "newAttribute", False ), + ( "newAttribute", True ), + ( "PORO", True ), # Partial attribute on cells already exist + ( "GLOBAL_IDS_CELLS", True ), # Global attribute on cells already exist ] ) def test_createConstantAttributeMultiBlock( dataSetTest: vtkMultiBlockDataSet, attributeName: str, - isNewOnBlock: tuple[ bool, ...], onPoints: bool, ) -> None: """Test creation of constant attribute in multiblock dataset.""" - MultiBlockDataSetRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - MultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + multiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) values: list[ float ] = [ np.nan ] - arrayModifiers.createConstantAttributeMultiBlock( MultiBlockDataSetTest, values, attributeName, onPoints=onPoints ) + assert arrayModifiers.createConstantAttributeMultiBlock( multiBlockDataSetTest, values, attributeName, onPoints=onPoints ) - nbBlock = MultiBlockDataSetRef.GetNumberOfBlocks() + nbBlock = multiBlockDataSetTest.GetNumberOfBlocks() for idBlock in range( nbBlock ): - datasetRef: vtkDataSet = cast( vtkDataSet, MultiBlockDataSetRef.GetBlock( idBlock ) ) - datasetTest: vtkDataSet = cast( vtkDataSet, MultiBlockDataSetTest.GetBlock( idBlock ) ) - dataRef: Union[ vtkPointData, vtkCellData ] + datasetTest: vtkDataSet = cast( vtkDataSet, multiBlockDataSetTest.GetBlock( idBlock ) ) dataTest: Union[ vtkPointData, vtkCellData ] if onPoints: - dataRef = datasetRef.GetPointData() dataTest = datasetTest.GetPointData() else: - dataRef = datasetRef.GetCellData() dataTest = datasetTest.GetCellData() - attributeRef: int = dataRef.HasArray( attributeName ) attributeTest: int = dataTest.HasArray( attributeName ) - if isNewOnBlock[ idBlock ]: - assert attributeRef != attributeTest - else: - assert attributeRef == attributeTest - - -@pytest.mark.parametrize( - "values, componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, valueType", [ - ( [ np.float32( 42 ) ], (), (), True, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ) ], (), (), False, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ) ], (), (), True, None, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ) ], (), (), False, None, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], (), - ( "Component0", "Component1" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], (), - ( "Component0", "Component1" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], (), - ( "Component0", "Component1" ), True, None, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], (), - ( "Component0", "Component1" ), False, None, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), - ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), - ( "X", "Y" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_FLOAT, "float32" ), - ( [ np.float32( 42 ), np.float32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_FLOAT, "float32" ), - ( [ np.float64( 42 ) ], (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ) ], (), (), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ) ], (), (), True, None, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ) ], (), (), False, None, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], (), - ( "Component0", "Component1" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], (), - ( "Component0", "Component1" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], (), - ( "Component0", "Component1" ), True, None, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], (), - ( "Component0", "Component1" ), False, None, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), - ( "X", "Y" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), - ( "X", "Y" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), - ( "X", "Y" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_DOUBLE, "float64" ), - ( [ np.float64( 42 ), np.float64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_DOUBLE, "float64" ), - ( [ np.int32( 42 ) ], (), (), True, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ) ], (), (), False, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ) ], (), (), True, None, VTK_INT, "int32" ), - ( [ np.int32( 42 ) ], (), (), False, None, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), True, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), False, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), True, None, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], (), ( "Component0", "Component1" ), False, None, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_INT, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_INT, "int32" ), - ( [ np.int32( 42 ), np.int32( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_INT, "int32" ), - ( [ np.int64( 42 ) ], (), (), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ) ], (), (), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ) ], (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ) ], (), (), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ) ], (), (), True, None, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ) ], (), (), False, None, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], (), - ( "Component0", "Component1" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], (), - ( "Component0", "Component1" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], (), - ( "Component0", "Component1" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], (), - ( "Component0", "Component1" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), True, None, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], (), ( "Component0", "Component1" ), False, None, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), - ( "X", "Y" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), - ( "X", "Y" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), - ( "X", "Y" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), - ( "X", "Y" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), - ( "X", "Y" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_LONG_LONG, "int64" ), - ( [ np.int64( 42 ), np.int64( 22 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_LONG_LONG, "int64" ), - ] ) + assert attributeTest == 1 + + +@pytest.mark.parametrize( "listValues, componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, attributeName", [ + # Test attribute names. + ## Test with an attributeName already existing on cells data. + ( [ np.float32( 42 ) ], (), (), True, VTK_FLOAT, VTK_FLOAT, "PORO" ), + ## Test with a new attributeName on cells and on points. + ( [ np.float32( 42 ) ], (), (), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), + ( [ np.float32( 42 ) ], (), (), False, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), + # Test the number of components and their names. + ( [ np.float32( 42 ) ], ( "X" ), (), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), + ( [ np.float32( 42 ), np.float32( 42 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), + ( [ np.float32( 42 ), np.float32( 42 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), + ( [ np.float32( 42 ), np.float32( 42 ) ], (), ( "Component0", "Component1" ), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), + # Test the type of the values. + ## With numpy scalar type. + ( [ np.int8( 42 ) ], (), (), True, None, VTK_SIGNED_CHAR, "newAttribute" ), + ( [ np.int8( 42 ) ], (), (), True, VTK_SIGNED_CHAR, VTK_SIGNED_CHAR, "newAttribute" ), + ( [ np.int16( 42 ) ], (), (), True, None, VTK_SHORT, "newAttribute" ), + ( [ np.int16( 42 ) ], (), (), True, VTK_SHORT, VTK_SHORT, "newAttribute" ), + ( [ np.int32( 42 ) ], (), (), True, None, VTK_INT, "newAttribute" ), + ( [ np.int32( 42 ) ], (), (), True, VTK_INT, VTK_INT, "newAttribute" ), + ( [ np.int64( 42 ) ], (), (), True, None, VTK_LONG_LONG, "newAttribute" ), + ( [ np.int64( 42 ) ], (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "newAttribute" ), + ( [ np.uint8( 42 ) ], (), (), True, None, VTK_UNSIGNED_CHAR, "newAttribute" ), + ( [ np.uint8( 42 ) ], (), (), True, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_CHAR, "newAttribute" ), + ( [ np.uint16( 42 ) ], (), (), True, None, VTK_UNSIGNED_SHORT, "newAttribute" ), + ( [ np.uint16( 42 ) ], (), (), True, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_SHORT, "newAttribute" ), + ( [ np.uint32( 42 ) ], (), (), True, None, VTK_UNSIGNED_INT, "newAttribute" ), + ( [ np.uint32( 42 ) ], (), (), True, VTK_UNSIGNED_INT, VTK_UNSIGNED_INT, "newAttribute" ), + ( [ np.uint64( 42 ) ], (), (), True, None, VTK_UNSIGNED_LONG_LONG, "newAttribute" ), + ( [ np.uint64( 42 ) ], (), (), True, VTK_UNSIGNED_LONG_LONG, VTK_UNSIGNED_LONG_LONG, "newAttribute" ), + ( [ np.float32( 42 ) ], (), (), True, None, VTK_FLOAT, "newAttribute" ), + ( [ np.float64( 42 ) ], (), (), True, None, VTK_DOUBLE, "newAttribute" ), + ( [ np.float64( 42 ) ], (), (), True, VTK_DOUBLE, VTK_DOUBLE, "newAttribute" ), + ## With python scalar type. + ( [ 42 ], (), (), True, None, VTK_LONG_LONG, "newAttribute" ), + ( [ 42 ], (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "newAttribute" ), + ( [ 42. ], (), (), True, None, VTK_DOUBLE, "newAttribute" ), + ( [ 42. ], (), (), True, VTK_DOUBLE, VTK_DOUBLE, "newAttribute" ), +] ) def test_createConstantAttributeDataSet( dataSetTest: vtkDataSet, - values: list[ Any ], + listValues: list[ Any ], componentNames: tuple[ str, ...], componentNamesTest: tuple[ str, ...], onPoints: bool, vtkDataType: Union[ int, Any ], vtkDataTypeTest: int, - valueType: str, + attributeName: str, ) -> None: """Test constant attribute creation in dataset.""" + # Get the dataSet from a vtu. dataSet: vtkDataSet = dataSetTest( "dataset" ) - attributeName: str = "newAttributedataset" - arrayModifiers.createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, - vtkDataType ) + # Create the new constant attribute in the dataSet. + assert arrayModifiers.createConstantAttributeDataSet( dataSet, listValues, attributeName, componentNames, onPoints, vtkDataType ) + + # Get the new attribute to check its properties. data: Union[ vtkPointData, vtkCellData ] nbElements: int if onPoints: @@ -334,104 +275,71 @@ def test_createConstantAttributeDataSet( else: data = dataSet.GetCellData() nbElements = dataSet.GetNumberOfCells() - createdAttribute: vtkDataArray = data.GetArray( attributeName ) - nbComponents: int = len( values ) + # Test the number of components and their names if multiple. + nbComponentsTest: int = len( listValues ) nbComponentsCreated: int = createdAttribute.GetNumberOfComponents() - assert nbComponents == nbComponentsCreated - - npArray: npt.NDArray[ Any ] - if nbComponents > 1: + assert nbComponentsCreated == nbComponentsTest + if nbComponentsTest > 1: componentNamesCreated: tuple[ str, ...] = tuple( - createdAttribute.GetComponentName( i ) for i in range( nbComponents ) ) - assert componentNamesTest == componentNamesCreated - - npArray = np.array( [ values for _ in range( nbElements ) ] ) + createdAttribute.GetComponentName( i ) for i in range( nbComponentsCreated ) ) + assert componentNamesCreated, componentNamesTest + + # Test values and their types. + ## Create the constant array test from values in the list values. + npArrayTest: npt.NDArray[ Any ] + if len( listValues ) > 1: + npArrayTest = np.array( [ listValues for _ in range( nbElements ) ] ) else: - npArray = np.array( [ values[ 0 ] for _ in range( nbElements ) ] ) + npArrayTest = np.array( [ listValues[ 0 ] for _ in range( nbElements ) ] ) - npArraycreated: npt.NDArray[ Any ] = vnp.vtk_to_numpy( createdAttribute ) - assert ( npArray == npArraycreated ).all() - assert valueType == npArraycreated.dtype + npArrayCreated: npt.NDArray[ Any ] = vnp.vtk_to_numpy( createdAttribute ) + assert ( npArrayCreated == npArrayTest ).all() + assert npArrayCreated.dtype == npArrayTest.dtype vtkDataTypeCreated: int = createdAttribute.GetDataType() - assert vtkDataTypeTest == vtkDataTypeCreated - - -@pytest.mark.parametrize( "componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, valueType", [ - ( (), (), True, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( (), (), False, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( (), (), True, None, VTK_FLOAT, "float32" ), - ( (), (), False, None, VTK_FLOAT, "float32" ), - ( (), ( "Component0", "Component1" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( (), ( "Component0", "Component1" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( (), ( "Component0", "Component1" ), True, None, VTK_FLOAT, "float32" ), - ( (), ( "Component0", "Component1" ), False, None, VTK_FLOAT, "float32" ), - ( ( "X", "Y" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( ( "X", "Y" ), ( "X", "Y" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_FLOAT, "float32" ), - ( ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_FLOAT, "float32" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_FLOAT, VTK_FLOAT, "float32" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_FLOAT, "float32" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_FLOAT, "float32" ), - ( (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( (), (), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( (), (), True, None, VTK_DOUBLE, "float64" ), - ( (), (), False, None, VTK_DOUBLE, "float64" ), - ( (), ( "Component0", "Component1" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( (), ( "Component0", "Component1" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( (), ( "Component0", "Component1" ), True, None, VTK_DOUBLE, "float64" ), - ( (), ( "Component0", "Component1" ), False, None, VTK_DOUBLE, "float64" ), - ( ( "X", "Y" ), ( "X", "Y" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( ( "X", "Y" ), ( "X", "Y" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_DOUBLE, "float64" ), - ( ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_DOUBLE, "float64" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_DOUBLE, VTK_DOUBLE, "float64" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_DOUBLE, "float64" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_DOUBLE, "float64" ), - ( (), (), True, VTK_INT, VTK_INT, "int32" ), - ( (), (), False, VTK_INT, VTK_INT, "int32" ), - ( (), (), True, None, VTK_INT, "int32" ), - ( (), (), False, None, VTK_INT, "int32" ), - ( (), ( "Component0", "Component1" ), True, VTK_INT, VTK_INT, "int32" ), - ( (), ( "Component0", "Component1" ), False, VTK_INT, VTK_INT, "int32" ), - ( (), ( "Component0", "Component1" ), True, None, VTK_INT, "int32" ), - ( (), ( "Component0", "Component1" ), False, None, VTK_INT, "int32" ), - ( ( "X", "Y" ), ( "X", "Y" ), True, VTK_INT, VTK_INT, "int32" ), - ( ( "X", "Y" ), ( "X", "Y" ), False, VTK_INT, VTK_INT, "int32" ), - ( ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_INT, "int32" ), - ( ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_INT, "int32" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_INT, VTK_INT, "int32" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_INT, VTK_INT, "int32" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_INT, "int32" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_INT, "int32" ), - ( (), (), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( (), (), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( (), (), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( (), (), True, None, VTK_LONG_LONG, "int64" ), - ( (), (), False, None, VTK_LONG_LONG, "int64" ), - ( (), ( "Component0", "Component1" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( (), ( "Component0", "Component1" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( (), ( "Component0", "Component1" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( (), ( "Component0", "Component1" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( (), ( "Component0", "Component1" ), True, None, VTK_LONG_LONG, "int64" ), - ( (), ( "Component0", "Component1" ), False, None, VTK_LONG_LONG, "int64" ), - ( ( "X", "Y" ), ( "X", "Y" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( ( "X", "Y" ), ( "X", "Y" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( ( "X", "Y" ), ( "X", "Y" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( ( "X", "Y" ), ( "X", "Y" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( ( "X", "Y" ), ( "X", "Y" ), True, None, VTK_LONG_LONG, "int64" ), - ( ( "X", "Y" ), ( "X", "Y" ), False, None, VTK_LONG_LONG, "int64" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_ID_TYPE, VTK_ID_TYPE, "int64" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, VTK_LONG_LONG, VTK_LONG_LONG, "int64" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, None, VTK_LONG_LONG, "int64" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), False, None, VTK_LONG_LONG, "int64" ), + assert vtkDataTypeCreated == vtkDataTypeTest + + +@pytest.mark.parametrize( "componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, valueType, attributeName", [ + # Test attribute names. + ## Test with an attributeName already existing on cells data. + ( (), (), True, VTK_FLOAT, VTK_FLOAT, "float32", "PORO" ), + ## Test with a new attributeName on cells and on points. + ( (), (), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), + ( (), (), False, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), + # Test the number of components and their names. + ( ( "X" ), (), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), + ( ( "X", "Y" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), + ( (), ( "Component0", "Component1" ), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), + # Test the type of the values. + ## With numpy scalar type. + ( (), (), True, None, VTK_SIGNED_CHAR, "int8", "newAttribute" ), + ( (), (), True, VTK_SIGNED_CHAR, VTK_SIGNED_CHAR, "int8", "newAttribute" ), + ( (), (), True, None, VTK_SHORT, "int16", "newAttribute" ), + ( (), (), True, VTK_SHORT, VTK_SHORT, "int16", "newAttribute" ), + ( (), (), True, None, VTK_INT, "int32", "newAttribute" ), + ( (), (), True, VTK_INT, VTK_INT, "int32", "newAttribute" ), + ( (), (), True, None, VTK_LONG_LONG, "int64", "newAttribute" ), + ( (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64", "newAttribute" ), + ( (), (), True, None, VTK_UNSIGNED_CHAR, "uint8", "newAttribute" ), + ( (), (), True, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_CHAR, "uint8", "newAttribute" ), + ( (), (), True, None, VTK_UNSIGNED_SHORT, "uint16", "newAttribute" ), + ( (), (), True, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_SHORT, "uint16", "newAttribute" ), + ( (), (), True, None, VTK_UNSIGNED_INT, "uint32", "newAttribute" ), + ( (), (), True, VTK_UNSIGNED_INT, VTK_UNSIGNED_INT, "uint32", "newAttribute" ), + ( (), (), True, None, VTK_UNSIGNED_LONG_LONG, "uint64", "newAttribute" ), + ( (), (), True, VTK_UNSIGNED_LONG_LONG, VTK_UNSIGNED_LONG_LONG, "uint64", "newAttribute" ), + ( (), (), True, None, VTK_FLOAT, "float32", "newAttribute" ), + ( (), (), True, None, VTK_DOUBLE, "float64", "newAttribute" ), + ( (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float64", "newAttribute" ), + ## With python scalar type. + ( (), (), True, None, VTK_LONG_LONG, "int", "newAttribute" ), + ( (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "int", "newAttribute" ), + ( (), (), True, None, VTK_DOUBLE, "float", "newAttribute" ), + ( (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float", "newAttribute" ), ] ) def test_createAttribute( dataSetTest: vtkDataSet, @@ -442,34 +350,40 @@ def test_createAttribute( vtkDataType: int, vtkDataTypeTest: int, valueType: str, + attributeName: str, ) -> None: """Test creation of dataset in dataset from given array.""" + # Get the dataSet from a vtu. dataSet: vtkDataSet = dataSetTest( "dataset" ) - attributeName: str = "AttributeName" - nbComponents: int = ( 1 if len( componentNamesTest ) == 0 else len( componentNamesTest ) ) - nbElements: int = ( dataSet.GetNumberOfPoints() if onPoints else dataSet.GetNumberOfCells() ) + # Get a array with random values of a given type. + nbComponentsTest: int = 1 if len( componentNamesTest ) == 0 else len( componentNamesTest ) + nbElementsTest: int = dataSet.GetNumberOfPoints() if onPoints else dataSet.GetNumberOfCells() + npArrayTest: npt.NDArray[ Any ] = getArrayWithSpeTypeValue( nbComponentsTest, nbElementsTest, valueType ) - npArray: npt.NDArray[ Any ] = getArrayWithSpeTypeValue( nbComponents, nbElements, valueType ) - arrayModifiers.createAttribute( dataSet, npArray, attributeName, componentNames, onPoints, vtkDataType ) + # Create the new attribute in the dataSet. + assert arrayModifiers.createAttribute( dataSet, npArrayTest, attributeName, componentNames, onPoints, vtkDataType ) + # Get the new attribute to check its properties. data: Union[ vtkPointData, vtkCellData ] data = dataSet.GetPointData() if onPoints else dataSet.GetCellData() - createdAttribute: vtkDataArray = data.GetArray( attributeName ) + + # Test the number of components and their names if multiple. nbComponentsCreated: int = createdAttribute.GetNumberOfComponents() - assert nbComponents == nbComponentsCreated - if nbComponents > 1: + assert nbComponentsCreated == nbComponentsTest + if nbComponentsTest > 1: componentsNamesCreated: tuple[ str, ...] = tuple( - createdAttribute.GetComponentName( i ) for i in range( nbComponents ) ) - assert componentNamesTest == componentsNamesCreated + createdAttribute.GetComponentName( i ) for i in range( nbComponentsCreated ) ) + assert componentsNamesCreated == componentNamesTest - npArraycreated: npt.NDArray[ Any ] = vnp.vtk_to_numpy( createdAttribute ) - assert ( npArray == npArraycreated ).all() - assert valueType == npArraycreated.dtype + # Test values and their types. + npArrayCreated: npt.NDArray[ Any ] = vnp.vtk_to_numpy( createdAttribute ) + assert ( npArrayCreated == npArrayTest ).all() + assert npArrayCreated.dtype == npArrayTest.dtype vtkDataTypeCreated: int = createdAttribute.GetDataType() - assert vtkDataTypeTest == vtkDataTypeCreated + assert vtkDataTypeCreated == vtkDataTypeTest @pytest.mark.parametrize( "attributeNameFrom, attributeNameTo, onPoints, idBlock", [ @@ -568,14 +482,14 @@ def test_copyAttributeDataSet( dataSetTest: vtkDataSet, attributeNameFrom: str, assert npArrayFrom.dtype == npArrayTo.dtype -@pytest.mark.parametrize( "attributeName, onpoints", [ +@pytest.mark.parametrize( "attributeName, onPoints", [ ( "CellAttribute", False ), ( "PointAttribute", True ), ] ) def test_renameAttributeMultiblock( dataSetTest: vtkMultiBlockDataSet, attributeName: str, - onpoints: bool, + onPoints: bool, ) -> None: """Test renaming attribute in a multiblock dataset.""" vtkMultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) @@ -584,11 +498,11 @@ def test_renameAttributeMultiblock( vtkMultiBlockDataSetTest, attributeName, newAttributeName, - onpoints, + onPoints, ) block: vtkDataSet = cast( vtkDataSet, vtkMultiBlockDataSetTest.GetBlock( 0 ) ) data: Union[ vtkPointData, vtkCellData ] - if onpoints: + if onPoints: data = block.GetPointData() assert data.HasArray( attributeName ) == 0 assert data.HasArray( newAttributeName ) == 1 @@ -599,11 +513,11 @@ def test_renameAttributeMultiblock( assert data.HasArray( newAttributeName ) == 1 -@pytest.mark.parametrize( "attributeName, onpoints", [ ( "CellAttribute", False ), ( "PointAttribute", True ) ] ) +@pytest.mark.parametrize( "attributeName, onPoints", [ ( "CellAttribute", False ), ( "PointAttribute", True ) ] ) def test_renameAttributeDataSet( dataSetTest: vtkDataSet, attributeName: str, - onpoints: bool, + onPoints: bool, ) -> None: """Test renaming an attribute in a dataset.""" vtkDataSetTest: vtkDataSet = dataSetTest( "dataset" ) @@ -611,8 +525,8 @@ def test_renameAttributeDataSet( arrayModifiers.renameAttribute( object=vtkDataSetTest, attributeName=attributeName, newAttributeName=newAttributeName, - onPoints=onpoints ) - if onpoints: + onPoints=onPoints ) + if onPoints: assert vtkDataSetTest.GetPointData().HasArray( attributeName ) == 0 assert vtkDataSetTest.GetPointData().HasArray( newAttributeName ) == 1 From 142348291c09130608813f99a5045501cace6be6 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 22 Jul 2025 15:11:13 +0200 Subject: [PATCH 19/56] Clean fillpartialattribute and its test --- .../src/geos/mesh/utils/arrayModifiers.py | 170 ++++---- geos-mesh/tests/test_arrayModifiers.py | 382 ++++++++---------- 2 files changed, 275 insertions(+), 277 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index 36131048..04f629f4 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -8,7 +8,9 @@ from geos.utils.Logger import getLogger, Logger from vtk import ( # type: ignore[import-untyped] - VTK_DOUBLE, VTK_FLOAT, VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG, + VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_LONG, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG_LONG, + VTK_CHAR, VTK_SIGNED_CHAR, VTK_SHORT, VTK_LONG, VTK_INT, VTK_LONG_LONG, VTK_ID_TYPE, + VTK_FLOAT, VTK_DOUBLE, ) from vtkmodules.vtkCommonDataModel import ( vtkMultiBlockDataSet, @@ -31,6 +33,7 @@ ) from geos.mesh.utils.arrayHelpers import ( getComponentNames, + getComponentNamesDataSet, getAttributesWithNumberOfComponents, getArrayInObject, isAttributeInObject, @@ -69,48 +72,55 @@ def fillPartialAttributes( attributeName (str): Attribute name. onPoints (bool, optional): Attribute is on Points (True) or on Cells (False). Defaults to False. - value (any, optional): Filling value. - Defaults to -1 for int VTK arrays, 0 for uint VTK arrays and nan otherwise. + value (any, optional): Filling value. It is better to use numpy scalar type for the values. + Defaults to -1 for int VTK arrays, 0 for uint VTK arrays and nan for float VTK arrays. logger (Logger, optional): A logger to manage the output messages. Defaults to an internal logger. Returns: bool: True if the attribute was correctly created and filled, False if not. """ + # Check if the input mesh is inherited from vtkMultiBlockDataSet. if not isinstance( multiBlockDataSet, vtkMultiBlockDataSet ): logger.error( f"Input mesh has to be inherited from vtkMultiBlockDataSet." ) return False + # Check if the attribute is partial. if isAttributeGlobal( multiBlockDataSet, attributeName, onPoints ): logger.error( f"The attribute { attributeName } is already global." ) return False + # Get information of the attribute to fill. vtkDataType: int = getVtkArrayTypeInMultiBlock( multiBlockDataSet, attributeName, onPoints ) infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints ) nbComponents: int = infoAttributes[ attributeName ] - componentNames: tuple[ str, ...] = () if nbComponents > 1: componentNames = getComponentNames( multiBlockDataSet, attributeName, onPoints ) - typeMapping: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() - valueType: Any = typeMapping[ vtkDataType ] + # Set the default value depending of the type of the attribute to fill if np.isnan( value ): - if vtkDataType in ( VTK_DOUBLE, VTK_FLOAT ): + typeMapping: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() + valueType: type = typeMapping[ vtkDataType ] + # Default value for float types is nan. + if vtkDataType in ( VTK_FLOAT, VTK_DOUBLE ): value = valueType( value ) - elif vtkDataType in ( VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG ): - logger.warning( f"{ attributeName } vtk array type is { valueType }, default value is automatically set to 0." ) + logger.warning( f"{ attributeName } vtk data type is { vtkDataType } cooresponding to { value.dtype } numpy type, default value is automatically set to nan." ) + # Default value for int types is -1. + elif vtkDataType in ( VTK_CHAR, VTK_SIGNED_CHAR, VTK_SHORT, VTK_LONG, VTK_INT, VTK_LONG_LONG, VTK_ID_TYPE ) : + value = valueType( -1 ) + logger.warning( f"{ attributeName } vtk data type is { vtkDataType } cooresponding to { value.dtype } numpy type, default value is automatically set to -1." ) + # Default value for uint types is 0. + elif vtkDataType in ( VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_LONG, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG_LONG ): value = valueType( 0 ) + logger.warning( f"{ attributeName } vtk data type is { vtkDataType } cooresponding to { value.dtype } numpy type, default value is automatically set to 0." ) else: - logger.warning( f"{ attributeName } vtk array type is { valueType }, default value is automatically set to -1." ) - value = valueType( -1 ) - - else: - value = valueType( value ) + logger.error( f"The type of the attribute { attributeName } is not compatible with the function.") + return False values: list[ Any ] = [ value for _ in range( nbComponents ) ] - # Parse the multiBlockDataSet to create and fill the attribute on blocks where the attribute is not. + # Parse the multiBlockDataSet to create and fill the attribute on blocks where it is not. iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() iter.SetDataSet( multiBlockDataSet ) iter.VisitOnlyLeavesOn() @@ -118,8 +128,7 @@ def fillPartialAttributes( while iter.GetCurrentDataObject() is not None: dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) if not isAttributeInObjectDataSet( dataSet, attributeName, onPoints ): - created: bool = createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkDataType, logger ) - if not created: + if not createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkDataType, logger ): return False iter.GoToNextItem() @@ -129,15 +138,17 @@ def fillPartialAttributes( def fillAllPartialAttributes( multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], - value: Any = np.nan, logger: Logger = getLogger( "fillAllPartialAttributes", True ), ) -> bool: - """Fill all the partial attributes of a multiBlockDataSet with a same value. All components of each attribute are filled with the same value. + """Fill all partial attributes of a multiBlockDataSet with the default value. + All components of each attributes are filled with the same value. + Depending of the type of the attribute, the default value is different: + - 0 for uint types (VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_LONG, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG_LONG). + - -1 for int types (VTK_CHAR, VTK_SIGNED_CHAR, VTK_SHORT, VTK_LONG, VTK_INT, VTK_LONG_LONG, VTK_ID_TYPE). + - nan for float types (VTK_FLOAT, VTK_DOUBLE). Args: - multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): MultiBlockDataSet where to fill the attribute. - value (any, optional): Filling value. - Defaults to -1 for int VTK arrays, 0 for uint VTK arrays and nan otherwise. + multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): MultiBlockDataSet where to fill attributes. logger (Logger, optional): A logger to manage the output messages. Defaults to an internal logger. @@ -149,7 +160,7 @@ def fillAllPartialAttributes( infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints ) for attributeName in infoAttributes: if not isAttributeGlobal( multiBlockDataSet, attributeName, onPoints ): - if not fillPartialAttributes( multiBlockDataSet, attributeName, onPoints, value, logger ): + if not fillPartialAttributes( multiBlockDataSet, attributeName, onPoints, logger=logger ): return False return True @@ -331,12 +342,6 @@ def createConstantAttributeDataSet( Returns: bool: True if the attribute was correctly created, False if it was not created. """ - # Check if listValues have at least one value. - if len( listValues ) == 0: - logger.error( f"To create a constant attribute, you have to give at least one value in the listValues." ) - logger.error( f"The constant attribute { attributeName } has not been created into the mesh." ) - return False - # Check if all the values of listValues have the same type. valueType: type = type( listValues[ 0 ] ) for value in listValues: @@ -487,20 +492,20 @@ def createAttribute( def copyAttribute( - objectFrom: vtkMultiBlockDataSet, - objectTo: vtkMultiBlockDataSet, + multiBlockDataSetFrom: vtkMultiBlockDataSet, + multiBlockDataSetTo: vtkMultiBlockDataSet, attributeNameFrom: str, attributeNameTo: str, onPoints: bool = False, logger: Logger = getLogger( "copyAttribute", True ), ) -> bool: - """Copy an attribute from a multiBlockDataSet to another. + """Copy an attribute from a multiBlockDataSet to a similare one on the same piece. Args: - objectFrom (vtkMultiBlockDataSet): MultiBlockDataSet from which to copy the attribute. - objectTo (vtkMultiBlockDataSet): MultiBlockDataSet where to copy the attribute. - attributeNameFrom (str): Attribute name in objectFrom. - attributeNameTo (str): Attribute name in objectTo. + multiBlockDataSetFrom (vtkMultiBlockDataSet): MultiBlockDataSet from which to copy the attribute. + multiBlockDataSetTo (vtkMultiBlockDataSet): MultiBlockDataSet where to copy the attribute. + attributeNameFrom (str): Attribute name in multiBlockDataSetFrom. + attributeNameTo (str): Attribute name in multiBlockDataSetTo. It will be a new attribute of multiBlockDataSetTo. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. logger (Logger, optional): A logger to manage the output messages. @@ -509,65 +514,74 @@ def copyAttribute( Returns: bool: True if copy successfully ended, False otherwise. """ - if not isinstance( objectFrom, vtkMultiBlockDataSet ): - logger.error( f"ObjectFrom has to be inherited from vtkMultiBlockDataSet." ) + # Check if the multiBlockDataSetFrom is inherited from vtkMultiBlockDataSet. + if not isinstance( multiBlockDataSetFrom, vtkMultiBlockDataSet ): + logger.error( f"multiBlockDataSetFrom has to be inherited from vtkMultiBlockDataSet." ) + logger.error( f"The attribute { attributeNameFrom } has not been copied." ) + return False + + # Check if the multiBlockDataSetTo is inherited from vtkMultiBlockDataSet. + if not isinstance( multiBlockDataSetTo, vtkMultiBlockDataSet ): + logger.error( f"multiBlockDataSetTo has to be inherited from vtkMultiBlockDataSet." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - if not isinstance( objectTo, vtkMultiBlockDataSet ): - logger.error( f"ObjectTo has to be inherited from vtkMultiBlockDataSet." ) + # Check if the attribute exist in the multiBlockDataSetFrom. + if not isAttributeInObjectMultiBlockDataSet( multiBlockDataSetFrom, attributeNameFrom, onPoints ): + logger.error( f"The attribute { attributeNameFrom } is not in the multiBlockDataSetFrom." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - if not isAttributeInObjectMultiBlockDataSet( objectFrom, attributeNameFrom, onPoints ): - logger.error( f"The attribute { attributeNameFrom } is not in the objectFrom." ) + # Check if the attribute already exist in the multiBlockDataSetTo. + if isAttributeInObjectMultiBlockDataSet( multiBlockDataSetTo, attributeNameTo, onPoints ): + logger.error( f"The attribute { attributeNameTo } is already in the multiBlockDataSetTo." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - elementaryBlockIndexesTo: list[ int ] = getBlockElementIndexesFlatten( objectTo ) - elementaryBlockIndexesFrom: list[ int ] = getBlockElementIndexesFlatten( objectFrom ) - + # Check if the two multiBlockDataSets are similare. + elementaryBlockIndexesTo: list[ int ] = getBlockElementIndexesFlatten( multiBlockDataSetTo ) + elementaryBlockIndexesFrom: list[ int ] = getBlockElementIndexesFlatten( multiBlockDataSetFrom ) if elementaryBlockIndexesTo != elementaryBlockIndexesFrom: - logger.error( f"ObjectFrom and objectTo do not have the same block indexes." ) + logger.error( f"multiBlockDataSetFrom and multiBlockDataSetTo do not have the same block indexes." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - for index in elementaryBlockIndexesTo: - blockFrom: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( objectFrom, index ) ) - if blockFrom is None: - logger.error( f"Block { str( index ) } of objectFrom is null." ) + # Parse blocks of the two mesh to copy the attribute. + for idBlock in elementaryBlockIndexesTo: + dataSetFrom: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( multiBlockDataSetFrom, idBlock ) ) + if dataSetFrom is None: + logger.error( f"Block { blockId } of multiBlockDataSetFrom is null." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - blockTo: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( objectTo, index ) ) - if blockTo is None: - logger.error( f"Block { str( index ) } of objectTo is null." ) + dataSetTo: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( multiBlockDataSetTo, idBlock ) ) + if dataSetTo is None: + logger.error( f"Block { blockId } of multiBlockDataSetTo is null." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - if isAttributeInObjectDataSet( blockFrom, attributeNameFrom, onPoints ): - copied: bool = copyAttributeDataSet( blockFrom, blockTo, attributeNameFrom, attributeNameTo, onPoints, logger ) - if not copied: + if isAttributeInObjectDataSet( dataSetFrom, attributeNameFrom, onPoints ): + if not copyAttributeDataSet( dataSetFrom, dataSetTo, attributeNameFrom, attributeNameTo, onPoints, logger ): return False return True def copyAttributeDataSet( - objectFrom: vtkDataSet, - objectTo: vtkDataSet, + dataSetFrom: vtkDataSet, + dataSetTo: vtkDataSet, attributeNameFrom: str, attributeNameTo: str, onPoints: bool = False, logger: Logger = getLogger( "copyAttributeDataSet", True ), ) -> bool: - """Copy an attribute from a dataSet to another. + """Copy an attribute from a dataSet to a similare one on the same piece. Args: - objectFrom (vtkDataSet): DataSet from which to copy the attribute. - objectTo (vtkDataSet): DataSet where to copy the attribute. - attributeNameFrom (str): Attribute name in objectFrom. - attributeNameTo (str): Attribute name in objectTo. + dataSetFrom (vtkDataSet): DataSet from which to copy the attribute. + dataSetTo (vtkDataSet): DataSet where to copy the attribute. + attributeNameFrom (str): Attribute name in dataSetFrom. + attributeNameTo (str): Attribute name in dataSetTo. It will be a new attribute of dataSetTo. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. logger (Logger, optional): A logger to manage the output messages. @@ -576,26 +590,36 @@ def copyAttributeDataSet( Returns: bool: True if copy successfully ended, False otherwise. """ - if not isinstance( objectFrom, vtkDataSet ): - logger.error( f"ObjectFrom has to be inherited from vtkDataSet." ) + # Check if the dataSetFrom is inherited from vtkDataSet. + if not isinstance( dataSetFrom, vtkDataSet ): + logger.error( f"dataSetFrom has to be inherited from vtkDataSet." ) + logger.error( f"The attribute { attributeNameFrom } has not been copied." ) + return False + + # Check if the dataSetTo is inherited from vtkDataSet. + if not isinstance( dataSetTo, vtkDataSet ): + logger.error( f"dataSetTo has to be inherited from vtkDataSet." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - if not isinstance( objectTo, vtkDataSet ): - logger.error( f"ObjectTo has to be inherited from vtkDataSet." ) + # Check if the attribute exist in the dataSetFrom. + if not isAttributeInObjectDataSet( dataSetFrom, attributeNameFrom, onPoints ): + logger.error( f"The attribute { attributeNameFrom } is not in the dataSetFrom." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - if not isAttributeInObjectDataSet( objectFrom, attributeNameFrom, onPoints ): - logger.error( f"The attribute { attributeNameFrom } is not in the objectFrom." ) + # Check if the attribute already exist in the dataSetTo. + if isAttributeInObjectDataSet( dataSetTo, attributeNameTo, onPoints ): + logger.error( f"The attribute { attributeNameTo } is already in the dataSetTo." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - npArray: npt.NDArray[ Any ] = getArrayInObject( objectFrom, attributeNameFrom, onPoints ) - componentNames: tuple[ str, ...] = getComponentNames( objectFrom, attributeNameFrom, onPoints ) - vtkArrayType: int = getVtkArrayTypeInObject( objectFrom, attributeNameFrom, onPoints ) + # Get the properties of the attribute to copied. + npArray: npt.NDArray[ Any ] = getArrayInObject( dataSetFrom, attributeNameFrom, onPoints ) + componentNames: tuple[ str, ...] = getComponentNamesDataSet( dataSetFrom, attributeNameFrom, onPoints ) + vtkArrayType: int = getVtkArrayTypeInObject( dataSetFrom, attributeNameFrom, onPoints ) - return createAttribute( objectTo, npArray, attributeNameTo, componentNames, onPoints, vtkArrayType, logger ) + return createAttribute( dataSetTo, npArray, attributeNameTo, componentNames, onPoints, vtkArrayType, logger ) def renameAttribute( diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index b8c22d31..8d9fb812 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -14,21 +14,10 @@ from vtkmodules.vtkCommonCore import vtkDataArray from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet, vtkPointData, vtkCellData ) -from geos.mesh.utils.arrayHelpers import getAttributesWithNumberOfComponents - from vtk import ( # type: ignore[import-untyped] - VTK_UNSIGNED_CHAR, - VTK_UNSIGNED_SHORT, - VTK_UNSIGNED_INT, - VTK_UNSIGNED_LONG_LONG, - VTK_SIGNED_CHAR, - VTK_SHORT, - VTK_INT, - VTK_LONG_LONG, - VTK_FLOAT, - VTK_DOUBLE, - VTK_ID_TYPE, - VTK_CHAR, + VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG_LONG, + VTK_CHAR, VTK_SIGNED_CHAR, VTK_SHORT, VTK_INT, VTK_LONG_LONG, VTK_ID_TYPE, + VTK_FLOAT, VTK_DOUBLE, ) # Information : @@ -56,108 +45,100 @@ from geos.mesh.utils import arrayModifiers -@pytest.mark.parametrize( - "idBlockToFill, attributeName, nbComponentsRef, componentNamesRef, onPoints, value, valueRef, vtkDataTypeRef, valueTypeRef", - [ - ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.nan, np.nan, VTK_DOUBLE, "float64" ), - ( 1, "CellAttribute", 3, - ( "AX1", "AX2", "AX3" ), False, np.float64( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), - ( 1, "CellAttribute", 3, - ( "AX1", "AX2", "AX3" ), False, np.int32( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), - ( 1, "PointAttribute", 3, ( "AX1", "AX2", "AX3" ), True, np.nan, np.nan, VTK_DOUBLE, "float64" ), - ( 1, "PointAttribute", 3, - ( "AX1", "AX2", "AX3" ), True, np.float64( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), - ( 1, "PointAttribute", 3, - ( "AX1", "AX2", "AX3" ), True, np.int32( 4 ), np.float64( 4 ), VTK_DOUBLE, "float64" ), - ( 1, "PORO", 1, (), False, np.nan, np.nan, VTK_FLOAT, "float32" ), - ( 1, "PORO", 1, (), False, np.float32( 4 ), np.float32( 4 ), VTK_FLOAT, "float32" ), - ( 1, "PORO", 1, (), False, np.int32( 4 ), np.float32( 4 ), VTK_FLOAT, "float32" ), - ( 1, "FAULT", 1, (), False, np.nan, np.int32( -1 ), VTK_INT, "int32" ), - ( 1, "FAULT", 1, (), False, np.int32( 4 ), np.int32( 4 ), VTK_INT, "int32" ), - ( 1, "FAULT", 1, (), False, np.float32( 4 ), np.int32( 4 ), VTK_INT, "int32" ), - ( 0, "collocated_nodes", 2, ( None, None ), True, np.nan, np.int64( -1 ), VTK_ID_TYPE, "int64" ), - ( 0, "collocated_nodes", 2, ( None, None ), True, np.int64( 4 ), np.int64( 4 ), VTK_ID_TYPE, "int64" ), - ( 0, "collocated_nodes", 2, ( None, None ), True, np.int32( 4 ), np.int64( 4 ), VTK_ID_TYPE, "int64" ), - ( 0, "collocated_nodes", 2, ( None, None ), True, np.float32( 4 ), np.int64( 4 ), VTK_ID_TYPE, "int64" ), - ] ) +@pytest.mark.parametrize( "idBlock, attributeName, nbComponentsTest, componentNamesTest, onPoints, value, valueTest, vtkDataTypeTest", [ + # Test fill an attribute on point and on cell. + ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.nan, np.nan, VTK_DOUBLE ), + ( 1, "PointAttribute", 3, ( "AX1", "AX2", "AX3" ), True, np.nan, np.nan, VTK_DOUBLE ), + # Test fill attributes with different number of componnent. + ( 1, "PORO", 1, (), False, np.nan, np.float32( np.nan ), VTK_FLOAT ), + ( 1, "PERM", 3, ( "AX1", "AX2", "AX3" ), False, np.nan, np.float32( np.nan ), VTK_FLOAT ), + # Test fill an attribute with default value. + ( 1, "FAULT", 1, (), False, np.nan, np.int32( -1 ), VTK_INT ), + ( 0, "collocated_nodes", 2, ( None, None ), True, np.nan, np.int64( -1 ), VTK_ID_TYPE ), + # Test fill an attribute with specified value. + ( 1, "PORO", 1, (), False, np.float32( 4 ), np.float32( 4 ), VTK_FLOAT ), + ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, 4. , np.float64( 4 ), VTK_DOUBLE ), + ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.float64( 4 ), np.float64( 4 ), VTK_DOUBLE ), + ( 1, "FAULT", 1, (), False, np.int32( 4 ), np.int32( 4 ), VTK_INT ), + ( 0, "collocated_nodes", 2, ( None, None ), True, 4 , np.int64( 4 ), VTK_ID_TYPE ), + ( 0, "collocated_nodes", 2, ( None, None ), True, np.int64( 4 ), np.int64( 4 ), VTK_ID_TYPE ), +] ) def test_fillPartialAttributes( dataSetTest: vtkMultiBlockDataSet, - idBlockToFill: int, + idBlock: int, attributeName: str, - nbComponentsRef: int, - componentNamesRef: tuple[ str, ...], + nbComponentsTest: int, + componentNamesTest: tuple[ str, ...], onPoints: bool, value: Any, - valueRef: Any, - vtkDataTypeRef: int, - valueTypeRef: str, + valueTest: Any, + vtkDataTypeTest: int, ) -> None: """Test filling a partial attribute from a multiblock with values.""" multiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - arrayModifiers.fillPartialAttributes( multiBlockDataSetTest, attributeName, onPoints, value ) + + # Fill the attribute in the multiBlockDataSet. + assert arrayModifiers.fillPartialAttributes( multiBlockDataSetTest, attributeName, onPoints, value ) - blockTest: vtkDataSet = cast( vtkDataSet, multiBlockDataSetTest.GetBlock( idBlockToFill ) ) - dataTest: Union[ vtkPointData, vtkCellData ] + # Get the dataSet where the attribute has been filled. + dataSet: vtkDataSet = cast( vtkDataSet, multiBlockDataSetTest.GetBlock( idBlock ) ) + + # Get the filled attribute. + data: Union[ vtkPointData, vtkCellData ] nbElements: int if onPoints: - nbElements = blockTest.GetNumberOfPoints() - dataTest = blockTest.GetPointData() + nbElements = dataSet.GetNumberOfPoints() + data = dataSet.GetPointData() else: - nbElements = blockTest.GetNumberOfCells() - dataTest = blockTest.GetCellData() - - attributeFillTest: vtkDataArray = dataTest.GetArray( attributeName ) - nbComponentsTest: int = attributeFillTest.GetNumberOfComponents() - assert nbComponentsTest == nbComponentsRef - - npArrayFillRef: npt.NDArray[ Any ] - if nbComponentsRef > 1: - componentNamesTest: tuple[ str, ...] = tuple( - attributeFillTest.GetComponentName( i ) for i in range( nbComponentsRef ) ) - assert componentNamesTest == componentNamesRef + nbElements = dataSet.GetNumberOfCells() + data = dataSet.GetCellData() + attributeFilled: vtkDataArray = data.GetArray( attributeName ) - npArrayFillRef = np.full( ( nbElements, nbComponentsRef ), valueRef ) + # Test the number of components and their names if multiple. + nbComponentsFilled: int = attributeFilled.GetNumberOfComponents() + assert nbComponentsFilled == nbComponentsTest + if nbComponentsTest > 1: + componentNamesFilled: tuple[ str, ...] = tuple( + attributeFilled.GetComponentName( i ) for i in range( nbComponentsFilled ) ) + assert componentNamesFilled == componentNamesTest - npArrayFillTest: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeFillTest ) - assert npArrayFillTest.dtype == valueTypeRef + # Test values and their types. + ## Create the constant array test from the value. + npArrayTest: npt.NDArray[ Any ] + if nbComponentsTest > 1: + npArrayTest = np.array( [ [ valueTest for _ in range( nbComponentsTest ) ] for _ in range( nbElements ) ] ) + else: + npArrayTest = np.array( [ valueTest for _ in range( nbElements ) ] ) - if np.isnan( valueRef ): - assert np.isnan( npArrayFillRef ).all() + npArrayFilled: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeFilled ) + assert npArrayFilled.dtype == npArrayTest.dtype + if np.isnan( value ) and vtkDataTypeTest in ( VTK_FLOAT, VTK_DOUBLE ): + assert np.isnan( npArrayFilled ).all() else: - assert ( npArrayFillTest == npArrayFillRef ).all() + assert ( npArrayFilled == npArrayTest ).all() - vtkDataTypeTest: int = attributeFillTest.GetDataType() - assert vtkDataTypeTest == vtkDataTypeRef + vtkDataTypeFilled: int = attributeFilled.GetDataType() + assert vtkDataTypeTest == vtkDataTypeFilled -@pytest.mark.parametrize( "value", [ - ( np.nan ), - ( np.int32( 42 ) ), - ( np.int64( 42 ) ), - ( np.float32( 42 ) ), - ( np.float64( 42 ) ), -] ) +@pytest.mark.parametrize( "multiBlockDataSetName", [ "multiblock" ] ) def test_FillAllPartialAttributes( dataSetTest: vtkMultiBlockDataSet, - value: Any, + multiBlockDataSetName: str, ) -> None: """Test to fill all the partial attributes of a vtkMultiBlockDataSet with a value.""" - multiBlockDataSetRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - multiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - arrayModifiers.fillAllPartialAttributes( multiBlockDataSetTest, value ) + multiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( multiBlockDataSetName ) + assert arrayModifiers.fillAllPartialAttributes( multiBlockDataSetTest ) - nbBlock = multiBlockDataSetRef.GetNumberOfBlocks() + nbBlock: int = multiBlockDataSetTest.GetNumberOfBlocks() for idBlock in range( nbBlock ): - datasetTest: vtkDataSet = cast( vtkDataSet, multiBlockDataSetTest.GetBlock( idBlock ) ) - for onPoints in [ True, False ]: - infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSetRef, onPoints ) - dataTest: Union[ vtkPointData, vtkCellData ] - dataTest = datasetTest.GetPointData() if onPoints else datasetTest.GetCellData() - - for attributeName in infoAttributes: - attributeTest: int = dataTest.HasArray( attributeName ) - assert attributeTest == 1 - + dataSet: vtkDataSet = cast( vtkDataSet, multiBlockDataSetTest.GetBlock( idBlock ) ) + for attributeNameOnPoint in [ "PointAttribute", "collocated_nodes" ]: + attributeExist: int = dataSet.GetPointData().HasArray( attributeNameOnPoint ) + assert attributeExist == 1 + for attributeNameOnCell in [ "CELL_MARKERS", "CellAttribute", "FAULT", "PERM", "PORO" ]: + attributeExist: int = dataSet.GetCellData().HasArray( attributeNameOnCell ) + assert attributeExist == 1 @pytest.mark.parametrize( "attributeName, dataType, expectedDatatypeArray", [ ( "test_double", VTK_DOUBLE, "vtkDoubleArray" ), @@ -182,10 +163,12 @@ def test_createEmptyAttribute( @pytest.mark.parametrize( "attributeName, onPoints", [ + # Test to create a new attribute on points and on cells. ( "newAttribute", False ), ( "newAttribute", True ), - ( "PORO", True ), # Partial attribute on cells already exist - ( "GLOBAL_IDS_CELLS", True ), # Global attribute on cells already exist + # Test to create a new attribute whenn an attribute with the same name already exist on the opposit piece. + ( "PORO", True ), # Partial attribute on cells already exist. + ( "GLOBAL_IDS_CELLS", True ), # Global attribute on cells already exist. ] ) def test_createConstantAttributeMultiBlock( dataSetTest: vtkMultiBlockDataSet, @@ -199,21 +182,19 @@ def test_createConstantAttributeMultiBlock( nbBlock = multiBlockDataSetTest.GetNumberOfBlocks() for idBlock in range( nbBlock ): - datasetTest: vtkDataSet = cast( vtkDataSet, multiBlockDataSetTest.GetBlock( idBlock ) ) - dataTest: Union[ vtkPointData, vtkCellData ] - if onPoints: - dataTest = datasetTest.GetPointData() - else: - dataTest = datasetTest.GetCellData() + dataSet: vtkDataSet = cast( vtkDataSet, multiBlockDataSetTest.GetBlock( idBlock ) ) + data: Union[ vtkPointData, vtkCellData ] + data = dataSet.GetPointData() if onPoints else dataSet.GetCellData() - attributeTest: int = dataTest.HasArray( attributeName ) - assert attributeTest == 1 + attributeWellCreated: int = data.HasArray( attributeName ) + assert attributeWellCreated == 1 @pytest.mark.parametrize( "listValues, componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, attributeName", [ - # Test attribute names. - ## Test with an attributeName already existing on cells data. - ( [ np.float32( 42 ) ], (), (), True, VTK_FLOAT, VTK_FLOAT, "PORO" ), + # Test attribute names. + ## Test with an attributeName already existing on opposit piece. + ( [ np.float64( 42 ) ], (), (), True, VTK_DOUBLE, VTK_DOUBLE, "CellAttribute" ), + ( [ np.float64( 42 ) ], (), (), False, VTK_DOUBLE, VTK_DOUBLE, "PointAttribute" ), ## Test with a new attributeName on cells and on points. ( [ np.float32( 42 ) ], (), (), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), ( [ np.float32( 42 ) ], (), (), False, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), @@ -260,13 +241,12 @@ def test_createConstantAttributeDataSet( attributeName: str, ) -> None: """Test constant attribute creation in dataset.""" - # Get the dataSet from a vtu. dataSet: vtkDataSet = dataSetTest( "dataset" ) # Create the new constant attribute in the dataSet. assert arrayModifiers.createConstantAttributeDataSet( dataSet, listValues, attributeName, componentNames, onPoints, vtkDataType ) - # Get the new attribute to check its properties. + # Get the created attribute. data: Union[ vtkPointData, vtkCellData ] nbElements: int if onPoints: @@ -275,15 +255,15 @@ def test_createConstantAttributeDataSet( else: data = dataSet.GetCellData() nbElements = dataSet.GetNumberOfCells() - createdAttribute: vtkDataArray = data.GetArray( attributeName ) + attributeCreated: vtkDataArray = data.GetArray( attributeName ) # Test the number of components and their names if multiple. nbComponentsTest: int = len( listValues ) - nbComponentsCreated: int = createdAttribute.GetNumberOfComponents() + nbComponentsCreated: int = attributeCreated.GetNumberOfComponents() assert nbComponentsCreated == nbComponentsTest if nbComponentsTest > 1: componentNamesCreated: tuple[ str, ...] = tuple( - createdAttribute.GetComponentName( i ) for i in range( nbComponentsCreated ) ) + attributeCreated.GetComponentName( i ) for i in range( nbComponentsCreated ) ) assert componentNamesCreated, componentNamesTest # Test values and their types. @@ -294,18 +274,19 @@ def test_createConstantAttributeDataSet( else: npArrayTest = np.array( [ listValues[ 0 ] for _ in range( nbElements ) ] ) - npArrayCreated: npt.NDArray[ Any ] = vnp.vtk_to_numpy( createdAttribute ) - assert ( npArrayCreated == npArrayTest ).all() + npArrayCreated: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeCreated ) assert npArrayCreated.dtype == npArrayTest.dtype + assert ( npArrayCreated == npArrayTest ).all() - vtkDataTypeCreated: int = createdAttribute.GetDataType() + vtkDataTypeCreated: int = attributeCreated.GetDataType() assert vtkDataTypeCreated == vtkDataTypeTest @pytest.mark.parametrize( "componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, valueType, attributeName", [ - # Test attribute names. - ## Test with an attributeName already existing on cells data. - ( (), (), True, VTK_FLOAT, VTK_FLOAT, "float32", "PORO" ), + # Test attribute names. + ## Test with an attributeName already existing on opposit piece. + ( (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float64", "CellAttribute" ), + ( (), (), False, VTK_DOUBLE, VTK_DOUBLE, "float64", "PointAttribute" ), ## Test with a new attributeName on cells and on points. ( (), (), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), ( (), (), False, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), @@ -353,133 +334,126 @@ def test_createAttribute( attributeName: str, ) -> None: """Test creation of dataset in dataset from given array.""" - # Get the dataSet from a vtu. dataSet: vtkDataSet = dataSetTest( "dataset" ) # Get a array with random values of a given type. + nbElements: int = dataSet.GetNumberOfPoints() if onPoints else dataSet.GetNumberOfCells() nbComponentsTest: int = 1 if len( componentNamesTest ) == 0 else len( componentNamesTest ) - nbElementsTest: int = dataSet.GetNumberOfPoints() if onPoints else dataSet.GetNumberOfCells() - npArrayTest: npt.NDArray[ Any ] = getArrayWithSpeTypeValue( nbComponentsTest, nbElementsTest, valueType ) + npArrayTest: npt.NDArray[ Any ] = getArrayWithSpeTypeValue( nbComponentsTest, nbElements, valueType ) # Create the new attribute in the dataSet. assert arrayModifiers.createAttribute( dataSet, npArrayTest, attributeName, componentNames, onPoints, vtkDataType ) - # Get the new attribute to check its properties. + # Get the created attribute. data: Union[ vtkPointData, vtkCellData ] data = dataSet.GetPointData() if onPoints else dataSet.GetCellData() - createdAttribute: vtkDataArray = data.GetArray( attributeName ) + attributeCreated: vtkDataArray = data.GetArray( attributeName ) # Test the number of components and their names if multiple. - nbComponentsCreated: int = createdAttribute.GetNumberOfComponents() + nbComponentsCreated: int = attributeCreated.GetNumberOfComponents() assert nbComponentsCreated == nbComponentsTest if nbComponentsTest > 1: componentsNamesCreated: tuple[ str, ...] = tuple( - createdAttribute.GetComponentName( i ) for i in range( nbComponentsCreated ) ) + attributeCreated.GetComponentName( i ) for i in range( nbComponentsCreated ) ) assert componentsNamesCreated == componentNamesTest # Test values and their types. - npArrayCreated: npt.NDArray[ Any ] = vnp.vtk_to_numpy( createdAttribute ) - assert ( npArrayCreated == npArrayTest ).all() + npArrayCreated: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeCreated ) assert npArrayCreated.dtype == npArrayTest.dtype + assert ( npArrayCreated == npArrayTest ).all() - vtkDataTypeCreated: int = createdAttribute.GetDataType() + vtkDataTypeCreated: int = attributeCreated.GetDataType() assert vtkDataTypeCreated == vtkDataTypeTest -@pytest.mark.parametrize( "attributeNameFrom, attributeNameTo, onPoints, idBlock", [ - ( "PORO", "POROTo", False, 0 ), - ( "CellAttribute", "CellAttributeTo", False, 0 ), - ( "FAULT", "FAULTTo", False, 0 ), - ( "PointAttribute", "PointAttributeTo", True, 0 ), - ( "collocated_nodes", "collocated_nodesTo", True, 1 ), +@pytest.mark.parametrize( "attributeNameFrom, attributeNameTo, onPoints", [ + # Test with global attibutes. + ( "GLOBAL_IDS_POINTS", "GLOBAL_IDS_POINTS_To", True ), + ( "GLOBAL_IDS_CELLS", 'GLOBAL_IDS_CELLS_To', False ), + # Test with partial attribute. + ( "CellAttribute", "CellAttributeTo", False ), + ( "PointAttribute", "PointAttributeTo", True ), ] ) -def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet, attributeNameFrom: str, attributeNameTo: str, onPoints: bool, - idBlock: int ) -> None: +def test_copyAttribute( + dataSetTest: vtkMultiBlockDataSet, + attributeNameFrom: str, + attributeNameTo: str, + onPoints: bool, +) -> None: """Test copy of cell attribute from one multiblock to another.""" - objectFrom: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - objectTo: vtkMultiBlockDataSet = dataSetTest( "emptymultiblock" ) - - arrayModifiers.copyAttribute( objectFrom, objectTo, attributeNameFrom, attributeNameTo, onPoints ) - - blockFrom: vtkDataSet = cast( vtkDataSet, objectFrom.GetBlock( idBlock ) ) - blockTo: vtkDataSet = cast( vtkDataSet, objectTo.GetBlock( idBlock ) ) - - dataFrom: Union[ vtkPointData, vtkCellData ] - dataTo: Union[ vtkPointData, vtkCellData ] - if onPoints: - dataFrom = blockFrom.GetPointData() - dataTo = blockTo.GetPointData() - else: - dataFrom = blockFrom.GetCellData() - dataTo = blockTo.GetCellData() - - attributeFrom: vtkDataArray = dataFrom.GetArray( attributeNameFrom ) - attributeTo: vtkDataArray = dataTo.GetArray( attributeNameTo ) - - nbComponentsFrom: int = attributeFrom.GetNumberOfComponents() - nbComponentsTo: int = attributeTo.GetNumberOfComponents() - assert nbComponentsFrom == nbComponentsTo - - if nbComponentsFrom > 1: - componentsNamesFrom: tuple[ str, ...] = tuple( - attributeFrom.GetComponentName( i ) for i in range( nbComponentsFrom ) ) - componentsNamesTo: tuple[ str, - ...] = tuple( attributeTo.GetComponentName( i ) for i in range( nbComponentsTo ) ) - assert componentsNamesFrom == componentsNamesTo - - npArrayFrom: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeFrom ) - npArrayTo: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeTo ) - assert ( npArrayFrom == npArrayTo ).all() - assert npArrayFrom.dtype == npArrayTo.dtype - - vtkDataTypeFrom: int = attributeFrom.GetDataType() - vtkDataTypeTo: int = attributeTo.GetDataType() - assert vtkDataTypeFrom == vtkDataTypeTo + multiBlockDataSetFrom: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + multiBlockDataSetTo: vtkMultiBlockDataSet = dataSetTest( "emptymultiblock" ) + + # Copy the attribute from the multiBlockDataSetFrom to the multiBlockDataSetTo. + assert arrayModifiers.copyAttribute( multiBlockDataSetFrom, multiBlockDataSetTo, attributeNameFrom, attributeNameTo, onPoints ) + + # Parse the two multiBlockDataSet and test if the attribute has been copied. + nbBlocks: int = multiBlockDataSetFrom.GetNumberOfBlocks() + for idBlock in range( nbBlocks ): + dataSetFrom: vtkDataSet = cast( vtkDataSet, multiBlockDataSetFrom.GetBlock( idBlock ) ) + dataSetTo: vtkDataSet = cast( vtkDataSet, multiBlockDataSetTo.GetBlock( idBlock ) ) + dataFrom: Union[ vtkPointData, vtkCellData ] + dataTo: Union[ vtkPointData, vtkCellData ] + if onPoints: + dataFrom = dataSetFrom.GetPointData() + dataTo = dataSetTo.GetPointData() + else: + dataFrom = dataSetFrom.GetCellData() + dataTo = dataSetTo.GetCellData() + attributeExistTest: int = dataFrom.HasArray( attributeNameFrom ) + attributeExistCopied: int = dataTo.HasArray( attributeNameTo ) + assert attributeExistCopied == attributeExistTest @pytest.mark.parametrize( "attributeNameFrom, attributeNameTo, onPoints", [ ( "CellAttribute", "CellAttributeTo", False ), ( "PointAttribute", "PointAttributeTo", True ), ] ) -def test_copyAttributeDataSet( dataSetTest: vtkDataSet, attributeNameFrom: str, attributeNameTo: str, - onPoints: bool ) -> None: +def test_copyAttributeDataSet( + dataSetTest: vtkDataSet, + attributeNameFrom: str, + attributeNameTo: str, + onPoints: bool, +) -> None: """Test copy of an attribute from one dataset to another.""" - objectFrom: vtkDataSet = dataSetTest( "dataset" ) - objectTo: vtkDataSet = dataSetTest( "emptydataset" ) + dataSetFrom: vtkMultiBlockDataSet = dataSetTest( "dataset" ) + dataSetTo: vtkMultiBlockDataSet = dataSetTest( "emptydataset" ) - arrayModifiers.copyAttributeDataSet( objectFrom, objectTo, attributeNameFrom, attributeNameTo, onPoints ) + # Copy the attribute from the dataSetFrom to the dataSetTo. + assert arrayModifiers.copyAttributeDataSet( dataSetFrom, dataSetTo, attributeNameFrom, attributeNameTo, onPoints ) + # Get the tested attribute and its copy. dataFrom: Union[ vtkPointData, vtkCellData ] dataTo: Union[ vtkPointData, vtkCellData ] if onPoints: - dataFrom = objectFrom.GetPointData() - dataTo = objectTo.GetPointData() + dataFrom = dataSetFrom.GetPointData() + dataTo = dataSetTo.GetPointData() else: - dataFrom = objectFrom.GetCellData() - dataTo = objectTo.GetCellData() - - attributeFrom: vtkDataArray = dataFrom.GetArray( attributeNameFrom ) - attributeTo: vtkDataArray = dataTo.GetArray( attributeNameTo ) - - nbComponentsFrom: int = attributeFrom.GetNumberOfComponents() - nbComponentsTo: int = attributeTo.GetNumberOfComponents() - assert nbComponentsFrom == nbComponentsTo - - if nbComponentsFrom > 1: - componentsNamesFrom: tuple[ str, ...] = tuple( - attributeFrom.GetComponentName( i ) for i in range( nbComponentsFrom ) ) - componentsNamesTo: tuple[ str, - ...] = tuple( attributeTo.GetComponentName( i ) for i in range( nbComponentsTo ) ) - assert componentsNamesFrom == componentsNamesTo - - vtkDataTypeFrom: int = attributeFrom.GetDataType() - vtkDataTypeTo: int = attributeTo.GetDataType() - assert vtkDataTypeFrom == vtkDataTypeTo - - npArrayFrom: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeFrom ) - npArrayTo: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeTo ) - assert ( npArrayFrom == npArrayTo ).all() - assert npArrayFrom.dtype == npArrayTo.dtype + dataFrom = dataSetFrom.GetCellData() + dataTo = dataSetTo.GetCellData() + attributeTest: vtkDataArray = dataFrom.GetArray( attributeNameFrom ) + attributeCopied: vtkDataArray = dataTo.GetArray( attributeNameTo ) + + # Test the number of components and their names if multiple. + nbComponentsTest: int = attributeTest.GetNumberOfComponents() + nbComponentsCopied: int = attributeCopied.GetNumberOfComponents() + assert nbComponentsCopied == nbComponentsTest + if nbComponentsTest > 1: + componentsNamesTest: tuple[ str, ... ] = tuple( + attributeTest.GetComponentName( i ) for i in range( nbComponentsTest ) ) + componentsNamesCopied: tuple[ str, ... ] = tuple( + attributeCopied.GetComponentName( i ) for i in range( nbComponentsCopied ) ) + assert componentsNamesCopied == componentsNamesTest + + # Test values and their types. + npArrayTest: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeTest ) + npArrayCopied: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeCopied ) + assert npArrayCopied.dtype == npArrayTest.dtype + assert ( npArrayCopied == npArrayTest ).all() + + vtkDataTypeTest: int = attributeTest.GetDataType() + vtkDataTypeCopied: int = attributeCopied.GetDataType() + assert vtkDataTypeCopied == vtkDataTypeTest @pytest.mark.parametrize( "attributeName, onPoints", [ From 0e2ded2570f67cd30700546ef97f5f435452deec Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 22 Jul 2025 15:35:49 +0200 Subject: [PATCH 20/56] clean the code and add a funtion to test if an attribute is partial. --- geos-mesh/src/geos/mesh/utils/arrayHelpers.py | 222 +++++++++--------- geos-mesh/tests/test_arrayHelpers.py | 16 +- 2 files changed, 122 insertions(+), 116 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index d466ef62..abd5cd42 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. -# SPDX-FileContributor: Martin Lemay, Paloma Martinez +# SPDX-FileContributor: Martin Lemay, Paloma Martinez, Romain Baville from copy import deepcopy import logging import numpy as np @@ -57,7 +57,7 @@ def getFieldType( data: vtkFieldData ) -> str: - vtkPointData (inheritance of vtkFieldData) Args: - data (vtkFieldData): vtk field data + data (vtkFieldData): Vtk field data. Returns: str: "vtkFieldData", "vtkCellData" or "vtkPointData" @@ -76,10 +76,10 @@ def getArrayNames( data: vtkFieldData ) -> list[ str ]: """Get the names of all arrays stored in a "vtkFieldData", "vtkCellData" or "vtkPointData". Args: - data (vtkFieldData): vtk field data + data (vtkFieldData): Vtk field data. Returns: - list[ str ]: The array names in the order that they are stored in the field data. + list[str]: The array names in the order that they are stored in the field data. """ if not data.IsA( "vtkFieldData" ): raise ValueError( f"data '{data}' entered is not a vtkFieldData object." ) @@ -90,9 +90,8 @@ def getArrayByName( data: vtkFieldData, name: str ) -> Optional[ vtkDataArray ]: """Get the vtkDataArray corresponding to the given name. Args: - data (vtkFieldData): vtk field data - name (str): array name - + data (vtkFieldData): Vtk field data. + name (str): Array name. Returns: Optional[ vtkDataArray ]: The vtkDataArray associated with the name given. None if not found. @@ -107,9 +106,8 @@ def getCopyArrayByName( data: vtkFieldData, name: str ) -> Optional[ vtkDataArra """Get the copy of a vtkDataArray corresponding to the given name. Args: - data (vtkFieldData): vtk field data - name (str): array name - + data (vtkFieldData): Vtk field data. + name (str): Array name. Returns: Optional[ vtkDataArray ]: The copy of the vtkDataArray associated with the name given. None if not found. @@ -126,7 +124,6 @@ def getNumpyGlobalIdsArray( data: Union[ vtkCellData, vtkPointData ] ) -> Option Args: data (Union[ vtkCellData, vtkPointData ]): Cell or point array. - Returns: Optional[ npt.NDArray[ np.int64 ] ]: The numpy array of GlobalIds. """ @@ -144,12 +141,12 @@ def getNumpyArrayByName( data: vtkCellData | vtkPointData, name: str, sorted: bo no reordering will be perform. Args: - data (vtkCellData | vtkPointData): vtk field data. - name (str): Array name to sort + data (vtkCellData | vtkPointData): Vtk field data. + name (str): Array name to sort. sorted (bool, optional): Sort the output array with the help of GlobalIds. Defaults to False. Returns: - Optional[ npt.NDArray ]: Sorted array + Optional[ npt.NDArray ]: Sorted array. """ dataArray: Optional[ vtkDataArray ] = getArrayByName( data, name ) if dataArray is not None: @@ -164,12 +161,11 @@ def getAttributeSet( object: Union[ vtkMultiBlockDataSet, vtkDataSet ], onPoints """Get the set of all attributes from an object on points or on cells. Args: - object (Any): object where to find the attributes. - onPoints (bool): True if attributes are on points, False if they are on - cells. + object (Any): Object where to find the attributes. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - set[str]: set of attribute names present in input object. + set[str]: Set of attribute names present in input object. """ attributes: dict[ str, int ] if isinstance( object, vtkMultiBlockDataSet ): @@ -191,14 +187,11 @@ def getAttributesWithNumberOfComponents( """Get the dictionnary of all attributes from object on points or cells. Args: - object (Any): object where to find the attributes. - onPoints (bool): True if attributes are on points, False if they are on - cells. + object (Any): Object where to find the attributes. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - dict[str, int]: dictionnary where keys are the names of the attributes - and values the number of components. - + dict[str, int]: Dictionnary where keys are the names of the attributes and values the number of components. """ attributes: dict[ str, int ] if isinstance( object, ( vtkMultiBlockDataSet, vtkCompositeDataSet ) ): @@ -215,15 +208,11 @@ def getAttributesFromMultiBlockDataSet( object: Union[ vtkMultiBlockDataSet, vtk """Get the dictionnary of all attributes of object on points or on cells. Args: - object (vtkMultiBlockDataSet | vtkCompositeDataSet): object where to find - the attributes. - onPoints (bool): True if attributes are on points, False if they are - on cells. + object (vtkMultiBlockDataSet | vtkCompositeDataSet): Object where to find the attributes. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - dict[str, int]: Dictionnary of the names of the attributes as keys, and - number of components as values. - + dict[str, int]: Dictionnary of the names of the attributes as keys, and number of components as values. """ attributes: dict[ str, int ] = {} # initialize data object tree iterator @@ -246,12 +235,11 @@ def getAttributesFromDataSet( object: vtkDataSet, onPoints: bool ) -> dict[ str, """Get the dictionnary of all attributes of a vtkDataSet on points or cells. Args: - object (vtkDataSet): object where to find the attributes. - onPoints (bool): True if attributes are on points, False if they are - on cells. + object (vtkDataSet): Object where to find the attributes. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - dict[str, int]: list of the names of the attributes. + dict[str, int]: List of the names of the attributes. """ attributes: dict[ str, int ] = {} data: Union[ vtkPointData, vtkCellData ] @@ -279,13 +267,12 @@ def isAttributeInObject( object: Union[ vtkMultiBlockDataSet, vtkDataSet ], attr """Check if an attribute is in the input object. Args: - object (vtkMultiBlockDataSet | vtkDataSet): input object - attributeName (str): name of the attribute - onPoints (bool): True if attributes are on points, False if they are - on cells. + object (vtkMultiBlockDataSet | vtkDataSet): Input object. + attributeName (str): Name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - bool: True if the attribute is in the table, False otherwise + bool: True if the attribute is in the table, False otherwise. """ if isinstance( object, vtkMultiBlockDataSet ): return isAttributeInObjectMultiBlockDataSet( object, attributeName, onPoints ) @@ -299,13 +286,12 @@ def isAttributeInObjectMultiBlockDataSet( object: vtkMultiBlockDataSet, attribut """Check if an attribute is in the input object. Args: - object (vtkMultiBlockDataSet): input multiblock object - attributeName (str): name of the attribute - onPoints (bool): True if attributes are on points, False if they are - on cells. + object (vtkMultiBlockDataSet): Input multiBlockDataSet. + attributeName (str): Name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - bool: True if the attribute is in the table, False otherwise + bool: True if the attribute is in the table, False otherwise. """ iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() iter.SetDataSet( object ) @@ -323,13 +309,12 @@ def isAttributeInObjectDataSet( object: vtkDataSet, attributeName: str, onPoints """Check if an attribute is in the input object. Args: - object (vtkDataSet): input object - attributeName (str): name of the attribute - onPoints (bool): True if attributes are on points, False if they are - on cells. + object (vtkDataSet): Input object. + attributeName (str): Name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - bool: True if the attribute is in the table, False otherwise + bool: True if the attribute is in the table, False otherwise. """ data: Union[ vtkPointData, vtkCellData ] sup: str = "" @@ -343,21 +328,42 @@ def isAttributeInObjectDataSet( object: vtkDataSet, attributeName: str, onPoints return bool( data.HasArray( attributeName ) ) +def isAttributeGlobal( object: vtkMultiBlockDataSet, attributeName: str, onPoints: bool ) -> bool: + """Check if an attribute is global in the input multiBlockDataSet. + + Args: + object (vtkMultiBlockDataSet): Input object. + attributeName (str): Name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. + + Returns: + bool: True if the attribute is global, False if not. + """ + isOnBlock: bool + nbBlock: int = object.GetNumberOfBlocks() + for idBlock in range( nbBlock ): + block: vtkDataSet = object.GetBlock( idBlock ) + isOnBlock = isAttributeInObjectDataSet( block, attributeName, onPoints ) + if not isOnBlock: + return False + + return True + + def getArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> npt.NDArray[ Any ]: """Return the numpy array corresponding to input attribute name in table. Args: - object (PointSet or UnstructuredGrid): input object - attributeName (str): name of the attribute - onPoints (bool): True if attributes are on points, False if they are - on cells. + object (PointSet or UnstructuredGrid): Input object. + attributeName (str): Name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - ArrayLike[float]: the array corresponding to input attribute name. + ArrayLike[Any]: The numpy array corresponding to input attribute name. """ - array: vtkDataArray = getVtkArrayInObject( object, attributeName, onPoints ) - nparray: npt.NDArray[ Any ] = vnp.vtk_to_numpy( array ) # type: ignore[no-untyped-call] - return nparray + vtkArray: vtkDataArray = getVtkArrayInObject( object, attributeName, onPoints ) + npArray: npt.NDArray[ Any ] = vnp.vtk_to_numpy( vtkArray ) # type: ignore[no-untyped-call] + return npArray def getVtkArrayTypeInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> int: @@ -369,7 +375,7 @@ def getVtkArrayTypeInObject( object: vtkDataSet, attributeName: str, onPoints: b onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - int: the type of the vtk array corresponding to input attribute name. + int: The type of the vtk array corresponding to input attribute name. """ array: vtkDataArray = getVtkArrayInObject( object, attributeName, onPoints ) vtkArrayType: int = array.GetDataType() @@ -402,13 +408,12 @@ def getVtkArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool """Return the array corresponding to input attribute name in table. Args: - object (PointSet or UnstructuredGrid): input object - attributeName (str): name of the attribute - onPoints (bool): True if attributes are on points, False if they are - on cells. + object (PointSet or UnstructuredGrid): Input object. + attributeName (str): Name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - vtkDataArray: the vtk array corresponding to input attribute name. + vtkDataArray: The vtk array corresponding to input attribute name. """ assert isAttributeInObject( object, attributeName, onPoints ), f"{attributeName} is not in input object." return object.GetPointData().GetArray( attributeName ) if onPoints else object.GetCellData().GetArray( @@ -423,14 +428,12 @@ def getNumberOfComponents( """Get the number of components of attribute attributeName in dataSet. Args: - dataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataSet): - dataSet where the attribute is. - attributeName (str): name of the attribute - onPoints (bool): True if attributes are on points, False if they are - on cells. + dataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataSet): DataSet where the attribute is. + attributeName (str): Name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - int: number of components. + int: Number of components. """ if isinstance( dataSet, vtkDataSet ): return getNumberOfComponentsDataSet( dataSet, attributeName, onPoints ) @@ -444,13 +447,12 @@ def getNumberOfComponentsDataSet( dataSet: vtkDataSet, attributeName: str, onPoi """Get the number of components of attribute attributeName in dataSet. Args: - dataSet (vtkDataSet): dataSet where the attribute is. - attributeName (str): name of the attribute - onPoints (bool): True if attributes are on points, False if they are - on cells. + dataSet (vtkDataSet): DataSet where the attribute is. + attributeName (str): Name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - int: number of components. + int: Number of components. """ array: vtkDataArray = getVtkArrayInObject( dataSet, attributeName, onPoints ) return array.GetNumberOfComponents() @@ -465,12 +467,11 @@ def getNumberOfComponentsMultiBlock( Args: dataSet (vtkMultiBlockDataSet | vtkCompositeDataSet): multi block data Set where the attribute is. - attributeName (str): name of the attribute - onPoints (bool): True if attributes are on points, False if they are - on cells. + attributeName (str): Name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - int: number of components. + int: Number of components. """ elementaryBlockIndexes: list[ int ] = getBlockElementIndexesFlatten( dataSet ) for blockIndex in elementaryBlockIndexes: @@ -489,15 +490,12 @@ def getComponentNames( """Get the name of the components of attribute attributeName in dataSet. Args: - dataSet (vtkDataSet | vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): dataSet - where the attribute is. - attributeName (str): name of the attribute - onPoints (bool): True if attributes are on points, False if they are - on cells. + dataSet (vtkDataSet | vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): DataSet where the attribute is. + attributeName (str): Name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - tuple[str,...]: names of the components. - + tuple[str,...]: Names of the components. """ if isinstance( dataSet, vtkDataSet ): return getComponentNamesDataSet( dataSet, attributeName, onPoints ) @@ -511,14 +509,12 @@ def getComponentNamesDataSet( dataSet: vtkDataSet, attributeName: str, onPoints: """Get the name of the components of attribute attributeName in dataSet. Args: - dataSet (vtkDataSet): dataSet where the attribute is. - attributeName (str): name of the attribute - onPoints (bool): True if attributes are on points, False if they are - on cells. + dataSet (vtkDataSet): DataSet where the attribute is. + attributeName (str): Name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - tuple[str,...]: names of the components. - + tuple[str,...]: Names of the components. """ array: vtkDataArray = getVtkArrayInObject( dataSet, attributeName, onPoints ) componentNames: list[ str ] = [] @@ -536,14 +532,12 @@ def getComponentNamesMultiBlock( """Get the name of the components of attribute in MultiBlockDataSet. Args: - dataSet (vtkMultiBlockDataSet | vtkCompositeDataSet): dataSet where the - attribute is. - attributeName (str): name of the attribute - onPoints (bool): True if attributes are on points, False if they are - on cells. + dataSet (vtkMultiBlockDataSet | vtkCompositeDataSet): DataSet where the attribute is. + attributeName (str): Name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - tuple[str,...]: names of the components. + tuple[str,...]: Names of the components. """ elementaryBlockIndexes: list[ int ] = getBlockElementIndexesFlatten( dataSet ) for blockIndex in elementaryBlockIndexes: @@ -557,8 +551,8 @@ def getAttributeValuesAsDF( surface: vtkPolyData, attributeNames: tuple[ str, .. """Get attribute values from input surface. Args: - surface (vtkPolyData): mesh where to get attribute values - attributeNames (tuple[str,...]): tuple of attribute names to get the values. + surface (vtkPolyData): Mesh where to get attribute values. + attributeNames (tuple[str,...]): Tuple of attribute names to get the values. Returns: pd.DataFrame: DataFrame containing property names as columns. @@ -585,8 +579,8 @@ def AsDF( surface: vtkPolyData, attributeNames: tuple[ str, ...] ) -> pd.DataFra """Get attribute values from input surface. Args: - surface (vtkPolyData): mesh where to get attribute values - attributeNames (tuple[str,...]): tuple of attribute names to get the values. + surface (vtkPolyData): Mesh where to get attribute values. + attributeNames (tuple[str,...]): Tuple of attribute names to get the values. Returns: pd.DataFrame: DataFrame containing property names as columns. @@ -615,11 +609,11 @@ def getBounds( """Get bounds of either single of composite data set. Args: - input (Union[vtkUnstructuredGrid, vtkMultiBlockDataSet]): input mesh + input (Union[vtkUnstructuredGrid, vtkMultiBlockDataSet]): Input mesh. Returns: - tuple[float, float, float, float, float, float]: tuple containing - bounds (xmin, xmax, ymin, ymax, zmin, zmax) + tuple[float, float, float, float, float, float]: Tuple containing + bounds (xmin, xmax, ymin, ymax, zmin, zmax). """ if isinstance( input, vtkMultiBlockDataSet ): @@ -632,11 +626,11 @@ def getMonoBlockBounds( input: vtkUnstructuredGrid, ) -> tuple[ float, float, fl """Get boundary box extrema coordinates for a vtkUnstructuredGrid. Args: - input (vtkMultiBlockDataSet): input single block mesh + input (vtkMultiBlockDataSet): Input single block mesh. Returns: - tuple[float, float, float, float, float, float]: tuple containing - bounds (xmin, xmax, ymin, ymax, zmin, zmax) + tuple[float, float, float, float, float, float]: Tuple containing + bounds (xmin, xmax, ymin, ymax, zmin, zmax). """ return input.GetBounds() @@ -646,10 +640,10 @@ def getMultiBlockBounds( input: vtkMultiBlockDataSet, ) -> tuple[ float, float, """Get boundary box extrema coordinates for a vtkMultiBlockDataSet. Args: - input (vtkMultiBlockDataSet): input multiblock mesh + input (vtkMultiBlockDataSet): Input multiblock mesh. Returns: - tuple[float, float, float, float, float, float]: bounds. + tuple[float, float, float, float, float, float]: Bounds. """ xmin, ymin, zmin = 3 * [ np.inf ] @@ -673,10 +667,10 @@ def computeCellCenterCoordinates( mesh: vtkDataSet ) -> vtkDataArray: """Get the coordinates of Cell center. Args: - mesh (vtkDataSet): input surface + mesh (vtkDataSet): Input surface. Returns: - vtkPoints: cell center coordinates + vtkPoints: Cell center coordinates. """ assert mesh is not None, "Surface is undefined." filter: vtkCellCenters = vtkCellCenters() @@ -693,8 +687,8 @@ def sortArrayByGlobalIds( data: Union[ vtkCellData, vtkPointData ], arr: npt.NDA """Sort an array following global Ids. Args: - data (vtkFieldData): Global Ids array - arr (npt.NDArray[ np.float64 ]): Array to sort + data (vtkFieldData): Global Ids array. + arr (npt.NDArray[ np.float64 ]): Array to sort. """ globalids: Optional[ npt.NDArray[ np.int64 ] ] = getNumpyGlobalIdsArray( data ) if globalids is not None: diff --git a/geos-mesh/tests/test_arrayHelpers.py b/geos-mesh/tests/test_arrayHelpers.py index d3d411d7..13d3fdf0 100644 --- a/geos-mesh/tests/test_arrayHelpers.py +++ b/geos-mesh/tests/test_arrayHelpers.py @@ -80,6 +80,20 @@ def test_isAttributeInObjectDataSet( dataSetTest: vtkDataSet, attributeName: str obtained: bool = arrayHelpers.isAttributeInObjectDataSet( vtkDataset, attributeName, onpoints ) assert obtained == expected +@pytest.mark.parametrize( "attributeName, onpoints, expected", [ + ( "PORO", False, False ), + ( "GLOBAL_IDS_POINTS", True, True ), +] ) +def test_isAttributeGlobal( + dataSetTest: vtkMultiBlockDataSet, + attributeName: str, onpoints: bool, + expected: bool, +) -> None: + """Test if the attribute is global or partial.""" + multiBlockDataset: vtkMultiBlockDataSet = dataSetTest( "multiBlock" ) + obtained: bool = arrayHelpers.isAttributeGlobal( multiBlockDataset, attributeName, onpoints ) + assert obtained == expected + @pytest.mark.parametrize( "arrayExpected, onpoints", [ ( "PORO", False ), @@ -104,8 +118,6 @@ def test_getArrayInObject( request: pytest.FixtureRequest, arrayExpected: npt.ND ( "CellAttribute", 11, False ), ( "PointAttribute", 11, True ), ( "collocated_nodes", 12, True ), - ( "collocated_nodes", -1, False ), - ( "newAttribute", -1, False ), ] ) def test_getVtkArrayTypeInMultiBlock( dataSetTest: vtkMultiBlockDataSet, attributeName: str, vtkDataType: int, onPoints: bool ) -> None: From 68d6c3c4520ad94674e6f85ccacce79ca72c32db Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 22 Jul 2025 15:39:11 +0200 Subject: [PATCH 21/56] fix the test of isAttributeGlobal --- geos-mesh/tests/test_arrayHelpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geos-mesh/tests/test_arrayHelpers.py b/geos-mesh/tests/test_arrayHelpers.py index 13d3fdf0..ebde5231 100644 --- a/geos-mesh/tests/test_arrayHelpers.py +++ b/geos-mesh/tests/test_arrayHelpers.py @@ -90,7 +90,7 @@ def test_isAttributeGlobal( expected: bool, ) -> None: """Test if the attribute is global or partial.""" - multiBlockDataset: vtkMultiBlockDataSet = dataSetTest( "multiBlock" ) + multiBlockDataset: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) obtained: bool = arrayHelpers.isAttributeGlobal( multiBlockDataset, attributeName, onpoints ) assert obtained == expected From 57c9bd2cf56b5475cf6fcb396a76921a7b129396 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 22 Jul 2025 15:51:32 +0200 Subject: [PATCH 22/56] Clean the code --- geos-mesh/tests/conftest.py | 61 +++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/geos-mesh/tests/conftest.py b/geos-mesh/tests/conftest.py index 3e26dced..2e5606a2 100644 --- a/geos-mesh/tests/conftest.py +++ b/geos-mesh/tests/conftest.py @@ -53,7 +53,22 @@ def _getarray( nb_component: int, nb_elements: int, valueType: str ) -> Any: Returns: npt.NDArray[Any]: random array of input type. """ - if valueType == "int32": + np.random.seed( 28 ) + if valueType == "int8": + if nb_component == 1: + return np.array( [ np.int8( 10 * np.random.random() ) for _ in range( nb_elements ) ] ) + else: + return np.array( [ [ np.int8( 10 * np.random.random() ) for _ in range( nb_component ) ] + for _ in range( nb_elements ) ] ) + + elif valueType == "int16": + if nb_component == 1: + return np.array( [ np.int16( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) + else: + return np.array( [ [ np.int16( 1000 * np.random.random() ) for _ in range( nb_component ) ] + for _ in range( nb_elements ) ] ) + + elif valueType == "int32": if nb_component == 1: return np.array( [ np.int32( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) else: @@ -67,6 +82,48 @@ def _getarray( nb_component: int, nb_elements: int, valueType: str ) -> Any: return np.array( [ [ np.int64( 1000 * np.random.random() ) for _ in range( nb_component ) ] for _ in range( nb_elements ) ] ) + if valueType == "uint8": + if nb_component == 1: + return np.array( [ np.uint8( 10 * np.random.random() ) for _ in range( nb_elements ) ] ) + else: + return np.array( [ [ np.uint8( 10 * np.random.random() ) for _ in range( nb_component ) ] + for _ in range( nb_elements ) ] ) + + elif valueType == "uint16": + if nb_component == 1: + return np.array( [ np.uint16( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) + else: + return np.array( [ [ np.uint16( 1000 * np.random.random() ) for _ in range( nb_component ) ] + for _ in range( nb_elements ) ] ) + + elif valueType == "uint32": + if nb_component == 1: + return np.array( [ np.uint32( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) + else: + return np.array( [ [ np.uint32( 1000 * np.random.random() ) for _ in range( nb_component ) ] + for _ in range( nb_elements ) ] ) + + elif valueType == "uint64": + if nb_component == 1: + return np.array( [ np.uint64( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) + else: + return np.array( [ [ np.uint64( 1000 * np.random.random() ) for _ in range( nb_component ) ] + for _ in range( nb_elements ) ] ) + + elif valueType == "int": + if nb_component == 1: + return np.array( [ int( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) + else: + return np.array( [ [ int( 1000 * np.random.random() ) for _ in range( nb_component ) ] + for _ in range( nb_elements ) ] ) + + elif valueType == "float": + if nb_component == 1: + return np.array( [ float( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) + else: + return np.array( [ [ float( 1000 * np.random.random() ) for _ in range( nb_component ) ] + for _ in range( nb_elements ) ] ) + elif valueType == "float32": if nb_component == 1: return np.array( [ np.float32( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) @@ -74,7 +131,7 @@ def _getarray( nb_component: int, nb_elements: int, valueType: str ) -> Any: return np.array( [ [ np.float32( 1000 * np.random.random() ) for _ in range( nb_component ) ] for _ in range( nb_elements ) ] ) - else: + elif valueType == "float64": if nb_component == 1: return np.array( [ np.float64( 1000 * np.random.random() ) for _ in range( nb_elements ) ] ) else: From b4ff24e30f8ebcae6d9b579ceed036b6de4f0efc Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 23 Jul 2025 10:25:41 +0200 Subject: [PATCH 23/56] Clean for ci --- .../src/geos/mesh/utils/arrayModifiers.py | 324 ++++++++++-------- 1 file changed, 189 insertions(+), 135 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index 04f629f4..689319ee 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -9,8 +9,7 @@ from vtk import ( # type: ignore[import-untyped] VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_LONG, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG_LONG, - VTK_CHAR, VTK_SIGNED_CHAR, VTK_SHORT, VTK_LONG, VTK_INT, VTK_LONG_LONG, VTK_ID_TYPE, - VTK_FLOAT, VTK_DOUBLE, + VTK_CHAR, VTK_SIGNED_CHAR, VTK_SHORT, VTK_LONG, VTK_INT, VTK_LONG_LONG, VTK_ID_TYPE, VTK_FLOAT, VTK_DOUBLE, ) from vtkmodules.vtkCommonDataModel import ( vtkMultiBlockDataSet, @@ -19,7 +18,7 @@ vtkCompositeDataSet, vtkDataObject, vtkDataObjectTreeIterator, - vtkPointData, + vtkPointData, vtkCellData, ) from vtkmodules.vtkFiltersCore import ( @@ -63,28 +62,35 @@ def fillPartialAttributes( attributeName: str, onPoints: bool = False, value: Any = np.nan, - logger: Logger = getLogger( "fillPartialAttributes", True ), + logger: Union[ Logger, None ] = None, ) -> bool: """Fill input partial attribute of multiBlockDataSet with the same value for all the components. Args: multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): MultiBlockDataSet where to fill the attribute. attributeName (str): Attribute name. - onPoints (bool, optional): Attribute is on Points (True) or on Cells (False). + onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. - value (any, optional): Filling value. It is better to use numpy scalar type for the values. - Defaults to -1 for int VTK arrays, 0 for uint VTK arrays and nan for float VTK arrays. - logger (Logger, optional): A logger to manage the output messages. - Defaults to an internal logger. + value (Any, optional): Filling value. It is better to use numpy scalar type for the values. + Defaults to: + -1 for int VTK arrays. + 0 for uint VTK arrays. + nan for float VTK arrays. + logger (Union[Logger, None], optional): A logger to manage the output messages. + Defaults to None, an internal logger is used. Returns: bool: True if the attribute was correctly created and filled, False if not. """ + # Check if an external logger is given. + if logger is None: + logger = getLogger( "fillPartialAttributes", True ) + # Check if the input mesh is inherited from vtkMultiBlockDataSet. if not isinstance( multiBlockDataSet, vtkMultiBlockDataSet ): - logger.error( f"Input mesh has to be inherited from vtkMultiBlockDataSet." ) + logger.error( "Input mesh has to be inherited from vtkMultiBlockDataSet." ) return False - + # Check if the attribute is partial. if isAttributeGlobal( multiBlockDataSet, attributeName, onPoints ): logger.error( f"The attribute { attributeName } is already global." ) @@ -100,22 +106,29 @@ def fillPartialAttributes( # Set the default value depending of the type of the attribute to fill if np.isnan( value ): - typeMapping: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() + typeMapping: dict[ int, type ] = vnp.get_vtk_to_numpy_typemap() valueType: type = typeMapping[ vtkDataType ] # Default value for float types is nan. if vtkDataType in ( VTK_FLOAT, VTK_DOUBLE ): value = valueType( value ) - logger.warning( f"{ attributeName } vtk data type is { vtkDataType } cooresponding to { value.dtype } numpy type, default value is automatically set to nan." ) + logger.warning( + f"{ attributeName } vtk data type is { vtkDataType } corresponding to { value.dtype } numpy type, default value is automatically set to nan." + ) # Default value for int types is -1. - elif vtkDataType in ( VTK_CHAR, VTK_SIGNED_CHAR, VTK_SHORT, VTK_LONG, VTK_INT, VTK_LONG_LONG, VTK_ID_TYPE ) : + elif vtkDataType in ( VTK_CHAR, VTK_SIGNED_CHAR, VTK_SHORT, VTK_LONG, VTK_INT, VTK_LONG_LONG, VTK_ID_TYPE ): value = valueType( -1 ) - logger.warning( f"{ attributeName } vtk data type is { vtkDataType } cooresponding to { value.dtype } numpy type, default value is automatically set to -1." ) + logger.warning( + f"{ attributeName } vtk data type is { vtkDataType } corresponding to { value.dtype } numpy type, default value is automatically set to -1." + ) # Default value for uint types is 0. - elif vtkDataType in ( VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_LONG, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG_LONG ): + elif vtkDataType in ( VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_LONG, VTK_UNSIGNED_INT, + VTK_UNSIGNED_LONG_LONG ): value = valueType( 0 ) - logger.warning( f"{ attributeName } vtk data type is { vtkDataType } cooresponding to { value.dtype } numpy type, default value is automatically set to 0." ) + logger.warning( + f"{ attributeName } vtk data type is { vtkDataType } corresponding to { value.dtype } numpy type, default value is automatically set to 0." + ) else: - logger.error( f"The type of the attribute { attributeName } is not compatible with the function.") + logger.error( f"The type of the attribute { attributeName } is not compatible with the function." ) return False values: list[ Any ] = [ value for _ in range( nbComponents ) ] @@ -127,10 +140,10 @@ def fillPartialAttributes( iter.GoToFirstItem() while iter.GetCurrentDataObject() is not None: dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) - if not isAttributeInObjectDataSet( dataSet, attributeName, onPoints ): - if not createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkDataType, logger ): - return False - + if not isAttributeInObjectDataSet( dataSet, attributeName, onPoints ) and \ + not createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkDataType, logger ): + return False + iter.GoToNextItem() return True @@ -138,30 +151,29 @@ def fillPartialAttributes( def fillAllPartialAttributes( multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], - logger: Logger = getLogger( "fillAllPartialAttributes", True ), + logger: Union[ Logger, None ] = None, ) -> bool: - """Fill all partial attributes of a multiBlockDataSet with the default value. - All components of each attributes are filled with the same value. - Depending of the type of the attribute, the default value is different: - - 0 for uint types (VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_LONG, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG_LONG). - - -1 for int types (VTK_CHAR, VTK_SIGNED_CHAR, VTK_SHORT, VTK_LONG, VTK_INT, VTK_LONG_LONG, VTK_ID_TYPE). - - nan for float types (VTK_FLOAT, VTK_DOUBLE). + """Fill all partial attributes of a multiBlockDataSet with the default value. All components of each attributes are filled with the same value. Depending of the type of the attribute, the default value is different 0, -1 and nan for respectively uint, int and float vtk type. Args: multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): MultiBlockDataSet where to fill attributes. - logger (Logger, optional): A logger to manage the output messages. - Defaults to an internal logger. + logger (Union[Logger, None], optional): A logger to manage the output messages. + Defaults to None, an internal logger is used. Returns: bool: True if attributes were correctly created and filled, False if not. - """ + """ + # Check if an external logger is given. + if logger is None: + logger = getLogger( "fillAllPartialAttributes", True ) + # Parse all partial attributes, onPoints and onCells to fill them. for onPoints in [ True, False ]: infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints ) for attributeName in infoAttributes: - if not isAttributeGlobal( multiBlockDataSet, attributeName, onPoints ): - if not fillPartialAttributes( multiBlockDataSet, attributeName, onPoints, logger=logger ): - return False + if not isAttributeGlobal( multiBlockDataSet, attributeName, onPoints ) and \ + not fillPartialAttributes( multiBlockDataSet, attributeName, onPoints, logger=logger ): + return False return True @@ -179,7 +191,7 @@ def createEmptyAttribute( vtkDataType (int): Data type. Returns: - bool: True if the attribute was correctly created. + vtkDataArray: The empty attribute. """ # Check if the vtk data type is correct. vtkNumpyTypeMap: dict[ int, type ] = vnp.get_vtk_to_numpy_typemap() @@ -203,41 +215,48 @@ def createConstantAttribute( attributeName: str, componentNames: tuple[ str, ...] = (), # noqa: C408 onPoints: bool = False, - vtkDataType: Union[ int, Any ] = None, - logger: Logger = getLogger( "createConstantAttribute", True ), + vtkDataType: Union[ int, None ] = None, + logger: Union[ Logger, None ] = None, ) -> bool: """Create a new attribute with a constant value in the object. Args: object (vtkDataObject): Object (vtkMultiBlockDataSet, vtkDataSet) where to create the attribute. - listValues (list[any]): List of values of the attribute for each components. It is better to use numpy scalar type for the values. + listValues (list[Any]): List of values of the attribute for each components. It is better to use numpy scalar type for the values. attributeName (str): Name of the attribute. componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. - vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. - If None the vtk data type is given by the type of the values. - Else, the values are converted to the corresponding numpy type. - Defaults to None. + vtkDataType (Union[int, None], optional): Vtk data type of the attribute to create. + Defaults to None, the vtk data type is given by the type of the values. + Warning with int8, uint8 and int64 type of value, the vtk data type corresponding are multiples. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG - logger (Logger, optional): A logger to manage the output messages. - Defaults to an internal logger. + logger (Union[Logger, None], optional): A logger to manage the output messages. + Defaults to None, an internal logger is used. Returns: bool: True if the attribute was correctly created, False if it was not created. - """ + """ + # Check if an external logger is given. + if logger is None: + logger = getLogger( "createConstantAttribute", True ) + + # Deals with multiBlocksDataSets. if isinstance( object, ( vtkMultiBlockDataSet, vtkCompositeDataSet ) ): - return createConstantAttributeMultiBlock( object, listValues, attributeName, componentNames, onPoints, vtkDataType, logger ) + return createConstantAttributeMultiBlock( object, listValues, attributeName, componentNames, onPoints, + vtkDataType, logger ) + # Deals with dataSets. elif isinstance( object, vtkDataSet ): - return createConstantAttributeDataSet( object, listValues, attributeName, componentNames, onPoints, vtkDataType, logger ) - + return createConstantAttributeDataSet( object, listValues, attributeName, componentNames, onPoints, vtkDataType, + logger ) + else: - logger.error( f"The mesh has to be inherited from a vtkMultiBlockDataSet or a vtkDataSet" ) + logger.error( "The mesh has to be inherited from a vtkMultiBlockDataSet or a vtkDataSet" ) logger.error( f"The constant attribute { attributeName } has not been created into the mesh." ) return False @@ -248,39 +267,42 @@ def createConstantAttributeMultiBlock( attributeName: str, componentNames: tuple[ str, ...] = (), # noqa: C408 onPoints: bool = False, - vtkDataType: Union[ int, Any ] = None, - logger: Logger = getLogger( "createConstantAttributeMultiBlock", True ), + vtkDataType: Union[ int, None ] = None, + logger: Union[ Logger, None ] = None, ) -> bool: """Create a new attribute with a constant value per component on every blocks of the multiBlockDataSet. Args: multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet): MultiBlockDataSet where to create the attribute. - listValues (list[any]): List of values of the attribute for each components. It is better to use numpy scalar type for the values. + listValues (list[Any]): List of values of the attribute for each components. It is better to use numpy scalar type for the values. attributeName (str): Name of the attribute. componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. - vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. - If None the vtk data type is given by the type of the values. - Else, values type have to correspond to the type of the vtk data, check https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/numpy_support.py for more information. - Defaults to None. + vtkDataType (Union[int, None], optional): Vtk data type of the attribute to create. + Defaults to None, the vtk data type is given by the type of the values. + Warning with int8, uint8 and int64 type of value, the vtk data type corresponding are multiples. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG - logger (Logger, optional): A logger to manage the output messages. - Defaults to an internal logger. + logger (Union[Logger, None], optional): A logger to manage the output messages. + Defaults to None, an internal logger is used. Returns: bool: True if the attribute was correctly created, False if it was not created. """ + # Check if an external logger is given. + if logger is None: + logger = getLogger( "createConstantAttributeMultiBlock", True ) + # Check if the input mesh is inherited from vtkMultiBlockDataSet. if not isinstance( multiBlockDataSet, vtkMultiBlockDataSet ): - logger.error( f"Input mesh has to be inherited from vtkMultiBlockDataSet." ) + logger.error( "Input mesh has to be inherited from vtkMultiBlockDataSet." ) logger.error( f"The constant attribute { attributeName } has not been created into the mesh." ) return False - + # Check if the attribute already exist in the input mesh. if isAttributeInObjectMultiBlockDataSet( multiBlockDataSet, attributeName, onPoints ): logger.error( f"The attribute { attributeName } is already present in the multiBlockDataSet." ) @@ -291,9 +313,12 @@ def createConstantAttributeMultiBlock( oppositePiece: bool = not onPoints oppositePieceName: str = "points" if oppositePiece else "cells" if isAttributeInObjectMultiBlockDataSet( multiBlockDataSet, attributeName, oppositePiece ): - oppositePieceState: str = "global" if isAttributeGlobal( multiBlockDataSet, attributeName, oppositePiece ) else "partial" - logger.warning( f"A { oppositePieceState } attribute with the same name ({ attributeName }) is already present in the multiBlockDataSet but on { oppositePieceName }." ) - + oppositePieceState: str = "global" if isAttributeGlobal( multiBlockDataSet, attributeName, + oppositePiece ) else "partial" + logger.warning( + f"A { oppositePieceState } attribute with the same name ({ attributeName }) is already present in the multiBlockDataSet but on { oppositePieceName }." + ) + # Parse the multiBlockDataSet to create the constant attribute on each blocks. iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() iter.SetDataSet( multiBlockDataSet ) @@ -301,9 +326,10 @@ def createConstantAttributeMultiBlock( iter.GoToFirstItem() while iter.GetCurrentDataObject() is not None: dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) - if not createConstantAttributeDataSet( dataSet, listValues, attributeName, componentNames, onPoints, vtkDataType, logger ): + if not createConstantAttributeDataSet( dataSet, listValues, attributeName, componentNames, onPoints, + vtkDataType, logger ): return False - + iter.GoToNextItem() return True @@ -315,49 +341,54 @@ def createConstantAttributeDataSet( attributeName: str, componentNames: tuple[ str, ...] = (), # noqa: C408 onPoints: bool = False, - vtkDataType: Union[ int, Any ] = None, - logger: Logger = getLogger( "createConstantAttributeDataSet", True ), + vtkDataType: Union[ int, None ] = None, + logger: Union[ Logger, None ] = None, ) -> bool: """Create an attribute with a constant value per component in the dataSet. Args: dataSet (vtkDataSet): DataSet where to create the attribute. - listValues (list[any]): List of values of the attribute for each components. It is better to use numpy scalar type for the values. + listValues (list[Any]): List of values of the attribute for each components. It is better to use numpy scalar type for the values. attributeName (str): Name of the attribute. componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. - vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. - If None the vtk data type is given by the type of the values of listValues. - Else, values type have to correspond to the type of the vtk data, check https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/numpy_support.py for more information. - Defaults to None. + vtkDataType (Union[int, None], optional): Vtk data type of the attribute to create. + Defaults to None, the vtk data type is given by the type of the values. + Warning with int8, uint8 and int64 type of value, the vtk data type corresponding are multiples. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG - logger (Logger, optional): A logger to manage the output messages. - Defaults to an internal logger. + logger (Union[Logger, None], optional): A logger to manage the output messages. + Defaults to None, an internal logger is used. Returns: bool: True if the attribute was correctly created, False if it was not created. """ + # Check if an external logger is given. + if logger is None: + logger = getLogger( "createConstantAttributeDataSet", True ) + # Check if all the values of listValues have the same type. valueType: type = type( listValues[ 0 ] ) for value in listValues: valueTypeTest: type = type( value ) if valueType != valueTypeTest: - logger.error( f"All values in the list of values have not the same type." ) + logger.error( "All values in the list of values have not the same type." ) logger.error( f"The constant attribute { attributeName } has not been created into the mesh." ) return False - + # Convert int and float type into numpy scalar type. if valueType in ( int, float ): npType: type = type( np.array( listValues )[ 0 ] ) - logger.warning( f"During the creation of the constant attribute { attributeName }, values will be converted from { valueType } to { npType }." ) - logger.warning( f"To avoid any issue with the conversion use directly numpy scalar type for the values" ) + logger.warning( + f"During the creation of the constant attribute { attributeName }, values will be converted from { valueType } to { npType }." + ) + logger.warning( "To avoid any issue with the conversion use directly numpy scalar type for the values" ) valueType = npType - + # Check the coherency between the given value type and the vtk array type if it exist. valueType = valueType().dtype if vtkDataType is not None: @@ -366,9 +397,11 @@ def createConstantAttributeDataSet( logger.error( f"The vtk data type { vtkDataType } is unknown." ) logger.error( f"The constant attribute { attributeName } has not been created into the mesh." ) return False - npArrayTypeFromVtk: type = vtkNumpyTypeMap[ vtkDataType ]().dtype + npArrayTypeFromVtk: npt.DTypeLike = vtkNumpyTypeMap[ vtkDataType ]().dtype if npArrayTypeFromVtk != valueType: - logger.error( f"Values type { valueType } is not coherent with the type of array created ({ npArrayTypeFromVtk }) from the given vtkDataType." ) + logger.error( + f"Values type { valueType } is not coherent with the type of array created ({ npArrayTypeFromVtk }) from the given vtkDataType." + ) logger.error( f"The constant attribute { attributeName } has not been created into the mesh." ) return False @@ -390,45 +423,48 @@ def createAttribute( attributeName: str, componentNames: tuple[ str, ...] = (), # noqa: C408 onPoints: bool = False, - vtkDataType: Union[ int, Any ] = None, - logger: Logger = getLogger( "createAttribute", True ), + vtkDataType: Union[ int, None ] = None, + logger: Union[ Logger, None ] = None, ) -> bool: """Create an attribute from the given numpy array. Args: dataSet (vtkDataSet): DataSet where to create the attribute. - npArray (npt.NDArray[any]): Array that contains the values. + npArray (NDArray[Any]): Array that contains the values. attributeName (str): Name of the attribute. componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. - vtkDataType (Union(any, int), optional): Vtk data type of the attribute to create. - If None the vtk data type is given by the type of the numpy array. - Else, numpy array type have to correspond to the type of the vtk data, check https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/numpy_support.py for more information. - Defaults to None. - Warning with int8, uint8 and int64 type of value, the vtk data type corresponding are multiples. By default: + vtkDataType (Union[int, None], optional): Vtk data type of the attribute to create. + Defaults to None, the vtk data type is given by the type of the array. + + Warning with int8, uint8 and int64 type, the vtk data type corresponding are multiples. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG - logger (Logger, optional): A logger to manage the output messages. - Defaults to an internal logger. + logger (Union[Logger, None], optional): A logger to manage the output messages. + Defaults to None, an internal logger is used. Returns: bool: True if the attribute was correctly created, False if it was not created. """ + # Check if an external logger is given. + if logger is None: + logger = getLogger( "createAttribute", True ) + # Check if the input mesh is inherited from vtkDataSet. if not isinstance( dataSet, vtkDataSet ): - logger.error( f"Input mesh has to be inherited from vtkDataSet." ) + logger.error( "Input mesh has to be inherited from vtkDataSet." ) # type: ignore[unreachable] logger.error( f"The attribute { attributeName } has not been created into the mesh." ) return False - + # Check if the attribute already exist in the input mesh. if isAttributeInObjectDataSet( dataSet, attributeName, onPoints ): logger.error( f"The attribute { attributeName } is already present in the dataSet." ) logger.error( f"The attribute { attributeName } has not been created into the mesh." ) return False - + # Check the coherency between the given array type and the vtk array type if it exist. if vtkDataType is not None: vtkNumpyTypeMap: dict[ int, type ] = vnp.get_vtk_to_numpy_typemap() @@ -436,14 +472,16 @@ def createAttribute( logger.error( f"The vtk data type { vtkDataType } is unknown." ) logger.error( f"The attribute { attributeName } has not been created into the mesh." ) return False - npArrayTypeFromVtk: type = vtkNumpyTypeMap[ vtkDataType ]().dtype - npArrayTypeFromInput: type = npArray.dtype + npArrayTypeFromVtk: npt.DTypeLike = vtkNumpyTypeMap[ vtkDataType ]().dtype + npArrayTypeFromInput: npt.DTypeLike = npArray.dtype if npArrayTypeFromVtk != npArrayTypeFromInput: - logger.error( f"The numpy array type { npArrayTypeFromInput } is not coherent with the type of array created ({ npArrayTypeFromVtk }) from the given vtkDataType." ) + logger.error( + f"The numpy array type { npArrayTypeFromInput } is not coherent with the type of array created ({ npArrayTypeFromVtk }) from the given vtkDataType." + ) logger.error( f"The attribute { attributeName } has not been created into the mesh." ) return False - data: Union[ vtkPointData, vtkCellData] + data: Union[ vtkPointData, vtkCellData ] nbElements: int oppositePieceName: str if onPoints: @@ -454,18 +492,20 @@ def createAttribute( data = dataSet.GetCellData() nbElements = dataSet.GetNumberOfCells() oppositePieceName = "points" - + # Check if the input array has the good size. if len( npArray ) != nbElements: logger.error( f"The array has to have { nbElements } elements, but have only { len( npArray ) } elements" ) logger.error( f"The attribute { attributeName } has not been created into the mesh." ) return False - + # Check if an attribute with the same name exist on the opposite piece (points or cells). oppositePiece: bool = not onPoints if isAttributeInObjectDataSet( dataSet, attributeName, oppositePiece ): - logger.warning( f"An attribute with the same name ({ attributeName }) is already present in the dataSet but on { oppositePieceName }." ) - + logger.warning( + f"An attribute with the same name ({ attributeName }) is already present in the dataSet but on { oppositePieceName }." + ) + # Convert the numpy array int a vtkDataArray. createdAttribute: vtkDataArray = vnp.numpy_to_vtk( npArray, deep=True, array_type=vtkDataType ) createdAttribute.SetName( attributeName ) @@ -473,14 +513,19 @@ def createAttribute( nbComponents: int = createdAttribute.GetNumberOfComponents() nbNames: int = len( componentNames ) if nbComponents == 1 and nbNames > 0: - logger.warning( f"The array has one component and no name, the components names you have enter will not be taking into account." ) - + logger.warning( + "The array has one component and no name, the components names you have enter will not be taking into account." + ) + if nbComponents > 1: if nbNames < nbComponents: componentNames = tuple( [ "Component" + str( i ) for i in range( nbComponents ) ] ) - logger.warning( f"Insufficient number of input component names. { attributeName } component names will be set to : Component0, Component1 ..." ) + logger.warning( + f"Insufficient number of input component names. { attributeName } component names will be set to : Component0, Component1 ..." + ) elif nbNames > nbComponents: - logger.warning( f"Excessive number of input component names, only the first { nbComponents } names will be used." ) + logger.warning( + f"Excessive number of input component names, only the first { nbComponents } names will be used." ) for i in range( nbComponents ): createdAttribute.SetComponentName( i, componentNames[ i ] ) @@ -497,9 +542,9 @@ def copyAttribute( attributeNameFrom: str, attributeNameTo: str, onPoints: bool = False, - logger: Logger = getLogger( "copyAttribute", True ), + logger: Union[ Logger, None ] = None, ) -> bool: - """Copy an attribute from a multiBlockDataSet to a similare one on the same piece. + """Copy an attribute from a multiBlockDataSet to a similar one on the same piece. Args: multiBlockDataSetFrom (vtkMultiBlockDataSet): MultiBlockDataSet from which to copy the attribute. @@ -508,61 +553,67 @@ def copyAttribute( attributeNameTo (str): Attribute name in multiBlockDataSetTo. It will be a new attribute of multiBlockDataSetTo. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. - logger (Logger, optional): A logger to manage the output messages. - Defaults to an internal logger. + logger (Union[Logger, None], optional): A logger to manage the output messages. + Defaults to None, an internal logger is used. Returns: bool: True if copy successfully ended, False otherwise. """ + # Check if an external logger is given. + if logger is None: + logger = getLogger( "copyAttribute", True ) + # Check if the multiBlockDataSetFrom is inherited from vtkMultiBlockDataSet. if not isinstance( multiBlockDataSetFrom, vtkMultiBlockDataSet ): - logger.error( f"multiBlockDataSetFrom has to be inherited from vtkMultiBlockDataSet." ) + logger.error( # type: ignore[unreachable] + "multiBlockDataSetFrom has to be inherited from vtkMultiBlockDataSet." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False # Check if the multiBlockDataSetTo is inherited from vtkMultiBlockDataSet. if not isinstance( multiBlockDataSetTo, vtkMultiBlockDataSet ): - logger.error( f"multiBlockDataSetTo has to be inherited from vtkMultiBlockDataSet." ) + logger.error( # type: ignore[unreachable] + "multiBlockDataSetTo has to be inherited from vtkMultiBlockDataSet." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - + # Check if the attribute exist in the multiBlockDataSetFrom. if not isAttributeInObjectMultiBlockDataSet( multiBlockDataSetFrom, attributeNameFrom, onPoints ): logger.error( f"The attribute { attributeNameFrom } is not in the multiBlockDataSetFrom." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - + # Check if the attribute already exist in the multiBlockDataSetTo. if isAttributeInObjectMultiBlockDataSet( multiBlockDataSetTo, attributeNameTo, onPoints ): logger.error( f"The attribute { attributeNameTo } is already in the multiBlockDataSetTo." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - - # Check if the two multiBlockDataSets are similare. + + # Check if the two multiBlockDataSets are similar. elementaryBlockIndexesTo: list[ int ] = getBlockElementIndexesFlatten( multiBlockDataSetTo ) elementaryBlockIndexesFrom: list[ int ] = getBlockElementIndexesFlatten( multiBlockDataSetFrom ) if elementaryBlockIndexesTo != elementaryBlockIndexesFrom: - logger.error( f"multiBlockDataSetFrom and multiBlockDataSetTo do not have the same block indexes." ) + logger.error( "multiBlockDataSetFrom and multiBlockDataSetTo do not have the same block indexes." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - + # Parse blocks of the two mesh to copy the attribute. for idBlock in elementaryBlockIndexesTo: dataSetFrom: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( multiBlockDataSetFrom, idBlock ) ) if dataSetFrom is None: - logger.error( f"Block { blockId } of multiBlockDataSetFrom is null." ) + logger.error( f"Block { idBlock } of multiBlockDataSetFrom is null." ) # type: ignore[unreachable] logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False dataSetTo: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( multiBlockDataSetTo, idBlock ) ) if dataSetTo is None: - logger.error( f"Block { blockId } of multiBlockDataSetTo is null." ) + logger.error( f"Block { idBlock } of multiBlockDataSetTo is null." ) # type: ignore[unreachable] logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - if isAttributeInObjectDataSet( dataSetFrom, attributeNameFrom, onPoints ): - if not copyAttributeDataSet( dataSetFrom, dataSetTo, attributeNameFrom, attributeNameTo, onPoints, logger ): - return False + if isAttributeInObjectDataSet( dataSetFrom, attributeNameFrom, onPoints ) and \ + not copyAttributeDataSet( dataSetFrom, dataSetTo, attributeNameFrom, attributeNameTo, onPoints, logger ): + return False return True @@ -573,9 +624,9 @@ def copyAttributeDataSet( attributeNameFrom: str, attributeNameTo: str, onPoints: bool = False, - logger: Logger = getLogger( "copyAttributeDataSet", True ), + logger: Union[ Logger, Any ] = None, ) -> bool: - """Copy an attribute from a dataSet to a similare one on the same piece. + """Copy an attribute from a dataSet to a similar one on the same piece. Args: dataSetFrom (vtkDataSet): DataSet from which to copy the attribute. @@ -584,37 +635,40 @@ def copyAttributeDataSet( attributeNameTo (str): Attribute name in dataSetTo. It will be a new attribute of dataSetTo. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. - logger (Logger, optional): A logger to manage the output messages. - Defaults to an internal logger. + logger (Union[Logger, None], optional): A logger to manage the output messages. + Defaults to None, an internal logger is used. Returns: bool: True if copy successfully ended, False otherwise. """ + # Check if an external logger is given. + if logger is None: + logger = getLogger( "copyAttributeDataSet", True ) + # Check if the dataSetFrom is inherited from vtkDataSet. if not isinstance( dataSetFrom, vtkDataSet ): - logger.error( f"dataSetFrom has to be inherited from vtkDataSet." ) + logger.error( "dataSetFrom has to be inherited from vtkDataSet." ) # type: ignore[unreachable] logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - + # Check if the dataSetTo is inherited from vtkDataSet. if not isinstance( dataSetTo, vtkDataSet ): - logger.error( f"dataSetTo has to be inherited from vtkDataSet." ) + logger.error( "dataSetTo has to be inherited from vtkDataSet." ) # type: ignore[unreachable] logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - + # Check if the attribute exist in the dataSetFrom. if not isAttributeInObjectDataSet( dataSetFrom, attributeNameFrom, onPoints ): logger.error( f"The attribute { attributeNameFrom } is not in the dataSetFrom." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - + # Check if the attribute already exist in the dataSetTo. if isAttributeInObjectDataSet( dataSetTo, attributeNameTo, onPoints ): logger.error( f"The attribute { attributeNameTo } is already in the dataSetTo." ) logger.error( f"The attribute { attributeNameFrom } has not been copied." ) return False - - # Get the properties of the attribute to copied. + npArray: npt.NDArray[ Any ] = getArrayInObject( dataSetFrom, attributeNameFrom, onPoints ) componentNames: tuple[ str, ...] = getComponentNamesDataSet( dataSetFrom, attributeNameFrom, onPoints ) vtkArrayType: int = getVtkArrayTypeInObject( dataSetFrom, attributeNameFrom, onPoints ) From 7da8f9b38708e18bf7cd86659490523770608e1e Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 23 Jul 2025 10:36:43 +0200 Subject: [PATCH 24/56] Clean for the ci --- geos-mesh/tests/test_arrayModifiers.py | 276 +++++++++++++------------ 1 file changed, 148 insertions(+), 128 deletions(-) diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index 8d9fb812..7df5838a 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -15,13 +15,12 @@ from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet, vtkPointData, vtkCellData ) from vtk import ( # type: ignore[import-untyped] - VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG_LONG, - VTK_CHAR, VTK_SIGNED_CHAR, VTK_SHORT, VTK_INT, VTK_LONG_LONG, VTK_ID_TYPE, - VTK_FLOAT, VTK_DOUBLE, + VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_INT, VTK_UNSIGNED_LONG_LONG, VTK_CHAR, VTK_SIGNED_CHAR, + VTK_SHORT, VTK_INT, VTK_LONG_LONG, VTK_ID_TYPE, VTK_FLOAT, VTK_DOUBLE, ) # Information : -# https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/numpy_support.py +# https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/numpy_support.py # https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/vtkConstants.py # vtk array type int numpy type # VTK_CHAR = 2 = np.int8 @@ -45,24 +44,26 @@ from geos.mesh.utils import arrayModifiers -@pytest.mark.parametrize( "idBlock, attributeName, nbComponentsTest, componentNamesTest, onPoints, value, valueTest, vtkDataTypeTest", [ - # Test fill an attribute on point and on cell. - ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.nan, np.nan, VTK_DOUBLE ), - ( 1, "PointAttribute", 3, ( "AX1", "AX2", "AX3" ), True, np.nan, np.nan, VTK_DOUBLE ), - # Test fill attributes with different number of componnent. - ( 1, "PORO", 1, (), False, np.nan, np.float32( np.nan ), VTK_FLOAT ), - ( 1, "PERM", 3, ( "AX1", "AX2", "AX3" ), False, np.nan, np.float32( np.nan ), VTK_FLOAT ), - # Test fill an attribute with default value. - ( 1, "FAULT", 1, (), False, np.nan, np.int32( -1 ), VTK_INT ), - ( 0, "collocated_nodes", 2, ( None, None ), True, np.nan, np.int64( -1 ), VTK_ID_TYPE ), - # Test fill an attribute with specified value. - ( 1, "PORO", 1, (), False, np.float32( 4 ), np.float32( 4 ), VTK_FLOAT ), - ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, 4. , np.float64( 4 ), VTK_DOUBLE ), - ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.float64( 4 ), np.float64( 4 ), VTK_DOUBLE ), - ( 1, "FAULT", 1, (), False, np.int32( 4 ), np.int32( 4 ), VTK_INT ), - ( 0, "collocated_nodes", 2, ( None, None ), True, 4 , np.int64( 4 ), VTK_ID_TYPE ), - ( 0, "collocated_nodes", 2, ( None, None ), True, np.int64( 4 ), np.int64( 4 ), VTK_ID_TYPE ), -] ) +@pytest.mark.parametrize( + "idBlock, attributeName, nbComponentsTest, componentNamesTest, onPoints, value, valueTest, vtkDataTypeTest", + [ + # Test fill an attribute on point and on cell. + ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.nan, np.nan, VTK_DOUBLE ), + ( 1, "PointAttribute", 3, ( "AX1", "AX2", "AX3" ), True, np.nan, np.nan, VTK_DOUBLE ), + # Test fill attributes with different number of componnent. + ( 1, "PORO", 1, (), False, np.nan, np.float32( np.nan ), VTK_FLOAT ), + ( 1, "PERM", 3, ( "AX1", "AX2", "AX3" ), False, np.nan, np.float32( np.nan ), VTK_FLOAT ), + # Test fill an attribute with default value. + ( 1, "FAULT", 1, (), False, np.nan, np.int32( -1 ), VTK_INT ), + ( 0, "collocated_nodes", 2, ( None, None ), True, np.nan, np.int64( -1 ), VTK_ID_TYPE ), + # Test fill an attribute with specified value. + ( 1, "PORO", 1, (), False, np.float32( 4 ), np.float32( 4 ), VTK_FLOAT ), + ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, 4., np.float64( 4 ), VTK_DOUBLE ), + ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.float64( 4 ), np.float64( 4 ), VTK_DOUBLE ), + ( 1, "FAULT", 1, (), False, np.int32( 4 ), np.int32( 4 ), VTK_INT ), + ( 0, "collocated_nodes", 2, ( None, None ), True, 4, np.int64( 4 ), VTK_ID_TYPE ), + ( 0, "collocated_nodes", 2, ( None, None ), True, np.int64( 4 ), np.int64( 4 ), VTK_ID_TYPE ), + ] ) def test_fillPartialAttributes( dataSetTest: vtkMultiBlockDataSet, idBlock: int, @@ -76,7 +77,7 @@ def test_fillPartialAttributes( ) -> None: """Test filling a partial attribute from a multiblock with values.""" multiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - + # Fill the attribute in the multiBlockDataSet. assert arrayModifiers.fillPartialAttributes( multiBlockDataSetTest, attributeName, onPoints, value ) @@ -133,13 +134,15 @@ def test_FillAllPartialAttributes( nbBlock: int = multiBlockDataSetTest.GetNumberOfBlocks() for idBlock in range( nbBlock ): dataSet: vtkDataSet = cast( vtkDataSet, multiBlockDataSetTest.GetBlock( idBlock ) ) + attributeExist: int for attributeNameOnPoint in [ "PointAttribute", "collocated_nodes" ]: - attributeExist: int = dataSet.GetPointData().HasArray( attributeNameOnPoint ) + attributeExist = dataSet.GetPointData().HasArray( attributeNameOnPoint ) assert attributeExist == 1 for attributeNameOnCell in [ "CELL_MARKERS", "CellAttribute", "FAULT", "PERM", "PORO" ]: - attributeExist: int = dataSet.GetCellData().HasArray( attributeNameOnCell ) + attributeExist = dataSet.GetCellData().HasArray( attributeNameOnCell ) assert attributeExist == 1 + @pytest.mark.parametrize( "attributeName, dataType, expectedDatatypeArray", [ ( "test_double", VTK_DOUBLE, "vtkDoubleArray" ), ( "test_float", VTK_FLOAT, "vtkFloatArray" ), @@ -162,14 +165,16 @@ def test_createEmptyAttribute( assert newAttr.IsA( str( expectedDatatypeArray ) ) -@pytest.mark.parametrize( "attributeName, onPoints", [ - # Test to create a new attribute on points and on cells. - ( "newAttribute", False ), - ( "newAttribute", True ), - # Test to create a new attribute whenn an attribute with the same name already exist on the opposit piece. - ( "PORO", True ), # Partial attribute on cells already exist. - ( "GLOBAL_IDS_CELLS", True ), # Global attribute on cells already exist. -] ) +@pytest.mark.parametrize( + "attributeName, onPoints", + [ + # Test to create a new attribute on points and on cells. + ( "newAttribute", False ), + ( "newAttribute", True ), + # Test to create a new attribute whenn an attribute with the same name already exist on the opposit piece. + ( "PORO", True ), # Partial attribute on cells already exist. + ( "GLOBAL_IDS_CELLS", True ), # Global attribute on cells already exist. + ] ) def test_createConstantAttributeMultiBlock( dataSetTest: vtkMultiBlockDataSet, attributeName: str, @@ -178,7 +183,10 @@ def test_createConstantAttributeMultiBlock( """Test creation of constant attribute in multiblock dataset.""" multiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) values: list[ float ] = [ np.nan ] - assert arrayModifiers.createConstantAttributeMultiBlock( multiBlockDataSetTest, values, attributeName, onPoints=onPoints ) + assert arrayModifiers.createConstantAttributeMultiBlock( multiBlockDataSetTest, + values, + attributeName, + onPoints=onPoints ) nbBlock = multiBlockDataSetTest.GetNumberOfBlocks() for idBlock in range( nbBlock ): @@ -190,46 +198,51 @@ def test_createConstantAttributeMultiBlock( assert attributeWellCreated == 1 -@pytest.mark.parametrize( "listValues, componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, attributeName", [ - # Test attribute names. - ## Test with an attributeName already existing on opposit piece. - ( [ np.float64( 42 ) ], (), (), True, VTK_DOUBLE, VTK_DOUBLE, "CellAttribute" ), - ( [ np.float64( 42 ) ], (), (), False, VTK_DOUBLE, VTK_DOUBLE, "PointAttribute" ), - ## Test with a new attributeName on cells and on points. - ( [ np.float32( 42 ) ], (), (), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), - ( [ np.float32( 42 ) ], (), (), False, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), - # Test the number of components and their names. - ( [ np.float32( 42 ) ], ( "X" ), (), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), - ( [ np.float32( 42 ), np.float32( 42 ) ], ( "X", "Y" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), - ( [ np.float32( 42 ), np.float32( 42 ) ], ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), - ( [ np.float32( 42 ), np.float32( 42 ) ], (), ( "Component0", "Component1" ), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), - # Test the type of the values. - ## With numpy scalar type. - ( [ np.int8( 42 ) ], (), (), True, None, VTK_SIGNED_CHAR, "newAttribute" ), - ( [ np.int8( 42 ) ], (), (), True, VTK_SIGNED_CHAR, VTK_SIGNED_CHAR, "newAttribute" ), - ( [ np.int16( 42 ) ], (), (), True, None, VTK_SHORT, "newAttribute" ), - ( [ np.int16( 42 ) ], (), (), True, VTK_SHORT, VTK_SHORT, "newAttribute" ), - ( [ np.int32( 42 ) ], (), (), True, None, VTK_INT, "newAttribute" ), - ( [ np.int32( 42 ) ], (), (), True, VTK_INT, VTK_INT, "newAttribute" ), - ( [ np.int64( 42 ) ], (), (), True, None, VTK_LONG_LONG, "newAttribute" ), - ( [ np.int64( 42 ) ], (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "newAttribute" ), - ( [ np.uint8( 42 ) ], (), (), True, None, VTK_UNSIGNED_CHAR, "newAttribute" ), - ( [ np.uint8( 42 ) ], (), (), True, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_CHAR, "newAttribute" ), - ( [ np.uint16( 42 ) ], (), (), True, None, VTK_UNSIGNED_SHORT, "newAttribute" ), - ( [ np.uint16( 42 ) ], (), (), True, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_SHORT, "newAttribute" ), - ( [ np.uint32( 42 ) ], (), (), True, None, VTK_UNSIGNED_INT, "newAttribute" ), - ( [ np.uint32( 42 ) ], (), (), True, VTK_UNSIGNED_INT, VTK_UNSIGNED_INT, "newAttribute" ), - ( [ np.uint64( 42 ) ], (), (), True, None, VTK_UNSIGNED_LONG_LONG, "newAttribute" ), - ( [ np.uint64( 42 ) ], (), (), True, VTK_UNSIGNED_LONG_LONG, VTK_UNSIGNED_LONG_LONG, "newAttribute" ), - ( [ np.float32( 42 ) ], (), (), True, None, VTK_FLOAT, "newAttribute" ), - ( [ np.float64( 42 ) ], (), (), True, None, VTK_DOUBLE, "newAttribute" ), - ( [ np.float64( 42 ) ], (), (), True, VTK_DOUBLE, VTK_DOUBLE, "newAttribute" ), - ## With python scalar type. - ( [ 42 ], (), (), True, None, VTK_LONG_LONG, "newAttribute" ), - ( [ 42 ], (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "newAttribute" ), - ( [ 42. ], (), (), True, None, VTK_DOUBLE, "newAttribute" ), - ( [ 42. ], (), (), True, VTK_DOUBLE, VTK_DOUBLE, "newAttribute" ), -] ) +@pytest.mark.parametrize( + "listValues, componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, attributeName", + [ + # Test attribute names. + ## Test with an attributeName already existing on opposit piece. + ( [ np.float64( 42 ) ], (), (), True, VTK_DOUBLE, VTK_DOUBLE, "CellAttribute" ), + ( [ np.float64( 42 ) ], (), (), False, VTK_DOUBLE, VTK_DOUBLE, "PointAttribute" ), + ## Test with a new attributeName on cells and on points. + ( [ np.float32( 42 ) ], (), (), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), + ( [ np.float32( 42 ) ], (), (), False, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), + # Test the number of components and their names. + ( [ np.float32( 42 ) ], ( "X" ), (), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), + ( [ np.float32( 42 ), np.float32( 42 ) ], ( "X", "Y" ), + ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), + ( [ np.float32( 42 ), np.float32( 42 ) ], ( "X", "Y", "Z" ), + ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), + ( [ np.float32( 42 ), np.float32( 42 ) ], (), + ( "Component0", "Component1" ), True, VTK_FLOAT, VTK_FLOAT, "newAttribute" ), + # Test the type of the values. + ## With numpy scalar type. + ( [ np.int8( 42 ) ], (), (), True, None, VTK_SIGNED_CHAR, "newAttribute" ), + ( [ np.int8( 42 ) ], (), (), True, VTK_SIGNED_CHAR, VTK_SIGNED_CHAR, "newAttribute" ), + ( [ np.int16( 42 ) ], (), (), True, None, VTK_SHORT, "newAttribute" ), + ( [ np.int16( 42 ) ], (), (), True, VTK_SHORT, VTK_SHORT, "newAttribute" ), + ( [ np.int32( 42 ) ], (), (), True, None, VTK_INT, "newAttribute" ), + ( [ np.int32( 42 ) ], (), (), True, VTK_INT, VTK_INT, "newAttribute" ), + ( [ np.int64( 42 ) ], (), (), True, None, VTK_LONG_LONG, "newAttribute" ), + ( [ np.int64( 42 ) ], (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "newAttribute" ), + ( [ np.uint8( 42 ) ], (), (), True, None, VTK_UNSIGNED_CHAR, "newAttribute" ), + ( [ np.uint8( 42 ) ], (), (), True, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_CHAR, "newAttribute" ), + ( [ np.uint16( 42 ) ], (), (), True, None, VTK_UNSIGNED_SHORT, "newAttribute" ), + ( [ np.uint16( 42 ) ], (), (), True, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_SHORT, "newAttribute" ), + ( [ np.uint32( 42 ) ], (), (), True, None, VTK_UNSIGNED_INT, "newAttribute" ), + ( [ np.uint32( 42 ) ], (), (), True, VTK_UNSIGNED_INT, VTK_UNSIGNED_INT, "newAttribute" ), + ( [ np.uint64( 42 ) ], (), (), True, None, VTK_UNSIGNED_LONG_LONG, "newAttribute" ), + ( [ np.uint64( 42 ) ], (), (), True, VTK_UNSIGNED_LONG_LONG, VTK_UNSIGNED_LONG_LONG, "newAttribute" ), + ( [ np.float32( 42 ) ], (), (), True, None, VTK_FLOAT, "newAttribute" ), + ( [ np.float64( 42 ) ], (), (), True, None, VTK_DOUBLE, "newAttribute" ), + ( [ np.float64( 42 ) ], (), (), True, VTK_DOUBLE, VTK_DOUBLE, "newAttribute" ), + ## With python scalar type. + ( [ 42 ], (), (), True, None, VTK_LONG_LONG, "newAttribute" ), + ( [ 42 ], (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "newAttribute" ), + ( [ 42. ], (), (), True, None, VTK_DOUBLE, "newAttribute" ), + ( [ 42. ], (), (), True, VTK_DOUBLE, VTK_DOUBLE, "newAttribute" ), + ] ) def test_createConstantAttributeDataSet( dataSetTest: vtkDataSet, listValues: list[ Any ], @@ -244,7 +257,8 @@ def test_createConstantAttributeDataSet( dataSet: vtkDataSet = dataSetTest( "dataset" ) # Create the new constant attribute in the dataSet. - assert arrayModifiers.createConstantAttributeDataSet( dataSet, listValues, attributeName, componentNames, onPoints, vtkDataType ) + assert arrayModifiers.createConstantAttributeDataSet( dataSet, listValues, attributeName, componentNames, onPoints, + vtkDataType ) # Get the created attribute. data: Union[ vtkPointData, vtkCellData ] @@ -282,46 +296,48 @@ def test_createConstantAttributeDataSet( assert vtkDataTypeCreated == vtkDataTypeTest -@pytest.mark.parametrize( "componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, valueType, attributeName", [ - # Test attribute names. - ## Test with an attributeName already existing on opposit piece. - ( (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float64", "CellAttribute" ), - ( (), (), False, VTK_DOUBLE, VTK_DOUBLE, "float64", "PointAttribute" ), - ## Test with a new attributeName on cells and on points. - ( (), (), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), - ( (), (), False, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), - # Test the number of components and their names. - ( ( "X" ), (), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), - ( ( "X", "Y" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), - ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), - ( (), ( "Component0", "Component1" ), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), - # Test the type of the values. - ## With numpy scalar type. - ( (), (), True, None, VTK_SIGNED_CHAR, "int8", "newAttribute" ), - ( (), (), True, VTK_SIGNED_CHAR, VTK_SIGNED_CHAR, "int8", "newAttribute" ), - ( (), (), True, None, VTK_SHORT, "int16", "newAttribute" ), - ( (), (), True, VTK_SHORT, VTK_SHORT, "int16", "newAttribute" ), - ( (), (), True, None, VTK_INT, "int32", "newAttribute" ), - ( (), (), True, VTK_INT, VTK_INT, "int32", "newAttribute" ), - ( (), (), True, None, VTK_LONG_LONG, "int64", "newAttribute" ), - ( (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64", "newAttribute" ), - ( (), (), True, None, VTK_UNSIGNED_CHAR, "uint8", "newAttribute" ), - ( (), (), True, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_CHAR, "uint8", "newAttribute" ), - ( (), (), True, None, VTK_UNSIGNED_SHORT, "uint16", "newAttribute" ), - ( (), (), True, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_SHORT, "uint16", "newAttribute" ), - ( (), (), True, None, VTK_UNSIGNED_INT, "uint32", "newAttribute" ), - ( (), (), True, VTK_UNSIGNED_INT, VTK_UNSIGNED_INT, "uint32", "newAttribute" ), - ( (), (), True, None, VTK_UNSIGNED_LONG_LONG, "uint64", "newAttribute" ), - ( (), (), True, VTK_UNSIGNED_LONG_LONG, VTK_UNSIGNED_LONG_LONG, "uint64", "newAttribute" ), - ( (), (), True, None, VTK_FLOAT, "float32", "newAttribute" ), - ( (), (), True, None, VTK_DOUBLE, "float64", "newAttribute" ), - ( (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float64", "newAttribute" ), - ## With python scalar type. - ( (), (), True, None, VTK_LONG_LONG, "int", "newAttribute" ), - ( (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "int", "newAttribute" ), - ( (), (), True, None, VTK_DOUBLE, "float", "newAttribute" ), - ( (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float", "newAttribute" ), -] ) +@pytest.mark.parametrize( + "componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, valueType, attributeName", + [ + # Test attribute names. + ## Test with an attributeName already existing on opposit piece. + ( (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float64", "CellAttribute" ), + ( (), (), False, VTK_DOUBLE, VTK_DOUBLE, "float64", "PointAttribute" ), + ## Test with a new attributeName on cells and on points. + ( (), (), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), + ( (), (), False, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), + # Test the number of components and their names. + ( ( "X" ), (), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), + ( ( "X", "Y" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), + ( ( "X", "Y", "Z" ), ( "X", "Y" ), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), + ( (), ( "Component0", "Component1" ), True, VTK_FLOAT, VTK_FLOAT, "float32", "newAttribute" ), + # Test the type of the values. + ## With numpy scalar type. + ( (), (), True, None, VTK_SIGNED_CHAR, "int8", "newAttribute" ), + ( (), (), True, VTK_SIGNED_CHAR, VTK_SIGNED_CHAR, "int8", "newAttribute" ), + ( (), (), True, None, VTK_SHORT, "int16", "newAttribute" ), + ( (), (), True, VTK_SHORT, VTK_SHORT, "int16", "newAttribute" ), + ( (), (), True, None, VTK_INT, "int32", "newAttribute" ), + ( (), (), True, VTK_INT, VTK_INT, "int32", "newAttribute" ), + ( (), (), True, None, VTK_LONG_LONG, "int64", "newAttribute" ), + ( (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "int64", "newAttribute" ), + ( (), (), True, None, VTK_UNSIGNED_CHAR, "uint8", "newAttribute" ), + ( (), (), True, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_CHAR, "uint8", "newAttribute" ), + ( (), (), True, None, VTK_UNSIGNED_SHORT, "uint16", "newAttribute" ), + ( (), (), True, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_SHORT, "uint16", "newAttribute" ), + ( (), (), True, None, VTK_UNSIGNED_INT, "uint32", "newAttribute" ), + ( (), (), True, VTK_UNSIGNED_INT, VTK_UNSIGNED_INT, "uint32", "newAttribute" ), + ( (), (), True, None, VTK_UNSIGNED_LONG_LONG, "uint64", "newAttribute" ), + ( (), (), True, VTK_UNSIGNED_LONG_LONG, VTK_UNSIGNED_LONG_LONG, "uint64", "newAttribute" ), + ( (), (), True, None, VTK_FLOAT, "float32", "newAttribute" ), + ( (), (), True, None, VTK_DOUBLE, "float64", "newAttribute" ), + ( (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float64", "newAttribute" ), + ## With python scalar type. + ( (), (), True, None, VTK_LONG_LONG, "int", "newAttribute" ), + ( (), (), True, VTK_LONG_LONG, VTK_LONG_LONG, "int", "newAttribute" ), + ( (), (), True, None, VTK_DOUBLE, "float", "newAttribute" ), + ( (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float", "newAttribute" ), + ] ) def test_createAttribute( dataSetTest: vtkDataSet, getArrayWithSpeTypeValue: npt.NDArray[ Any ], @@ -366,14 +382,16 @@ def test_createAttribute( assert vtkDataTypeCreated == vtkDataTypeTest -@pytest.mark.parametrize( "attributeNameFrom, attributeNameTo, onPoints", [ - # Test with global attibutes. - ( "GLOBAL_IDS_POINTS", "GLOBAL_IDS_POINTS_To", True ), - ( "GLOBAL_IDS_CELLS", 'GLOBAL_IDS_CELLS_To', False ), - # Test with partial attribute. - ( "CellAttribute", "CellAttributeTo", False ), - ( "PointAttribute", "PointAttributeTo", True ), -] ) +@pytest.mark.parametrize( + "attributeNameFrom, attributeNameTo, onPoints", + [ + # Test with global attibutes. + ( "GLOBAL_IDS_POINTS", "GLOBAL_IDS_POINTS_To", True ), + ( "GLOBAL_IDS_CELLS", 'GLOBAL_IDS_CELLS_To', False ), + # Test with partial attribute. + ( "CellAttribute", "CellAttributeTo", False ), + ( "PointAttribute", "PointAttributeTo", True ), + ] ) def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet, attributeNameFrom: str, @@ -385,7 +403,8 @@ def test_copyAttribute( multiBlockDataSetTo: vtkMultiBlockDataSet = dataSetTest( "emptymultiblock" ) # Copy the attribute from the multiBlockDataSetFrom to the multiBlockDataSetTo. - assert arrayModifiers.copyAttribute( multiBlockDataSetFrom, multiBlockDataSetTo, attributeNameFrom, attributeNameTo, onPoints ) + assert arrayModifiers.copyAttribute( multiBlockDataSetFrom, multiBlockDataSetTo, attributeNameFrom, attributeNameTo, + onPoints ) # Parse the two multiBlockDataSet and test if the attribute has been copied. nbBlocks: int = multiBlockDataSetFrom.GetNumberOfBlocks() @@ -405,6 +424,7 @@ def test_copyAttribute( attributeExistCopied: int = dataTo.HasArray( attributeNameTo ) assert attributeExistCopied == attributeExistTest + @pytest.mark.parametrize( "attributeNameFrom, attributeNameTo, onPoints", [ ( "CellAttribute", "CellAttributeTo", False ), ( "PointAttribute", "PointAttributeTo", True ), @@ -416,8 +436,8 @@ def test_copyAttributeDataSet( onPoints: bool, ) -> None: """Test copy of an attribute from one dataset to another.""" - dataSetFrom: vtkMultiBlockDataSet = dataSetTest( "dataset" ) - dataSetTo: vtkMultiBlockDataSet = dataSetTest( "emptydataset" ) + dataSetFrom: vtkDataSet = dataSetTest( "dataset" ) + dataSetTo: vtkDataSet = dataSetTest( "emptydataset" ) # Copy the attribute from the dataSetFrom to the dataSetTo. assert arrayModifiers.copyAttributeDataSet( dataSetFrom, dataSetTo, attributeNameFrom, attributeNameTo, onPoints ) @@ -439,9 +459,9 @@ def test_copyAttributeDataSet( nbComponentsCopied: int = attributeCopied.GetNumberOfComponents() assert nbComponentsCopied == nbComponentsTest if nbComponentsTest > 1: - componentsNamesTest: tuple[ str, ... ] = tuple( + componentsNamesTest: tuple[ str, ...] = tuple( attributeTest.GetComponentName( i ) for i in range( nbComponentsTest ) ) - componentsNamesCopied: tuple[ str, ... ] = tuple( + componentsNamesCopied: tuple[ str, ...] = tuple( attributeCopied.GetComponentName( i ) for i in range( nbComponentsCopied ) ) assert componentsNamesCopied == componentsNamesTest From 3c8f5d681545aadd0c54e53a9b87d1ed45db4d8e Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 23 Jul 2025 10:46:53 +0200 Subject: [PATCH 25/56] Clean doc --- geos-mesh/tests/test_arrayModifiers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index 7df5838a..cf9b6311 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -50,7 +50,7 @@ # Test fill an attribute on point and on cell. ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, np.nan, np.nan, VTK_DOUBLE ), ( 1, "PointAttribute", 3, ( "AX1", "AX2", "AX3" ), True, np.nan, np.nan, VTK_DOUBLE ), - # Test fill attributes with different number of componnent. + # Test fill attributes with different number of component. ( 1, "PORO", 1, (), False, np.nan, np.float32( np.nan ), VTK_FLOAT ), ( 1, "PERM", 3, ( "AX1", "AX2", "AX3" ), False, np.nan, np.float32( np.nan ), VTK_FLOAT ), # Test fill an attribute with default value. @@ -171,7 +171,7 @@ def test_createEmptyAttribute( # Test to create a new attribute on points and on cells. ( "newAttribute", False ), ( "newAttribute", True ), - # Test to create a new attribute whenn an attribute with the same name already exist on the opposit piece. + # Test to create a new attribute when an attribute with the same name already exist on the opposite piece. ( "PORO", True ), # Partial attribute on cells already exist. ( "GLOBAL_IDS_CELLS", True ), # Global attribute on cells already exist. ] ) @@ -202,7 +202,7 @@ def test_createConstantAttributeMultiBlock( "listValues, componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, attributeName", [ # Test attribute names. - ## Test with an attributeName already existing on opposit piece. + ## Test with an attributeName already existing on opposite piece. ( [ np.float64( 42 ) ], (), (), True, VTK_DOUBLE, VTK_DOUBLE, "CellAttribute" ), ( [ np.float64( 42 ) ], (), (), False, VTK_DOUBLE, VTK_DOUBLE, "PointAttribute" ), ## Test with a new attributeName on cells and on points. @@ -300,7 +300,7 @@ def test_createConstantAttributeDataSet( "componentNames, componentNamesTest, onPoints, vtkDataType, vtkDataTypeTest, valueType, attributeName", [ # Test attribute names. - ## Test with an attributeName already existing on opposit piece. + ## Test with an attributeName already existing on opposite piece. ( (), (), True, VTK_DOUBLE, VTK_DOUBLE, "float64", "CellAttribute" ), ( (), (), False, VTK_DOUBLE, VTK_DOUBLE, "float64", "PointAttribute" ), ## Test with a new attributeName on cells and on points. @@ -385,10 +385,10 @@ def test_createAttribute( @pytest.mark.parametrize( "attributeNameFrom, attributeNameTo, onPoints", [ - # Test with global attibutes. + # Test with global attributes. ( "GLOBAL_IDS_POINTS", "GLOBAL_IDS_POINTS_To", True ), ( "GLOBAL_IDS_CELLS", 'GLOBAL_IDS_CELLS_To', False ), - # Test with partial attribute. + # Test with partial attributes. ( "CellAttribute", "CellAttributeTo", False ), ( "PointAttribute", "PointAttributeTo", True ), ] ) From 1d168523285cd5cc4677b9ba68dae2d776bd7b2e Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 23 Jul 2025 10:53:07 +0200 Subject: [PATCH 26/56] Clean for ci --- geos-mesh/src/geos/mesh/utils/arrayHelpers.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index abd5cd42..78e98adf 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -184,14 +184,14 @@ def getAttributesWithNumberOfComponents( object: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataSet, vtkDataObject ], onPoints: bool, ) -> dict[ str, int ]: - """Get the dictionnary of all attributes from object on points or cells. + """Get the dictionary of all attributes from object on points or cells. Args: object (Any): Object where to find the attributes. onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - dict[str, int]: Dictionnary where keys are the names of the attributes and values the number of components. + dict[str, int]: Dictionary where keys are the names of the attributes and values the number of components. """ attributes: dict[ str, int ] if isinstance( object, ( vtkMultiBlockDataSet, vtkCompositeDataSet ) ): @@ -205,14 +205,14 @@ def getAttributesWithNumberOfComponents( def getAttributesFromMultiBlockDataSet( object: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet ], onPoints: bool ) -> dict[ str, int ]: - """Get the dictionnary of all attributes of object on points or on cells. + """Get the dictionary of all attributes of object on points or on cells. Args: object (vtkMultiBlockDataSet | vtkCompositeDataSet): Object where to find the attributes. onPoints (bool): True if attributes are on points, False if they are on cells. Returns: - dict[str, int]: Dictionnary of the names of the attributes as keys, and number of components as values. + dict[str, int]: Dictionary of the names of the attributes as keys, and number of components as values. """ attributes: dict[ str, int ] = {} # initialize data object tree iterator @@ -232,7 +232,7 @@ def getAttributesFromMultiBlockDataSet( object: Union[ vtkMultiBlockDataSet, vtk def getAttributesFromDataSet( object: vtkDataSet, onPoints: bool ) -> dict[ str, int ]: - """Get the dictionnary of all attributes of a vtkDataSet on points or cells. + """Get the dictionary of all attributes of a vtkDataSet on points or cells. Args: object (vtkDataSet): Object where to find the attributes. @@ -256,7 +256,7 @@ def getAttributesFromDataSet( object: vtkDataSet, onPoints: bool ) -> dict[ str, for i in range( nbAttributes ): attributeName: str = data.GetArrayName( i ) attribute: vtkDataArray = data.GetArray( attributeName ) - assert attribute is not None, f"Attribut {attributeName} is null" + assert attribute is not None, f"Attribute {attributeName} is null" nbComponents: int = attribute.GetNumberOfComponents() attributes[ attributeName ] = nbComponents return attributes @@ -342,11 +342,11 @@ def isAttributeGlobal( object: vtkMultiBlockDataSet, attributeName: str, onPoint isOnBlock: bool nbBlock: int = object.GetNumberOfBlocks() for idBlock in range( nbBlock ): - block: vtkDataSet = object.GetBlock( idBlock ) + block: vtkDataSet = cast( vtkDataSet, object.GetBlock( idBlock ) ) isOnBlock = isAttributeInObjectDataSet( block, attributeName, onPoints ) if not isOnBlock: return False - + return True From b4e2084d09a36cad5815aff5d408dc8cd24f2fd3 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 23 Jul 2025 10:58:12 +0200 Subject: [PATCH 27/56] Clean For ci --- geos-mesh/tests/test_arrayHelpers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/geos-mesh/tests/test_arrayHelpers.py b/geos-mesh/tests/test_arrayHelpers.py index ebde5231..35951f74 100644 --- a/geos-mesh/tests/test_arrayHelpers.py +++ b/geos-mesh/tests/test_arrayHelpers.py @@ -80,13 +80,15 @@ def test_isAttributeInObjectDataSet( dataSetTest: vtkDataSet, attributeName: str obtained: bool = arrayHelpers.isAttributeInObjectDataSet( vtkDataset, attributeName, onpoints ) assert obtained == expected + @pytest.mark.parametrize( "attributeName, onpoints, expected", [ ( "PORO", False, False ), ( "GLOBAL_IDS_POINTS", True, True ), ] ) def test_isAttributeGlobal( dataSetTest: vtkMultiBlockDataSet, - attributeName: str, onpoints: bool, + attributeName: str, + onpoints: bool, expected: bool, ) -> None: """Test if the attribute is global or partial.""" @@ -126,7 +128,7 @@ def test_getVtkArrayTypeInMultiBlock( dataSetTest: vtkMultiBlockDataSet, attribu vtkDataTypeTest: int = arrayHelpers.getVtkArrayTypeInMultiBlock( multiBlockDataSet, attributeName, onPoints ) - assert ( vtkDataTypeTest == vtkDataType ) + assert ( vtkDataTypeTest == vtkDataType ) @pytest.mark.parametrize( "attributeName, onPoints", [ From f052c14b2db98ea3525891297b3f0b785fc61fa9 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 23 Jul 2025 11:02:52 +0200 Subject: [PATCH 28/56] Clean For ci --- geos-mesh/tests/conftest.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/geos-mesh/tests/conftest.py b/geos-mesh/tests/conftest.py index 2e5606a2..31058d3c 100644 --- a/geos-mesh/tests/conftest.py +++ b/geos-mesh/tests/conftest.py @@ -39,19 +39,19 @@ def getArrayWithSpeTypeValue() -> Any: """Get a random array of input type with the function _getarray(). Returns: - npt.NDArray[Any]: random array of input type. + npt.NDArray[Any]: Random array of input type. """ def _getarray( nb_component: int, nb_elements: int, valueType: str ) -> Any: """Get a random array of input type. Args: - nb_component (int): nb of components. - nb_elements (int): nb of elements. - valueType (str): the type of the value. + nb_component (int): Nb of components. + nb_elements (int): Nb of elements. + valueType (str): The type of the value. Returns: - npt.NDArray[Any]: random array of input type. + npt.NDArray[Any]: Random array of input type. """ np.random.seed( 28 ) if valueType == "int8": @@ -146,17 +146,17 @@ def dataSetTest() -> Any: """Get a vtkObject from a file with the function _get_dataset(). Returns: - (vtkMultiBlockDataSet, vtkPolyData, vtkDataSet): the vtk object. + (vtkMultiBlockDataSet, vtkPolyData, vtkDataSet): The vtk object. """ def _get_dataset( datasetType: str ) -> Union[ vtkMultiBlockDataSet, vtkPolyData, vtkDataSet ]: """Get a vtkObject from a file. Args: - datasetType (str): the type of vtk object wanted. + datasetType (str): The type of vtk object wanted. Returns: - (vtkMultiBlockDataSet, vtkPolyData, vtkDataSet): the vtk object. + (vtkMultiBlockDataSet, vtkPolyData, vtkDataSet): The vtk object. """ reader: Union[ vtkXMLMultiBlockDataReader, vtkXMLUnstructuredGridReader ] if datasetType == "multiblock": From b6cfe258b65ec845405178708a2856e34b450385 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Mon, 28 Jul 2025 13:19:03 +0200 Subject: [PATCH 29/56] Change the color of info to green --- geos-utils/src/geos/utils/Logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geos-utils/src/geos/utils/Logger.py b/geos-utils/src/geos/utils/Logger.py index 69ec6ec5..00f469f4 100644 --- a/geos-utils/src/geos/utils/Logger.py +++ b/geos-utils/src/geos/utils/Logger.py @@ -78,7 +78,7 @@ class CustomLoggerFormatter( logging.Formatter ): #: format for each logger output type with colors FORMATS_COLOR: dict[ int, str ] = { DEBUG: grey + format2 + reset, - INFO: grey + format1 + reset, + INFO: green + format1 + reset, WARNING: yellow + format1 + reset, ERROR: red + format1 + reset, CRITICAL: bold_red + format2 + reset, From 3cdda2f603f1d848351f4736b80564a68311266c Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Mon, 28 Jul 2025 13:20:38 +0200 Subject: [PATCH 30/56] create the filter to use without paraview --- .../CreateConstantAttributePerRegion.py | 261 ++++++++++++------ 1 file changed, 173 insertions(+), 88 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py index 01a1dcb1..7694ae25 100644 --- a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py +++ b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py @@ -3,55 +3,110 @@ # SPDX-FileContributor: Romain Baville import numpy as np import numpy.typing as npt +import logging from typing import Union, Any from typing_extensions import Self import vtkmodules.util.numpy_support as vnp - -from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] - VTKPythonAlgorithmBase, -) +from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import ( vtkInformation, vtkInformationVector, ) from vtkmodules.vtkCommonDataModel import ( vtkMultiBlockDataSet, - vtkUnstructuredGrid, vtkDataSet, ) -from geos.utils.Logger import Logger, getLogger -from geos.mesh.utils.arrayHelpers import isAttributeInObject +from geos.utils.Logger import getLogger, Logger +from geos.mesh.utils.arrayHelpers import isAttributeInObject, getNumberOfComponents, getArrayInObject, isAttributeGlobal from geos.mesh.utils.arrayModifiers import createAttribute __doc__ = """ -TO DO +CreateConstantAttributePerRegion is a vtk filter that allows to create an attribute +with constant values for each chosen indexes of a reference/region attribute. +The region attribute has to have one component and the created attribute has one component. +Regions indexes, values and values types are choose by the user, for the other region index +values are set to nan or -1 if int type. + +Input and output meshes are either vtkMultiBlockDataSet or vtkDataSet. +The value type is encoded by a int using the vtk typecode to preserve the coherency +(https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/numpy_support.py). +The relation index/value is given by a dictionary. Its keys are the indexes and its items are values. +If you have a specific handler for your logger you can set the variable speHandler to True and use the +member function addLoggerHandler (useful for paraview for example). + +To use it: + +.. code-block:: python + + from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion + + # filter inputs + input_mesh: Union[vtkMultiBlockDataSet, vtkDataSet] + regionName: str + newAttributeName: str + dictRegion: dict[Any, Any] + valueType: int, optional defaults to 10 (float32) + speHandler: bool, optional defaults to False + + # instantiate the filter + filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( regionName, + newAttributeName, + dictRegion, + valueType, + speHandler, + ) + # Set your specific handler (only if speHandler is True) + filter.addLoggerHandler( YourHandler ) + # Set the mesh + filter.SetInputDataObject( input_mesh ) + # Do calculations + filter.Update() + + # get output object + output: Union[vtkMultiBlockDataSet, vtkDataSet] = filter.GetOutputDataObject( 0 ) """ - +loggerTitle: str = "Create constant attribute per region" class CreateConstantAttributePerRegion( VTKPythonAlgorithmBase ): - def __init__( self: Self ) -> None: - """Create an attribute with constant value per region.""" - super().__init__( nInputPorts=1, nOutputPorts=1, outputType="vtkMultiBlockDataSet, vtkDataSet" ) - - self._SetRegionName() - self._SetAttributeName() - self._SetInfoRegion() - - # logger - self.m_logger: Logger = getLogger( "Create Constant Attribute Per Region Filter" ) - - def SetLogger( self: Self, logger: Logger ) -> None: - """Set filter logger. - + def __init__( + self: Self, + regionName: str, + newAttributeName: str, + dictRegion: dict[ Any, Any ], + valueType: int = 10, + speHandler: bool = False, + ) -> None: + """Create an attribute with constant value per region. + An intern logger is used in this filter. If you want to personalize it, you can user a handler of yours that will + be add at the place of the intern handler. The level by fault of the logger is INFO. + Args: - logger (Logger): logger + regionName (str): The name of the attribute with the region indexes. + newAttributeName (str): The name of the new attribute to create. + dictRegion (dict[ Any, Any ]): The dictionary with the region indexes as keys and their values as items. + valueType (int, optional): The type of the value using the vtk typecode. + Defaults to 10. + speHandler (bool, optional): True if you want to use a specific handler of yours, False otherwise. + Defaults to False. """ - self.m_logger = logger + super().__init__( nInputPorts=1, nOutputPorts=1, + inputType="vtkDataObject", outputType="vtkDataObject" ) + + self.regionName: str = regionName + self.newAttributeName: str = newAttributeName + self.setInfoRegion( dictRegion, valueType ) + + # Logger + if not speHandler: + self.m_logger: Logger = getLogger( loggerTitle, True ) + else: + self.m_logger: Logger = logging.getLogger( loggerTitle ) + self.m_logger.setLevel( logging.INFO ) def RequestDataObject( self: Self, @@ -78,123 +133,153 @@ def RequestDataObject( return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] def RequestData( - self: Self, - request: vtkInformation, # noqa: F841 - inInfoVec: list[ vtkInformationVector ], # noqa: F841 - outInfoVec: vtkInformationVector, # noqa: F841 + self: Self, + request: vtkInformation, # noqa: F841 + inInfoVec: list[ vtkInformationVector ], + outInfoVec: vtkInformationVector, ) -> int: """Inherited from VTKPythonAlgorithmBase::RequestData. Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects + request (vtkInformation): Request + inInfoVec (list[vtkInformationVector]): Input objects + outInfoVec (vtkInformationVector): Output objects Returns: int: 1 if calculation successfully ended, 0 otherwise. """ - self.m_logger.info( f"Apply filter {__name__}" ) + self.m_logger.info( f"Apply filter { self.m_logger.name }." ) + mess: str try: - input0: Union[ vtkUnstructuredGrid, vtkMultiBlockDataSet ] = ( self.GetInputData( inInfoVec, 0, 0 ) ) - output: Union[ vtkUnstructuredGrid, vtkMultiBlockDataSet ] = ( self.GetOutputData( outInfoVec, 0 ) ) - - assert input0 is not None, "Input Surface is null." - assert output is not None, "Output pipeline is null." - - output.ShallowCopy( input0 ) - assert ( len( self.regionName ) > 0 ), "Region attribute is undefined, please select an attribute." + + inputMesh: Union[ vtkDataSet, vtkMultiBlockDataSet ] = self.GetInputData( inInfoVec, 0, 0 ) + outData: Union[ vtkDataSet, vtkMultiBlockDataSet ] = self.GetOutputData( outInfoVec, 0 ) + assert inputMesh is not None, "Input mesh is null." + assert outData is not None, "Output pipeline is null." + + outData.ShallowCopy( inputMesh ) + # Fields data ? onPoints: bool - if isAttributeInObject( input0, self.regionName, False ): + isOnMesh: bool = False + if isAttributeInObject( inputMesh, self.regionName, False ): onPoints = False - elif isAttributeInObject( input0, self.regionName, True ): + isOnMesh = True + elif isAttributeInObject( inputMesh, self.regionName, True ): onPoints = True - else: - mess = f"{self.regionName} is not in the mesh." - self.m_logger.info( mess ) - return 0 + isOnMesh = True + + assert isOnMesh, f"{ self.regionName } is not in the mesh." + nbComponents: int = getNumberOfComponents( inputMesh, self.regionName, onPoints ) + assert nbComponents == 1, "The region attribute has to have only one component" + regionNpArray: npt.NDArray[ Any ] npArray: npt.NDArray[ Any ] - if isinstance( output, vtkMultiBlockDataSet ): - nbBlock: int = output.GetNumberOfBlocks() + if isinstance( inputMesh, vtkMultiBlockDataSet ): + assert isAttributeGlobal( inputMesh, self.regionName, onPoints ), "Region attribute has to be global" + nbBlock: int = outData.GetNumberOfBlocks() for idBlock in range( nbBlock ): - dataSetOutput: vtkDataSet = output.GetBlock( idBlock ) - dataSetInput0: vtkDataSet = input0.GetBlock( idBlock ) - if onPoints: - regionNpArray = vnp.vtk_to_numpy( dataSetInput0.GetPointData().GetArray( self.regionName ) ) - else: - regionNpArray = vnp.vtk_to_numpy( dataSetInput0.GetCellData().GetArray( self.regionName ) ) + dataSetOutput: vtkDataSet = outData.GetBlock( idBlock ) + dataSetInput0: vtkDataSet = inputMesh.GetBlock( idBlock ) + regionNpArray = getArrayInObject( dataSetInput0, self.regionName, onPoints ) npArray = self.createNpArray( regionNpArray ) - createAttribute( dataSetOutput, npArray, self.attributeName, onPoints=onPoints ) + createAttribute( dataSetOutput, npArray, self.newAttributeName, onPoints=onPoints ) else: - if onPoints: - regionNpArray = vnp.vtk_to_numpy( input0.GetPointData().GetArray( self.regionName ) ) - else: - regionNpArray = vnp.vtk_to_numpy( input0.GetCellData().GetArray( self.regionName ) ) + regionNpArray = getArrayInObject( inputMesh, self.regionName, onPoints ) npArray = self.createNpArray( regionNpArray ) - createAttribute( output, npArray, self.attributeName, onPoints=onPoints ) - - mess: str = ( f"The new attribute {self.attributeName} was successfully added." ) + createAttribute( outData, npArray, self.newAttributeName, onPoints=onPoints ) + + regionIndexes: list[ Any ] = self.dictRegion.keys() + indexValuesMess: str = "" + for index in regionIndexes: + value: Any = self.dictRegion[ index ] + indexValuesMess = "index " + indexValuesMess + str( index ) + ": " + str( value ) + " " + + if indexValuesMess == "": + indexValuesMess = f"No index or no value enter, the new attribute is constant with the value {self.defaultValue}." + self.m_logger.warning( indexValuesMess ) + + else: + indexValuesMess = indexValuesMess + "other indexes: " + str( self.defaultValue ) + "." + + mess = f"The attribute { self.regionName } allows to create the new attribute { self.newAttributeName } with the following constant values per region indexes: { indexValuesMess }" self.Modified() self.m_logger.info( mess ) except AssertionError as e: - mess1: str = "The new attribute was not added due to:" - self.m_logger.error( mess1 ) - self.m_logger.error( e, exc_info=True ) + mess = f"The filter { self.m_logger.name } failed due to:" + self.m_logger.error( mess ) + self.m_logger.error( e, exc_info=True) return 0 except Exception as e: - mess0: str = "The new attribute was not added due to:" - self.m_logger.critical( mess0 ) - self.m_logger.critical( e, exc_info=True ) + mess = f"The filter { self.m_logger.name } failed due to:" + self.m_logger.critical( mess ) + self.m_logger.critical( e, exc_info=True) return 0 return 1 - def _SetRegionName( self: Self, regionName: str = "" ) -> None: - self.regionName: str = regionName - - - def _SetAttributeName( self: Self, attributeName: str = "Attribute" ) -> None: - self.attributeName: str = attributeName + def setInfoRegion( self: Self, dictRegion: dict[ Any, Any ], valueType: int ) -> None: + """Set attributes self.valueType, self.dictRegion and self.defaultValue. + The type of the constant values and the default value are set with value type read with numpy. + The default value is set to nan if the type is float or double, -1 otherwise. - - def _SetInfoRegion( self: Self, dictRegion: dict[ Any, Any ] = {}, valueType: int = 10, defaultValue: Any = np.nan ) -> None: + Args: + dictRegion (dict[Any, Any]): The dictionary with the indexes and its constant value. + valueType (int): The type of the constant value with the VTK typecode. + """ + # Get the numpy type from the vtk typecode. dictType: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() self.valueType: type = dictType[ valueType ] + # Set the correct type of the items to ensure the coherency. self.dictRegion: dict[ Any, Any ] = dictRegion for idRegion in self.dictRegion.keys(): self.dictRegion[ idRegion ] = self.valueType( self.dictRegion[ idRegion ] ) - if np.isnan( defaultValue ): - if valueType not in [ 10, 11 ]: - defaultValue = -1 - - self.defaultValue = self.valueType(defaultValue) + # Set the default value depending of the type. + if valueType not in [ 10, 11 ]: + self.defaultValue = self.valueType( -1 ) + else: + self.defaultValue = self.valueType( np.nan ) + + + def setLoggerHandler( self: Self, handler: logging.Handler ) -> None: + """Set a specific handler for the logger of the filter. + 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.m_logger.hasHandlers(): + self.m_logger.addHandler( handler ) + else: + self.m_logger.warning( "The logger already has a handler, to use yours set the argument 'speHandler' to True during the filter initialization" ) def createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArray[ Any ]: """Create numpy arrays from input data. Args: - regionNpArray (npt.NDArray[ Any ]): Region attribute + regionNpArray (npt.NDArray[Any]): Region attribute. + regionVTKArrayType (int): The type of the vtk array. Returns: - npt.NDArray[np.float64]: numpy array of the new attribute. + npt.NDArray[np.float64]: Numpy array of the new attribute. """ - nbElements: int = len ( regionNpArray ) + nbElements: int = len( regionNpArray ) npArray: npt.NDArray[ Any ] = np.ones( nbElements, self.valueType ) for elem in range( nbElements ): - idRegion: Any = regionNpArray[elem] + idRegion: Any = regionNpArray[ elem ] if idRegion in self.dictRegion.keys(): - npArray[elem] = self.dictRegion[idRegion] + npArray[ elem ] = self.dictRegion[ idRegion ] else: - npArray[elem] = self.defaultValue + npArray[ elem ] = self.defaultValue return npArray From 3c82b65cde8e351b6ee112116882a867aba605ef Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Mon, 28 Jul 2025 13:23:50 +0200 Subject: [PATCH 31/56] Move the paraview plugin to the good folder --- .../PVCreateConstantAttributePerRegion.py | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py diff --git a/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py b/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py new file mode 100644 index 00000000..a30da630 --- /dev/null +++ b/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py @@ -0,0 +1,293 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Martin Lemay, Romain Baville +# ruff: noqa: E402 # disable Module level import not at top of file +import sys +import numpy +from pathlib import Path +from typing import Union, Any + +from typing_extensions import Self + +from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] + smdomain, smhint, smproperty, smproxy, +) # source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/util/vtkAlgorithm.py +from paraview.detail.loghandler import ( # type: ignore[import-not-found] + VTKHandler, +) # source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/detail/loghandler.py + +from vtk import VTK_DOUBLE # type: ignore[import-untyped] +from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase +from vtkmodules.vtkCommonCore import ( + vtkInformation, + vtkInformationVector, +) +from vtkmodules.vtkCommonDataModel import ( + vtkMultiBlockDataSet, + vtkDataSet, +) + + +# update sys.path to load all GEOS Python Package dependencies +geos_pv_path: Path = Path( __file__ ).parent.parent.parent.parent.parent +sys.path.insert( 0, str( geos_pv_path / "src" ) ) +from geos.pv.utils.config import update_paths + +update_paths() + +from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion + +__doc__ = """ +PVCreateConstantAttributePerRegion is a paraview Plugin that allows to create an attribute +with constant values for each chosen indexes of a reference/region attribute. +The region attribute has to have one component and the created attribute has one component. +Regions indexes, values and values types are choose by the user, for the other region index +values are set to nan or -1 if int type. + +Input and output meshes are either vtkMultiBlockDataSet or vtkDataSet. + +To use it: + +* Load the module in Paraview: Tools>Manage Plugins...>Load new>PVCreateConstantAttributePerRegion. +* Select the mesh you want to create the attributes and containing a region attribute. +* Select the filter Create Constant Attribute Per Region in filter|0- Geos Pre-processing. +* Set variables (region attribute, value type, attribute name, index and its value) and Apply. + +""" + +@smproxy.filter( + name="PVCreateConstantAttributePerRegion", + label="Create Constant Attribute Per Region", +) +@smhint.xml( """""" ) +@smproperty.input( name="Input", port_index=0 ) +@smdomain.datatype( + dataTypes=[ "vtkMultiBlockDataSet", "vtkDataSet" ], + composite_data_supported=True, +) +class PVCreateConstantAttributePerRegion( VTKPythonAlgorithmBase ): + + def __init__( self: Self ) -> None: + """Create an attribute with constant value per region.""" + super().__init__( nInputPorts=1, + nOutputPorts=1, + inputType="vtkDataObject", + outputType="vtkDataObject" ) + + self.dictRegion: dict[ Any, Any ] = {} + self.clearDictRegion: bool = True + self.regionName: str = "Region" + self.newAttributeName: str = "newAttribute" + self.valueType: int = 10 + + @smproperty.xml( """ + + + Name of the new attribute + + + """ ) + def a02SetAttributeName( self: Self, value: str ) -> None: + """Set attribute name. + + Args: + value (str): attribute name. + """ + if self.newAttributeName != value: + self.newAttributeName = value + self.Modified() + + @smproperty.intvector( + name="ValueType", + label="Values type", + number_of_elements=1, + default_values=10, + panel_visibility="default", + ) + @smdomain.xml( """ + + + + + + + + + + + + + + + The values type of the attribute. Each type is encoded by a int using the vtk typecode. + + """ ) + def a02IntSingle( self: Self, value: int ) -> None: + """Define an input int field. + + Args: + value (int): Input + """ + if value != self.valueType: + self.valueType = value + self.Modified() + + @smproperty.xml( """ + + + + """ ) + def b02GroupFlow1( self: Self ) -> None: + """Organize groups.""" + self.Modified() + + @smproperty.stringvector( + name="ChooseRegionAttribute", + label="Attribute with region indexes", + command="a01SetRegionAttributeName", + default_values="Choose an attribute", + number_of_elements="1", + element_types="2", + ) + @smdomain.xml(""" + + + + + + + Select an attribute containing the indexes of the regions + + + + + """ ) + def a01SetRegionAttributeName( self: Self, name: str ) -> None: + """Set region attribute name.""" + if self.regionName != name: + self.regionName = name + self.Modified() + + @smproperty.xml(""" + + + + + + + + + + Set the constant value of the new attribute for each region indexes. + + + """ ) + def b01SetAttributeValues( self: Self, regionIndex: str, value: str ) -> None: + """Set the constant value of the new attribute for each region indexes. + + Args: + regionIndex (int): Region index. + value (float): Attribute constant value for the regionIndex. + """ + if self.clearDictRegion: + self.dictRegion = {} + self.clearDictRegion = False + + if regionIndex != None and value != None : + assert "," not in regionIndex, "Use the '.' not the ',' for decimal numbers" + assert "," not in value, "Use the '.' not the ',' for decimal numbers" + regionIndex = float( regionIndex ) + value = float( value ) + if regionIndex not in self.dictRegion.keys(): + self.dictRegion[regionIndex] = value + self.Modified() + + @smproperty.xml( """ + + + + """ ) + def b02GroupFlow( self: Self ) -> None: + """Organize groups.""" + self.Modified() + + def RequestDataObject( + self: Self, + request: vtkInformation, + inInfoVec: list[ vtkInformationVector ], + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestDataObject. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + inData = self.GetInputData( inInfoVec, 0, 0 ) + outData = self.GetOutputData( outInfoVec, 0 ) + assert inData is not None + if outData is None or ( not outData.IsA( inData.GetClassName() ) ): + outData = inData.NewInstance() + outInfoVec.GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), outData ) + return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] + + def RequestData( + self: Self, + request: vtkInformation, # noqa: F841 + inInfoVec: list[ vtkInformationVector ], # noqa: F841 + outInfoVec: vtkInformationVector, # noqa: F841 + ) -> 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. + """ + inputMesh: Union[ vtkDataSet, vtkMultiBlockDataSet ] = ( self.GetInputData( inInfoVec, 0, 0 ) ) + outputMesh: Union[ vtkDataSet, vtkMultiBlockDataSet ] = ( self.GetOutputData( outInfoVec, 0 ) ) + + assert inputMesh is not None, "Input Surface is null." + assert outputMesh is not None, "Output pipeline is null." + + filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( self.regionName, + self.newAttributeName, + self.dictRegion, + self.valueType, + True, ) + vtkHandler: VTKHandler = VTKHandler() + filter.setLoggerHandler( vtkHandler ) + filter.SetInputDataObject( inputMesh ) + + filter.Update() + outputMesh.ShallowCopy( filter.GetOutputDataObject( 0 ) ) + + + self.clearDictRegion = True + + return 1 + From 75c1cdffae1bcbfb33978acc4ba0207f026d057e Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Mon, 28 Jul 2025 13:25:48 +0200 Subject: [PATCH 32/56] Some test for the filter CreateConstanteAttributePerRegion --- .../test_CreateConstantAttributePerRegion.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 geos-mesh/tests/test_CreateConstantAttributePerRegion.py diff --git a/geos-mesh/tests/test_CreateConstantAttributePerRegion.py b/geos-mesh/tests/test_CreateConstantAttributePerRegion.py new file mode 100644 index 00000000..b8d31751 --- /dev/null +++ b/geos-mesh/tests/test_CreateConstantAttributePerRegion.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Romain Baville +# SPDX-License-Identifier: Apache 2.0 +# ruff: noqa: E402 # disable Module level import not at top of file +# mypy: disable-error-code="operator" +import pytest +import os +from typing import Union, Tuple, cast, Any + +import numpy as np +import numpy.typing as npt +from geos.utils.Logger import * + +import vtkmodules.util.numpy_support as vnp +from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet, vtkPointData, vtkCellData ) +from vtkmodules.vtkIOXML import vtkXMLUnstructuredGridReader, vtkXMLMultiBlockDataReader + +from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion + +datasetType: str = "dataset" + +reader: Union[ vtkXMLMultiBlockDataReader, vtkXMLUnstructuredGridReader ] +if datasetType == "multiblock": + reader = vtkXMLMultiBlockDataReader() + vtkFilename = "data/displacedFault.vtm" +elif datasetType == "dataset": + reader = vtkXMLUnstructuredGridReader() + vtkFilename = "data/domain_res5_id.vtu" +elif datasetType == "polydata": + reader = vtkXMLUnstructuredGridReader() + vtkFilename = "data/surface.vtu" + +datapath: str = os.path.join( os.path.dirname( os.path.realpath( __file__ ) ), vtkFilename ) +reader.SetFileName( datapath ) +reader.Update() + +input_mesh: Union[vtkMultiBlockDataSet, vtkDataSet] = reader.GetOutput() +regionName: str = "PORO" +newAttributeName: str = "Test" +dictRegion: dict[Any, Any] = {} +valueType: int = 11 +use_color = True + + # instantiate the filter +filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( regionName, + newAttributeName, + dictRegion, + valueType, + use_color, ) +ch = logging.StreamHandler() +ch.setFormatter( CustomLoggerFormatter( use_color ) ) +filter.setLoggerHandler( ch ) + # Set the mesh +filter.SetInputDataObject( input_mesh ) + # Do calculations +filter.Update() + + # get output object +output: Union[vtkMultiBlockDataSet, vtkDataSet] = filter.GetOutputDataObject( 0 ) \ No newline at end of file From 0728db781e89596762c6bcf6dfa1710e019fd4d1 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Mon, 28 Jul 2025 13:26:56 +0200 Subject: [PATCH 33/56] adapte some utils function for the filtre CreateConstanteAttributePerRegion --- geos-mesh/src/geos/mesh/utils/arrayHelpers.py | 22 ++ .../src/geos/mesh/utils/arrayModifiers.py | 3 +- .../PVCreateConstantAttributePerRegion.py | 250 ------------------ 3 files changed, 24 insertions(+), 251 deletions(-) delete mode 100644 geos-pv/src/PVplugins/PVCreateConstantAttributePerRegion.py diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index 4498203f..3a2bb484 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -342,6 +342,28 @@ def isAttributeInObjectDataSet( object: vtkDataSet, attributeName: str, onPoints assert data is not None, f"{sup} data was not recovered." return bool( data.HasArray( attributeName ) ) +def isAttributeGlobal( object: vtkMultiBlockDataSet, attributeName: str, onPoints: bool ) -> bool: + """Check if an attribute is global in the input vtkMultiBlockDataSet. + + Args: + object (vtkMultiBlockDataSet): input object + attributeName (str): name of the attribute + onPoints (bool): True if attributes are on points, False if they are + on cells. + + Returns: + bool: True if the attribute is global, False if not. + """ + isOnBlock: list[ bool ] = [] + nbBlock: int = object.GetNumberOfBlocks() + for idBlock in range( nbBlock ): + block: vtkDataSet = object.GetBlock( idBlock ) + isOnBlock.append( isAttributeInObjectDataSet( block, attributeName, onPoints ) ) + + if False in isOnBlock: + return False + + return True def getArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> npt.NDArray[ Any ]: """Return the numpy array corresponding to input attribute name in table. diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index df530189..c3b7c4f7 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -319,6 +319,7 @@ def createAttribute( bool: True if the attribute was correctly created. """ assert isinstance( dataSet, vtkDataSet ), "Attribute can only be created in vtkDataSet object." + assert not isAttributeInObject( dataSet, attributeName, onPoints ), f"The attribute { attributeName } already exist in the mesh" createdAttribute: vtkDataArray = vnp.numpy_to_vtk( array, deep=True, array_type=vtkDataType ) createdAttribute.SetName( attributeName ) @@ -329,7 +330,7 @@ def createAttribute( if nbNames < nbComponents: componentNames = tuple( [ "Component" + str( i ) for i in range( nbComponents ) ] ) - print( "Not enough component name enter, component names are seted to : Component0, Component1 ..." ) + print( "Not enough component name enter, component names are set to : Component0, Component1 ..." ) elif nbNames > nbComponents: print( "To many component names enter, the lastest will not be taken into account." ) diff --git a/geos-pv/src/PVplugins/PVCreateConstantAttributePerRegion.py b/geos-pv/src/PVplugins/PVCreateConstantAttributePerRegion.py deleted file mode 100644 index 7001576f..00000000 --- a/geos-pv/src/PVplugins/PVCreateConstantAttributePerRegion.py +++ /dev/null @@ -1,250 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. -# SPDX-FileContributor: Martin Lemay, Romain Baville -# ruff: noqa: E402 # disable Module level import not at top of file -import sys -from pathlib import Path -from typing import Union - -import numpy as np -import numpy.typing as npt -from typing_extensions import Self - -import vtkmodules.util.numpy_support as vnp -from geos.utils.Logger import Logger, getLogger -from geos.mesh.utils.multiblockHelpers import ( - getBlockElementIndexesFlatten, - getBlockFromFlatIndex, -) -from geos.mesh.utils.arrayHelpers import isAttributeInObject -from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] - VTKPythonAlgorithmBase, smdomain, smhint, smproperty, smproxy, -) -from vtk import VTK_DOUBLE # type: ignore[import-untyped] -from vtkmodules.vtkCommonCore import ( - vtkDataArray, - vtkInformation, - vtkInformationVector, -) -from vtkmodules.vtkCommonDataModel import ( - vtkDataObject, - vtkMultiBlockDataSet, - vtkUnstructuredGrid, -) - -# update sys.path to load all GEOS Python Package dependencies -geos_pv_path: Path = Path( __file__ ).parent.parent.parent -sys.path.insert( 0, str( geos_pv_path / "src" ) ) -from geos.pv.utils.config import update_paths - -update_paths() - -from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion - -__doc__ = """ -PVCreateConstantAttributePerRegion is a Paraview plugin that allows to -create 2 attributes whom values are constant for each region index. - -Input and output are either vtkMultiBlockDataSet or vtkUnstructuredGrid. - -To use it: - -* Load the module in Paraview: Tools>Manage Plugins...>Load new>PVCreateConstantAttributePerRegion. -* Select the mesh you want to create the attributes and containing a region attribute. -* Search and Apply Create Constant Attribute Per Region Filter. - -""" - -SOURCE_NAME: str = "" -DEFAULT_REGION_ATTRIBUTE_NAME = "region" - - -@smproxy.filter( - name="PVCreateConstantAttributePerRegion", - label="Create Constant Attribute Per Region", -) -@smhint.xml( """""" ) -@smproperty.input( name="Input", port_index=0 ) -@smdomain.datatype( - dataTypes=[ "vtkMultiBlockDataSet", "vtkUnstructuredGrid" ], - composite_data_supported=True, -) -class PVCreateConstantAttributePerRegion( VTKPythonAlgorithmBase ): - - def __init__( self: Self ) -> None: - """Create an attribute with constant value per region.""" - super().__init__( nInputPorts=1, nOutputPorts=1, outputType="vtkDataSet" ) - - self.m_table: list[ tuple[ int, float ] ] = [] - self.m_regionAttributeName: str = DEFAULT_REGION_ATTRIBUTE_NAME - self.m_attributeName: str = "attribute" - - # logger - self.m_logger: Logger = getLogger( "Create Constant Attribute Per Region Filter" ) - - def SetLogger( self: Self, logger: Logger ) -> None: - """Set filter logger. - - Args: - logger (Logger): logger - """ - self.m_logger = logger - - @smproperty.xml( """ - - - - - - - - Select an attribute containing the indexes of the regions - - - """ ) - def a01SetRegionAttributeName( self: Self, name: str ) -> None: - """Set region attribute name.""" - self.m_regionAttributeName = name - self.Modified() - - @smproperty.xml( """ - - - Name of the new attribute - - - """ ) - def a02SetAttributeName( self: Self, value: str ) -> None: - """Set attribute name. - - Args: - value (str): attribute name. - """ - self.m_attributeName = value - self.Modified() - - @smproperty.xml( """ - - - - - - - - - Set new attributes values for each region index. - - - """ ) - def b01SetAttributeValues( self: Self, regionIndex: int, value: float ) -> None: - """Set attribute values per region. - - Args: - regionIndex (int): region index. - - value (float): attribute value. - """ - self.m_table.append( ( regionIndex, value ) ) - self.Modified() - - @smproperty.xml( """ - - """ ) - def b02GroupFlow( self: Self ) -> None: - """Organize groups.""" - self.Modified() - - def RequestDataObject( - self: Self, - request: vtkInformation, - inInfoVec: list[ vtkInformationVector ], - outInfoVec: vtkInformationVector, - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestDataObject. - - Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - inData = self.GetInputData( inInfoVec, 0, 0 ) - outData = self.GetOutputData( outInfoVec, 0 ) - assert inData is not None - if outData is None or ( not outData.IsA( inData.GetClassName() ) ): - outData = inData.NewInstance() - outInfoVec.GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), outData ) - return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] - - def RequestData( - self: Self, - request: vtkInformation, # noqa: F841 - inInfoVec: list[ vtkInformationVector ], # noqa: F841 - outInfoVec: vtkInformationVector, # noqa: F841 - ) -> 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. - """ - self.m_logger.info( f"Apply filter {__name__}" ) - try: - input0: Union[ vtkUnstructuredGrid, vtkMultiBlockDataSet ] = ( self.GetInputData( inInfoVec, 0, 0 ) ) - output: Union[ vtkUnstructuredGrid, vtkMultiBlockDataSet ] = ( self.GetOutputData( outInfoVec, 0 ) ) - - assert input0 is not None, "Input Surface is null." - assert output is not None, "Output pipeline is null." - - output.ShallowCopy( input0 ) - - assert ( len( self.m_regionAttributeName ) - > 0 ), "Region attribute is undefined, please select an attribute." - if isinstance( output, vtkMultiBlockDataSet ): - self.createAttributesMultiBlock( output ) - else: - self.createAttributes( output ) - - mess: str = ( f"The new attribute {self.m_attributeName} was successfully added." ) - self.Modified() - self.m_logger.info( mess ) - except AssertionError as e: - mess1: str = "The new attribute was not added due to:" - self.m_logger.error( mess1 ) - self.m_logger.error( e, exc_info=True ) - return 0 - except Exception as e: - mess0: str = "The new attribute was not added due to:" - self.m_logger.critical( mess0 ) - self.m_logger.critical( e, exc_info=True ) - return 0 - self.m_compute = True - return 1 - From 6fc4f5dd00ed3e389ae9b00e12f85b6aa80afe0f Mon Sep 17 00:00:00 2001 From: Romain Baville <126683264+RomainBaville@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:39:09 +0200 Subject: [PATCH 34/56] Apply suggestions from Paloma's code review Co-authored-by: paloma-martinez <104762252+paloma-martinez@users.noreply.github.com> --- .../src/geos/mesh/utils/arrayModifiers.py | 24 +++++++++---------- geos-mesh/tests/conftest.py | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index 689319ee..ba76a973 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -71,7 +71,7 @@ def fillPartialAttributes( attributeName (str): Attribute name. onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. - value (Any, optional): Filling value. It is better to use numpy scalar type for the values. + value (Any, optional): Filling value. It is recommended to use numpy scalar type for the values. Defaults to: -1 for int VTK arrays. 0 for uint VTK arrays. @@ -222,7 +222,7 @@ def createConstantAttribute( Args: object (vtkDataObject): Object (vtkMultiBlockDataSet, vtkDataSet) where to create the attribute. - listValues (list[Any]): List of values of the attribute for each components. It is better to use numpy scalar type for the values. + listValues (list[Any]): List of values of the attribute for each components. It is recommended to use numpy scalar type for the values. attributeName (str): Name of the attribute. componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. @@ -231,7 +231,7 @@ def createConstantAttribute( vtkDataType (Union[int, None], optional): Vtk data type of the attribute to create. Defaults to None, the vtk data type is given by the type of the values. - Warning with int8, uint8 and int64 type of value, the vtk data type corresponding are multiples. By default: + Warning with int8, uint8 and int64 type of value, the corresponding vtk data type are multiples. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG @@ -270,11 +270,11 @@ def createConstantAttributeMultiBlock( vtkDataType: Union[ int, None ] = None, logger: Union[ Logger, None ] = None, ) -> bool: - """Create a new attribute with a constant value per component on every blocks of the multiBlockDataSet. + """Create a new attribute with a constant value per component on every block of the multiBlockDataSet. Args: multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet): MultiBlockDataSet where to create the attribute. - listValues (list[Any]): List of values of the attribute for each components. It is better to use numpy scalar type for the values. + listValues (list[Any]): List of values of the attribute for each components. It is recommended to use numpy scalar type for the values. attributeName (str): Name of the attribute. componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. @@ -283,7 +283,7 @@ def createConstantAttributeMultiBlock( vtkDataType (Union[int, None], optional): Vtk data type of the attribute to create. Defaults to None, the vtk data type is given by the type of the values. - Warning with int8, uint8 and int64 type of value, the vtk data type corresponding are multiples. By default: + Warning with int8, uint8 and int64 type of value, the corresponding vtk data type are multiples. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG @@ -348,7 +348,7 @@ def createConstantAttributeDataSet( Args: dataSet (vtkDataSet): DataSet where to create the attribute. - listValues (list[Any]): List of values of the attribute for each components. It is better to use numpy scalar type for the values. + listValues (list[Any]): List of values of the attribute for each components. It is recommended to use numpy scalar type for the values. attributeName (str): Name of the attribute. componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. Defaults to an empty tuple. @@ -357,7 +357,7 @@ def createConstantAttributeDataSet( vtkDataType (Union[int, None], optional): Vtk data type of the attribute to create. Defaults to None, the vtk data type is given by the type of the values. - Warning with int8, uint8 and int64 type of value, the vtk data type corresponding are multiples. By default: + Warning with int8, uint8 and int64 type of value, the corresponding vtk data type are multiples. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG @@ -376,7 +376,7 @@ def createConstantAttributeDataSet( for value in listValues: valueTypeTest: type = type( value ) if valueType != valueTypeTest: - logger.error( "All values in the list of values have not the same type." ) + logger.error( "All values in the list of values don't have the same type." ) logger.error( f"The constant attribute { attributeName } has not been created into the mesh." ) return False @@ -386,10 +386,10 @@ def createConstantAttributeDataSet( logger.warning( f"During the creation of the constant attribute { attributeName }, values will be converted from { valueType } to { npType }." ) - logger.warning( "To avoid any issue with the conversion use directly numpy scalar type for the values" ) + logger.warning( "To avoid any issue with the conversion, please use directly numpy scalar type for the values" ) valueType = npType - # Check the coherency between the given value type and the vtk array type if it exist. + # Check the consistency between the given value type and the vtk array type if it exists. valueType = valueType().dtype if vtkDataType is not None: vtkNumpyTypeMap: dict[ int, type ] = vnp.get_vtk_to_numpy_typemap() @@ -439,7 +439,7 @@ def createAttribute( vtkDataType (Union[int, None], optional): Vtk data type of the attribute to create. Defaults to None, the vtk data type is given by the type of the array. - Warning with int8, uint8 and int64 type, the vtk data type corresponding are multiples. By default: + Warning with int8, uint8 and int64 type, the corresponding vtk data type are multiples. By default: - int8 -> VTK_SIGNED_CHAR - uint8 -> VTK_UNSIGNED_CHAR - int64 -> VTK_LONG_LONG diff --git a/geos-mesh/tests/conftest.py b/geos-mesh/tests/conftest.py index 31058d3c..9cff83d5 100644 --- a/geos-mesh/tests/conftest.py +++ b/geos-mesh/tests/conftest.py @@ -46,8 +46,8 @@ def _getarray( nb_component: int, nb_elements: int, valueType: str ) -> Any: """Get a random array of input type. Args: - nb_component (int): Nb of components. - nb_elements (int): Nb of elements. + nb_component (int): Number of components. + nb_elements (int): Number of elements. valueType (str): The type of the value. Returns: From 36d715fe381e7f6a168de0d7c107db93d9174043 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Mon, 28 Jul 2025 14:53:13 +0200 Subject: [PATCH 35/56] fix error in transferAttributes --- .../src/geos_posp/filters/AttributeMappingFromCellCoords.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geos-posp/src/geos_posp/filters/AttributeMappingFromCellCoords.py b/geos-posp/src/geos_posp/filters/AttributeMappingFromCellCoords.py index 5f23d1b6..4d9500b1 100644 --- a/geos-posp/src/geos_posp/filters/AttributeMappingFromCellCoords.py +++ b/geos-posp/src/geos_posp/filters/AttributeMappingFromCellCoords.py @@ -219,7 +219,7 @@ def transferAttributes( self: Self ) -> bool: for i in range( nbComponents ): componentNames.append( array.GetComponentName( i ) ) newArray: vtkDataArray = createEmptyAttribute( self.m_clientMesh, attributeName, tuple( componentNames ), - dataType, False ) + dataType ) nanValues: list[ float ] = [ np.nan for _ in range( nbComponents ) ] for indexClient in range( self.m_clientMesh.GetNumberOfCells() ): indexServer: int = self.m_cellMap[ indexClient ] From c8e596522e6b55b3d9b4e43b0e5357b0f097dbb9 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 29 Jul 2025 09:26:56 +0200 Subject: [PATCH 36/56] update default value for uint type --- .../CreateConstantAttributePerRegion.py | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py index 7694ae25..ab7478fd 100644 --- a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py +++ b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py @@ -82,6 +82,12 @@ def __init__( speHandler: bool = False, ) -> None: """Create an attribute with constant value per region. + + If a region is not given, the attribute will be set with a default value: + 0 for uint data. + -1 for int data. + np.nan for float data. + An intern logger is used in this filter. If you want to personalize it, you can user a handler of yours that will be add at the place of the intern handler. The level by fault of the logger is INFO. @@ -94,8 +100,7 @@ def __init__( speHandler (bool, optional): True if you want to use a specific handler of yours, False otherwise. Defaults to False. """ - super().__init__( nInputPorts=1, nOutputPorts=1, - inputType="vtkDataObject", outputType="vtkDataObject" ) + super().__init__( nInputPorts=1, nOutputPorts=1, inputType="vtkDataObject", outputType="vtkDataObject" ) self.regionName: str = regionName self.newAttributeName: str = newAttributeName @@ -151,8 +156,6 @@ def RequestData( self.m_logger.info( f"Apply filter { self.m_logger.name }." ) mess: str try: - assert ( len( self.regionName ) > 0 ), "Region attribute is undefined, please select an attribute." - inputMesh: Union[ vtkDataSet, vtkMultiBlockDataSet ] = self.GetInputData( inInfoVec, 0, 0 ) outData: Union[ vtkDataSet, vtkMultiBlockDataSet ] = self.GetOutputData( outInfoVec, 0 ) assert inputMesh is not None, "Input mesh is null." @@ -160,17 +163,19 @@ def RequestData( outData.ShallowCopy( inputMesh ) - # Fields data ? onPoints: bool - isOnMesh: bool = False + piece: str = "" if isAttributeInObject( inputMesh, self.regionName, False ): onPoints = False - isOnMesh = True - elif isAttributeInObject( inputMesh, self.regionName, True ): + piece = "cells" + if isAttributeInObject( inputMesh, self.regionName, True ): + if piece == "cells": + self.m_logger.warning( f"The attribute { self.regionName } is on both cells and points, by default the new attribute { self.newAttributeName } will be created on points.") + onPoints = True - isOnMesh = True + piece = "points" - assert isOnMesh, f"{ self.regionName } is not in the mesh." + assert piece in ( "points", "cells" ), f"{ self.regionName } is not in the mesh." nbComponents: int = getNumberOfComponents( inputMesh, self.regionName, onPoints ) assert nbComponents == 1, "The region attribute has to have only one component" @@ -186,19 +191,19 @@ def RequestData( regionNpArray = getArrayInObject( dataSetInput0, self.regionName, onPoints ) npArray = self.createNpArray( regionNpArray ) - createAttribute( dataSetOutput, npArray, self.newAttributeName, onPoints=onPoints ) + assert createAttribute( dataSetOutput, npArray, self.newAttributeName, onPoints=onPoints, logger=self.m_logger ), "The function createAttribute failed." else: regionNpArray = getArrayInObject( inputMesh, self.regionName, onPoints ) npArray = self.createNpArray( regionNpArray ) - createAttribute( outData, npArray, self.newAttributeName, onPoints=onPoints ) + assert createAttribute( outData, npArray, self.newAttributeName, onPoints=onPoints, logger=self.m_logger ), "The function createAttribute failed." regionIndexes: list[ Any ] = self.dictRegion.keys() indexValuesMess: str = "" for index in regionIndexes: value: Any = self.dictRegion[ index ] - indexValuesMess = "index " + indexValuesMess + str( index ) + ": " + str( value ) + " " + indexValuesMess = indexValuesMess + "index " + str( index ) + ": " + str( value ) + ", " if indexValuesMess == "": indexValuesMess = f"No index or no value enter, the new attribute is constant with the value {self.defaultValue}." @@ -207,19 +212,19 @@ def RequestData( else: indexValuesMess = indexValuesMess + "other indexes: " + str( self.defaultValue ) + "." - mess = f"The attribute { self.regionName } allows to create the new attribute { self.newAttributeName } with the following constant values per region indexes: { indexValuesMess }" + mess = f"The attribute { self.regionName } allows to create on { piece } the new attribute { self.newAttributeName } with the following constant values per region indexes: { indexValuesMess }" self.Modified() self.m_logger.info( mess ) except AssertionError as e: mess = f"The filter { self.m_logger.name } failed due to:" self.m_logger.error( mess ) self.m_logger.error( e, exc_info=True) - return 0 + return 1 except Exception as e: mess = f"The filter { self.m_logger.name } failed due to:" self.m_logger.critical( mess ) self.m_logger.critical( e, exc_info=True) - return 0 + return 1 return 1 @@ -241,12 +246,6 @@ def setInfoRegion( self: Self, dictRegion: dict[ Any, Any ], valueType: int ) -> self.dictRegion: dict[ Any, Any ] = dictRegion for idRegion in self.dictRegion.keys(): self.dictRegion[ idRegion ] = self.valueType( self.dictRegion[ idRegion ] ) - - # Set the default value depending of the type. - if valueType not in [ 10, 11 ]: - self.defaultValue = self.valueType( -1 ) - else: - self.defaultValue = self.valueType( np.nan ) def setLoggerHandler( self: Self, handler: logging.Handler ) -> None: @@ -273,6 +272,18 @@ def createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArra Returns: npt.NDArray[np.float64]: Numpy array of the new attribute. """ + # Set the default value depending of the type. + self.defaultValue: Any + ## Default value for float types is nan. + if self.valueType().dtype in ( "float32", "float64" ): + self.defaultValue = self.valueType( np.nan ) + ## Default value for int types is -1. + elif self.valueType().dtype in ( "int8", "int16", "int32", "int64" ): + self.defaultValue = self.valueType( -1 ) + ## Default value for uint types is 0. + elif self.valueType().dtype in ( "uint8", "uint16", "uint32", "uint64" ): + self.defaultValue = self.valueType( 0 ) + nbElements: int = len( regionNpArray ) npArray: npt.NDArray[ Any ] = np.ones( nbElements, self.valueType ) for elem in range( nbElements ): From 1acc3f10c5faa8e5884b64ede48eb70657d0d11a Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 29 Jul 2025 17:43:25 +0200 Subject: [PATCH 37/56] Clean output message and optimization of the code --- .../CreateConstantAttributePerRegion.py | 234 ++++++++++-------- 1 file changed, 136 insertions(+), 98 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py index ab7478fd..3efeb95d 100644 --- a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py +++ b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py @@ -20,20 +20,20 @@ from geos.utils.Logger import getLogger, Logger from geos.mesh.utils.arrayHelpers import isAttributeInObject, getNumberOfComponents, getArrayInObject, isAttributeGlobal -from geos.mesh.utils.arrayModifiers import createAttribute +from geos.mesh.utils.arrayModifiers import createAttribute, createConstantAttribute, createConstantAttributeDataSet, createConstantAttributeMultiBlock __doc__ = """ CreateConstantAttributePerRegion is a vtk filter that allows to create an attribute with constant values for each chosen indexes of a reference/region attribute. The region attribute has to have one component and the created attribute has one component. Regions indexes, values and values types are choose by the user, for the other region index -values are set to nan or -1 if int type. +values are set to nan for float type, -1 for int type or 0 for uint type. Input and output meshes are either vtkMultiBlockDataSet or vtkDataSet. The value type is encoded by a int using the vtk typecode to preserve the coherency (https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/numpy_support.py). The relation index/value is given by a dictionary. Its keys are the indexes and its items are values. -If you have a specific handler for your logger you can set the variable speHandler to True and use the +To use a specific handler for the logger, set the variable 'speHandler' to True and use the member function addLoggerHandler (useful for paraview for example). To use it: @@ -57,14 +57,15 @@ valueType, speHandler, ) - # Set your specific handler (only if speHandler is True) - filter.addLoggerHandler( YourHandler ) - # Set the mesh + # Set the specific handler (only if speHandler is True). + specificHandler: logging.Handler + filter.addLoggerHandler( specificHandler ) + # Set the mesh. filter.SetInputDataObject( input_mesh ) - # Do calculations + # Do calculations. filter.Update() - # get output object + # Get output object. output: Union[vtkMultiBlockDataSet, vtkDataSet] = filter.GetOutputDataObject( 0 ) """ @@ -82,22 +83,18 @@ def __init__( speHandler: bool = False, ) -> None: """Create an attribute with constant value per region. - - If a region is not given, the attribute will be set with a default value: - 0 for uint data. - -1 for int data. - np.nan for float data. - - An intern logger is used in this filter. If you want to personalize it, you can user a handler of yours that will - be add at the place of the intern handler. The level by fault of the logger is INFO. Args: regionName (str): The name of the attribute with the region indexes. newAttributeName (str): The name of the new attribute to create. dictRegion (dict[ Any, Any ]): The dictionary with the region indexes as keys and their values as items. + For other region indexes, the attribute will be filled with a default value: + 0 for uint data. + -1 for int data. + nan for float data. valueType (int, optional): The type of the value using the vtk typecode. - Defaults to 10. - speHandler (bool, optional): True if you want to use a specific handler of yours, False otherwise. + Defaults to 10 (float32). + speHandler (bool, optional): True To use a specific handler, False to use the internal handler. Defaults to False. """ super().__init__( nInputPorts=1, nOutputPorts=1, inputType="vtkDataObject", outputType="vtkDataObject" ) @@ -108,10 +105,10 @@ def __init__( # Logger if not speHandler: - self.m_logger: Logger = getLogger( loggerTitle, True ) + self.logger: Logger = getLogger( loggerTitle, True ) else: - self.m_logger: Logger = logging.getLogger( loggerTitle ) - self.m_logger.setLevel( logging.INFO ) + self.logger: Logger = logging.getLogger( loggerTitle ) + self.logger.setLevel( logging.INFO ) def RequestDataObject( self: Self, @@ -153,86 +150,126 @@ def RequestData( Returns: int: 1 if calculation successfully ended, 0 otherwise. """ - self.m_logger.info( f"Apply filter { self.m_logger.name }." ) - mess: str - try: - inputMesh: Union[ vtkDataSet, vtkMultiBlockDataSet ] = self.GetInputData( inInfoVec, 0, 0 ) - outData: Union[ vtkDataSet, vtkMultiBlockDataSet ] = self.GetOutputData( outInfoVec, 0 ) - assert inputMesh is not None, "Input mesh is null." - assert outData is not None, "Output pipeline is null." - - outData.ShallowCopy( inputMesh ) - - onPoints: bool - piece: str = "" - if isAttributeInObject( inputMesh, self.regionName, False ): - onPoints = False - piece = "cells" - if isAttributeInObject( inputMesh, self.regionName, True ): - if piece == "cells": - self.m_logger.warning( f"The attribute { self.regionName } is on both cells and points, by default the new attribute { self.newAttributeName } will be created on points.") - - onPoints = True - piece = "points" - - assert piece in ( "points", "cells" ), f"{ self.regionName } is not in the mesh." - - nbComponents: int = getNumberOfComponents( inputMesh, self.regionName, onPoints ) - assert nbComponents == 1, "The region attribute has to have only one component" + self.logger.info( f"Apply filter { self.logger.name }." ) + + # Check meshes. + inputMesh: Union[ vtkDataSet, vtkMultiBlockDataSet ] = self.GetInputData( inInfoVec, 0, 0 ) + if inputMesh is None: + self.logger.error( "Input mesh is null." ) + self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) + self.logger.error( f"The filter { self.logger.name } failed.") + return 1 + + outData: Union[ vtkDataSet, vtkMultiBlockDataSet ] = self.GetOutputData( outInfoVec, 0 ) + if outData is None: + self.logger.error( "Output pipeline is null." ) + self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) + self.logger.error( f"The filter { self.logger.name } failed.") + return 1 + outData.ShallowCopy( inputMesh ) + + # Get the piece of the attribute region if it is in the mesh. + onPoints: bool + piece: str = "" + if isAttributeInObject( inputMesh, self.regionName, False ): + onPoints = False + piece = "cells" + if isAttributeInObject( inputMesh, self.regionName, True ): + # Check if the attribute is on the two pieces. + if piece == "cells": + self.logger.warning( f"The attribute { self.regionName } is on both cells and points, by default the new attribute { self.newAttributeName } will be created on points.") + + onPoints = True + piece = "points" + if piece not in ( "points", "cells" ): + self.logger.error( f"{ self.regionName } is not in the mesh." ) + self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) + self.logger.error( f"The filter { self.logger.name } failed.") + return 1 + + # Check the validity of the attribute region. + nbComponents: int = getNumberOfComponents( inputMesh, self.regionName, onPoints ) + if nbComponents != 1: + self.logger.error( f"The region attribute { self.regionName } has to many components, one is requires." ) + self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) + self.logger.error( f"The filter { self.logger.name } failed.") + return 1 + + # Check if their is region indexes. + regionIndexes: list[ Any ] = self.dictRegion.keys() + nbFalseIndexes: int = 0 + if len( regionIndexes ) == 0: + self.logger.warning( "No region indexes entered." ) + if not createConstantAttribute( outData, [ self.defaultValue ], self.newAttributeName, onPoints=onPoints, logger=self.logger ): + self.logger.error( f"The new attribute { self.newAttributeName } has not been created." ) + self.logger.error( f"The filter { self.logger.name } failed.") + return 1 + + else: regionNpArray: npt.NDArray[ Any ] npArray: npt.NDArray[ Any ] if isinstance( inputMesh, vtkMultiBlockDataSet ): - assert isAttributeGlobal( inputMesh, self.regionName, onPoints ), "Region attribute has to be global" + if not isAttributeGlobal( inputMesh, self.regionName, onPoints ): + self.logger.error( f"The region attribute { self.regionName } has to be global." ) + self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) + self.logger.error( f"The filter { self.logger.name } failed.") + return 1 + + # Parse the mesh to add the attribute on each block. nbBlock: int = outData.GetNumberOfBlocks() for idBlock in range( nbBlock ): + dataSetInput: vtkDataSet = inputMesh.GetBlock( idBlock ) dataSetOutput: vtkDataSet = outData.GetBlock( idBlock ) - dataSetInput0: vtkDataSet = inputMesh.GetBlock( idBlock ) - regionNpArray = getArrayInObject( dataSetInput0, self.regionName, onPoints ) + regionNpArray = getArrayInObject( dataSetInput, self.regionName, onPoints ) npArray = self.createNpArray( regionNpArray ) - assert createAttribute( dataSetOutput, npArray, self.newAttributeName, onPoints=onPoints, logger=self.m_logger ), "The function createAttribute failed." + if not createAttribute( dataSetOutput, npArray, self.newAttributeName, onPoints=onPoints, logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed.") + return 1 else: regionNpArray = getArrayInObject( inputMesh, self.regionName, onPoints ) - npArray = self.createNpArray( regionNpArray ) - assert createAttribute( outData, npArray, self.newAttributeName, onPoints=onPoints, logger=self.m_logger ), "The function createAttribute failed." - - regionIndexes: list[ Any ] = self.dictRegion.keys() - indexValuesMess: str = "" + # Check if all the region indexes given are in the region attribute. + for index in regionIndexes: + if index not in regionNpArray: + nbFalseIndexes += 1 + if nbFalseIndexes == len( regionIndexes ): + self.logger.warning( f"The region indexes entered { regionIndexes } are not in the region attribute { self.regionName }." ) + if not createConstantAttributeDataSet( outData, [ self.defaultValue ], self.newAttributeName, onPoints=onPoints, logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed.") + return 1 + + else: + npArray = self.createNpArray( regionNpArray ) + if not createAttribute( outData, npArray, self.newAttributeName, onPoints=onPoints, logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed.") + return 1 + + # Set the output message. + self.logger.info( f"The new attribute { self.newAttributeName } was successfully created on { piece }." ) + + mess: str = f"The new attribute { self.newAttributeName } is constant" + if len( regionIndexes ) == 0 or len( regionIndexes ) == nbFalseIndexes: + mess = f"{ mess } with the value { self.defaultValue }." + + else: + mess = f"{ mess } per region indexes with:" for index in regionIndexes: - value: Any = self.dictRegion[ index ] - indexValuesMess = indexValuesMess + "index " + str( index ) + ": " + str( value ) + ", " - - if indexValuesMess == "": - indexValuesMess = f"No index or no value enter, the new attribute is constant with the value {self.defaultValue}." - self.m_logger.warning( indexValuesMess ) + mess = f"{ mess } { self.dictRegion[ index ] } for index { index }," - else: - indexValuesMess = indexValuesMess + "other indexes: " + str( self.defaultValue ) + "." + mess = f"{ mess } and { self.defaultValue } for the other indexes." - mess = f"The attribute { self.regionName } allows to create on { piece } the new attribute { self.newAttributeName } with the following constant values per region indexes: { indexValuesMess }" - self.Modified() - self.m_logger.info( mess ) - except AssertionError as e: - mess = f"The filter { self.m_logger.name } failed due to:" - self.m_logger.error( mess ) - self.m_logger.error( e, exc_info=True) - return 1 - except Exception as e: - mess = f"The filter { self.m_logger.name } failed due to:" - self.m_logger.critical( mess ) - self.m_logger.critical( e, exc_info=True) - return 1 - + self.logger.info( mess ) + return 1 def setInfoRegion( self: Self, dictRegion: dict[ Any, Any ], valueType: int ) -> None: """Set attributes self.valueType, self.dictRegion and self.defaultValue. The type of the constant values and the default value are set with value type read with numpy. - The default value is set to nan if the type is float or double, -1 otherwise. + The default value is set to nan for float data, -1 for int data and 0 for uint data. Args: dictRegion (dict[Any, Any]): The dictionary with the indexes and its constant value. @@ -246,6 +283,18 @@ def setInfoRegion( self: Self, dictRegion: dict[ Any, Any ], valueType: int ) -> self.dictRegion: dict[ Any, Any ] = dictRegion for idRegion in self.dictRegion.keys(): self.dictRegion[ idRegion ] = self.valueType( self.dictRegion[ idRegion ] ) + + # Set the default value depending of the type. + self.defaultValue: Any + ## Default value for float types is nan. + if self.valueType().dtype in ( "float32", "float64" ): + self.defaultValue = self.valueType( np.nan ) + ## Default value for int types is -1. + elif self.valueType().dtype in ( "int8", "int16", "int32", "int64" ): + self.defaultValue = self.valueType( -1 ) + ## Default value for uint types is 0. + elif self.valueType().dtype in ( "uint8", "uint16", "uint32", "uint64" ): + self.defaultValue = self.valueType( 0 ) def setLoggerHandler( self: Self, handler: logging.Handler ) -> None: @@ -256,34 +305,23 @@ def setLoggerHandler( self: Self, handler: logging.Handler ) -> None: Args: handler (logging.Handler): The handler to add. """ - if not self.m_logger.hasHandlers(): - self.m_logger.addHandler( handler ) + if not self.logger.hasHandlers(): + self.logger.addHandler( handler ) else: - self.m_logger.warning( "The logger already has a handler, to use yours set the argument 'speHandler' to True during the filter initialization" ) + self.logger.warning( "The logger already has an handler, to use yours set the argument 'speHandler' to True during the filter initialization." ) def createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArray[ Any ]: - """Create numpy arrays from input data. + """Create an array from the input one. + If the value of the input array is a key of self.dictRegion, the corresponding value of the created array is its item. + For the other value, the value self.defaultValue is set. Args: - regionNpArray (npt.NDArray[Any]): Region attribute. - regionVTKArrayType (int): The type of the vtk array. + regionNpArray (npt.NDArray[Any]): The array with the region indexes. Returns: - npt.NDArray[np.float64]: Numpy array of the new attribute. + npt.NDArray[Any]: The array with the value instead of the region index. """ - # Set the default value depending of the type. - self.defaultValue: Any - ## Default value for float types is nan. - if self.valueType().dtype in ( "float32", "float64" ): - self.defaultValue = self.valueType( np.nan ) - ## Default value for int types is -1. - elif self.valueType().dtype in ( "int8", "int16", "int32", "int64" ): - self.defaultValue = self.valueType( -1 ) - ## Default value for uint types is 0. - elif self.valueType().dtype in ( "uint8", "uint16", "uint32", "uint64" ): - self.defaultValue = self.valueType( 0 ) - nbElements: int = len( regionNpArray ) npArray: npt.NDArray[ Any ] = np.ones( nbElements, self.valueType ) for elem in range( nbElements ): From b8c1bc346088ff775c145d9afda3e19e382b6c69 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 29 Jul 2025 17:54:37 +0200 Subject: [PATCH 38/56] Clean the doc --- .../CreateConstantAttributePerRegion.py | 14 ++++++------- .../PVCreateConstantAttributePerRegion.py | 21 ++++++++----------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py index 3efeb95d..b3f7ff63 100644 --- a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py +++ b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py @@ -26,7 +26,7 @@ CreateConstantAttributePerRegion is a vtk filter that allows to create an attribute with constant values for each chosen indexes of a reference/region attribute. The region attribute has to have one component and the created attribute has one component. -Regions indexes, values and values types are choose by the user, for the other region index +Region indexes, values and values types are choose by the user, if other region indexes exist values are set to nan for float type, -1 for int type or 0 for uint type. Input and output meshes are either vtkMultiBlockDataSet or vtkDataSet. @@ -119,9 +119,9 @@ def RequestDataObject( """Inherited from VTKPythonAlgorithmBase::RequestDataObject. Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects + request (vtkInformation): Request. + inInfoVec (list[vtkInformationVector]): Input objects. + outInfoVec (vtkInformationVector): Output objects. Returns: int: 1 if calculation successfully ended, 0 otherwise. @@ -143,9 +143,9 @@ def RequestData( """Inherited from VTKPythonAlgorithmBase::RequestData. Args: - request (vtkInformation): Request - inInfoVec (list[vtkInformationVector]): Input objects - outInfoVec (vtkInformationVector): Output objects + request (vtkInformation): Request. + inInfoVec (list[vtkInformationVector]): Input objects. + outInfoVec (vtkInformationVector): Output objects. Returns: int: 1 if calculation successfully ended, 0 otherwise. diff --git a/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py b/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py index a30da630..6466c857 100644 --- a/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py +++ b/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py @@ -3,10 +3,9 @@ # SPDX-FileContributor: Martin Lemay, Romain Baville # ruff: noqa: E402 # disable Module level import not at top of file import sys -import numpy from pathlib import Path -from typing import Union, Any +from typing import Union, Any from typing_extensions import Self from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] @@ -16,7 +15,6 @@ VTKHandler, ) # source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/detail/loghandler.py -from vtk import VTK_DOUBLE # type: ignore[import-untyped] from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import ( vtkInformation, @@ -41,8 +39,8 @@ PVCreateConstantAttributePerRegion is a paraview Plugin that allows to create an attribute with constant values for each chosen indexes of a reference/region attribute. The region attribute has to have one component and the created attribute has one component. -Regions indexes, values and values types are choose by the user, for the other region index -values are set to nan or -1 if int type. +Region indexes, values and values types are choose by the user, if other region indexes exist +values are set to nan for float type, -1 for int type or 0 for uint type. Input and output meshes are either vtkMultiBlockDataSet or vtkDataSet. @@ -100,7 +98,7 @@ def a02SetAttributeName( self: Self, value: str ) -> None: """ if self.newAttributeName != value: self.newAttributeName = value - self.Modified() + self.Modified() @smproperty.intvector( name="ValueType", @@ -135,7 +133,7 @@ def a02IntSingle( self: Self, value: int ) -> None: """ if value != self.valueType: self.valueType = value - self.Modified() + self.Modified() @smproperty.xml( """ None: """Set region attribute name.""" if self.regionName != name: self.regionName = name - self.Modified() + self.Modified() @smproperty.xml(""" Date: Tue, 29 Jul 2025 18:18:37 +0200 Subject: [PATCH 39/56] Start of the tests --- .../test_CreateConstantAttributePerRegion.py | 76 +++++++------------ 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/geos-mesh/tests/test_CreateConstantAttributePerRegion.py b/geos-mesh/tests/test_CreateConstantAttributePerRegion.py index b8d31751..f6283b78 100644 --- a/geos-mesh/tests/test_CreateConstantAttributePerRegion.py +++ b/geos-mesh/tests/test_CreateConstantAttributePerRegion.py @@ -5,56 +5,36 @@ # ruff: noqa: E402 # disable Module level import not at top of file # mypy: disable-error-code="operator" import pytest -import os -from typing import Union, Tuple, cast, Any - -import numpy as np -import numpy.typing as npt -from geos.utils.Logger import * - -import vtkmodules.util.numpy_support as vnp +from typing import Union, Any from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet, vtkPointData, vtkCellData ) -from vtkmodules.vtkIOXML import vtkXMLUnstructuredGridReader, vtkXMLMultiBlockDataReader from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion -datasetType: str = "dataset" - -reader: Union[ vtkXMLMultiBlockDataReader, vtkXMLUnstructuredGridReader ] -if datasetType == "multiblock": - reader = vtkXMLMultiBlockDataReader() - vtkFilename = "data/displacedFault.vtm" -elif datasetType == "dataset": - reader = vtkXMLUnstructuredGridReader() - vtkFilename = "data/domain_res5_id.vtu" -elif datasetType == "polydata": - reader = vtkXMLUnstructuredGridReader() - vtkFilename = "data/surface.vtu" - -datapath: str = os.path.join( os.path.dirname( os.path.realpath( __file__ ) ), vtkFilename ) -reader.SetFileName( datapath ) -reader.Update() - -input_mesh: Union[vtkMultiBlockDataSet, vtkDataSet] = reader.GetOutput() -regionName: str = "PORO" -newAttributeName: str = "Test" -dictRegion: dict[Any, Any] = {} -valueType: int = 11 -use_color = True - - # instantiate the filter -filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( regionName, - newAttributeName, - dictRegion, - valueType, - use_color, ) -ch = logging.StreamHandler() -ch.setFormatter( CustomLoggerFormatter( use_color ) ) -filter.setLoggerHandler( ch ) - # Set the mesh -filter.SetInputDataObject( input_mesh ) - # Do calculations -filter.Update() +@pytest.mark.parametrize( "mesh, regionName, newAttributeName, dictRegion, valueType", [ + ( "dataset", "FAULT", "newAttribute", { 0: 0, 100: 1 }, 10 ) +] ) +def test_CreateConstantAttributePerRegion( + dataSetTest: Union[ vtkMultiBlockDataSet, vtkDataSet ], + mesh: str, + regionName: str, + newAttributeName: str, + dictRegion: dict[ Any, Any ], + valueType: int, +) -> None: + input_mesh: Union[ vtkMultiBlockDataSet, vtkDataSet ] = dataSetTest( mesh ) + filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( regionName, + newAttributeName, + dictRegion, + valueType, + ) + filter.SetInputDataObject( input_mesh ) + filter.Update() + + mesh: Union[ vtkMultiBlockDataSet, vtkDataSet ] = filter.GetOutputDataObject( 0 ) + + if isinstance( mesh, vtkMultiBlockDataSet ): + assert 1 == 1 + + else: + assert mesh.GetCellData().HasArray( newAttributeName ) == 1 - # get output object -output: Union[vtkMultiBlockDataSet, vtkDataSet] = filter.GetOutputDataObject( 0 ) \ No newline at end of file From 9eb7454ef6ce6db907b6b5795ecc6f76ac6fdff7 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 30 Jul 2025 17:54:03 +0200 Subject: [PATCH 40/56] Manadge the input region index --- .../CreateConstantAttributePerRegion.py | 92 ++++++++++++++----- 1 file changed, 69 insertions(+), 23 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py index b3f7ff63..edaae713 100644 --- a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py +++ b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py @@ -101,6 +101,7 @@ def __init__( self.regionName: str = regionName self.newAttributeName: str = newAttributeName + self.useDefaultValue: bool = False self.setInfoRegion( dictRegion, valueType ) # Logger @@ -198,14 +199,14 @@ def RequestData( # Check if their is region indexes. regionIndexes: list[ Any ] = self.dictRegion.keys() - nbFalseIndexes: int = 0 + trueIndexes: list[ Any ] = [] + falseIndexes: list[ Any ] = [] if len( regionIndexes ) == 0: self.logger.warning( "No region indexes entered." ) if not createConstantAttribute( outData, [ self.defaultValue ], self.newAttributeName, onPoints=onPoints, logger=self.logger ): self.logger.error( f"The new attribute { self.newAttributeName } has not been created." ) self.logger.error( f"The filter { self.logger.name } failed.") return 1 - else: regionNpArray: npt.NDArray[ Any ] npArray: npt.NDArray[ Any ] @@ -215,33 +216,43 @@ def RequestData( self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) self.logger.error( f"The filter { self.logger.name } failed.") return 1 - - # Parse the mesh to add the attribute on each block. - nbBlock: int = outData.GetNumberOfBlocks() - for idBlock in range( nbBlock ): - dataSetInput: vtkDataSet = inputMesh.GetBlock( idBlock ) - dataSetOutput: vtkDataSet = outData.GetBlock( idBlock ) - - regionNpArray = getArrayInObject( dataSetInput, self.regionName, onPoints ) - npArray = self.createNpArray( regionNpArray ) - if not createAttribute( dataSetOutput, npArray, self.newAttributeName, onPoints=onPoints, logger=self.logger ): + + trueIndexes, falseIndexes = self.getTrueIndexesInMultiBlock( inputMesh, onPoints ) + if len( trueIndexes ) == 0: + self.logger.warning( f"The region indexes entered are not in the region attribute { self.regionName }." ) + if not createConstantAttributeMultiBlock( outData, [ self.defaultValue ], self.newAttributeName, onPoints=onPoints, logger=self.logger ): + self.logger.error( f"The new attribute { self.newAttributeName } has not been created." ) self.logger.error( f"The filter { self.logger.name } failed.") return 1 + + else: + if len( falseIndexes ) > 0: + self.logger.warning( f"The region indexes { falseIndexes } are not in the region attribute { self.regionName }." ) + + # Parse the mesh to add the attribute on each block. + nbBlock: int = outData.GetNumberOfBlocks() + for idBlock in range( nbBlock ): + dataSetInput: vtkDataSet = inputMesh.GetBlock( idBlock ) + dataSetOutput: vtkDataSet = outData.GetBlock( idBlock ) + + regionNpArray = getArrayInObject( dataSetInput, self.regionName, onPoints ) + npArray = self.createNpArray( regionNpArray ) + if not createAttribute( dataSetOutput, npArray, self.newAttributeName, onPoints=onPoints, logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed.") + return 1 else: - regionNpArray = getArrayInObject( inputMesh, self.regionName, onPoints ) - - # Check if all the region indexes given are in the region attribute. - for index in regionIndexes: - if index not in regionNpArray: - nbFalseIndexes += 1 - if nbFalseIndexes == len( regionIndexes ): - self.logger.warning( f"The region indexes entered { regionIndexes } are not in the region attribute { self.regionName }." ) + trueIndexes, falseIndexes = self.getTrueIndexesInDataSet( inputMesh, onPoints ) + if len( trueIndexes ) == 0: + self.logger.warning( f"The region indexes entered are not in the region attribute { self.regionName }." ) if not createConstantAttributeDataSet( outData, [ self.defaultValue ], self.newAttributeName, onPoints=onPoints, logger=self.logger ): self.logger.error( f"The filter { self.logger.name } failed.") return 1 - else: + if len( falseIndexes ) > 0: + self.logger.warning( f"The region indexes { falseIndexes } are not in the region attribute { self.regionName }." ) + + regionNpArray = getArrayInObject( inputMesh, self.regionName, onPoints ) npArray = self.createNpArray( regionNpArray ) if not createAttribute( outData, npArray, self.newAttributeName, onPoints=onPoints, logger=self.logger ): self.logger.error( f"The filter { self.logger.name } failed.") @@ -251,7 +262,7 @@ def RequestData( self.logger.info( f"The new attribute { self.newAttributeName } was successfully created on { piece }." ) mess: str = f"The new attribute { self.newAttributeName } is constant" - if len( regionIndexes ) == 0 or len( regionIndexes ) == nbFalseIndexes: + if len( regionIndexes ) == 0 or len( trueIndexes ) == 0: mess = f"{ mess } with the value { self.defaultValue }." else: @@ -259,7 +270,10 @@ def RequestData( for index in regionIndexes: mess = f"{ mess } { self.dictRegion[ index ] } for index { index }," - mess = f"{ mess } and { self.defaultValue } for the other indexes." + if self.useDefaultValue: + mess = f"{ mess } and { self.defaultValue } for the other indexes." + else: + mess = f"{ mess[:-1] }." self.logger.info( mess ) @@ -330,5 +344,37 @@ def createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArra npArray[ elem ] = self.dictRegion[ idRegion ] else: npArray[ elem ] = self.defaultValue + self.useDefaultValue = True return npArray + + + def getTrueIndexesInDataSet( self: Self, inputMesh: vtkDataSet, onPoints: bool ) -> tuple[ list[ Any ], list[ Any ]]: + regionIndexes: list[ Any ] = self.dictRegion.keys() + regionNpArray = getArrayInObject( inputMesh, self.regionName, onPoints ) + trueIndexes: list[ Any ] = [] + falseIndexes: list[ Any ] = [] + for index in regionIndexes: + if index in regionNpArray: + trueIndexes.append( index ) + else: + falseIndexes.append( index ) + + return ( trueIndexes, falseIndexes ) + + def getTrueIndexesInMultiBlock( self: Self, inputMesh: vtkMultiBlockDataSet, onPoints: bool ) -> tuple[ list[ Any ], list[ Any ]]: + trueIndexes: list[ Any ] = [] + falseIndexes: list[ Any ] = [] + nbBlock: int = inputMesh.GetNumberOfBlocks() + for idBlock in range( nbBlock ): + dataSetInput: vtkDataSet = inputMesh.GetBlock( idBlock ) + trueIndexes.extend( self.getTrueIndexesInDataSet( dataSetInput, onPoints )[ 0 ] ) + falseIndexes.extend( self.getTrueIndexesInDataSet( dataSetInput, onPoints )[ 1 ] ) + + return ( list( set( trueIndexes ) ), list( set( falseIndexes ) ) ) + + def getTrueIndexes( self:Self, inputMesh: Union[ vtkDataSet, vtkMultiBlockDataSet ], onPoints: bool ) -> tuple[ list[ Any ], list[ Any ]]: + if isinstance( inputMesh, vtkMultiBlockDataSet ): + return self.getTrueIndexesInMultiBlock( inputMesh, onPoints ) + else: + return self.getTrueIndexesInDataSet( inputMesh, onPoints ) \ No newline at end of file From 6956e9769cf04702aa84e1e3d34c4d51c1e824ea Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Thu, 31 Jul 2025 16:08:54 +0200 Subject: [PATCH 41/56] Remove the use of VTKPythonAlgorithmBase in the filter CreateConstantAttributePerRegion --- .../CreateConstantAttributePerRegion.py | 241 +++++++----------- .../test_CreateConstantAttributePerRegion.py | 12 +- .../PVCreateConstantAttributePerRegion.py | 22 +- 3 files changed, 116 insertions(+), 159 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py index edaae713..0cb7b872 100644 --- a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py +++ b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py @@ -3,22 +3,17 @@ # SPDX-FileContributor: Romain Baville import numpy as np import numpy.typing as npt -import logging + from typing import Union, Any from typing_extensions import Self import vtkmodules.util.numpy_support as vnp -from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase -from vtkmodules.vtkCommonCore import ( - vtkInformation, - vtkInformationVector, -) from vtkmodules.vtkCommonDataModel import ( vtkMultiBlockDataSet, vtkDataSet, ) -from geos.utils.Logger import getLogger, Logger +from geos.utils.Logger import getLogger, Logger, logging from geos.mesh.utils.arrayHelpers import isAttributeInObject, getNumberOfComponents, getArrayInObject, isAttributeGlobal from geos.mesh.utils.arrayModifiers import createAttribute, createConstantAttribute, createConstantAttributeDataSet, createConstantAttributeMultiBlock @@ -29,7 +24,7 @@ Region indexes, values and values types are choose by the user, if other region indexes exist values are set to nan for float type, -1 for int type or 0 for uint type. -Input and output meshes are either vtkMultiBlockDataSet or vtkDataSet. +Input mesh is either vtkMultiBlockDataSet or vtkDataSet. The value type is encoded by a int using the vtk typecode to preserve the coherency (https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/numpy_support.py). The relation index/value is given by a dictionary. Its keys are the indexes and its items are values. @@ -43,7 +38,7 @@ from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion # filter inputs - input_mesh: Union[vtkMultiBlockDataSet, vtkDataSet] + mesh: Union[vtkMultiBlockDataSet, vtkDataSet] regionName: str newAttributeName: str dictRegion: dict[Any, Any] @@ -51,7 +46,8 @@ speHandler: bool, optional defaults to False # instantiate the filter - filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( regionName, + filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( mesh, + regionName, newAttributeName, dictRegion, valueType, @@ -60,22 +56,18 @@ # Set the specific handler (only if speHandler is True). specificHandler: logging.Handler filter.addLoggerHandler( specificHandler ) - # Set the mesh. - filter.SetInputDataObject( input_mesh ) # Do calculations. - filter.Update() - - # Get output object. - output: Union[vtkMultiBlockDataSet, vtkDataSet] = filter.GetOutputDataObject( 0 ) + filter.applyFilter() """ loggerTitle: str = "Create constant attribute per region" -class CreateConstantAttributePerRegion( VTKPythonAlgorithmBase ): +class CreateConstantAttributePerRegion: def __init__( self: Self, + mesh: Union[ vtkDataSet, vtkMultiBlockDataSet ], regionName: str, newAttributeName: str, dictRegion: dict[ Any, Any ], @@ -85,6 +77,7 @@ def __init__( """Create an attribute with constant value per region. Args: + mesh (Union[ vtkDataSet, vtkMultiBlockDataSet ]): The mesh where to create the constant attribute per region. regionName (str): The name of the attribute with the region indexes. newAttributeName (str): The name of the new attribute to create. dictRegion (dict[ Any, Any ]): The dictionary with the region indexes as keys and their values as items. @@ -97,11 +90,10 @@ def __init__( speHandler (bool, optional): True To use a specific handler, False to use the internal handler. Defaults to False. """ - super().__init__( nInputPorts=1, nOutputPorts=1, inputType="vtkDataObject", outputType="vtkDataObject" ) - + self.mesh: Union[ vtkDataSet, vtkMultiBlockDataSet ] = mesh self.regionName: str = regionName self.newAttributeName: str = newAttributeName - self.useDefaultValue: bool = False + self.useDefaultValue: bool = False # Check if the new component have default values (information for the output message) self.setInfoRegion( dictRegion, valueType ) # Logger @@ -111,157 +103,124 @@ def __init__( self.logger: Logger = logging.getLogger( loggerTitle ) self.logger.setLevel( logging.INFO ) - def RequestDataObject( - self: Self, - request: vtkInformation, - inInfoVec: list[ vtkInformationVector ], - outInfoVec: vtkInformationVector, - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestDataObject. - - Args: - request (vtkInformation): Request. - inInfoVec (list[vtkInformationVector]): Input objects. - outInfoVec (vtkInformationVector): Output objects. - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - inData = self.GetInputData( inInfoVec, 0, 0 ) - outData = self.GetOutputData( outInfoVec, 0 ) - assert inData is not None - if outData is None or ( not outData.IsA( inData.GetClassName() ) ): - outData = inData.NewInstance() - outInfoVec.GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), outData ) - return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] - - def RequestData( - self: Self, - request: vtkInformation, # noqa: F841 - inInfoVec: list[ vtkInformationVector ], - outInfoVec: vtkInformationVector, - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestData. - Args: - request (vtkInformation): Request. - inInfoVec (list[vtkInformationVector]): Input objects. - outInfoVec (vtkInformationVector): Output objects. + def applyFilter( self: Self ) -> bool: + """Create a constant attribute per region in the mesh Returns: - int: 1 if calculation successfully ended, 0 otherwise. + boolean (bool): True if calculation successfully ended, False otherwise. """ self.logger.info( f"Apply filter { self.logger.name }." ) - # Check meshes. - inputMesh: Union[ vtkDataSet, vtkMultiBlockDataSet ] = self.GetInputData( inInfoVec, 0, 0 ) - if inputMesh is None: - self.logger.error( "Input mesh is null." ) - self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) - self.logger.error( f"The filter { self.logger.name } failed.") - return 1 - - outData: Union[ vtkDataSet, vtkMultiBlockDataSet ] = self.GetOutputData( outInfoVec, 0 ) - if outData is None: - self.logger.error( "Output pipeline is null." ) - self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) - self.logger.error( f"The filter { self.logger.name } failed.") - return 1 - - outData.ShallowCopy( inputMesh ) - # Get the piece of the attribute region if it is in the mesh. onPoints: bool piece: str = "" - if isAttributeInObject( inputMesh, self.regionName, False ): + if isAttributeInObject( self.mesh, self.regionName, False ): onPoints = False piece = "cells" - if isAttributeInObject( inputMesh, self.regionName, True ): + if isAttributeInObject( self.mesh, self.regionName, True ): # Check if the attribute is on the two pieces. if piece == "cells": self.logger.warning( f"The attribute { self.regionName } is on both cells and points, by default the new attribute { self.newAttributeName } will be created on points.") onPoints = True piece = "points" + + # Check if the attribute is on points or on cells. if piece not in ( "points", "cells" ): self.logger.error( f"{ self.regionName } is not in the mesh." ) self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) self.logger.error( f"The filter { self.logger.name } failed.") - return 1 + return False # Check the validity of the attribute region. - nbComponents: int = getNumberOfComponents( inputMesh, self.regionName, onPoints ) + nbComponents: int = getNumberOfComponents( self.mesh, self.regionName, onPoints ) if nbComponents != 1: self.logger.error( f"The region attribute { self.regionName } has to many components, one is requires." ) self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) self.logger.error( f"The filter { self.logger.name } failed.") - return 1 + return False - # Check if their is region indexes. - regionIndexes: list[ Any ] = self.dictRegion.keys() trueIndexes: list[ Any ] = [] falseIndexes: list[ Any ] = [] - if len( regionIndexes ) == 0: - self.logger.warning( "No region indexes entered." ) - if not createConstantAttribute( outData, [ self.defaultValue ], self.newAttributeName, onPoints=onPoints, logger=self.logger ): - self.logger.error( f"The new attribute { self.newAttributeName } has not been created." ) + regionNpArray: npt.NDArray[ Any ] + npArray: npt.NDArray[ Any ] + if isinstance( self.mesh, vtkMultiBlockDataSet ): + if not isAttributeGlobal( self.mesh, self.regionName, onPoints ): + self.logger.error( f"The region attribute { self.regionName } has to be global." ) + self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) self.logger.error( f"The filter { self.logger.name } failed.") - return 1 - else: - regionNpArray: npt.NDArray[ Any ] - npArray: npt.NDArray[ Any ] - if isinstance( inputMesh, vtkMultiBlockDataSet ): - if not isAttributeGlobal( inputMesh, self.regionName, onPoints ): - self.logger.error( f"The region attribute { self.regionName } has to be global." ) - self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) - self.logger.error( f"The filter { self.logger.name } failed.") - return 1 + return False - trueIndexes, falseIndexes = self.getTrueIndexesInMultiBlock( inputMesh, onPoints ) - if len( trueIndexes ) == 0: + trueIndexes, falseIndexes = self.getTrueIndexesInMultiBlock( self.mesh, onPoints ) + if len( trueIndexes ) == 0: + if len( self.dictRegion.keys() ) == 0: + self.logger.warning( "No region indexes entered." ) + else: self.logger.warning( f"The region indexes entered are not in the region attribute { self.regionName }." ) - if not createConstantAttributeMultiBlock( outData, [ self.defaultValue ], self.newAttributeName, onPoints=onPoints, logger=self.logger ): - self.logger.error( f"The new attribute { self.newAttributeName } has not been created." ) - self.logger.error( f"The filter { self.logger.name } failed.") - return 1 + if not createConstantAttributeMultiBlock( self.mesh, [ self.defaultValue ], self.newAttributeName, onPoints=onPoints, logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed.") + return False - else: - if len( falseIndexes ) > 0: - self.logger.warning( f"The region indexes { falseIndexes } are not in the region attribute { self.regionName }." ) - - # Parse the mesh to add the attribute on each block. - nbBlock: int = outData.GetNumberOfBlocks() - for idBlock in range( nbBlock ): - dataSetInput: vtkDataSet = inputMesh.GetBlock( idBlock ) - dataSetOutput: vtkDataSet = outData.GetBlock( idBlock ) - - regionNpArray = getArrayInObject( dataSetInput, self.regionName, onPoints ) - npArray = self.createNpArray( regionNpArray ) - if not createAttribute( dataSetOutput, npArray, self.newAttributeName, onPoints=onPoints, logger=self.logger ): - self.logger.error( f"The filter { self.logger.name } failed.") - return 1 - else: - trueIndexes, falseIndexes = self.getTrueIndexesInDataSet( inputMesh, onPoints ) - if len( trueIndexes ) == 0: - self.logger.warning( f"The region indexes entered are not in the region attribute { self.regionName }." ) - if not createConstantAttributeDataSet( outData, [ self.defaultValue ], self.newAttributeName, onPoints=onPoints, logger=self.logger ): - self.logger.error( f"The filter { self.logger.name } failed.") - return 1 - else: - if len( falseIndexes ) > 0: - self.logger.warning( f"The region indexes { falseIndexes } are not in the region attribute { self.regionName }." ) + if len( falseIndexes ) > 0: + self.logger.warning( f"The region indexes { falseIndexes } are not in the region attribute { self.regionName }." ) + + # Parse the mesh to add the attribute on each block. + nbBlock: int = self.mesh.GetNumberOfBlocks() + for idBlock in range( nbBlock ): + dataSetInput: vtkDataSet = self.mesh.GetBlock( idBlock ) - regionNpArray = getArrayInObject( inputMesh, self.regionName, onPoints ) + regionNpArray = getArrayInObject( dataSetInput, self.regionName, onPoints ) npArray = self.createNpArray( regionNpArray ) - if not createAttribute( outData, npArray, self.newAttributeName, onPoints=onPoints, logger=self.logger ): + if not createAttribute( dataSetInput, npArray, self.newAttributeName, onPoints=onPoints, logger=self.logger ): self.logger.error( f"The filter { self.logger.name } failed.") - return 1 + return False + + else: + trueIndexes, falseIndexes = self.getTrueIndexesInDataSet( self.mesh, onPoints ) + if len( trueIndexes ) == 0: + if len( self.dictRegion.keys() ) == 0: + self.logger.warning( "No region indexes entered." ) + else: + self.logger.warning( f"The region indexes entered are not in the region attribute { self.regionName }." ) + + if not createConstantAttributeDataSet( self.mesh, [ self.defaultValue ], self.newAttributeName, onPoints=onPoints, logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed.") + return False + + else: + if len( falseIndexes ) > 0: + self.logger.warning( f"The region indexes { falseIndexes } are not in the region attribute { self.regionName }." ) + + regionNpArray = getArrayInObject( self.mesh, self.regionName, onPoints ) + npArray = self.createNpArray( regionNpArray ) + if not createAttribute( self.mesh, npArray, self.newAttributeName, onPoints=onPoints, logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed.") + return False # Set the output message. - self.logger.info( f"The new attribute { self.newAttributeName } was successfully created on { piece }." ) + self.logger.info( f"The filter { self.logger.name } succeed." ) + self.logger.info( f"The new attribute { self.newAttributeName } is created on { piece }." ) + + mess: str = self.setOutputMessage( trueIndexes ) + self.logger.info( mess ) + + return True + + def setOutputMessage( self: Self, trueIndexes: list[ Any ] ) -> str: + """Create the result message of the filter. + + Args: + trueIndexes (list[Any]): The list of the region indexes use to create the attribute. + + Returns: + message (str): The result message of the filter with the value per region. + + """ mess: str = f"The new attribute { self.newAttributeName } is constant" + regionIndexes: list[ Any ] = self.dictRegion.keys() if len( regionIndexes ) == 0 or len( trueIndexes ) == 0: mess = f"{ mess } with the value { self.defaultValue }." @@ -274,10 +233,8 @@ def RequestData( mess = f"{ mess } and { self.defaultValue } for the other indexes." else: mess = f"{ mess[:-1] }." - - self.logger.info( mess ) - return 1 + return mess def setInfoRegion( self: Self, dictRegion: dict[ Any, Any ], valueType: int ) -> None: @@ -328,7 +285,7 @@ def setLoggerHandler( self: Self, handler: logging.Handler ) -> None: def createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArray[ Any ]: """Create an array from the input one. If the value of the input array is a key of self.dictRegion, the corresponding value of the created array is its item. - For the other value, the value self.defaultValue is set. + If their is other value, the value self.defaultValue is set and the self.useDefaultValue is set to True. Args: regionNpArray (npt.NDArray[Any]): The array with the region indexes. @@ -349,9 +306,9 @@ def createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArra return npArray - def getTrueIndexesInDataSet( self: Self, inputMesh: vtkDataSet, onPoints: bool ) -> tuple[ list[ Any ], list[ Any ]]: + def getTrueIndexesInDataSet( self: Self, dataSet: vtkDataSet, onPoints: bool ) -> tuple[ list[ Any ], list[ Any ]]: regionIndexes: list[ Any ] = self.dictRegion.keys() - regionNpArray = getArrayInObject( inputMesh, self.regionName, onPoints ) + regionNpArray = getArrayInObject( dataSet, self.regionName, onPoints ) trueIndexes: list[ Any ] = [] falseIndexes: list[ Any ] = [] for index in regionIndexes: @@ -362,19 +319,15 @@ def getTrueIndexesInDataSet( self: Self, inputMesh: vtkDataSet, onPoints: bool ) return ( trueIndexes, falseIndexes ) - def getTrueIndexesInMultiBlock( self: Self, inputMesh: vtkMultiBlockDataSet, onPoints: bool ) -> tuple[ list[ Any ], list[ Any ]]: + + def getTrueIndexesInMultiBlock( self: Self, multiBlockDataSet: vtkMultiBlockDataSet, onPoints: bool ) -> tuple[ list[ Any ], list[ Any ]]: trueIndexes: list[ Any ] = [] falseIndexes: list[ Any ] = [] - nbBlock: int = inputMesh.GetNumberOfBlocks() + nbBlock: int = multiBlockDataSet.GetNumberOfBlocks() for idBlock in range( nbBlock ): - dataSetInput: vtkDataSet = inputMesh.GetBlock( idBlock ) + dataSetInput: vtkDataSet = multiBlockDataSet.GetBlock( idBlock ) trueIndexes.extend( self.getTrueIndexesInDataSet( dataSetInput, onPoints )[ 0 ] ) falseIndexes.extend( self.getTrueIndexesInDataSet( dataSetInput, onPoints )[ 1 ] ) return ( list( set( trueIndexes ) ), list( set( falseIndexes ) ) ) - - def getTrueIndexes( self:Self, inputMesh: Union[ vtkDataSet, vtkMultiBlockDataSet ], onPoints: bool ) -> tuple[ list[ Any ], list[ Any ]]: - if isinstance( inputMesh, vtkMultiBlockDataSet ): - return self.getTrueIndexesInMultiBlock( inputMesh, onPoints ) - else: - return self.getTrueIndexesInDataSet( inputMesh, onPoints ) \ No newline at end of file + \ No newline at end of file diff --git a/geos-mesh/tests/test_CreateConstantAttributePerRegion.py b/geos-mesh/tests/test_CreateConstantAttributePerRegion.py index f6283b78..d0bf43d6 100644 --- a/geos-mesh/tests/test_CreateConstantAttributePerRegion.py +++ b/geos-mesh/tests/test_CreateConstantAttributePerRegion.py @@ -22,19 +22,19 @@ def test_CreateConstantAttributePerRegion( valueType: int, ) -> None: input_mesh: Union[ vtkMultiBlockDataSet, vtkDataSet ] = dataSetTest( mesh ) - filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( regionName, + filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( input_mesh, + regionName, newAttributeName, dictRegion, valueType, ) - filter.SetInputDataObject( input_mesh ) - filter.Update() + filter.applyFilter() - mesh: Union[ vtkMultiBlockDataSet, vtkDataSet ] = filter.GetOutputDataObject( 0 ) + #meshFiltered: Union[ vtkMultiBlockDataSet, vtkDataSet ] = filter.mesh - if isinstance( mesh, vtkMultiBlockDataSet ): + if isinstance( input_mesh, vtkMultiBlockDataSet ): assert 1 == 1 else: - assert mesh.GetCellData().HasArray( newAttributeName ) == 1 + assert input_mesh.GetCellData().HasArray( newAttributeName ) == 1 diff --git a/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py b/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py index 6466c857..7d0adfb1 100644 --- a/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py +++ b/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py @@ -77,6 +77,7 @@ def __init__( self: Self ) -> None: self.regionName: str = "Region" self.newAttributeName: str = "newAttribute" self.valueType: int = 10 + self.speHandler: bool = True @smproperty.xml( """ Date: Fri, 1 Aug 2025 11:06:46 +0200 Subject: [PATCH 42/56] Update log to be more clear --- .../CreateConstantAttributePerRegion.py | 177 ++++++++++-------- 1 file changed, 102 insertions(+), 75 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py index 0cb7b872..5db3ba7c 100644 --- a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py +++ b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py @@ -15,7 +15,7 @@ from geos.utils.Logger import getLogger, Logger, logging from geos.mesh.utils.arrayHelpers import isAttributeInObject, getNumberOfComponents, getArrayInObject, isAttributeGlobal -from geos.mesh.utils.arrayModifiers import createAttribute, createConstantAttribute, createConstantAttributeDataSet, createConstantAttributeMultiBlock +from geos.mesh.utils.arrayModifiers import createAttribute, createConstantAttributeDataSet, createConstantAttributeMultiBlock __doc__ = """ CreateConstantAttributePerRegion is a vtk filter that allows to create an attribute @@ -94,7 +94,7 @@ def __init__( self.regionName: str = regionName self.newAttributeName: str = newAttributeName self.useDefaultValue: bool = False # Check if the new component have default values (information for the output message) - self.setInfoRegion( dictRegion, valueType ) + self._setInfoRegion( dictRegion, valueType ) # Logger if not speHandler: @@ -102,6 +102,20 @@ def __init__( else: self.logger: Logger = logging.getLogger( loggerTitle ) self.logger.setLevel( logging.INFO ) + + + def setLoggerHandler( self: Self, handler: logging.Handler ) -> None: + """Set a specific handler for the filter logger. + In this filter 4 log levels are use, .info, .error, .warning and .critical, + be sure to have at least the same 4 levels. + + Args: + handler (logging.Handler): The handler to add. + """ + if not 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 applyFilter( self: Self ) -> bool: @@ -152,7 +166,7 @@ def applyFilter( self: Self ) -> bool: self.logger.error( f"The filter { self.logger.name } failed.") return False - trueIndexes, falseIndexes = self.getTrueIndexesInMultiBlock( self.mesh, onPoints ) + trueIndexes, falseIndexes = self._getTrueIndexesInMultiBlock( self.mesh, onPoints ) if len( trueIndexes ) == 0: if len( self.dictRegion.keys() ) == 0: self.logger.warning( "No region indexes entered." ) @@ -172,13 +186,13 @@ def applyFilter( self: Self ) -> bool: dataSetInput: vtkDataSet = self.mesh.GetBlock( idBlock ) regionNpArray = getArrayInObject( dataSetInput, self.regionName, onPoints ) - npArray = self.createNpArray( regionNpArray ) + npArray = self._createNpArray( regionNpArray ) if not createAttribute( dataSetInput, npArray, self.newAttributeName, onPoints=onPoints, logger=self.logger ): self.logger.error( f"The filter { self.logger.name } failed.") return False else: - trueIndexes, falseIndexes = self.getTrueIndexesInDataSet( self.mesh, onPoints ) + trueIndexes, falseIndexes = self._getTrueIndexesInDataSet( self.mesh, onPoints ) if len( trueIndexes ) == 0: if len( self.dictRegion.keys() ) == 0: self.logger.warning( "No region indexes entered." ) @@ -194,63 +208,31 @@ def applyFilter( self: Self ) -> bool: self.logger.warning( f"The region indexes { falseIndexes } are not in the region attribute { self.regionName }." ) regionNpArray = getArrayInObject( self.mesh, self.regionName, onPoints ) - npArray = self.createNpArray( regionNpArray ) + npArray = self._createNpArray( regionNpArray ) if not createAttribute( self.mesh, npArray, self.newAttributeName, onPoints=onPoints, logger=self.logger ): self.logger.error( f"The filter { self.logger.name } failed.") return False - # Set the output message. - self.logger.info( f"The filter { self.logger.name } succeed." ) - self.logger.info( f"The new attribute { self.newAttributeName } is created on { piece }." ) - - mess: str = self.setOutputMessage( trueIndexes ) - self.logger.info( mess ) + # Log the output message. + self._logOutputMessage( trueIndexes, piece ) return True - - - def setOutputMessage( self: Self, trueIndexes: list[ Any ] ) -> str: - """Create the result message of the filter. - - Args: - trueIndexes (list[Any]): The list of the region indexes use to create the attribute. - - Returns: - message (str): The result message of the filter with the value per region. - - """ - mess: str = f"The new attribute { self.newAttributeName } is constant" - regionIndexes: list[ Any ] = self.dictRegion.keys() - if len( regionIndexes ) == 0 or len( trueIndexes ) == 0: - mess = f"{ mess } with the value { self.defaultValue }." - - else: - mess = f"{ mess } per region indexes with:" - for index in regionIndexes: - mess = f"{ mess } { self.dictRegion[ index ] } for index { index }," - - if self.useDefaultValue: - mess = f"{ mess } and { self.defaultValue } for the other indexes." - else: - mess = f"{ mess[:-1] }." - return mess - - def setInfoRegion( self: Self, dictRegion: dict[ Any, Any ], valueType: int ) -> None: + def _setInfoRegion( self: Self, dictRegion: dict[ Any, Any ], valueType: int ) -> None: """Set attributes self.valueType, self.dictRegion and self.defaultValue. - The type of the constant values and the default value are set with value type read with numpy. + The type of values and the default value are set with the numpy type given by valueType. The default value is set to nan for float data, -1 for int data and 0 for uint data. Args: - dictRegion (dict[Any, Any]): The dictionary with the indexes and its constant value. + dictRegion (dict[Any, Any]): The dictionary with the region indexes and its corresponding value. valueType (int): The type of the constant value with the VTK typecode. """ # Get the numpy type from the vtk typecode. dictType: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() self.valueType: type = dictType[ valueType ] - # Set the correct type of the items to ensure the coherency. + # Set the correct type of the values for each region index to ensure the coherency. self.dictRegion: dict[ Any, Any ] = dictRegion for idRegion in self.dictRegion.keys(): self.dictRegion[ idRegion ] = self.valueType( self.dictRegion[ idRegion ] ) @@ -267,22 +249,63 @@ def setInfoRegion( self: Self, dictRegion: dict[ Any, Any ], valueType: int ) -> elif self.valueType().dtype in ( "uint8", "uint16", "uint32", "uint64" ): self.defaultValue = self.valueType( 0 ) + + def _getTrueIndexesInMultiBlock( self: Self, multiBlockDataSet: vtkMultiBlockDataSet, onPoints: bool ) -> tuple[ list[ Any ], list[ Any ] ]: + """Check for each region index if it is a true index -ie the index is value of the attribute of at least one block, or a false index. + + Args: + dataSet (vtkDataSet): The mesh with the attribute to check. + onPoints (bool): True if the attribute is on point, False if it is on cell. - def setLoggerHandler( self: Self, handler: logging.Handler ) -> None: - """Set a specific handler for the logger of the filter. - In this filter 4 log levels are use, .info, .error, .warning and .critical, - be sure to have at least the same 4 levels. + Returns: + tuple(list[Any], list[Any]): The tuple with the list of the true indexes and the list of the false indexes. + """ + trueIndexes: list[ Any ] = [] + falseIndexes: list[ Any ] = [] + nbBlock: int = multiBlockDataSet.GetNumberOfBlocks() + # Parse all blocks to get the true indexes of each block. + for idBlock in range( nbBlock ): + block: vtkDataSet = multiBlockDataSet.GetBlock( idBlock ) + # Get the true and false indexes of the block. + trueIndexesBlock: list[ Any ] = self._getTrueIndexesInDataSet( block, onPoints )[ 0 ] + + # Keep the new true indexes. + for index in trueIndexesBlock: + if index not in trueIndexes: + trueIndexes.append( index ) + # Get the false indexes. + for index in self.dictRegion.keys(): + if index not in trueIndexes: + falseIndexes.append( index ) + + return ( trueIndexes, falseIndexes ) + + + def _getTrueIndexesInDataSet( self: Self, dataSet: vtkDataSet, onPoints: bool ) -> tuple[ list[ Any ], list[ Any ] ]: + """Check for each region index if it is a true index -ie the index is value of the attribute, or a false index. + Args: - handler (logging.Handler): The handler to add. + dataSet (vtkDataSet): The mesh with the attribute to check. + onPoints (bool): True if the attribute is on point, False if it is on cell. + + Returns: + tuple(list[Any], list[Any]): The tuple with the list of the true indexes and the list of the false indexes. """ - 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." ) + regionIndexes: list[ Any ] = self.dictRegion.keys() + regionNpArray = getArrayInObject( dataSet, self.regionName, onPoints ) + trueIndexes: list[ Any ] = [] + falseIndexes: list[ Any ] = [] + for index in regionIndexes: + if index in regionNpArray: + trueIndexes.append( index ) + else: + falseIndexes.append( index ) + + return ( trueIndexes, falseIndexes ) - def createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArray[ Any ]: + def _createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArray[ Any ]: """Create an array from the input one. If the value of the input array is a key of self.dictRegion, the corresponding value of the created array is its item. If their is other value, the value self.defaultValue is set and the self.useDefaultValue is set to True. @@ -304,30 +327,34 @@ def createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArra self.useDefaultValue = True return npArray + + def _logOutputMessage( self: Self, trueIndexes: list[ Any ], piece: str ) -> None: + """Create the result message of the filter and output it. - def getTrueIndexesInDataSet( self: Self, dataSet: vtkDataSet, onPoints: bool ) -> tuple[ list[ Any ], list[ Any ]]: - regionIndexes: list[ Any ] = self.dictRegion.keys() - regionNpArray = getArrayInObject( dataSet, self.regionName, onPoints ) - trueIndexes: list[ Any ] = [] - falseIndexes: list[ Any ] = [] - for index in regionIndexes: - if index in regionNpArray: - trueIndexes.append( index ) - else: - falseIndexes.append( index ) + Args: + trueIndexes (list[Any]): The list of the region indexes use to create the attribute. + piece (str): The piece where the data is (cell or point). + """ + # The Filter succeed. + self.logger.info( f"The filter { self.logger.name } succeed." ) - return ( trueIndexes, falseIndexes ) - + # Info about the created attribute. + ## The piece where the attribute is created. + self.logger.info( f"The new attribute { self.newAttributeName } is created on { piece }." ) - def getTrueIndexesInMultiBlock( self: Self, multiBlockDataSet: vtkMultiBlockDataSet, onPoints: bool ) -> tuple[ list[ Any ], list[ Any ]]: - trueIndexes: list[ Any ] = [] - falseIndexes: list[ Any ] = [] - nbBlock: int = multiBlockDataSet.GetNumberOfBlocks() - for idBlock in range( nbBlock ): - dataSetInput: vtkDataSet = multiBlockDataSet.GetBlock( idBlock ) - trueIndexes.extend( self.getTrueIndexesInDataSet( dataSetInput, onPoints )[ 0 ] ) - falseIndexes.extend( self.getTrueIndexesInDataSet( dataSetInput, onPoints )[ 1 ] ) + ## The values of the attribute. + mess: str = f"The new attribute { self.newAttributeName } is constant" + if len( trueIndexes ) == 0: + self.logger.warning( f"{ mess } with the value { self.defaultValue }." ) + + else: + mess = f"{ mess } per region indexes with:" + for index in trueIndexes: + mess = f"{ mess } { self.dictRegion[ index ] } for index { index }," - return ( list( set( trueIndexes ) ), list( set( falseIndexes ) ) ) + if self.useDefaultValue: + self.logger.warning( f"{ mess } and { self.defaultValue } for the other indexes." ) + else: + self.logger.info( f"{ mess[:-1] }." ) \ No newline at end of file From 1e2007fa8b3baa3036914d414fa62fd7208b4a7f Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 10:20:49 +0200 Subject: [PATCH 43/56] Add a Handler to count warnings messages --- geos-utils/src/geos/utils/Logger.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/geos-utils/src/geos/utils/Logger.py b/geos-utils/src/geos/utils/Logger.py index 00f469f4..d8cecb8d 100644 --- a/geos-utils/src/geos/utils/Logger.py +++ b/geos-utils/src/geos/utils/Logger.py @@ -11,6 +11,14 @@ Code was modified from """ +class CountWarningHandler( logging.Handler ): + def __init__( self: Self ): + super().__init__() + self.warningCount = 0 + + def emit( self: Self, record: logging.LogRecord ): + if record.levelno == logging.WARNING: + self.warningCount += 1 # Add the convenience method for the logger def results( self: logging.Logger, message: str, *args: Any, **kws: Any ) -> None: @@ -49,7 +57,7 @@ def results( self: logging.Logger, message: str, *args: Any, **kws: Any ) -> Non class CustomLoggerFormatter( logging.Formatter ): """Custom formatter for the logger. - .. WARNING:: Colors do not work in the ouput message window of Paraview. + .. WARNING:: Colors do not work in the output message window of Paraview. To use it: @@ -151,7 +159,7 @@ def getLogger( title: str, use_color: bool = False ) -> Logger: # module import import Logger - # logger instanciation + # logger instantiation logger :Logger.Logger = Logger.getLogger("My application") # logger use From 19329105657d03673f500aae93be48f3d90c7727 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 10:22:08 +0200 Subject: [PATCH 44/56] Add the function to get the vtk data type of a mesh (single or multi block) --- geos-mesh/src/geos/mesh/utils/arrayHelpers.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index 9fd3bd78..f75e1ba5 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -388,6 +388,23 @@ def getArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) - return npArray +def getVtkDataTypeInObject( object: Union[ vtkDataSet, vtkMultiBlockDataSet ], attributeName: str, onPoints: bool ) -> int: + """Return VTK type of requested array from input mesh. + + Args: + object (Union[vtkDataSet, vtkMultiBlockDataSet]): Input object. + attributeName (str): Name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. + + Returns: + int: The type of the vtk array corresponding to input attribute name. + """ + if isinstance( object, vtkDataSet ): + return getVtkArrayTypeInObject( object, attributeName, onPoints ) + else: + return getVtkArrayTypeInMultiBlock( object, attributeName, onPoints ) + + def getVtkArrayTypeInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> int: """Return VTK type of requested array from dataset input. From c24c0046b8cdbe1a5c3931fdcc6b4fc8655a2143 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 10:25:27 +0200 Subject: [PATCH 45/56] update test for the filter --- .../test_CreateConstantAttributePerRegion.py | 76 ++++++++++++++----- 1 file changed, 59 insertions(+), 17 deletions(-) diff --git a/geos-mesh/tests/test_CreateConstantAttributePerRegion.py b/geos-mesh/tests/test_CreateConstantAttributePerRegion.py index d0bf43d6..3cb3b57f 100644 --- a/geos-mesh/tests/test_CreateConstantAttributePerRegion.py +++ b/geos-mesh/tests/test_CreateConstantAttributePerRegion.py @@ -6,35 +6,77 @@ # mypy: disable-error-code="operator" import pytest from typing import Union, Any -from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet, vtkPointData, vtkCellData ) +from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet ) -from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion +from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion, getArrayInObject, vnp, np -@pytest.mark.parametrize( "mesh, regionName, newAttributeName, dictRegion, valueType", [ - ( "dataset", "FAULT", "newAttribute", { 0: 0, 100: 1 }, 10 ) +@pytest.mark.parametrize( "mesh, newAttributeName, regionName, dictRegion, componentNames, componentNamesTest, valueNpType, succeed", [ + # Test the name of the new attribute (new on the mesh, one present on the other piece). + ## For vtkDataSet. + ( "dataset", "newAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + ( "dataset", "CellAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + ## For vtkMultiBlockDataSet. + ( "multiblock", "newAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + ( "multiblock", "CellAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + ( "multiblock", "GLOBAL_IDS_CELLS", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + # Test if the region attribute is on cells or on points. + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.float32, True ), + # Test the component name. + ( "dataset", "newAttribute", "FAULT", {}, ( "X" ), (), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), ( "Component0", "Component1" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", {}, ( "X" ), ( "Component0", "Component1" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", {}, ( "X" , "Y" ), ( "X" , "Y" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", {}, ( "X" , "Y", "Z" ), ( "X" , "Y" ), np.float32, True ), + # Test the type of value. + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int8, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int16, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int32, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int64, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint8, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint16, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint32, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint64, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.float64, True ), + # Test index/value. + ( "dataset", "newAttribute", "FAULT", { 0: [ 0 ], 100: [ 1 ] }, (), (), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { 0: [ 0 ], 100: [ 1 ], 101: [ 2 ] }, (), (), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { 0: [ 0 ], 100: [ 1 ], 101: [ 2 ], 2: [ 3 ] }, (), (), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { 0: [ 0, 0 ], 100: [ 1, 1 ] }, (), ( "Component0", "Component1" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { 0: [ 0, 0 ], 100: [ 1, 1 ], 101: [ 2, 2 ] }, (), ( "Component0", "Component1" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { 0: [ 0, 0 ], 100: [ 1, 1 ], 101: [ 2, 2 ], 2: [ 3, 3 ] }, (), ( "Component0", "Component1" ), np.float32, True ), + # Test common error. + ## Number of components. + ( "dataset", "newAttribute", "FAULT", { 0: [ 0 ], 100: [ 1, 1 ] }, (), (), np.float32, False ), # Number of value inconsistent. + ( "dataset", "newAttribute", "FAULT", { 0: [ 0, 0 ], 100: [ 1, 1 ] }, (), (), np.float32, False ), # More values than components. + ( "dataset", "newAttribute", "FAULT", { 0: [ 0 ], 100: [ 1 ] }, ( "X" , "Y" ), ( "X" , "Y" ), np.float32, False ), # More components than value. + ## Attribute name. + ( "dataset", "PERM", "FAULT", {}, (), (), np.float32, False ), # The attribute name already exist. + ## Region attribute. + ( "dataset", "newAttribute", "PERM", {}, (), (), np.float32, False ), # Region attribute has too many components. + ( "multiblock", "newAttribute", "FAULT", {}, (), (), np.float32, False ), # Region attribute is partial. ] ) def test_CreateConstantAttributePerRegion( dataSetTest: Union[ vtkMultiBlockDataSet, vtkDataSet ], mesh: str, - regionName: str, newAttributeName: str, + regionName: str, dictRegion: dict[ Any, Any ], - valueType: int, + componentNames: tuple[ str, ... ], + componentNamesTest: tuple[ str, ... ], + valueNpType: int, + succeed: bool, ) -> None: input_mesh: Union[ vtkMultiBlockDataSet, vtkDataSet ] = dataSetTest( mesh ) + nbComponents: int = len( componentNamesTest ) + if nbComponents == 0: + nbComponents += 1 filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( input_mesh, regionName, - newAttributeName, dictRegion, - valueType, + newAttributeName, + nbComponents=nbComponents, + componentNames=componentNames, + valueNpType=valueNpType, ) - filter.applyFilter() - - #meshFiltered: Union[ vtkMultiBlockDataSet, vtkDataSet ] = filter.mesh - - if isinstance( input_mesh, vtkMultiBlockDataSet ): - assert 1 == 1 - - else: - assert input_mesh.GetCellData().HasArray( newAttributeName ) == 1 + assert filter.applyFilter() == succeed From c9ae02878a1d7fb0077df5ff59fa09a0d5a03b74 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 10:56:36 +0200 Subject: [PATCH 46/56] Clean the log and the doc --- .../CreateConstantAttributePerRegion.py | 335 +++++++++++------- 1 file changed, 209 insertions(+), 126 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py index 5db3ba7c..127827fb 100644 --- a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py +++ b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py @@ -13,23 +13,18 @@ vtkDataSet, ) -from geos.utils.Logger import getLogger, Logger, logging -from geos.mesh.utils.arrayHelpers import isAttributeInObject, getNumberOfComponents, getArrayInObject, isAttributeGlobal +from geos.utils.Logger import getLogger, Logger, logging, CountWarningHandler +from geos.mesh.utils.arrayHelpers import ( getArrayInObject, getComponentNames, getNumberOfComponents, getVtkDataTypeInObject, isAttributeGlobal, isAttributeInObject ) from geos.mesh.utils.arrayModifiers import createAttribute, createConstantAttributeDataSet, createConstantAttributeMultiBlock __doc__ = """ CreateConstantAttributePerRegion is a vtk filter that allows to create an attribute -with constant values for each chosen indexes of a reference/region attribute. -The region attribute has to have one component and the created attribute has one component. -Region indexes, values and values types are choose by the user, if other region indexes exist -values are set to nan for float type, -1 for int type or 0 for uint type. - -Input mesh is either vtkMultiBlockDataSet or vtkDataSet. -The value type is encoded by a int using the vtk typecode to preserve the coherency -(https://github.com/Kitware/VTK/blob/master/Wrapping/Python/vtkmodules/util/numpy_support.py). -The relation index/value is given by a dictionary. Its keys are the indexes and its items are values. -To use a specific handler for the logger, set the variable 'speHandler' to True and use the -member function addLoggerHandler (useful for paraview for example). +with constant values per components for each chosen indexes of a reference/region attribute. +If other region indexes exist values are set to nan for float type, -1 for int type or 0 for uint type. + +Input mesh is either vtkMultiBlockDataSet or vtkDataSet and the region attribute must have one component. +The relation index/values is given by a dictionary. Its keys are the indexes and its items are the list of values for each component. +To use a specific handler for the logger, set the variable 'speHandler' to True and use the member function addLoggerHandler. To use it: @@ -40,25 +35,30 @@ # filter inputs mesh: Union[vtkMultiBlockDataSet, vtkDataSet] regionName: str + dictRegionValues: dict[ Any, Any ] newAttributeName: str - dictRegion: dict[Any, Any] - valueType: int, optional defaults to 10 (float32) + valueNpType: type, optional defaults to numpy.float32 + nbComponents: int, optional default to 1. + componentNames: tuple[ str, ... ], optional defaults to an empty tuple. speHandler: bool, optional defaults to False # instantiate the filter filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( mesh, regionName, + dictRegionValues, newAttributeName, - dictRegion, - valueType, + valueNpType, + nbComponents, + componentNames, speHandler, ) + # Set the specific handler (only if speHandler is True). specificHandler: logging.Handler filter.addLoggerHandler( specificHandler ) + # Do calculations. filter.applyFilter() - """ loggerTitle: str = "Create constant attribute per region" @@ -69,9 +69,11 @@ def __init__( self: Self, mesh: Union[ vtkDataSet, vtkMultiBlockDataSet ], regionName: str, + dictRegionValues: dict[ Any, Any ], newAttributeName: str, - dictRegion: dict[ Any, Any ], - valueType: int = 10, + valueNpType: type = np.float32, + nbComponents: int = 1, + componentNames: tuple[ str, ... ] = (), # noqa: C408 speHandler: bool = False, ) -> None: """Create an attribute with constant value per region. @@ -79,24 +81,36 @@ def __init__( Args: mesh (Union[ vtkDataSet, vtkMultiBlockDataSet ]): The mesh where to create the constant attribute per region. regionName (str): The name of the attribute with the region indexes. + dictRegionValues (dict[ Any, Any ]): The dictionary with the region indexes as keys and the list of values as items. newAttributeName (str): The name of the new attribute to create. - dictRegion (dict[ Any, Any ]): The dictionary with the region indexes as keys and their values as items. - For other region indexes, the attribute will be filled with a default value: - 0 for uint data. - -1 for int data. - nan for float data. - valueType (int, optional): The type of the value using the vtk typecode. - Defaults to 10 (float32). - speHandler (bool, optional): True To use a specific handler, False to use the internal handler. + nbComponents (int, optional): Number of components for the new attribute. + Defaults to 1. + componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. + Defaults to an empty tuple. + valueNpType (type, optional): The numpy scalar type for values. + Defaults to numpy.float32. + speHandler (bool, optional): True to use a specific handler, False to use the internal handler. Defaults to False. """ self.mesh: Union[ vtkDataSet, vtkMultiBlockDataSet ] = mesh - self.regionName: str = regionName + + # New attribute settings. self.newAttributeName: str = newAttributeName - self.useDefaultValue: bool = False # Check if the new component have default values (information for the output message) - self._setInfoRegion( dictRegion, valueType ) + self.valueNpType: type = valueNpType + self.nbComponents: int = nbComponents + self.componentNames: tuple[ str, ... ] = componentNames + + # Region attribute settings. + self.regionName: str = regionName + self.dictRegionValues: dict[ Any, Any ] = dictRegionValues - # Logger + self.useDefaultValue: bool = False # Check if the new component have default values (information for the output message). + + # Warnings counter. + self.counter: CountWarningHandler = CountWarningHandler() + self.counter.setLevel( logging.INFO ) + + # Logger. if not speHandler: self.logger: Logger = getLogger( loggerTitle, True ) else: @@ -115,64 +129,69 @@ def setLoggerHandler( self: Self, handler: logging.Handler ) -> None: if not self.logger.hasHandlers(): self.logger.addHandler( handler ) else: + # This warning does not count for the number of warning created during the application of the filter. self.logger.warning( "The logger already has an handler, to use yours set the argument 'speHandler' to True during the filter initialization." ) def applyFilter( self: Self ) -> bool: - """Create a constant attribute per region in the mesh + """Create a constant attribute per region in the mesh. Returns: boolean (bool): True if calculation successfully ended, False otherwise. """ self.logger.info( f"Apply filter { self.logger.name }." ) - # Get the piece of the attribute region if it is in the mesh. - onPoints: bool - piece: str = "" - if isAttributeInObject( self.mesh, self.regionName, False ): - onPoints = False - piece = "cells" - if isAttributeInObject( self.mesh, self.regionName, True ): - # Check if the attribute is on the two pieces. - if piece == "cells": - self.logger.warning( f"The attribute { self.regionName } is on both cells and points, by default the new attribute { self.newAttributeName } will be created on points.") + # Add the handler to count warnings messages. + self.logger.addHandler( self.counter ) - onPoints = True - piece = "points" - - # Check if the attribute is on points or on cells. - if piece not in ( "points", "cells" ): + # Check the validity of the attribute region. + self._setPieceRegionAttribute() + if self.onPoints is None: self.logger.error( f"{ self.regionName } is not in the mesh." ) self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) self.logger.error( f"The filter { self.logger.name } failed.") return False - # Check the validity of the attribute region. - nbComponents: int = getNumberOfComponents( self.mesh, self.regionName, onPoints ) - if nbComponents != 1: + if self.onBoth: + self.logger.error( f"Their is two attribute named { self.regionName }, one on points and the other on cells. The region attribute must be unique." ) + self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) + self.logger.error( f"The filter { self.logger.name } failed.") + return False + + nbComponentsRegion: int = getNumberOfComponents( self.mesh, self.regionName, self.onPoints ) + if nbComponentsRegion != 1: self.logger.error( f"The region attribute { self.regionName } has to many components, one is requires." ) self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) self.logger.error( f"The filter { self.logger.name } failed.") return False - + + self._setInfoRegion() + # Check if the number of components and number of values for the region indexes are coherent. + for index in self.dictRegionValues: + if len( self.dictRegionValues[ index ] ) != self.nbComponents: + self.logger.error( f"The number of value given for the region index { index } is not correct. You must set a value for each component, in this case { self.nbComponents }." ) + return False + trueIndexes: list[ Any ] = [] falseIndexes: list[ Any ] = [] regionNpArray: npt.NDArray[ Any ] npArray: npt.NDArray[ Any ] if isinstance( self.mesh, vtkMultiBlockDataSet ): - if not isAttributeGlobal( self.mesh, self.regionName, onPoints ): + # Check if the attribute region is global. + if not isAttributeGlobal( self.mesh, self.regionName, self.onPoints ): self.logger.error( f"The region attribute { self.regionName } has to be global." ) self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) self.logger.error( f"The filter { self.logger.name } failed.") return False - trueIndexes, falseIndexes = self._getTrueIndexesInMultiBlock( self.mesh, onPoints ) + trueIndexes, falseIndexes = self._getTrueIndexesInMultiBlock( self.mesh ) if len( trueIndexes ) == 0: - if len( self.dictRegion.keys() ) == 0: + if len( self.dictRegionValues ) == 0: self.logger.warning( "No region indexes entered." ) else: self.logger.warning( f"The region indexes entered are not in the region attribute { self.regionName }." ) - if not createConstantAttributeMultiBlock( self.mesh, [ self.defaultValue ], self.newAttributeName, onPoints=onPoints, logger=self.logger ): + + if not createConstantAttributeMultiBlock( self.mesh, self.defaultValue, self.newAttributeName, componentNames=self.componentNames, onPoints=self.onPoints, logger=self.logger ): self.logger.error( f"The filter { self.logger.name } failed.") return False @@ -183,91 +202,105 @@ def applyFilter( self: Self ) -> bool: # Parse the mesh to add the attribute on each block. nbBlock: int = self.mesh.GetNumberOfBlocks() for idBlock in range( nbBlock ): - dataSetInput: vtkDataSet = self.mesh.GetBlock( idBlock ) + dataSet: vtkDataSet = vtkDataSet.SafeDownCast( self.mesh.GetBlock( idBlock ) ) - regionNpArray = getArrayInObject( dataSetInput, self.regionName, onPoints ) + regionNpArray = getArrayInObject( dataSet, self.regionName, self.onPoints ) npArray = self._createNpArray( regionNpArray ) - if not createAttribute( dataSetInput, npArray, self.newAttributeName, onPoints=onPoints, logger=self.logger ): + if not createAttribute( dataSet, npArray, self.newAttributeName, componentNames=self.componentNames, onPoints=self.onPoints, logger=self.logger ): self.logger.error( f"The filter { self.logger.name } failed.") return False else: - trueIndexes, falseIndexes = self._getTrueIndexesInDataSet( self.mesh, onPoints ) + trueIndexes, falseIndexes = self._getTrueIndexesInDataSet( self.mesh ) if len( trueIndexes ) == 0: - if len( self.dictRegion.keys() ) == 0: + if len( self.dictRegionValues ) == 0: self.logger.warning( "No region indexes entered." ) else: self.logger.warning( f"The region indexes entered are not in the region attribute { self.regionName }." ) - - if not createConstantAttributeDataSet( self.mesh, [ self.defaultValue ], self.newAttributeName, onPoints=onPoints, logger=self.logger ): + + if not createConstantAttributeDataSet( self.mesh, self.defaultValue, self.newAttributeName, componentNames=self.componentNames, onPoints=self.onPoints, logger=self.logger ): self.logger.error( f"The filter { self.logger.name } failed.") return False - + else: if len( falseIndexes ) > 0: self.logger.warning( f"The region indexes { falseIndexes } are not in the region attribute { self.regionName }." ) - regionNpArray = getArrayInObject( self.mesh, self.regionName, onPoints ) + regionNpArray = getArrayInObject( self.mesh, self.regionName, self.onPoints ) npArray = self._createNpArray( regionNpArray ) - if not createAttribute( self.mesh, npArray, self.newAttributeName, onPoints=onPoints, logger=self.logger ): + if not createAttribute( self.mesh, npArray, self.newAttributeName, componentNames=self.componentNames, onPoints=self.onPoints, logger=self.logger ): self.logger.error( f"The filter { self.logger.name } failed.") return False - + # Log the output message. - self._logOutputMessage( trueIndexes, piece ) - + self._logOutputMessage( trueIndexes ) + return True - - def _setInfoRegion( self: Self, dictRegion: dict[ Any, Any ], valueType: int ) -> None: - """Set attributes self.valueType, self.dictRegion and self.defaultValue. - The type of values and the default value are set with the numpy type given by valueType. - The default value is set to nan for float data, -1 for int data and 0 for uint data. - Args: - dictRegion (dict[Any, Any]): The dictionary with the region indexes and its corresponding value. - valueType (int): The type of the constant value with the VTK typecode. + def _setPieceRegionAttribute( self: Self ) -> None: + """Set the attribute self.onPoints and self.onBoth. + + self.onPoints is True if the region attribute is on points, False if it is on cells, None otherwise. + + self.onBoth is True if a region attribute is on points and on cells, False otherwise. + """ + self.onPoints: Union[ bool, None ] = None + self.onBoth: bool = False + if isAttributeInObject( self.mesh, self.regionName, False ): + self.onPoints = False + if isAttributeInObject( self.mesh, self.regionName, True ): + if self.onPoints == False: + self.onBoth = True + self.onPoints = True + + + def _setInfoRegion( self: Self ) -> None: + """Update self.dictRegion and set self.defaultValue. + Values and default value type are set with the numpy type given by self.valueNpType. + Default value is set to nan for float data, -1 for int data and 0 for uint data. """ # Get the numpy type from the vtk typecode. dictType: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() - self.valueType: type = dictType[ valueType ] + regionVtkType: int = getVtkDataTypeInObject( self.mesh, self.regionName, self.onPoints ) + regionNpType: type = dictType[ regionVtkType ] - # Set the correct type of the values for each region index to ensure the coherency. - self.dictRegion: dict[ Any, Any ] = dictRegion - for idRegion in self.dictRegion.keys(): - self.dictRegion[ idRegion ] = self.valueType( self.dictRegion[ idRegion ] ) + # Set the correct type of values and region index. + dictRegionValuesUpdateType: dict[ Any, Any ] = {} + for idRegion in self.dictRegionValues: + dictRegionValuesUpdateType[ regionNpType( idRegion ) ] = [ self.valueNpType( value ) for value in self.dictRegionValues[ idRegion ] ] + self.dictRegionValues = dictRegionValuesUpdateType - # Set the default value depending of the type. - self.defaultValue: Any + # Set the list of default value for each component depending of the type. + self.defaultValue: list [ Any ] ## Default value for float types is nan. - if self.valueType().dtype in ( "float32", "float64" ): - self.defaultValue = self.valueType( np.nan ) + if self.valueNpType in ( np.float32, np.float64 ): + self.defaultValue = [ self.valueNpType( np.nan ) for _ in range( self.nbComponents ) ] ## Default value for int types is -1. - elif self.valueType().dtype in ( "int8", "int16", "int32", "int64" ): - self.defaultValue = self.valueType( -1 ) + elif self.valueNpType in ( np.int8, np.int16, np.int32, np.int64 ): + self.defaultValue = [ self.valueNpType( -1 ) for _ in range( self.nbComponents ) ] ## Default value for uint types is 0. - elif self.valueType().dtype in ( "uint8", "uint16", "uint32", "uint64" ): - self.defaultValue = self.valueType( 0 ) + elif self.valueNpType in ( np.uint8, np.uint16, np.uint32, np.uint64 ): + self.defaultValue = [ self.valueNpType( 0 ) for _ in range( self.nbComponents ) ] - def _getTrueIndexesInMultiBlock( self: Self, multiBlockDataSet: vtkMultiBlockDataSet, onPoints: bool ) -> tuple[ list[ Any ], list[ Any ] ]: - """Check for each region index if it is a true index -ie the index is value of the attribute of at least one block, or a false index. + def _getTrueIndexesInMultiBlock( self: Self, multiBlockDataSet: vtkMultiBlockDataSet ) -> tuple[ list[ Any ], list[ Any ] ]: + """Check for each region index if it is a true index (the index is value of the attribute of at least one block), or a false index. Args: dataSet (vtkDataSet): The mesh with the attribute to check. - onPoints (bool): True if the attribute is on point, False if it is on cell. Returns: - tuple(list[Any], list[Any]): The tuple with the list of the true indexes and the list of the false indexes. + tuple(list[Any], list[Any]): Tuple with the list of the true indexes and the list of the false indexes. """ trueIndexes: list[ Any ] = [] falseIndexes: list[ Any ] = [] nbBlock: int = multiBlockDataSet.GetNumberOfBlocks() # Parse all blocks to get the true indexes of each block. for idBlock in range( nbBlock ): - block: vtkDataSet = multiBlockDataSet.GetBlock( idBlock ) + block: vtkDataSet = vtkDataSet.SafeDownCast( multiBlockDataSet.GetBlock( idBlock ) ) # Get the true and false indexes of the block. - trueIndexesBlock: list[ Any ] = self._getTrueIndexesInDataSet( block, onPoints )[ 0 ] + trueIndexesBlock: list[ Any ] = self._getTrueIndexesInDataSet( block )[ 0 ] # Keep the new true indexes. for index in trueIndexesBlock: @@ -275,28 +308,26 @@ def _getTrueIndexesInMultiBlock( self: Self, multiBlockDataSet: vtkMultiBlockDat trueIndexes.append( index ) # Get the false indexes. - for index in self.dictRegion.keys(): + for index in self.dictRegion: if index not in trueIndexes: falseIndexes.append( index ) return ( trueIndexes, falseIndexes ) - def _getTrueIndexesInDataSet( self: Self, dataSet: vtkDataSet, onPoints: bool ) -> tuple[ list[ Any ], list[ Any ] ]: - """Check for each region index if it is a true index -ie the index is value of the attribute, or a false index. + def _getTrueIndexesInDataSet( self: Self, dataSet: vtkDataSet ) -> tuple[ list[ Any ], list[ Any ] ]: + """Check for each region index if it is a true index (the index is value of the attribute), or a false index. Args: dataSet (vtkDataSet): The mesh with the attribute to check. - onPoints (bool): True if the attribute is on point, False if it is on cell. Returns: tuple(list[Any], list[Any]): The tuple with the list of the true indexes and the list of the false indexes. """ - regionIndexes: list[ Any ] = self.dictRegion.keys() - regionNpArray = getArrayInObject( dataSet, self.regionName, onPoints ) + regionNpArray = getArrayInObject( dataSet, self.regionName, self.onPoints ) trueIndexes: list[ Any ] = [] falseIndexes: list[ Any ] = [] - for index in regionIndexes: + for index in self.dictRegionValues: if index in regionNpArray: trueIndexes.append( index ) else: @@ -307,54 +338,106 @@ def _getTrueIndexesInDataSet( self: Self, dataSet: vtkDataSet, onPoints: bool ) def _createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArray[ Any ]: """Create an array from the input one. - If the value of the input array is a key of self.dictRegion, the corresponding value of the created array is its item. - If their is other value, the value self.defaultValue is set and the self.useDefaultValue is set to True. + If the value of the input array is a key of self.dictRegionValues, the corresponding list of value for each component of the created array is its item. + If their is other indexes than those given, their list of values are self.defaultValue and self.useDefaultValue is set to True. Args: regionNpArray (npt.NDArray[Any]): The array with the region indexes. Returns: - npt.NDArray[Any]: The array with the value instead of the region index. + npt.NDArray[Any]: The array with values instead of indexes. """ nbElements: int = len( regionNpArray ) - npArray: npt.NDArray[ Any ] = np.ones( nbElements, self.valueType ) + npArray: npt.NDArray[ Any ] + if self.nbComponents == 1: + npArray = np.ones( nbElements, self.valueNpType ) + else: + npArray = np.ones( ( nbElements, self.nbComponents ), self.valueNpType ) + for elem in range( nbElements ): - idRegion: Any = regionNpArray[ elem ] - if idRegion in self.dictRegion.keys(): - npArray[ elem ] = self.dictRegion[ idRegion ] + value: Any = regionNpArray[ elem ] + if value in self.dictRegionValues: + if self.nbComponents == 1: + npArray[ elem ] = self.dictRegionValues[ value ][ 0 ] + else: + npArray[ elem ] = self.dictRegionValues[ value ] else: - npArray[ elem ] = self.defaultValue - self.useDefaultValue = True + if self.nbComponents == 1: + npArray[ elem ] = self.defaultValue[ 0 ] + self.useDefaultValue = True + else: + npArray[ elem ] = self.defaultValue + self.useDefaultValue = True return npArray - def _logOutputMessage( self: Self, trueIndexes: list[ Any ], piece: str ) -> None: - """Create the result message of the filter and output it. + def _logOutputMessage( self: Self, trueIndexes: list[ Any ] ) -> None: + """Create and log result messages of the filter. Args: - trueIndexes (list[Any]): The list of the region indexes use to create the attribute. - piece (str): The piece where the data is (cell or point). - """ + trueIndexes (list[Any]): The list of the true region indexes use to create the attribute. + """ # The Filter succeed. self.logger.info( f"The filter { self.logger.name } succeed." ) # Info about the created attribute. ## The piece where the attribute is created. + piece: str = "points" if self.onPoints else "cells" self.logger.info( f"The new attribute { self.newAttributeName } is created on { piece }." ) - ## The values of the attribute. - mess: str = f"The new attribute { self.newAttributeName } is constant" - if len( trueIndexes ) == 0: - self.logger.warning( f"{ mess } with the value { self.defaultValue }." ) + ## The number of component and they names if multiple. + componentNamesCreated: tuple[ str, ... ] = getComponentNames( self.mesh, self.newAttributeName, self.onPoints ) + if self.nbComponents > 1: + messComponent: str = f"The new attribute { self.newAttributeName } has { self.nbComponents } components named { componentNamesCreated }." + if componentNamesCreated != self.componentNames: + ### Warn the user because other component names than those given have been used. + self.logger.warning( messComponent ) + else: + self.logger.info( messComponent ) + ## The values of the attribute. + messValue: str = f"The new attribute { self.newAttributeName } is constant" + if len( trueIndexes ) == 0: + ### Create the message to have the value of each component. + messValue = f"{ messValue } with" + if self.nbComponents > 1: + for idComponent in range( self.nbComponents ): + messValue = f"{ messValue } the value { self.defaultValue[ idComponent ] } for the component { componentNamesCreated[ idComponent ] }," + messValue = f"{ messValue[:-1] }." + else: + messValue = f"{ messValue } the value { self.defaultValue[ 0 ] }." + ### Warn the user because no region index has been used. + self.logger.warning( messValue ) + else: - mess = f"{ mess } per region indexes with:" + ### Create the message to have for each component the value of the region index. + messValue = f"{ messValue } per region indexes with:\n" for index in trueIndexes: - mess = f"{ mess } { self.dictRegion[ index ] } for index { index }," - + messValue = f"{ messValue }\tThe value { self.dictRegionValues[ index ][ 0 ] } for the" + if self.nbComponents > 1: + messValue = f"{ messValue } component { componentNamesCreated[ 0 ] }," + for idComponent in range( 1, self.nbComponents - 1 ): + messValue = f"{ messValue } the value { self.dictRegionValues[ index ][ idComponent ] } for the component { componentNamesCreated[ idComponent ] }," + messValue = f"{ messValue[ : -1 ] } and the value { self.dictRegionValues[ index ][ -1 ] } for the component { componentNamesCreated[ -1 ] } for the index { index }.\n" + else: + messValue = f"{ messValue } index { index }.\n" + if self.useDefaultValue: - self.logger.warning( f"{ mess } and { self.defaultValue } for the other indexes." ) + messValue = f"{ messValue }\tThe value { self.defaultValue[ 0 ] } for the" + if self.nbComponents > 1: + messValue = f"{ messValue } component { componentNamesCreated[ 0 ] }," + for idComponent in range( 1, self.nbComponents - 1 ): + messValue = f"{ messValue } the value { self.defaultValue[ idComponent ] } for the component { componentNamesCreated[ idComponent ] }," + messValue = f"{ messValue[ : -1 ] } and the value { self.defaultValue[ -1 ] } for the component { componentNamesCreated[ -1 ] } for the other indexes." + else: + messValue = f"{ messValue } other indexes." + ### Warn the user because a default value has been used. + self.logger.warning( messValue ) else: - self.logger.info( f"{ mess[:-1] }." ) + if self.counter.warningCount > 0: + ### Warn the user because other component names than those given have been used. + self.logger.warning( messValue ) + else: + self.logger.info( messValue ) \ No newline at end of file From e00c918d29ca372b9c517e48e6979390dc090b2c Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 11:06:44 +0200 Subject: [PATCH 47/56] Clean variables names --- .../CreateConstantAttributePerRegion.py | 4 ++-- .../test_CreateConstantAttributePerRegion.py | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py index 127827fb..3858d815 100644 --- a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py +++ b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py @@ -256,7 +256,7 @@ def _setPieceRegionAttribute( self: Self ) -> None: def _setInfoRegion( self: Self ) -> None: - """Update self.dictRegion and set self.defaultValue. + """Update self.dictRegionValues and set self.defaultValue. Values and default value type are set with the numpy type given by self.valueNpType. Default value is set to nan for float data, -1 for int data and 0 for uint data. """ @@ -308,7 +308,7 @@ def _getTrueIndexesInMultiBlock( self: Self, multiBlockDataSet: vtkMultiBlockDat trueIndexes.append( index ) # Get the false indexes. - for index in self.dictRegion: + for index in self.dictRegionValues: if index not in trueIndexes: falseIndexes.append( index ) diff --git a/geos-mesh/tests/test_CreateConstantAttributePerRegion.py b/geos-mesh/tests/test_CreateConstantAttributePerRegion.py index 3cb3b57f..67471679 100644 --- a/geos-mesh/tests/test_CreateConstantAttributePerRegion.py +++ b/geos-mesh/tests/test_CreateConstantAttributePerRegion.py @@ -8,9 +8,9 @@ from typing import Union, Any from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet ) -from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion, getArrayInObject, vnp, np +from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion, np -@pytest.mark.parametrize( "mesh, newAttributeName, regionName, dictRegion, componentNames, componentNamesTest, valueNpType, succeed", [ +@pytest.mark.parametrize( "meshType, newAttributeName, regionName, dictRegionValues, componentNames, componentNamesTest, valueNpType, succeed", [ # Test the name of the new attribute (new on the mesh, one present on the other piece). ## For vtkDataSet. ( "dataset", "newAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), @@ -57,26 +57,27 @@ ] ) def test_CreateConstantAttributePerRegion( dataSetTest: Union[ vtkMultiBlockDataSet, vtkDataSet ], - mesh: str, + meshType: str, newAttributeName: str, regionName: str, - dictRegion: dict[ Any, Any ], + dictRegionValues: dict[ Any, Any ], componentNames: tuple[ str, ... ], componentNamesTest: tuple[ str, ... ], valueNpType: int, succeed: bool, ) -> None: - input_mesh: Union[ vtkMultiBlockDataSet, vtkDataSet ] = dataSetTest( mesh ) + mesh: Union[ vtkMultiBlockDataSet, vtkDataSet ] = dataSetTest( meshType ) nbComponents: int = len( componentNamesTest ) - if nbComponents == 0: + if nbComponents == 0: # If one component their is no name. nbComponents += 1 - filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( input_mesh, + + filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( mesh, regionName, - dictRegion, + dictRegionValues, newAttributeName, + valueNpType=valueNpType, nbComponents=nbComponents, componentNames=componentNames, - valueNpType=valueNpType, ) assert filter.applyFilter() == succeed From 2632df86f6581a1f3d9f06002031d55312c8303d Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 11:32:22 +0200 Subject: [PATCH 48/56] Clean the doc and the variables names --- .../PVCreateConstantAttributePerRegion.py | 291 +++++++++++------- 1 file changed, 180 insertions(+), 111 deletions(-) diff --git a/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py b/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py index 7d0adfb1..93aaf25f 100644 --- a/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py +++ b/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py @@ -33,23 +33,23 @@ update_paths() -from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion +from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion, vnp, np __doc__ = """ -PVCreateConstantAttributePerRegion is a paraview Plugin that allows to create an attribute -with constant values for each chosen indexes of a reference/region attribute. -The region attribute has to have one component and the created attribute has one component. -Region indexes, values and values types are choose by the user, if other region indexes exist -values are set to nan for float type, -1 for int type or 0 for uint type. +PVCreateConstantAttributePerRegion is a Paraview plugin that allows to create an attribute +with constant values per components for each chosen indexes of a reference/region attribute. +If other region indexes exist values are set to nan for float type, -1 for int type or 0 for uint type. -Input and output meshes are either vtkMultiBlockDataSet or vtkDataSet. +Input mesh is either vtkMultiBlockDataSet or vtkDataSet and the region attribute must have one component. +The relation index/values is given by a dictionary. Its keys are the indexes and its items are the list of values for each component. To use it: * Load the module in Paraview: Tools>Manage Plugins...>Load new>PVCreateConstantAttributePerRegion. * Select the mesh you want to create the attributes and containing a region attribute. * Select the filter Create Constant Attribute Per Region in filter|0- Geos Pre-processing. -* Set variables (region attribute, value type, attribute name, index and its value) and Apply. +* Choose the region attribute, the relation index/values, the new attribute name, the type of the value, the number of components and their names. +* Apply. """ @@ -71,39 +71,134 @@ def __init__( self: Self ) -> None: nOutputPorts=1, inputType="vtkDataObject", outputType="vtkDataObject" ) + + self.clearDictRegionValues: bool = True + + # Region attribute settings. + self.regionName: str = "" + self.dictRegionValues: dict[ Any, Any ] = {} - self.dictRegion: dict[ Any, Any ] = {} - self.clearDictRegion: bool = True - self.regionName: str = "Region" + # New attribute settings. self.newAttributeName: str = "newAttribute" - self.valueType: int = 10 + self.valueNpType: type = np.float32 + self.nbComponents: int = 1 + self.componentNames: tuple[ str, ... ] = () + + # Use the handler of paraview for the log. self.speHandler: bool = True + + # Settings of the attribute with the region indexes: + @smproperty.stringvector( + name="ChooseRegionAttribute", + label="Attribute with region indexes", + default_values="Choose an attribute", + number_of_elements="1", + element_types="2", + ) + @smdomain.xml(""" + + + + + + + Select the attribute to consider as the region attribute containing the indexes of the regions. + + + + + """ ) + def _setRegionAttributeName( self: Self, regionName: str ) -> None: + """Set region attribute name. + + Args: + regionName (str): The name of the attribute to consider as the region attribute. + """ + self.regionName = regionName + self.Modified() + + + @smproperty.xml(""" + + + Set the value of the new attribute for each region indexes, use a space between the value of each components:\n + valueRegionIndex | valueComponent1 valueComponent2 ...\n + If the region attribute has other indexes than those given, a default value is use:\n + 0 for uint type, -1 for int type and nan for float type. + + + + + + + + + + """ ) + def _setDictRegionValues( self: Self, regionIndex: str, value: str ) -> None: + """Set the the dictionary with the region indexes and its corresponding list of value for each components. + + Args: + regionIndex (str): Region index of the region attribute to consider. + value (str): List of value to use for the regionIndex. If multiple components use a coma between the value of each component. + """ + if self.clearDictRegionValues: + self.dictRegionValues = {} + self.clearDictRegionValues = False + + if regionIndex != None and value != None : + self.dictRegionValues[ regionIndex ] = list( value.split( "," ) ) + + self.Modified() + + + @smproperty.xml( """ + + + + """ ) + def _groupeRegionAttributeSettingsWidgets( self: Self ) -> None: + """Group the widgets to set the settings of the region attribute.""" + self.Modified() + + + # Settings of the new attribute: @smproperty.xml( """ - Name of the new attribute + Name of the new attribute to create. - """ ) - def a02SetAttributeName( self: Self, value: str ) -> None: + """ ) + def _setAttributeName( self: Self, newAttributeName: str ) -> None: """Set attribute name. Args: - value (str): attribute name. + newAttributeName (str): Name of the new attribute to create. """ - if self.newAttributeName != value: - self.newAttributeName = value + self.newAttributeName = newAttributeName self.Modified() + @smproperty.intvector( name="ValueType", - label="Values type", + label="The type of the values:", number_of_elements=1, default_values=10, panel_visibility="default", @@ -120,113 +215,86 @@ def a02SetAttributeName( self: Self, value: str ) -> None: - - The values type of the attribute. Each type is encoded by a int using the vtk typecode. + The wanted numpy scalar type for values of the new attribute. - """ ) - def a02IntSingle( self: Self, value: int ) -> None: - """Define an input int field. + """ ) + def _setValueType( self: Self, valueType: int ) -> None: + """Set the type for the value used to create the new attribute. Args: - value (int): Input + valueType (int): The type for the value encoding with the vtk typecode. """ - if value != self.valueType: - self.valueType = value - self.Modified() - - @smproperty.xml( """ - - - - """ ) - def b02GroupFlow1( self: Self ) -> None: - """Organize groups.""" + dictType: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() + self.valueNpType: type = dictType[ valueType ] self.Modified() - @smproperty.stringvector( - name="ChooseRegionAttribute", - label="Attribute with region indexes", - command="a01SetRegionAttributeName", - default_values="Choose an attribute", - number_of_elements="1", - element_types="2", - ) - @smdomain.xml(""" - - - - - + + @smproperty.intvector( + name="NumberOfComponents", + label="Number of components:", + number_of_elements=1, + default_values=1, + panel_visibility="default", + ) + @smdomain.xml( """ - Select an attribute containing the indexes of the regions + The number of components for the new attribute to create. - - - """ ) - def a01SetRegionAttributeName( self: Self, name: str ) -> None: - """Set region attribute name.""" - if self.regionName != name: - self.regionName = name + def _setNbComponent( self: Self, nbComponents: int ) -> None: + """Set the number of components of the attribute to create. + + Args: + nbComponents (int): Number of components for the new attribute. + """ + self.nbComponents = nbComponents self.Modified() - @smproperty.xml(""" - - - - - - - - - - Set the constant value of the new attribute for each region indexes. - - - """ ) - def b01SetAttributeValues( self: Self, regionIndex: str, value: str ) -> None: - """Set the constant value of the new attribute for each region indexes. + + @smproperty.stringvector( + name="ComponentNames", + label="Names of components:", + number_of_elements=1, + default_values="Change if multiple components", + panel_visibility="default", + ) + @smdomain.xml( """ + + Names of components if multiple for the new attribute to create. + Use the coma and a space between each component name:\n + Names of components: X Y Z + + """ ) + def _setComponentNames( self: Self, componentNames: str ) -> None: + """Set the names of the components of the attribute to create. Args: - regionIndex (int): Region index. - value (float): Attribute constant value for the regionIndex. + componentNamesStr (str): Names of component for the new attribute. Use a coma between each component names. """ - if self.clearDictRegion: - self.dictRegion = {} - self.clearDictRegion = False + if componentNames == "" or componentNames == "Change if multiple components" or self.nbComponents == 1: + self.componentNames = () + else: + self.componentNames = tuple( componentNames.split( "," ) ) - if regionIndex != None and value != None : - assert "," not in regionIndex, "Use the '.' not the ',' for decimal numbers" - assert "," not in value, "Use the '.' not the ',' for decimal numbers" - regionIndex = float( regionIndex ) - value = float( value ) - if regionIndex not in self.dictRegion.keys(): - self.dictRegion[regionIndex] = value self.Modified() + @smproperty.xml( """ - - + + + + """ ) - def b02GroupFlow( self: Self ) -> None: - """Organize groups.""" + def _groupNewAttributeSettingsWidgets( self: Self ) -> None: + """Group the widgets to set the settings of the new attribute.""" self.Modified() + def RequestDataObject( self: Self, request: vtkInformation, @@ -251,6 +319,7 @@ def RequestDataObject( outInfoVec.GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), outData ) return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] + def RequestData( self: Self, request: vtkInformation, # noqa: F841 @@ -274,21 +343,21 @@ def RequestData( assert outputMesh is not None, "Output pipeline is null." outputMesh.ShallowCopy( inputMesh ) - filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( outputMesh, self.regionName, + self.dictRegionValues, self.newAttributeName, - self.dictRegion, - self.valueType, - self.speHandler, ) - if self.speHandler and not filter.logger.hasHandlers(): + self.valueNpType, + self.nbComponents, + self.componentNames, + self.speHandler, + ) + + if not filter.logger.hasHandlers(): filter.setLoggerHandler( VTKHandler() ) - + filter.applyFilter() - outputMesh.ShallowCopy( filter.mesh ) self.clearDictRegion = True - self.speHandler = False return 1 - From 96f22373ea0f1f936310c33cf4bac007face9e4c Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 11:51:50 +0200 Subject: [PATCH 49/56] Clean variables name and typing --- geos-mesh/src/geos/mesh/utils/arrayHelpers.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index 78e98adf..26def6ea 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -37,7 +37,7 @@ def has_array( mesh: vtkUnstructuredGrid, array_names: list[ str ] ) -> bool: bool: True if at least one array is found, else False. """ # Check the cell data fields - data: vtkFieldData | None + data: Union[ vtkFieldData, None ] for data in ( mesh.GetCellData(), mesh.GetFieldData(), mesh.GetPointData() ): if data is None: continue # type: ignore[unreachable] @@ -63,7 +63,7 @@ def getFieldType( data: vtkFieldData ) -> str: str: "vtkFieldData", "vtkCellData" or "vtkPointData" """ if not data.IsA( "vtkFieldData" ): - raise ValueError( f"data '{data}' entered is not a vtkFieldData object." ) + raise ValueError( f"data '{ data }' entered is not a vtkFieldData object." ) if data.IsA( "vtkCellData" ): return "vtkCellData" elif data.IsA( "vtkPointData" ): @@ -82,7 +82,7 @@ def getArrayNames( data: vtkFieldData ) -> list[ str ]: list[str]: The array names in the order that they are stored in the field data. """ if not data.IsA( "vtkFieldData" ): - raise ValueError( f"data '{data}' entered is not a vtkFieldData object." ) + raise ValueError( f"data '{ data }' entered is not a vtkFieldData object." ) return [ data.GetArrayName( i ) for i in range( data.GetNumberOfArrays() ) ] @@ -98,7 +98,7 @@ def getArrayByName( data: vtkFieldData, name: str ) -> Optional[ vtkDataArray ]: """ if data.HasArray( name ): return data.GetArray( name ) - logging.warning( f"No array named '{name}' was found in '{data}'." ) + logging.warning( f"No array named '{ name }' was found in '{ data }'." ) return None @@ -134,14 +134,14 @@ def getNumpyGlobalIdsArray( data: Union[ vtkCellData, vtkPointData ] ) -> Option return vtk_to_numpy( global_ids ) -def getNumpyArrayByName( data: vtkCellData | vtkPointData, name: str, sorted: bool = False ) -> Optional[ npt.NDArray ]: +def getNumpyArrayByName( data: Union[ vtkCellData, vtkPointData ], name: str, sorted: bool = False ) -> Optional[ npt.NDArray ]: """Get the numpy array of a given vtkDataArray found by its name. If sorted is selected, this allows the option to reorder the values wrt GlobalIds. If not GlobalIds was found, no reordering will be perform. Args: - data (vtkCellData | vtkPointData): Vtk field data. + data (Union[vtkCellData, vtkPointData]): Vtk field data. name (str): Array name to sort. sorted (bool, optional): Sort the output array with the help of GlobalIds. Defaults to False. @@ -216,18 +216,18 @@ def getAttributesFromMultiBlockDataSet( object: Union[ vtkMultiBlockDataSet, vtk """ attributes: dict[ str, int ] = {} # initialize data object tree iterator - iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() - iter.SetDataSet( object ) - iter.VisitOnlyLeavesOn() - iter.GoToFirstItem() - while iter.GetCurrentDataObject() is not None: - dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) + iterator: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() + iterator.SetDataSet( object ) + iterator.VisitOnlyLeavesOn() + iterator.GoToFirstItem() + while iterator.GetCurrentDataObject() is not None: + dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iterator.GetCurrentDataObject() ) blockAttributes: dict[ str, int ] = getAttributesFromDataSet( dataSet, onPoints ) for attributeName, nbComponents in blockAttributes.items(): if attributeName not in attributes: attributes[ attributeName ] = nbComponents - iter.GoToNextItem() + iterator.GoToNextItem() return attributes @@ -293,15 +293,15 @@ def isAttributeInObjectMultiBlockDataSet( object: vtkMultiBlockDataSet, attribut Returns: bool: True if the attribute is in the table, False otherwise. """ - iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() - iter.SetDataSet( object ) - iter.VisitOnlyLeavesOn() - iter.GoToFirstItem() - while iter.GetCurrentDataObject() is not None: - dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) + iterator: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() + iterator.SetDataSet( object ) + iterator.VisitOnlyLeavesOn() + iterator.GoToFirstItem() + while iterator.GetCurrentDataObject() is not None: + dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iterator.GetCurrentDataObject() ) if isAttributeInObjectDataSet( dataSet, attributeName, onPoints ): return True - iter.GoToNextItem() + iterator.GoToNextItem() return False @@ -324,7 +324,7 @@ def isAttributeInObjectDataSet( object: vtkDataSet, attributeName: str, onPoints else: data = object.GetCellData() sup = "Cell" - assert data is not None, f"{sup} data was not recovered." + assert data is not None, f"{ sup } data was not recovered." return bool( data.HasArray( attributeName ) ) @@ -342,7 +342,7 @@ def isAttributeGlobal( object: vtkMultiBlockDataSet, attributeName: str, onPoint isOnBlock: bool nbBlock: int = object.GetNumberOfBlocks() for idBlock in range( nbBlock ): - block: vtkDataSet = cast( vtkDataSet, object.GetBlock( idBlock ) ) + block: vtkDataSet = vtkDataSet.SafeDownCast( object.GetBlock( idBlock ) ) isOnBlock = isAttributeInObjectDataSet( block, attributeName, onPoints ) if not isOnBlock: return False @@ -396,7 +396,7 @@ def getVtkArrayTypeInMultiBlock( multiBlockDataSet: vtkMultiBlockDataSet, attrib """ nbBlocks = multiBlockDataSet.GetNumberOfBlocks() for idBlock in range( nbBlocks ): - object: vtkDataSet = cast( vtkDataSet, multiBlockDataSet.GetBlock( idBlock ) ) + object: vtkDataSet = vtkDataSet.SafeDownCast( multiBlockDataSet.GetBlock( idBlock ) ) listAttributes: set[ str ] = getAttributeSet( object, onPoints ) if attributeName in listAttributes: return getVtkArrayTypeInObject( object, attributeName, onPoints ) @@ -475,7 +475,7 @@ def getNumberOfComponentsMultiBlock( """ elementaryBlockIndexes: list[ int ] = getBlockElementIndexesFlatten( dataSet ) for blockIndex in elementaryBlockIndexes: - block: vtkDataSet = cast( vtkDataSet, getBlockFromFlatIndex( dataSet, blockIndex ) ) + block: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( dataSet, blockIndex ) ) if isAttributeInObject( block, attributeName, onPoints ): array: vtkDataArray = getVtkArrayInObject( block, attributeName, onPoints ) return array.GetNumberOfComponents() @@ -541,7 +541,7 @@ def getComponentNamesMultiBlock( """ elementaryBlockIndexes: list[ int ] = getBlockElementIndexesFlatten( dataSet ) for blockIndex in elementaryBlockIndexes: - block: vtkDataSet = cast( vtkDataSet, getBlockFromFlatIndex( dataSet, blockIndex ) ) + block: vtkDataSet = vtkDataSet.SafeDownCast( getBlockFromFlatIndex( dataSet, blockIndex ) ) if isAttributeInObject( block, attributeName, onPoints ): return getComponentNamesDataSet( block, attributeName, onPoints ) return () From 2ac03bccb76e78be0d2c8599918f640093dbeb67 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 11:55:23 +0200 Subject: [PATCH 50/56] Remove the AsDF function --- geos-mesh/src/geos/mesh/utils/arrayHelpers.py | 32 ++----------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index 26def6ea..0e8939d6 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -7,7 +7,7 @@ import numpy.typing as npt import pandas as pd # type: ignore[import-untyped] import vtkmodules.util.numpy_support as vnp -from typing import Optional, Union, Any, cast +from typing import Optional, Union, Any from vtkmodules.util.numpy_support import vtk_to_numpy from vtkmodules.vtkCommonCore import vtkDataArray, vtkPoints from vtkmodules.vtkCommonDataModel import ( vtkUnstructuredGrid, vtkFieldData, vtkMultiBlockDataSet, vtkDataSet, @@ -568,35 +568,7 @@ def getAttributeValuesAsDF( surface: vtkPolyData, attributeNames: tuple[ str, .. if len( array.shape ) > 1: for i in range( array.shape[ 1 ] ): - data[ attributeName + f"_{i}" ] = array[ :, i ] - data.drop( columns=[ attributeName ], inplace=True ) - else: - data[ attributeName ] = array - return data - - -def AsDF( surface: vtkPolyData, attributeNames: tuple[ str, ...] ) -> pd.DataFrame: - """Get attribute values from input surface. - - Args: - surface (vtkPolyData): Mesh where to get attribute values. - attributeNames (tuple[str,...]): Tuple of attribute names to get the values. - - Returns: - pd.DataFrame: DataFrame containing property names as columns. - - """ - nbRows: int = surface.GetNumberOfCells() - data: pd.DataFrame = pd.DataFrame( np.full( ( nbRows, len( attributeNames ) ), np.nan ), columns=attributeNames ) - for attributeName in attributeNames: - if not isAttributeInObject( surface, attributeName, False ): - logging.warning( f"Attribute {attributeName} is not in the mesh." ) - continue - array: npt.NDArray[ np.float64 ] = getArrayInObject( surface, attributeName, False ) - - if len( array.shape ) > 1: - for i in range( array.shape[ 1 ] ): - data[ attributeName + f"_{i}" ] = array[ :, i ] + data[ attributeName + f"_{ i }" ] = array[ :, i ] data.drop( columns=[ attributeName ], inplace=True ) else: data[ attributeName ] = array From a021fa78d00b76265f11aa68a5cfc8d40855bb6e Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 11:58:18 +0200 Subject: [PATCH 51/56] Change variables iter to iterator --- .../src/geos/mesh/utils/arrayModifiers.py | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index ba76a973..79a1b6b6 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -134,17 +134,17 @@ def fillPartialAttributes( values: list[ Any ] = [ value for _ in range( nbComponents ) ] # Parse the multiBlockDataSet to create and fill the attribute on blocks where it is not. - iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() - iter.SetDataSet( multiBlockDataSet ) - iter.VisitOnlyLeavesOn() - iter.GoToFirstItem() - while iter.GetCurrentDataObject() is not None: - dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) + iterator: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() + iterator.SetDataSet( multiBlockDataSet ) + iterator.VisitOnlyLeavesOn() + iterator.GoToFirstItem() + while iterator.GetCurrentDataObject() is not None: + dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iterator.GetCurrentDataObject() ) if not isAttributeInObjectDataSet( dataSet, attributeName, onPoints ) and \ not createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkDataType, logger ): return False - iter.GoToNextItem() + iterator.GoToNextItem() return True @@ -320,17 +320,17 @@ def createConstantAttributeMultiBlock( ) # Parse the multiBlockDataSet to create the constant attribute on each blocks. - iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() - iter.SetDataSet( multiBlockDataSet ) - iter.VisitOnlyLeavesOn() - iter.GoToFirstItem() - while iter.GetCurrentDataObject() is not None: - dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) + iterator: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() + iterator.SetDataSet( multiBlockDataSet ) + iterator.VisitOnlyLeavesOn() + iterator.GoToFirstItem() + while iterator.GetCurrentDataObject() is not None: + dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iterator.GetCurrentDataObject() ) if not createConstantAttributeDataSet( dataSet, listValues, attributeName, componentNames, onPoints, vtkDataType, logger ): return False - iter.GoToNextItem() + iterator.GoToNextItem() return True @@ -722,14 +722,14 @@ def createCellCenterAttribute( mesh: Union[ vtkMultiBlockDataSet, vtkDataSet ], ret: int = 1 if isinstance( mesh, vtkMultiBlockDataSet ): # initialize data object tree iterator - iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() - iter.SetDataSet( mesh ) - iter.VisitOnlyLeavesOn() - iter.GoToFirstItem() - while iter.GetCurrentDataObject() is not None: - block: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) + iterator: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() + iterator.SetDataSet( mesh ) + iterator.VisitOnlyLeavesOn() + iterator.GoToFirstItem() + while iterator.GetCurrentDataObject() is not None: + block: vtkDataSet = vtkDataSet.SafeDownCast( iterator.GetCurrentDataObject() ) ret *= int( doCreateCellCenterAttribute( block, cellCenterAttributeName ) ) - iter.GoToNextItem() + iterator.GoToNextItem() elif isinstance( mesh, vtkDataSet ): ret = int( doCreateCellCenterAttribute( mesh, cellCenterAttributeName ) ) else: From f100bb87f9e3b98078d0fa744cda295c849853c6 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 13:10:26 +0200 Subject: [PATCH 52/56] Clean for ci --- geos-mesh/src/geos/mesh/utils/arrayHelpers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index 0e8939d6..a4ae8018 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -134,7 +134,9 @@ def getNumpyGlobalIdsArray( data: Union[ vtkCellData, vtkPointData ] ) -> Option return vtk_to_numpy( global_ids ) -def getNumpyArrayByName( data: Union[ vtkCellData, vtkPointData ], name: str, sorted: bool = False ) -> Optional[ npt.NDArray ]: +def getNumpyArrayByName( data: Union[ vtkCellData, vtkPointData ], + name: str, + sorted: bool = False ) -> Optional[ npt.NDArray ]: """Get the numpy array of a given vtkDataArray found by its name. If sorted is selected, this allows the option to reorder the values wrt GlobalIds. If not GlobalIds was found, From 0d6542592c80c897ed5712fdd0fbed01eb84377f Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 13:44:06 +0200 Subject: [PATCH 53/56] Remove the plugin from geos_prep and update the doc.rst file --- docs/geos_mesh_docs/processing.rst | 7 + docs/geos_posp_docs/PVplugins.rst | 6 - .../PVCreateConstantAttributePerRegion.py | 303 ------------------ 3 files changed, 7 insertions(+), 309 deletions(-) delete mode 100644 geos-posp/src/PVplugins/PVCreateConstantAttributePerRegion.py diff --git a/docs/geos_mesh_docs/processing.rst b/docs/geos_mesh_docs/processing.rst index d79db6db..97613eb4 100644 --- a/docs/geos_mesh_docs/processing.rst +++ b/docs/geos_mesh_docs/processing.rst @@ -3,6 +3,13 @@ Processing filters The `processing` module of `geos-mesh` package contains filters to process meshes. +geos.mesh.processing.CreateConstantAttributePerRegion filter +-------------------------------------- + +.. automodule:: geos.mesh.processing.CreateConstantAttributePerRegion + :members: + :undoc-members: + :show-inheritance: geos.mesh.processing.SplitMesh filter -------------------------------------- diff --git a/docs/geos_posp_docs/PVplugins.rst b/docs/geos_posp_docs/PVplugins.rst index 91c859b6..293e0a65 100644 --- a/docs/geos_posp_docs/PVplugins.rst +++ b/docs/geos_posp_docs/PVplugins.rst @@ -16,12 +16,6 @@ PVAttributeMapping plugin .. automodule:: PVplugins.PVAttributeMapping -PVCreateConstantAttributePerRegion plugin ---------------------------------------------------- - -.. automodule:: PVplugins.PVCreateConstantAttributePerRegion - - PVExtractMergeBlocksVolume plugin ------------------------------------------- diff --git a/geos-posp/src/PVplugins/PVCreateConstantAttributePerRegion.py b/geos-posp/src/PVplugins/PVCreateConstantAttributePerRegion.py deleted file mode 100644 index 8f25df08..00000000 --- a/geos-posp/src/PVplugins/PVCreateConstantAttributePerRegion.py +++ /dev/null @@ -1,303 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. -# SPDX-FileContributor: Martin Lemay -# ruff: noqa: E402 # disable Module level import not at top of file -import os -import sys -from typing import Union - -import numpy as np -import numpy.typing as npt -from typing_extensions import Self - -dir_path = os.path.dirname( os.path.realpath( __file__ ) ) -parent_dir_path = os.path.dirname( dir_path ) -if parent_dir_path not in sys.path: - sys.path.append( parent_dir_path ) - -import PVplugins # noqa: F401 - -import vtkmodules.util.numpy_support as vnp -from geos.utils.Logger import Logger, getLogger -from geos.mesh.utils.multiblockHelpers import ( - getBlockElementIndexesFlatten, - getBlockFromFlatIndex, -) -from geos.mesh.utils.arrayHelpers import isAttributeInObject -from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] - VTKPythonAlgorithmBase, smdomain, smhint, smproperty, smproxy, -) -from vtk import VTK_DOUBLE # type: ignore[import-untyped] -from vtkmodules.vtkCommonCore import ( - vtkDataArray, - vtkInformation, - vtkInformationVector, -) -from vtkmodules.vtkCommonDataModel import ( - vtkDataObject, - vtkMultiBlockDataSet, - vtkUnstructuredGrid, -) - -__doc__ = """ -PVCreateConstantAttributePerRegion is a Paraview plugin that allows to -create 2 attributes whom values are constant for each region index. - -Input and output are either vtkMultiBlockDataSet or vtkUnstructuredGrid. - -To use it: - -* Load the module in Paraview: Tools>Manage Plugins...>Load new>PVCreateConstantAttributePerRegion. -* Select the mesh you want to create the attributes and containing a region attribute. -* Search and Apply Create Constant Attribute Per Region Filter. - -""" - -SOURCE_NAME: str = "" -DEFAULT_REGION_ATTRIBUTE_NAME = "region" - - -@smproxy.filter( - name="PVCreateConstantAttributePerRegion", - label="Create Constant Attribute Per Region", -) -@smhint.xml( """""" ) -@smproperty.input( name="Input", port_index=0 ) -@smdomain.datatype( - dataTypes=[ "vtkMultiBlockDataSet", "vtkUnstructuredGrid" ], - composite_data_supported=True, -) -class PVCreateConstantAttributePerRegion( VTKPythonAlgorithmBase ): - - def __init__( self: Self ) -> None: - """Create an attribute with constant value per region.""" - super().__init__( nInputPorts=1, nOutputPorts=1, outputType="vtkDataSet" ) - - self.m_table: list[ tuple[ int, float ] ] = [] - self.m_regionAttributeName: str = DEFAULT_REGION_ATTRIBUTE_NAME - self.m_attributeName: str = "attribute" - - # logger - self.m_logger: Logger = getLogger( "Create Constant Attribute Per Region Filter" ) - - def SetLogger( self: Self, logger: Logger ) -> None: - """Set filter logger. - - Args: - logger (Logger): logger - """ - self.m_logger = logger - - @smproperty.xml( """ - - - - - - - - Select an attribute containing the indexes of the regions - - - """ ) - def a01SetRegionAttributeName( self: Self, name: str ) -> None: - """Set region attribute name.""" - self.m_regionAttributeName = name - self.Modified() - - @smproperty.xml( """ - - - Name of the new attribute - - - """ ) - def a02SetAttributeName( self: Self, value: str ) -> None: - """Set attribute name. - - Args: - value (str): attribute name. - """ - self.m_attributeName = value - self.Modified() - - @smproperty.xml( """ - - - - - - - - - Set new attributes values for each region index. - - - """ ) - def b01SetAttributeValues( self: Self, regionIndex: int, value: float ) -> None: - """Set attribute values per region. - - Args: - regionIndex (int): region index. - - value (float): attribute value. - """ - self.m_table.append( ( regionIndex, value ) ) - self.Modified() - - @smproperty.xml( """ - - """ ) - def b02GroupFlow( self: Self ) -> None: - """Organize groups.""" - self.Modified() - - def RequestDataObject( - self: Self, - request: vtkInformation, - inInfoVec: list[ vtkInformationVector ], - outInfoVec: vtkInformationVector, - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestDataObject. - - Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - inData = self.GetInputData( inInfoVec, 0, 0 ) - outData = self.GetOutputData( outInfoVec, 0 ) - assert inData is not None - if outData is None or ( not outData.IsA( inData.GetClassName() ) ): - outData = inData.NewInstance() - outInfoVec.GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), outData ) - return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] - - def RequestData( - self: Self, - request: vtkInformation, # noqa: F841 - inInfoVec: list[ vtkInformationVector ], # noqa: F841 - outInfoVec: vtkInformationVector, # noqa: F841 - ) -> 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. - """ - self.m_logger.info( f"Apply filter {__name__}" ) - try: - input0: Union[ vtkUnstructuredGrid, vtkMultiBlockDataSet ] = ( self.GetInputData( inInfoVec, 0, 0 ) ) - output: Union[ vtkUnstructuredGrid, vtkMultiBlockDataSet ] = ( self.GetOutputData( outInfoVec, 0 ) ) - - assert input0 is not None, "Input Surface is null." - assert output is not None, "Output pipeline is null." - - output.ShallowCopy( input0 ) - - assert ( len( self.m_regionAttributeName ) - > 0 ), "Region attribute is undefined, please select an attribute." - if isinstance( output, vtkMultiBlockDataSet ): - self.createAttributesMultiBlock( output ) - else: - self.createAttributes( output ) - - mess: str = ( f"The new attribute {self.m_attributeName} was successfully added." ) - self.Modified() - self.m_logger.info( mess ) - except AssertionError as e: - mess1: str = "The new attribute was not added due to:" - self.m_logger.error( mess1 ) - self.m_logger.error( e, exc_info=True ) - return 0 - except Exception as e: - mess0: str = "The new attribute was not added due to:" - self.m_logger.critical( mess0 ) - self.m_logger.critical( e, exc_info=True ) - return 0 - self.m_compute = True - return 1 - - def createAttributesMultiBlock( self: Self, output: vtkMultiBlockDataSet ) -> None: - """Create attributes on vtkMultiBlockDataSet from input data. - - Args: - output (vtkMultiBlockDataSet): mesh where to create the attributes. - """ - # for each block - blockIndexes: list[ int ] = getBlockElementIndexesFlatten( output ) - for blockIndex in blockIndexes: - block0: vtkDataObject = getBlockFromFlatIndex( output, blockIndex ) - assert block0 is not None, "Block is undefined." - block: vtkUnstructuredGrid = vtkUnstructuredGrid.SafeDownCast( block0 ) - try: - self.createAttributes( block ) - except AssertionError as e: - self.m_logger.warning( f"Block {blockIndex}: {e}" ) - output.Modified() - - def createAttributes( self: Self, mesh: vtkUnstructuredGrid ) -> None: - """Create attributes on vtkUnstructuredGrid from input data. - - Args: - mesh (vtkUnstructuredGrid): mesh where to create the attributes. - """ - assert isAttributeInObject( mesh, self.m_regionAttributeName, - False ), f"{self.m_regionAttributeName} is not in the mesh." - regionAttr: vtkDataArray = mesh.GetCellData().GetArray( self.m_regionAttributeName ) - assert regionAttr is not None, "Region attribute is undefined" - npArray: npt.NDArray[ np.float64 ] = self.createNpArray( regionAttr ) - newAttr: vtkDataArray = vnp.numpy_to_vtk( npArray, True, VTK_DOUBLE ) - newAttr.SetName( self.m_attributeName ) - mesh.GetCellData().AddArray( newAttr ) - mesh.GetCellData().Modified() - mesh.Modified() - - def createNpArray( self: Self, regionAttr: vtkDataArray ) -> npt.NDArray[ np.float64 ]: - """Create numpy arrays from input data. - - Args: - regionAttr (vtkDataArray): Region attribute - - Returns: - npt.NDArray[np.float64]: numpy array of the new attribute. - """ - regionNpArray: npt.NDArray[ np.float64 ] = vnp.vtk_to_numpy( regionAttr ) - npArray: npt.NDArray[ np.float64 ] = np.full_like( regionNpArray, np.nan ) - # for each region - for regionIndex, value in self.m_table: - if regionIndex in np.unique( regionNpArray ): - mask: npt.NDArray[ np.bool_ ] = regionNpArray == regionIndex - npArray[ mask ] = value - else: - self.m_logger.warning( f"Index {regionIndex} is not in the values of the region" + - f" attribute '{regionAttr.GetName()}'" ) - return npArray From 2eb0ebbc0b7758ccc77ad823d337106881f37d1c Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 13:46:16 +0200 Subject: [PATCH 54/56] Fix doc --- docs/geos_mesh_docs/processing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/geos_mesh_docs/processing.rst b/docs/geos_mesh_docs/processing.rst index 97613eb4..0b6f4e6d 100644 --- a/docs/geos_mesh_docs/processing.rst +++ b/docs/geos_mesh_docs/processing.rst @@ -4,7 +4,7 @@ Processing filters The `processing` module of `geos-mesh` package contains filters to process meshes. geos.mesh.processing.CreateConstantAttributePerRegion filter --------------------------------------- +------------------------------------------------------------- .. automodule:: geos.mesh.processing.CreateConstantAttributePerRegion :members: From 06f57f93881b4f611f79a777a82f37ea0551cae0 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Thu, 7 Aug 2025 15:04:24 +0200 Subject: [PATCH 55/56] Clean for ci --- .../CreateConstantAttributePerRegion.py | 189 +++++++++++------- geos-mesh/src/geos/mesh/utils/arrayHelpers.py | 3 +- .../test_CreateConstantAttributePerRegion.py | 153 ++++++++------ .../PVCreateConstantAttributePerRegion.py | 83 ++++---- geos-utils/src/geos/utils/Logger.py | 16 +- 5 files changed, 259 insertions(+), 185 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py index 3858d815..8c97c950 100644 --- a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py +++ b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py @@ -13,9 +13,11 @@ vtkDataSet, ) -from geos.utils.Logger import getLogger, Logger, logging, CountWarningHandler -from geos.mesh.utils.arrayHelpers import ( getArrayInObject, getComponentNames, getNumberOfComponents, getVtkDataTypeInObject, isAttributeGlobal, isAttributeInObject ) -from geos.mesh.utils.arrayModifiers import createAttribute, createConstantAttributeDataSet, createConstantAttributeMultiBlock +from geos.utils.Logger import ( getLogger, Logger, logging, CountWarningHandler ) +from geos.mesh.utils.arrayHelpers import ( getArrayInObject, getComponentNames, getNumberOfComponents, + getVtkDataTypeInObject, isAttributeGlobal, isAttributeInObject ) +from geos.mesh.utils.arrayModifiers import ( createAttribute, createConstantAttributeDataSet, + createConstantAttributeMultiBlock ) __doc__ = """ CreateConstantAttributePerRegion is a vtk filter that allows to create an attribute @@ -24,7 +26,9 @@ Input mesh is either vtkMultiBlockDataSet or vtkDataSet and the region attribute must have one component. The relation index/values is given by a dictionary. Its keys are the indexes and its items are the list of values for each component. -To use a specific handler for the logger, set the variable 'speHandler' to True and use the member function addLoggerHandler. +To use a handler of yours, set the variable 'speHandler' to True and add it using the member function addLoggerHandler. + +By default, the value type is set to float32, their is one component and no name and the logger use an intern handler. To use it: @@ -32,17 +36,19 @@ from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion - # filter inputs + # Filter inputs. mesh: Union[vtkMultiBlockDataSet, vtkDataSet] regionName: str dictRegionValues: dict[ Any, Any ] newAttributeName: str - valueNpType: type, optional defaults to numpy.float32 - nbComponents: int, optional default to 1. - componentNames: tuple[ str, ... ], optional defaults to an empty tuple. - speHandler: bool, optional defaults to False - # instantiate the filter + # Optional inputs. + valueNpType: type + nbComponents: int + componentNames: tuple[ str, ... ] + speHandler: bool + + # Instantiate the filter filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( mesh, regionName, dictRegionValues, @@ -53,15 +59,16 @@ speHandler, ) - # Set the specific handler (only if speHandler is True). - specificHandler: logging.Handler - filter.addLoggerHandler( specificHandler ) + # Set your handler (only if speHandler is True). + yourHandler: logging.Handler + filter.addLoggerHandler( yourHandler ) # Do calculations. filter.applyFilter() """ -loggerTitle: str = "Create constant attribute per region" +loggerTitle: str = "Create Constant Attribute Per Region" + class CreateConstantAttributePerRegion: @@ -73,11 +80,11 @@ def __init__( newAttributeName: str, valueNpType: type = np.float32, nbComponents: int = 1, - componentNames: tuple[ str, ... ] = (), # noqa: C408 + componentNames: tuple[ str, ...] = (), # noqa: C408 speHandler: bool = False, - ) -> None: + ) -> None: """Create an attribute with constant value per region. - + Args: mesh (Union[ vtkDataSet, vtkMultiBlockDataSet ]): The mesh where to create the constant attribute per region. regionName (str): The name of the attribute with the region indexes. @@ -98,40 +105,42 @@ def __init__( self.newAttributeName: str = newAttributeName self.valueNpType: type = valueNpType self.nbComponents: int = nbComponents - self.componentNames: tuple[ str, ... ] = componentNames + self.componentNames: tuple[ str, ...] = componentNames # Region attribute settings. self.regionName: str = regionName self.dictRegionValues: dict[ Any, Any ] = dictRegionValues - self.useDefaultValue: bool = False # Check if the new component have default values (information for the output message). + self.useDefaultValue: bool = False # Check if the new component have default values (information for the output message). # Warnings counter. self.counter: CountWarningHandler = CountWarningHandler() self.counter.setLevel( logging.INFO ) # Logger. + self.logger: Logger if not speHandler: - self.logger: Logger = getLogger( loggerTitle, True ) + self.logger = getLogger( loggerTitle, True ) else: - self.logger: Logger = logging.getLogger( loggerTitle ) + self.logger = logging.getLogger( loggerTitle ) self.logger.setLevel( logging.INFO ) - - + def setLoggerHandler( self: Self, handler: logging.Handler ) -> None: """Set a specific handler for the filter logger. + In this filter 4 log levels are use, .info, .error, .warning and .critical, be sure to have at least the same 4 levels. - + Args: - handler (logging.Handler): The handler to add. + handler (logging.Handler): The handler to add. """ if not self.logger.hasHandlers(): self.logger.addHandler( handler ) else: # This warning does not count for the number of warning created during the application of the filter. - self.logger.warning( "The logger already has an handler, to use yours set the argument 'speHandler' to True during the filter initialization." ) - + self.logger.warning( + "The logger already has an handler, to use yours set the argument 'speHandler' to True during the filter initialization." + ) def applyFilter( self: Self ) -> bool: """Create a constant attribute per region in the mesh. @@ -149,29 +158,33 @@ def applyFilter( self: Self ) -> bool: if self.onPoints is None: self.logger.error( f"{ self.regionName } is not in the mesh." ) self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) - self.logger.error( f"The filter { self.logger.name } failed.") + self.logger.error( f"The filter { self.logger.name } failed." ) return False - + if self.onBoth: - self.logger.error( f"Their is two attribute named { self.regionName }, one on points and the other on cells. The region attribute must be unique." ) + self.logger.error( + f"Their is two attribute named { self.regionName }, one on points and the other on cells. The region attribute must be unique." + ) self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) - self.logger.error( f"The filter { self.logger.name } failed.") + self.logger.error( f"The filter { self.logger.name } failed." ) return False nbComponentsRegion: int = getNumberOfComponents( self.mesh, self.regionName, self.onPoints ) if nbComponentsRegion != 1: self.logger.error( f"The region attribute { self.regionName } has to many components, one is requires." ) self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) - self.logger.error( f"The filter { self.logger.name } failed.") + self.logger.error( f"The filter { self.logger.name } failed." ) return False self._setInfoRegion() # Check if the number of components and number of values for the region indexes are coherent. for index in self.dictRegionValues: if len( self.dictRegionValues[ index ] ) != self.nbComponents: - self.logger.error( f"The number of value given for the region index { index } is not correct. You must set a value for each component, in this case { self.nbComponents }." ) + self.logger.error( + f"The number of value given for the region index { index } is not correct. You must set a value for each component, in this case { self.nbComponents }." + ) return False - + trueIndexes: list[ Any ] = [] falseIndexes: list[ Any ] = [] regionNpArray: npt.NDArray[ Any ] @@ -181,23 +194,30 @@ def applyFilter( self: Self ) -> bool: if not isAttributeGlobal( self.mesh, self.regionName, self.onPoints ): self.logger.error( f"The region attribute { self.regionName } has to be global." ) self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) - self.logger.error( f"The filter { self.logger.name } failed.") + self.logger.error( f"The filter { self.logger.name } failed." ) return False - + trueIndexes, falseIndexes = self._getTrueIndexesInMultiBlock( self.mesh ) if len( trueIndexes ) == 0: if len( self.dictRegionValues ) == 0: self.logger.warning( "No region indexes entered." ) else: - self.logger.warning( f"The region indexes entered are not in the region attribute { self.regionName }." ) - - if not createConstantAttributeMultiBlock( self.mesh, self.defaultValue, self.newAttributeName, componentNames=self.componentNames, onPoints=self.onPoints, logger=self.logger ): - self.logger.error( f"The filter { self.logger.name } failed.") + self.logger.warning( + f"The region indexes entered are not in the region attribute { self.regionName }." ) + + if not createConstantAttributeMultiBlock( self.mesh, + self.defaultValue, + self.newAttributeName, + componentNames=self.componentNames, + onPoints=self.onPoints, + logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed." ) return False else: if len( falseIndexes ) > 0: - self.logger.warning( f"The region indexes { falseIndexes } are not in the region attribute { self.regionName }." ) + self.logger.warning( + f"The region indexes { falseIndexes } are not in the region attribute { self.regionName }." ) # Parse the mesh to add the attribute on each block. nbBlock: int = self.mesh.GetNumberOfBlocks() @@ -206,30 +226,47 @@ def applyFilter( self: Self ) -> bool: regionNpArray = getArrayInObject( dataSet, self.regionName, self.onPoints ) npArray = self._createNpArray( regionNpArray ) - if not createAttribute( dataSet, npArray, self.newAttributeName, componentNames=self.componentNames, onPoints=self.onPoints, logger=self.logger ): - self.logger.error( f"The filter { self.logger.name } failed.") + if not createAttribute( dataSet, + npArray, + self.newAttributeName, + componentNames=self.componentNames, + onPoints=self.onPoints, + logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed." ) return False - + else: trueIndexes, falseIndexes = self._getTrueIndexesInDataSet( self.mesh ) if len( trueIndexes ) == 0: if len( self.dictRegionValues ) == 0: self.logger.warning( "No region indexes entered." ) else: - self.logger.warning( f"The region indexes entered are not in the region attribute { self.regionName }." ) - - if not createConstantAttributeDataSet( self.mesh, self.defaultValue, self.newAttributeName, componentNames=self.componentNames, onPoints=self.onPoints, logger=self.logger ): - self.logger.error( f"The filter { self.logger.name } failed.") + self.logger.warning( + f"The region indexes entered are not in the region attribute { self.regionName }." ) + + if not createConstantAttributeDataSet( self.mesh, + self.defaultValue, + self.newAttributeName, + componentNames=self.componentNames, + onPoints=self.onPoints, + logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed." ) return False else: if len( falseIndexes ) > 0: - self.logger.warning( f"The region indexes { falseIndexes } are not in the region attribute { self.regionName }." ) + self.logger.warning( + f"The region indexes { falseIndexes } are not in the region attribute { self.regionName }." ) regionNpArray = getArrayInObject( self.mesh, self.regionName, self.onPoints ) npArray = self._createNpArray( regionNpArray ) - if not createAttribute( self.mesh, npArray, self.newAttributeName, componentNames=self.componentNames, onPoints=self.onPoints, logger=self.logger ): - self.logger.error( f"The filter { self.logger.name } failed.") + if not createAttribute( self.mesh, + npArray, + self.newAttributeName, + componentNames=self.componentNames, + onPoints=self.onPoints, + logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed." ) return False # Log the output message. @@ -237,26 +274,25 @@ def applyFilter( self: Self ) -> bool: return True - def _setPieceRegionAttribute( self: Self ) -> None: """Set the attribute self.onPoints and self.onBoth. - self.onPoints is True if the region attribute is on points, False if it is on cells, None otherwise. + self.onPoints is True if the region attribute is on points, False if it is on cells, None otherwise. - self.onBoth is True if a region attribute is on points and on cells, False otherwise. + self.onBoth is True if a region attribute is on points and on cells, False otherwise. """ self.onPoints: Union[ bool, None ] = None self.onBoth: bool = False if isAttributeInObject( self.mesh, self.regionName, False ): self.onPoints = False if isAttributeInObject( self.mesh, self.regionName, True ): - if self.onPoints == False: + if self.onPoints is False: self.onBoth = True self.onPoints = True - def _setInfoRegion( self: Self ) -> None: """Update self.dictRegionValues and set self.defaultValue. + Values and default value type are set with the numpy type given by self.valueNpType. Default value is set to nan for float data, -1 for int data and 0 for uint data. """ @@ -268,11 +304,13 @@ def _setInfoRegion( self: Self ) -> None: # Set the correct type of values and region index. dictRegionValuesUpdateType: dict[ Any, Any ] = {} for idRegion in self.dictRegionValues: - dictRegionValuesUpdateType[ regionNpType( idRegion ) ] = [ self.valueNpType( value ) for value in self.dictRegionValues[ idRegion ] ] + dictRegionValuesUpdateType[ regionNpType( idRegion ) ] = [ + self.valueNpType( value ) for value in self.dictRegionValues[ idRegion ] + ] self.dictRegionValues = dictRegionValuesUpdateType # Set the list of default value for each component depending of the type. - self.defaultValue: list [ Any ] + self.defaultValue: list[ Any ] ## Default value for float types is nan. if self.valueNpType in ( np.float32, np.float64 ): self.defaultValue = [ self.valueNpType( np.nan ) for _ in range( self.nbComponents ) ] @@ -282,14 +320,14 @@ def _setInfoRegion( self: Self ) -> None: ## Default value for uint types is 0. elif self.valueNpType in ( np.uint8, np.uint16, np.uint32, np.uint64 ): self.defaultValue = [ self.valueNpType( 0 ) for _ in range( self.nbComponents ) ] - - def _getTrueIndexesInMultiBlock( self: Self, multiBlockDataSet: vtkMultiBlockDataSet ) -> tuple[ list[ Any ], list[ Any ] ]: + def _getTrueIndexesInMultiBlock( self: Self, + multiBlockDataSet: vtkMultiBlockDataSet ) -> tuple[ list[ Any ], list[ Any ] ]: """Check for each region index if it is a true index (the index is value of the attribute of at least one block), or a false index. Args: - dataSet (vtkDataSet): The mesh with the attribute to check. - + multiBlockDataSet (vtkMultiBlockDataSet): The mesh with the attribute to check. + Returns: tuple(list[Any], list[Any]): Tuple with the list of the true indexes and the list of the false indexes. """ @@ -301,26 +339,25 @@ def _getTrueIndexesInMultiBlock( self: Self, multiBlockDataSet: vtkMultiBlockDat block: vtkDataSet = vtkDataSet.SafeDownCast( multiBlockDataSet.GetBlock( idBlock ) ) # Get the true and false indexes of the block. trueIndexesBlock: list[ Any ] = self._getTrueIndexesInDataSet( block )[ 0 ] - + # Keep the new true indexes. for index in trueIndexesBlock: if index not in trueIndexes: trueIndexes.append( index ) - + # Get the false indexes. for index in self.dictRegionValues: if index not in trueIndexes: - falseIndexes.append( index ) + falseIndexes.append( index ) return ( trueIndexes, falseIndexes ) - def _getTrueIndexesInDataSet( self: Self, dataSet: vtkDataSet ) -> tuple[ list[ Any ], list[ Any ] ]: """Check for each region index if it is a true index (the index is value of the attribute), or a false index. Args: dataSet (vtkDataSet): The mesh with the attribute to check. - + Returns: tuple(list[Any], list[Any]): The tuple with the list of the true indexes and the list of the false indexes. """ @@ -335,9 +372,9 @@ def _getTrueIndexesInDataSet( self: Self, dataSet: vtkDataSet ) -> tuple[ list[ return ( trueIndexes, falseIndexes ) - def _createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArray[ Any ]: """Create an array from the input one. + If the value of the input array is a key of self.dictRegionValues, the corresponding list of value for each component of the created array is its item. If their is other indexes than those given, their list of values are self.defaultValue and self.useDefaultValue is set to True. @@ -370,14 +407,13 @@ def _createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArr self.useDefaultValue = True return npArray - def _logOutputMessage( self: Self, trueIndexes: list[ Any ] ) -> None: """Create and log result messages of the filter. Args: - trueIndexes (list[Any]): The list of the true region indexes use to create the attribute. - """ + trueIndexes (list[Any]): The list of the true region indexes use to create the attribute. + """ # The Filter succeed. self.logger.info( f"The filter { self.logger.name } succeed." ) @@ -387,7 +423,7 @@ def _logOutputMessage( self: Self, trueIndexes: list[ Any ] ) -> None: self.logger.info( f"The new attribute { self.newAttributeName } is created on { piece }." ) ## The number of component and they names if multiple. - componentNamesCreated: tuple[ str, ... ] = getComponentNames( self.mesh, self.newAttributeName, self.onPoints ) + componentNamesCreated: tuple[ str, ...] = getComponentNames( self.mesh, self.newAttributeName, self.onPoints ) if self.nbComponents > 1: messComponent: str = f"The new attribute { self.newAttributeName } has { self.nbComponents } components named { componentNamesCreated }." if componentNamesCreated != self.componentNames: @@ -409,7 +445,7 @@ def _logOutputMessage( self: Self, trueIndexes: list[ Any ] ) -> None: messValue = f"{ messValue } the value { self.defaultValue[ 0 ] }." ### Warn the user because no region index has been used. self.logger.warning( messValue ) - + else: ### Create the message to have for each component the value of the region index. messValue = f"{ messValue } per region indexes with:\n" @@ -421,17 +457,17 @@ def _logOutputMessage( self: Self, trueIndexes: list[ Any ] ) -> None: messValue = f"{ messValue } the value { self.dictRegionValues[ index ][ idComponent ] } for the component { componentNamesCreated[ idComponent ] }," messValue = f"{ messValue[ : -1 ] } and the value { self.dictRegionValues[ index ][ -1 ] } for the component { componentNamesCreated[ -1 ] } for the index { index }.\n" else: - messValue = f"{ messValue } index { index }.\n" + messValue = f"{ messValue } index { index }.\n" if self.useDefaultValue: - messValue = f"{ messValue }\tThe value { self.defaultValue[ 0 ] } for the" + messValue = f"{ messValue }\tThe value { self.defaultValue[ 0 ] } for the" if self.nbComponents > 1: messValue = f"{ messValue } component { componentNamesCreated[ 0 ] }," for idComponent in range( 1, self.nbComponents - 1 ): messValue = f"{ messValue } the value { self.defaultValue[ idComponent ] } for the component { componentNamesCreated[ idComponent ] }," messValue = f"{ messValue[ : -1 ] } and the value { self.defaultValue[ -1 ] } for the component { componentNamesCreated[ -1 ] } for the other indexes." else: - messValue = f"{ messValue } other indexes." + messValue = f"{ messValue } other indexes." ### Warn the user because a default value has been used. self.logger.warning( messValue ) else: @@ -440,4 +476,3 @@ def _logOutputMessage( self: Self, trueIndexes: list[ Any ] ) -> None: self.logger.warning( messValue ) else: self.logger.info( messValue ) - \ No newline at end of file diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index 9b32b95e..184021c5 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -368,7 +368,8 @@ def getArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) - return npArray -def getVtkDataTypeInObject( object: Union[ vtkDataSet, vtkMultiBlockDataSet ], attributeName: str, onPoints: bool ) -> int: +def getVtkDataTypeInObject( object: Union[ vtkDataSet, vtkMultiBlockDataSet ], attributeName: str, + onPoints: bool ) -> int: """Return VTK type of requested array from input mesh. Args: diff --git a/geos-mesh/tests/test_CreateConstantAttributePerRegion.py b/geos-mesh/tests/test_CreateConstantAttributePerRegion.py index 67471679..857fbfe6 100644 --- a/geos-mesh/tests/test_CreateConstantAttributePerRegion.py +++ b/geos-mesh/tests/test_CreateConstantAttributePerRegion.py @@ -10,74 +10,113 @@ from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion, np -@pytest.mark.parametrize( "meshType, newAttributeName, regionName, dictRegionValues, componentNames, componentNamesTest, valueNpType, succeed", [ - # Test the name of the new attribute (new on the mesh, one present on the other piece). - ## For vtkDataSet. - ( "dataset", "newAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), - ( "dataset", "CellAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), - ## For vtkMultiBlockDataSet. - ( "multiblock", "newAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), - ( "multiblock", "CellAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), - ( "multiblock", "GLOBAL_IDS_CELLS", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), - # Test if the region attribute is on cells or on points. - ( "dataset", "newAttribute", "FAULT", {}, (), (), np.float32, True ), - # Test the component name. - ( "dataset", "newAttribute", "FAULT", {}, ( "X" ), (), np.float32, True ), - ( "dataset", "newAttribute", "FAULT", {}, (), ( "Component0", "Component1" ), np.float32, True ), - ( "dataset", "newAttribute", "FAULT", {}, ( "X" ), ( "Component0", "Component1" ), np.float32, True ), - ( "dataset", "newAttribute", "FAULT", {}, ( "X" , "Y" ), ( "X" , "Y" ), np.float32, True ), - ( "dataset", "newAttribute", "FAULT", {}, ( "X" , "Y", "Z" ), ( "X" , "Y" ), np.float32, True ), - # Test the type of value. - ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int8, True ), - ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int16, True ), - ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int32, True ), - ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int64, True ), - ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint8, True ), - ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint16, True ), - ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint32, True ), - ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint64, True ), - ( "dataset", "newAttribute", "FAULT", {}, (), (), np.float64, True ), - # Test index/value. - ( "dataset", "newAttribute", "FAULT", { 0: [ 0 ], 100: [ 1 ] }, (), (), np.float32, True ), - ( "dataset", "newAttribute", "FAULT", { 0: [ 0 ], 100: [ 1 ], 101: [ 2 ] }, (), (), np.float32, True ), - ( "dataset", "newAttribute", "FAULT", { 0: [ 0 ], 100: [ 1 ], 101: [ 2 ], 2: [ 3 ] }, (), (), np.float32, True ), - ( "dataset", "newAttribute", "FAULT", { 0: [ 0, 0 ], 100: [ 1, 1 ] }, (), ( "Component0", "Component1" ), np.float32, True ), - ( "dataset", "newAttribute", "FAULT", { 0: [ 0, 0 ], 100: [ 1, 1 ], 101: [ 2, 2 ] }, (), ( "Component0", "Component1" ), np.float32, True ), - ( "dataset", "newAttribute", "FAULT", { 0: [ 0, 0 ], 100: [ 1, 1 ], 101: [ 2, 2 ], 2: [ 3, 3 ] }, (), ( "Component0", "Component1" ), np.float32, True ), - # Test common error. - ## Number of components. - ( "dataset", "newAttribute", "FAULT", { 0: [ 0 ], 100: [ 1, 1 ] }, (), (), np.float32, False ), # Number of value inconsistent. - ( "dataset", "newAttribute", "FAULT", { 0: [ 0, 0 ], 100: [ 1, 1 ] }, (), (), np.float32, False ), # More values than components. - ( "dataset", "newAttribute", "FAULT", { 0: [ 0 ], 100: [ 1 ] }, ( "X" , "Y" ), ( "X" , "Y" ), np.float32, False ), # More components than value. - ## Attribute name. - ( "dataset", "PERM", "FAULT", {}, (), (), np.float32, False ), # The attribute name already exist. - ## Region attribute. - ( "dataset", "newAttribute", "PERM", {}, (), (), np.float32, False ), # Region attribute has too many components. - ( "multiblock", "newAttribute", "FAULT", {}, (), (), np.float32, False ), # Region attribute is partial. -] ) + +@pytest.mark.parametrize( + "meshType, newAttributeName, regionName, dictRegionValues, componentNames, componentNamesTest, valueNpType, succeed", + [ + # Test the name of the new attribute (new on the mesh, one present on the other piece). + ## For vtkDataSet. + ( "dataset", "newAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + ( "dataset", "CellAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + ## For vtkMultiBlockDataSet. + ( "multiblock", "newAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + ( "multiblock", "CellAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + ( "multiblock", "GLOBAL_IDS_CELLS", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + # Test if the region attribute is on cells or on points. + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.float32, True ), + # Test the component name. + ( "dataset", "newAttribute", "FAULT", {}, ( "X" ), (), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), ( "Component0", "Component1" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", {}, ( "X" ), ( "Component0", "Component1" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", {}, ( "X", "Y" ), ( "X", "Y" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", {}, ( "X", "Y", "Z" ), ( "X", "Y" ), np.float32, True ), + # Test the type of value. + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int8, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int16, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int32, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int64, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint8, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint16, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint32, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint64, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.float64, True ), + # Test index/value. + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0 ], + 100: [ 1 ] + }, (), (), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0 ], + 100: [ 1 ], + 101: [ 2 ] + }, (), (), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0 ], + 100: [ 1 ], + 101: [ 2 ], + 2: [ 3 ] + }, (), (), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0, 0 ], + 100: [ 1, 1 ] + }, (), ( "Component0", "Component1" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0, 0 ], + 100: [ 1, 1 ], + 101: [ 2, 2 ] + }, (), ( "Component0", "Component1" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0, 0 ], + 100: [ 1, 1 ], + 101: [ 2, 2 ], + 2: [ 3, 3 ] + }, (), ( "Component0", "Component1" ), np.float32, True ), + # Test common error. + ## Number of components. + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0 ], + 100: [ 1, 1 ] + }, (), (), np.float32, False ), # Number of value inconsistent. + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0, 0 ], + 100: [ 1, 1 ] + }, (), (), np.float32, False ), # More values than components. + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0 ], + 100: [ 1 ] + }, ( "X", "Y" ), ( "X", "Y" ), np.float32, False ), # More components than value. + ## Attribute name. + ( "dataset", "PERM", "FAULT", {}, (), (), np.float32, False ), # The attribute name already exist. + ## Region attribute. + ( "dataset", "newAttribute", "PERM", {}, (), + (), np.float32, False ), # Region attribute has too many components. + ( "multiblock", "newAttribute", "FAULT", {}, (), (), np.float32, False ), # Region attribute is partial. + ] ) def test_CreateConstantAttributePerRegion( dataSetTest: Union[ vtkMultiBlockDataSet, vtkDataSet ], meshType: str, newAttributeName: str, regionName: str, dictRegionValues: dict[ Any, Any ], - componentNames: tuple[ str, ... ], - componentNamesTest: tuple[ str, ... ], + componentNames: tuple[ str, ...], + componentNamesTest: tuple[ str, ...], valueNpType: int, succeed: bool, ) -> None: + """Test CreateConstantAttributePerRegion.""" mesh: Union[ vtkMultiBlockDataSet, vtkDataSet ] = dataSetTest( meshType ) nbComponents: int = len( componentNamesTest ) - if nbComponents == 0: # If one component their is no name. + if nbComponents == 0: # If one component their is no name. nbComponents += 1 - - filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( mesh, - regionName, - dictRegionValues, - newAttributeName, - valueNpType=valueNpType, - nbComponents=nbComponents, - componentNames=componentNames, - ) + + filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( + mesh, + regionName, + dictRegionValues, + newAttributeName, + valueNpType=valueNpType, + nbComponents=nbComponents, + componentNames=componentNames, + ) assert filter.applyFilter() == succeed diff --git a/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py b/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py index 93aaf25f..7965634d 100644 --- a/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py +++ b/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py @@ -10,10 +10,10 @@ from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] smdomain, smhint, smproperty, smproxy, -) # source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/util/vtkAlgorithm.py +) # source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/util/vtkAlgorithm.py from paraview.detail.loghandler import ( # type: ignore[import-not-found] VTKHandler, -) # source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/detail/loghandler.py +) # source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/detail/loghandler.py from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from vtkmodules.vtkCommonCore import ( @@ -25,7 +25,6 @@ vtkDataSet, ) - # update sys.path to load all GEOS Python Package dependencies geos_pv_path: Path = Path( __file__ ).parent.parent.parent.parent.parent sys.path.insert( 0, str( geos_pv_path / "src" ) ) @@ -53,6 +52,7 @@ """ + @smproxy.filter( name="PVCreateConstantAttributePerRegion", label="Create Constant Attribute Per Region", @@ -67,11 +67,8 @@ class PVCreateConstantAttributePerRegion( VTKPythonAlgorithmBase ): def __init__( self: Self ) -> None: """Create an attribute with constant value per region.""" - super().__init__( nInputPorts=1, - nOutputPorts=1, - inputType="vtkDataObject", - outputType="vtkDataObject" ) - + super().__init__( nInputPorts=1, nOutputPorts=1, inputType="vtkDataObject", outputType="vtkDataObject" ) + self.clearDictRegionValues: bool = True # Region attribute settings. @@ -82,12 +79,11 @@ def __init__( self: Self ) -> None: self.newAttributeName: str = "newAttribute" self.valueNpType: type = np.float32 self.nbComponents: int = 1 - self.componentNames: tuple[ str, ... ] = () + self.componentNames: tuple[ str, ...] = () # Use the handler of paraview for the log. self.speHandler: bool = True - # Settings of the attribute with the region indexes: @smproperty.stringvector( name="ChooseRegionAttribute", @@ -96,7 +92,7 @@ def __init__( self: Self ) -> None: number_of_elements="1", element_types="2", ) - @smdomain.xml(""" + @smdomain.xml( """ None: """ ) def _setRegionAttributeName( self: Self, regionName: str ) -> None: """Set region attribute name. - + Args: regionName (str): The name of the attribute to consider as the region attribute. """ self.regionName = regionName self.Modified() - - @smproperty.xml(""" + @smproperty.xml( """ - Set the value of the new attribute for each region indexes, use a space between the value of each components:\n - valueRegionIndex | valueComponent1 valueComponent2 ...\n + Set the value of the new attribute for each region indexes, use a coma between the value of each components:\n + valueRegionIndex : valueComponent1, valueComponent2 ...\n If the region attribute has other indexes than those given, a default value is use:\n 0 for uint type, -1 for int type and nan for float type. - + @@ -154,25 +149,24 @@ def _setDictRegionValues( self: Self, regionIndex: str, value: str ) -> None: if self.clearDictRegionValues: self.dictRegionValues = {} self.clearDictRegionValues = False - - if regionIndex != None and value != None : + + if regionIndex is not None and value is not None: self.dictRegionValues[ regionIndex ] = list( value.split( "," ) ) - - self.Modified() + self.Modified() @smproperty.xml( """ - - - """ ) + + + """ ) def _groupeRegionAttributeSettingsWidgets( self: Self ) -> None: """Group the widgets to set the settings of the region attribute.""" self.Modified() - # Settings of the new attribute: @smproperty.xml( """ None: self.newAttributeName = newAttributeName self.Modified() - @smproperty.intvector( name="ValueType", label="The type of the values:", @@ -227,10 +220,9 @@ def _setValueType( self: Self, valueType: int ) -> None: valueType (int): The type for the value encoding with the vtk typecode. """ dictType: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() - self.valueNpType: type = dictType[ valueType ] + self.valueNpType = dictType[ valueType ] self.Modified() - @smproperty.intvector( name="NumberOfComponents", label="Number of components:", @@ -252,7 +244,6 @@ def _setNbComponent( self: Self, nbComponents: int ) -> None: self.nbComponents = nbComponents self.Modified() - @smproperty.stringvector( name="ComponentNames", label="Names of components:", @@ -263,26 +254,25 @@ def _setNbComponent( self: Self, nbComponents: int ) -> None: @smdomain.xml( """ Names of components if multiple for the new attribute to create. - Use the coma and a space between each component name:\n - Names of components: X Y Z + Use the coma and a coma between each component name:\n + Names of components: X, Y, Z """ ) def _setComponentNames( self: Self, componentNames: str ) -> None: """Set the names of the components of the attribute to create. Args: - componentNamesStr (str): Names of component for the new attribute. Use a coma between each component names. + componentNames (str): Names of component for the new attribute. Use a coma between each component names. """ if componentNames == "" or componentNames == "Change if multiple components" or self.nbComponents == 1: self.componentNames = () else: self.componentNames = tuple( componentNames.split( "," ) ) - - self.Modified() + self.Modified() @smproperty.xml( """ - @@ -294,7 +284,6 @@ def _groupNewAttributeSettingsWidgets( self: Self ) -> None: """Group the widgets to set the settings of the new attribute.""" self.Modified() - def RequestDataObject( self: Self, request: vtkInformation, @@ -319,7 +308,6 @@ def RequestDataObject( outInfoVec.GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), outData ) return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] - def RequestData( self: Self, request: vtkInformation, # noqa: F841 @@ -343,19 +331,20 @@ def RequestData( assert outputMesh is not None, "Output pipeline is null." outputMesh.ShallowCopy( inputMesh ) - filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( outputMesh, - self.regionName, - self.dictRegionValues, - self.newAttributeName, - self.valueNpType, - self.nbComponents, - self.componentNames, - self.speHandler, + filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( + outputMesh, + self.regionName, + self.dictRegionValues, + self.newAttributeName, + self.valueNpType, + self.nbComponents, + self.componentNames, + self.speHandler, ) if not filter.logger.hasHandlers(): filter.setLoggerHandler( VTKHandler() ) - + filter.applyFilter() self.clearDictRegion = True diff --git a/geos-utils/src/geos/utils/Logger.py b/geos-utils/src/geos/utils/Logger.py index d8cecb8d..9d3fe5c4 100644 --- a/geos-utils/src/geos/utils/Logger.py +++ b/geos-utils/src/geos/utils/Logger.py @@ -11,15 +11,25 @@ Code was modified from """ + class CountWarningHandler( logging.Handler ): - def __init__( self: Self ): + """Create an handler to count the warnings logged.""" + + def __init__( self: Self ) -> None: + """Init the handler.""" super().__init__() self.warningCount = 0 - - def emit( self: Self, record: logging.LogRecord ): + + def emit( self: Self, record: logging.LogRecord ) -> None: + """Count all the warnings logged. + + Args: + record (logging.LogRecord): Record. + """ if record.levelno == logging.WARNING: self.warningCount += 1 + # Add the convenience method for the logger def results( self: logging.Logger, message: str, *args: Any, **kws: Any ) -> None: """Logs a message with the custom 'RESULTS' severity level. From 4ea154e9b0947085624fe71c27370197fbd5c48f Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 12 Aug 2025 10:39:02 +0200 Subject: [PATCH 56/56] Add the plugin to the doc. --- docs/geos_pv_docs/processing.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/geos_pv_docs/processing.rst b/docs/geos_pv_docs/processing.rst index 13a6d04d..9a7cd57f 100644 --- a/docs/geos_pv_docs/processing.rst +++ b/docs/geos_pv_docs/processing.rst @@ -1,6 +1,13 @@ Post-/Pre-processing ========================= +PVCreateConstantAttributePerRegion +----------------------------------- + +.. automodule:: geos.pv.plugins.PVCreateConstantAttributePerRegion + + + PVSplitMesh ----------------------------------