From 0ca3fe52c08a988494295ab331e0348e38199a47 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 24 Jun 2025 16:48:09 +0200 Subject: [PATCH 01/31] 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/31] 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==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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==EQAAAACAAADAcAAAVRMAAFMRAADuEQAAZhMAANwTAAD/EgAAqBIAADwRAACTEgAAbRQAAHwTAACZEwAAxBEAACYRAADmEgAAehQAALgQAAA=eJydm2eYVeUVhZ1j7sSSZKzpPeRJ1Bild5AiSEcUpQhEo2joIE0EVFAUUaotCgqCiBUEOwQELIAiIiCKqKiI2GPsIcb8cK15nvPm7svh8Gd/d761115rne+ce4eZ+e5+3/4rqF5R9m1dqPqp6meqj5Wl8Z20uEB1ueoK1StVvxPwu68TcJ5jfKeAjzqXQ49x1H8ldBpHvyvgq3y/NP5W4T6BDusyrp/6/wE+6nSfefsFOM41b7+c+VD/cuj9rnC+7oOFW6r6L+i5FeekvhaXqS4DP3W733PqBzjqMH/9nHnRzzLoNo5+/wFfzGuccA+qfow5S5FXWy0uVr26orQe8ruvbeDP+LYBH3V6/sU586LfZfDF+2qxcP+EDusybrR4rqoordN95h1dKI7jXPOOzpkP9RtnXp6TC4W7W/Uj6FmMc3KiFsNVJ4GfusnvvhODHIz3Pvmo0/OH58yVfo2bFOQ1Srg7VT+EnruRV3Mthvr9BPzUTX73NQ9yML45rotx1On5Q3PmSr/GXRnkdZlwN6l+AD13Iq8OWpyjOhH81E1+93UIcjC+A66LcdTp+efkzJV+jZsYPIemCfc+dFiXcd015wrwUaf7zNu9UBzHuebtnjMf6jfuiuCcXCncQ6rvQc80nJNTtbhE9XLwU7f7Pcf9xFGH+U/NmRf9WOclwNGv+S4P8hruzwGq72LOQ8jrBOekOgH81ON+zzmhUBxHHeY/IWcO9FOpEzj6Nd+E4L6aI9w74Pc84/r68wT4ON995u1bKI7jXPP2zemb+o27bC++l6nuhh76nqB6KXgjPuPpJ6tv6vLcCcBRt/kuDe6PEcI9qvo25niu8U3sW3U8+KmH/O5rEvgzvglyNo46K+fnzIt+jRsfnJOZwu2CjkdxTnprzjjwUaf7zNu7UBzHuebtnTMf6jduXHBOLhVuuepb0DMT56S933f9PAQ/dbvfc9xPHHWYv33OvOjHOq8Ajn7Nd0mQ1xDhblfdiTnLkVcDLQb7+x/wUw/53dcg8Gd8A1wX46jT8wfnzIt+jbs4uK88f4nqm9BT+f8KmjNG9SLwRnxjkO+++qYuzx0DHHWb76LgnPxNuLGqb2DOEpyT6lqcpDoW/NRDfvdVD/wZXx05G0ednn9Szrzo17ixQV7nCXef6uvUg7yqaXGh/YCfut3vOdUKxXHUYf5qOfOiH+u8ELj/81uRxvO+miDcDvB7nnEdNWc0+DjffebtWCiO41zzdszpm/qNGx2ck4uEm6v6GvRMwDlppUV/+wE/dbvfc9xPHHWYv1XOvOjHOvsDR7/muzDIq69wC1RfxZy5yKumFkNUR4GfetzvOTULxXHUYf6aOXOgH+scAhz9mm9UkNfFwk1WbV6R5lmAvFpr0UX1AvBTD/nd1zrwZ3xrXBfjqNPzu+TMi35HgZd5ddfXv5d8W49X/Vz9Z1Sk8VP1+gV94d+qj6lOw/u9+c3j/akBzjrMTxx1me8x5EAfj0GvcfTtfc9nXt2sR/gaSZqnO/KaotfP6gv/BT/9mb87cpoS4KzD/MRlzZV+rPNZ5EW/L8AX3+e6ak5Fkp7TDfMn6/VLmEs/5uuGXCYHOM81L3FZc6R+414KzkkX9X+t83dEkubpinOyVIunVF8FP3Wbvyv8Ly0Ux1mH+YnLmhf9vArdxtHvS/DFvE7XnP2Fr5uk53VBXo9qsV51//LS/szfBTk9GuCsY32Ay5orfTwFvcbRt/fXB3mdpvlVhW/mviTtw/hHtNij+n3w05/5zeP9RwKcdewJcFlzpY/10GscfXt/T5BXZ83/0u/HSZrnNOT1sBarVI8AP/2Z3zzefzjAWceqAJc1V/qxzlXA0e8e+OJz+1TN/2WSntMZz4OHxLMbedGP+dzv/YcCnOfuDnBZc6T+VdBL36eI9ydJuv9U+H5Q/W+CjzrN537vPxjgPPfNAJc1H+rfDb28PzqJ9xvl9OckzXMK7o8HtFir+iX4qdv85vH+AwHOOtYGuKx50c+X0G0c/b4JX8zrZM3/vvCNkvS8Tsjrfi22qh5QXtqf+c3j/fsDnHVsDXBZc6WPtdBrHH17f2uQV0fN/7HwTf3cTtI+jF+ixRuq3wM//ZnfPN5fEuCs440AlzVX+tgKvcbRt/c9n8+hDpr/8yTd3xHPocXq34W86Md87vf+4gDnubsCXNYcqf8N6OU5aS/eL3T+6iVpng44J/dpsdJzy0vrNr95vH9fgLOOlQEua170Y50rgaPfXfDFvNpp/kHCN0jS89ojr0VabPa5gw76M795vL8owFnH5gCXNVf6WAm9xtG39yv97ZfGt9X8w5J0fzvcVwvVvx150Y/53O/9hQHOc7cHuKw5Uv9m6OU5aSPeH/o5kqR52uKc3KvFDtWflpfWbX7zeP/eAGcdOwJc1rzoYzv0Gkff3t8R5NVa838rfKskzdMGed2jxfuqh4Of/sxvHu/fE+Cs4/0AlzVX+tgBvcbRt/c9n/dVK83/UZLub4376m71v4686Md87vf+3QHOc18PcFlzpP73oZfn5CTx/tSfY5I0Tyuck7u02Kn6s/LSus1vHu/fFeCsY2eAy5oXfbwOvcbRt/d3BuekpeaX+T5M0vqNu1P9TyMv+jGf+71/Z4Dz3KcDXNYcqX8n9PKctBDvgf4ck6R5WuKc3KHFJtWflJfWbX7zeP+OAGcdmwJc1rzo42noNY6+vb8pyOtEzf+d8MclaZ4WyGuBFh+ofoX86M/85vH+ggBnHR8EuKy50scm6DWOvr+CP/veX/WVsnSfdXv/dvRTv/vd5/3bAxzzIG5vufG54J+D3ai6HT8Xq/x/V/WfrTpf9QLgyGc8f66W9edv1DUfOoyjbvPND3w30/7RyumPqs7ZuJHCfSqefwW5m8/93h9ZURznuZ8GOOq5HfONo37vfxqc17vE9zJy9f4wNdyG6+x57DduWHA9jB8W8FHHfMw/QDhfv3OFm6q6DXo874aydF9V8XVTnYc51ncW9HGe+6sC53nGVw34qHsedDFn854V5Jz1ejCn25DDgcL5/aCpzmO5ztUf/P2K3wdUff58rt0/Qq+fE//Hqj/UBTlENTr/nm9e748IcNbpecRlve/o17qfQ57Mw74+Bo45ee4heJ9mrt43L+/jJvDdFO87w5E/83B/U+Q6PMAxX+L2dh34/PV9cL3qS7gvjPN9cabqXJzbiO/M4L7Kev9R11zoMI6650EnfZ+gPI5RTr9XbYI8h+n1Z+L5KMjdfE1w/YYFOM81L3HUY76P4Jv6vW9entdrlc+LyNX7vdQwVPPmYh77jesVXA/je+F6GEcdczGfn4unCDdLtVFFmsfzjO+qxbmq51eU1u1+z3E/cdRh/q4586If6zwXOPodCjzzGuT7QfUFzJmFvOppMUB1yF70uN9z6hWK46jD/PVy5kA/1jkAOPo135Agr5HCP6C6BXPmIa+mWlykegtyox73e07TQnEcdZi/ac4c6OcW6DaOfs1nPJ+j04XbDH7PM+4M9d+MnDjffeY9I8BxrnnPyOmb+m+BXn7+7SPcVaqboMf6+uHzbw3xnaY6C3Osvxb0mcdzawQ46vKcGkF+5q0V5Ed/s6B/X68Hc7oZOfB8ef581eehxzjrGaQ6E7wR36DAT1bf1DUTOoyj7lnQyefQUJ9D1Y2Y47nGN9Lib6o3gZ96yO++RoE/4xsFfNR5E/Tsa170OxO+mFd/4RapPgc9tyCv2lqMUr0R/NTtfs+pHeCow/y1c+ZFPzdCt3H0exN88b66RrgN4Pc843qq/+/g43z3mbdngONc8/bM6Zv6b4TeyPdtqs9CD30PVL0BvBHfwMBPVt/UdQN0GEfdf4dO3h/nC3e/6nrM8VzjG2ox1ueyorQe93tOwwBHHeZvmDMH+rHOscDR7w3wxbwuEG6i6jOYcz/yaqbFKarnVZTWQ373NQv8Gd8M18U46vT8U3LmRb/GnRfkNUy4q1Wfhp6JyKuxFqerngt+6na/5zQuFMdRh/kb58yLfqzzdODo13zG8zlk3tmq6zDHOM/po9obvBFfH+S7r36oy3P7AEfd5usdnJPRwj2suhZzZuOctNBinOo54Kce93tOi0JxHHWYv0XOHOjHOscBR7/mOyfIa4xwd6iuwZyHkVdLLc5XPRv81EN+97UM/BnfEtfFOOr0/PNz5kW/xp0d5HWJcDNUn4KeO5BXGy16qP4V/NRNfve1CXIwvg2ui3HU6fk9cuZKv8b9NXgOef49qk9Cj3HWM0L1LPBGfCOQ7776pi7PHQEcdZvvrOCcDBDuctUnMOcenJM6Wpyseib4qYf87qsT+DO+DnI2jjo9/+ScedGvcWcGeQ0U7hHVx6HncuRVV4vxqn8BP3W733PqForjqMP8dXPmRT/WOR44+jXfX4K8xgt3s+pqzHkEebXz50bVXuCnHvK7r13gz/h2uC7GUafnn5czL/o1rlfwHPL8e1VXQY9x1jPS3z+BN+IbiXz31Td1ee5I4KjbfD2DczJJuBWqKzHnXpyTzlpMVJ2EHKjH/Z7TuVAcRx3m75wzB/qZBN3G0a/5jGdejbX/a/28q5aqfw7mn68Zf6sW7/oLOnjRz93Mbx7v31oojrMO8xOX9ed99GOd74KPfj+DL+a1Vbke4t/bTdLzGiOvOVpsUz0SOujP/I2R05wAZx3bAlzWXOnHOrcBR7/vwhfzaqj5ic+Tfw8gSfswfrYWz6geBB30Z37zeH92gLOOZwJc1lzpxzqf4XmF323wxed2A+n8WZKe0xDne7Bev4W86Md87vf+4IriOM99K8BlzZH6jTMvf/5TX7w1ha/v3+P17wEnaZ3uG6TX34i3oI3Dykv78DzzeX9QgLMuzyEua370Y9xh+L0Z5mBf3yBn5mSeQnC+6knPV2WYA50D9Xp1IT2Xvs1XH/kNDHCea17isuZN/caZl8+huur/lX8/2r8HW5bWV/n9jF6/oy8cWl5at/nrwf+AAGcd5icua170sRp6jaNv778T5FVHc6oI3y5J89RFXv31+kN94Ufgpz/z10VO/QOcdZifuKy50sc70GscfXv/wyCv2przG+GrJWmeOsirn16/py/8p1Dan/nrIKd+Ac46zE9c1lzpxzrfw3OIfj+ELz6HamnOv8vSc2pjfl+9fhxz6cd8tZFL3wDnueYlLmuO1G/c48E5qan+/6qveZLmqYVz0kev1+gLPygvrdv8teC/T4CzDvMTlzUv+ngceo2jb++vCfKqoTlHCl89SfPURF7Xa/Ga6teF0v7MXxM5XV8ojrMO8xOXNVf6WAO9xtH31/DH+6q65vynLN1fA/OvU/+T4KMf89VALtcFOM99MsBlzZH6X4NenpNq/v8Afx/m51VZWp/x12qxUfXA8tK6zW8e718b4KxjY4DLmhd9PAm9xtG39zcGeVXV/F/492KTNE815HWNFm+rHgx++jO/ebx/TYCzjrcDXNZc6WMj9BpH3973fN5Xx2v+n5J0f1XcVzPU/znyoh/zud/7MwKc534e4LLmSP1vQy/PyXHiPUD4Y5M0z/E4J9O1eF71C/BTt/nN4/3pAc46ng9wWfOiny+g2zj6/Ry+mNefnYfwJybpecchr2labFCtKC/tz/zm8f60AGcdGwJc1lzpxzo3AEe/z8MX8zpW8w/352R/v5vAh/BTtXhFNYEO+jO/ebw/NcBZxysBLmuu9LEBeo2jb+97Pp9Df9L8HyTp/mPxHJqi/heRF/2Yz/3enxLgPPfFAJc1R+p/BXp5To4R737+PsGfH31/4pxM1mKdall5ad3mN4/3Jwc461gX4LLmRT/WuQ44+n0RvpjX0Zp/qPDtk/S8Y5DX1Vq8rPpj6KA/85vH+1cHOOt4OcBlzZU+1kGvcfTtfc/nfXWU5u8pS/cfjfvqKvU/gbzox3zu9/5VAc5znwhwWXOk/pehl+fkj+I9WPiGSZrnKJyTHnq9xXzlpXWb3zze71FRHGcdWwJc1rzo4wnoNY6+ve/59s2/i/ffJR+VpPf598yfMA8/78qK4yt9BriIz/hPwMf/Zzfef7fpv8vsqtoNfO7j31/77zh/p1qlPJ++yvOdpPGcZxz1ma8KrmvWv0+lf/shjvlUgX/eV8aPU52heg14jDdPW9Ueqj2Rb1Y9xFlH2wBHXVUw3zj66QHdld8HwW9P+Pq/35sSbp7qbeg3zv0DVAcGOvc2P5o7IMBRT0/MN476B0Ivz4nxd6kuVl0CHuPNM0x1tOqYnHqIs45hAY66BmK+cfQzGrqNo98x8MVzYvwa1bXoN87901VnBDr3Nj+aOz3AUc8YzDeO+mdAL30b/4LqVvQb5/7ZqnNyzo/mzg5w1DMD842j/jnQy/vD+FdV31LdBR7jzbNAdaHqopx6iLOOBQGOuuZgvnH0sxC6jaPfRfDFc2L8x34fRb9x7l+quizQubf50dylAY56FmG+cdS/DHr5ucb4z1S/Uv1G1R/QzOc+861QXa261t//5NRHnHWtCHDUtwzzjaO/1dBvHP2vhT/jmM86+Of5Mv4g4Q/2B9/90zj3b1bdEvjZ2/xo7uYARz3rMN846t8CvTxfxh+meqTqL1V/BT73mW+76muqu1X/BzLR5bJ4nJ2df8xWhXmGEfQDnakC6pooCFbAopuAtd0U2GYnP2qzZOI6BQoLKGjQimAnCmKX9A/RtgOliXag1dUfm8Iqra2YqdtsFMV0ii010U1tJ1Zwbadg23XqknlfJ/ku9uQ9nP5zf3qu57nv57znfd/vfZ+vxzf6Bvzf/34YPfgDGXDYoA/0twb1P/6DghsWfangji/6vSGOPkdHX+7r3xeO4yOjP+3rz8NxHH/84CP78adEf0f1cNS/E/1lMU8v/8r3nYJznjfkD+f8v1TeIeH6xH8senp0SnSq+lFHv/ei70cHD47P4G75zJHrvYJzPvrhD+f53ld+OM/PPO+L8/nBD97XF/yM6EzVw1E/LDpc57Otf+U7rOCcZ4j84Zx/uPJGBhwi/pzoedE/Ux94+hwdHRk9vmMec+Q4uuCca7j84TzPSOWG87zHay5fJ/Bzo59VffO6kfqx0XFFzl7+le/YgnOe4+UP5/zjlNdzwy+OXqx6OOonRid19K98Jxac84yTP5zzT1Jezw2/LLpc9XDUT45O6ehf+U4uOOeZJH8455+ivJ4b/troatXDUT89OqOjf+U7veCcZ4r84Zx/hvJ6bvi10XWqh6N+dnROR//Kd3bBOc8M+cM5/xzl9dzwX4v+jerhqL8welFH/8r3woJznjnyh3P+i5TX73/wd0Tvid6rPvD0WRK9IrqsYx5z5FhScM51kfzhPM8Vyg3neZdpLl8n8N+MPqB6OOqvia4scvbyr3yvKTjnWSZ/OOdfqby+TuC/E/3n6L+oDzx9vhC9MfqljnnMkeMLBedcK+UP53luVG44z/slzcXcOTzg3w76QD868AP9yMD+x+9JwdvR/zykvx/11HH8HnH0h3dfc+4H58cbfnZ0TvRU9YGnz4l5woyJ/rqYr1ee5vV2YH/ePnAfUb7K13PAjVE/z81xfH2+4NdHvxpdpD7w9JkXnR+doPPXNg/cej1u9oFzrjHyh/McE5TXvvDz5evzBX9X9O7oRvWBp8/l0aXRxR3zwN2lx80+cM41X/5wnmOx8toXfql8/b4D/63ot1UPR/210dUd/eG+pcfJ/eGcZ6n83Q9+tfr5OoF/Kvp09GH1gafPzdH10S92zAP3lM6/feCca7X84TzHF5XXvvDr5evzBf+j6AvRf1UfePrcEb0zemvHPHA/0uNmHzjnWi9/OM9xq/LaF/5O+fp5Bb8r+rrqm98nU/jN6AMd/eF26XFyfzjnuVP+7gf/gPr5e1X4t3hf5X04+lP1o45+/xh9JPrd6JaO+eDe0uNhv+Z77qLfFnGeZ4vywnn+72o+54N/RPl8fcHzi99Bg/rXw1H/dHR7R384/ODdH855HpG/+8FvVz+/DsGzVzmcz0fqA08f9i87o9/vmAcOf3j7wDnXdvnDeY7vK6994XfK19cJPPuTUT5vA/rXs1/Z3dEfru3+y3l2yt/94Hern68TePYjvxsdrT7w3iv9KrqnYx64tvsr59otfzjPsUd57Qv/K/n6fMGzB/mD6GnqA+9906HRdzvmgWu713Iu+r2rfp6D44eqn+fm+JDiewt49iWfip6lPvDeKx0VPVznr20euLb7K+c6VP5wnuNw5bUv/FHy9fmCZ3/ymeifqg+890+josd2zAPXds/lXEfJH85zHKu89oUfJV+/bsOzd5mn+uZ1KPXsZU7q6A/Xds/lPKPk737wJ6mfrxN49i6XRBeoD7z3UadFT+mYB67t3su5TpI/nOc4RXntC3+afH2+4NnXXBldoj7w3mNNjZ7eMQ9c232Zc50mfzjPcbry2hd+qnx9vuDZ81wXXaE+8N5/zYye1TEPXNs9m3NNlT+c5zhLee0LP1O+Pl/w7Iduit6gPs3fPaQBe6S50fM65oFru59zrpnyh/Mc5ymvfeHnytev2/DskTaovvm+OPXsmRZ19Idru8dznrnydz/4Rern6wSe/dDfRTeqD7z3a8ujizvmgWu7x3OuRfKH8xyLlde+8Mvl6/MFzx5pS/Tv1Qfe+7ZV0Ss75jnQvZ5zLZc/nOe4UnntC79Kvj5f8OzJHo8+qD7w3st9OXpdxzxwbfd/zrVK/nCe4zrltS/8l+XrPeEfHfGBjsn3L2MH9j/+lzn+8xT+Qvss6uhjHm5swVX94DlOP3+PCH9y9OPRz0T/XP2oo98+TkgOjIqO7uuWD4488PaDcz76jdbn049rTuc2x/zMY87nZ7Tm9/MK/sro2ug69YGnz9To7Ogcnd+2ecyRY2rBOddo+Te/P2qe2coN53nnaC6/v8Pfwffmqoejfkn00iJnL//Kd0nBOc8c+Td7B+W/VHl9ncDfy/fnfD+uPvD0WRa9JrqyYx5z5FhWcM51qfyb90nNc41yw3nelZrL5wv+segT0SfVp/m7lvRZE10bXVfk7pXHHDnWFJxzrZQ/nOdZq9xwnned5vL5gt8R/UH0h+oDT58N0duitxe5e+UxR44NBedc6+QP53luU244z3u75vLrEPyPoz9RPRz190c3FTl7+Ve+9xec89wufzjn36S8vk7g90R/xvuw+sDT58HoQ9GtHfOYI8eDBedcm+QP53keUm44z7tVc/l8wf939H+i76oPPH2+F30i+mSRu1cec+T4XsE511b5w3meJ5QbzvM+qbn8vIIfnF+UhwzqX9+8TqTwueiOImcv/8r3uYJzniflD+f8O5TX1wn8h6LHRo9TH3j6vBB9LbqrYx5z5Hih4Jxrh/zhPM9ryg3neXdpLp8v+JOiH42OVx94+rwVfTu6t8jdK485crxVcM61S/5wnudt5YbzvHs1l59X8GdEz1Q9HPUH87l28P+fs5d/5Utfc85Dv0P0Odz5m+PF9xbwfxg9OzrNfQb073NY9IjokTofbfOYI8dhBbdfLvnDeZ4jlBvO8x6puXy+4P8kem50lvrA0+fD0eOiI4rcvfKYI8eHC865jpQ/nOc5TrnhPO8IzeXnFfzs6BzVN6+XqT8xOqbI2cu/8j2x4JxnhPybz8vKP0Z5fZ3A/0X0ougi9Wn+LjoNxkdPjU7omMccOcYXnHONkT+c5zlVueE87wTN5fMFf1l0afQK9YGnzyeiZ0TPLHL3ymOOHJ8oOOeaIH84z3OGcsN53jM1l88X/NXRldFV6tO8zqfBJ6NnR6cVuXvlMUeOTxacc50pfzjPc7Zyw3neaZrLr0PwX4n+teqb19XUnx+9oMjZy7/yPb/gnGea/OGc/wLl9XUCvz56S/RW9YGnz7zogujCjnnMkWNewTnXBfKH8zwLlBvO8y7UXL5O4L8RvUv1cNR/Lnp5kbOXf+X7uYJznoXyh3P+y5XXc8Nvim5WPRz1V0VXdPSvfK8qOOe5XP5wzr9Cef38gP929LHoP6kPPH1WR9dEb+iYxxw5Vhecc62QP5znWaPczfelmvcGzeXrhD0ae6Nx0T59/+A9239Fn9Uein7Uc5x6uLZ7vD7lqnydH45+vk7g2eucH/20+sB7n3ZC9Ji+/n5t88C13duNU77K13Mco7z2hec4vj5f8Ox5bor+lfrAe082N3pOxzxwbfdxznWC/OE8xznKa1/4ufL18wqefdDfqr75vJJC9kWXdfSHa7uPc5658nc/+MvUz9cJPHudLdH71Afee7JV0c93zAPXdh/nXJfJ3xzzOKd9za0srhN49jrbVN/83plC9j436Xy19Ydru3/bolyVr/PfpH6eG559zU7VN9d9CtnnfF3zt/WHa7tH26Zcla/zf139/PyAZz/zH9F/Vx9477E2R+/V+WibB67tvmyn8lW+nuNe5bUv/Gb5+jqBZz/zC9U3v2+lkP3Nwx394dru0Zxns/zdD/5h9fPfI8Gzd3kv+uvoPvWjzvurbdHHo491zAfXdl/mfA/L3xxzOSec539c8zkf/Dbl8/UFz17n0EH965vXlxSy93m+oz9c272a82yTv/vBP69+vr7g2c+MiHLfx2HqR533Xq9HfR/JA83XfG88qD9f7dmc73n5m+t1v0vP/7Lmcz7415XP1xc8e56TVQ/n/di+jv5wbfdvzvO6/N0Pfp/6+fqCZ68zOcp9Hz+mftR5D9YX9X0kDzQfXNu9m/Ptk7+5Xve79PxwffL1+eL4IcXnd3j2QdNVD+e92lCd37b+cG33ds7TJ3/3gx+qfv49Cp49D/dVPEd94L0n830kDzQPXNt9nHMNlb+5Xve79Nwj5evrBJ590FzVN6+DqWdfVN2fspd/8/lWj1O1j2t7X0znH6t+nhuevc5i1cN5P1bdn7KXP1zb/Vvb+2I6/0T189zw7GeWqR7Oe67q/pS9/OHa7tHa3hfT+Sern+eGZ+9yreqb17nUs5ep7k/Zyx+u7T6s7X0xnX+6+nluePYsa1XfvK6lnj1MdX/KXv5wbfdcbe+L6fyz1c9zw7M/+Zrq4bx3qu5P2csfru1eq+19MZ3/QvXz+x88+5O7o3eoD7z3Skuj1f0pe+WBa7u/antfTM+xRHntC79Uvr5O4Nmz/IPq4byHurqjP1zbPZfzLJW/+8FfrX6+TuDZB3H/x++oD7z3T76P5IHmgWu753Kuq+Vvrtf9Lj33jfL1dcI/Hx09JjpY3LPp93L0lehzup6o61OdOfvS91lxzvOc/OGc/xXl9dzwo6KjVQ9H/e7ono7+le/ugnOeV+QP5/x7lNdzw4+Pnqx6OOr3Rvd19K989xac8+yRf/P5V/w+5fXc8L8X/X3Vw1E/MP9i0MHd/Ctf+ppzHvrhD+f8HKev54afEZ2pejjqh0WHa/62/pXvsIJznkHyh3P+4crrueFnRc9TffN9WX4YER3Z0b/yHVFwzjNc/s3nTvEjlXe/z73RudHPqr753JsfxkbHdfSvfMcWnPOMlH/zOVH8OOX13PCLoxerHo76idFJHf0r34kF5zzj5A/n/JOU13PDL4suVz0c9ZOjUzr6V76TC855JskfzvmnKK/nhl8VvVb1cNRPi07v6F/5Tis455kifzjnn668+33ujV4fXaN6OOrPjc7q6F/5nltwzjNd/nDOP0t5PTf8+uhXVQ9H/bzo/I7+le+8gnOeWfKHc/75yuu54W+L3q56OOovjl7S0b/yvbjgnGe+/OGc/xLl9dzwm6KbVd+8ruaHq6IrOvpXvlcVnPNcIn8451+hvJ4b/tHoY6qHo/766JqO/pXv9QXnPCvkD+f8a5TXc8M/FX1a9c3zMT/cHF3f0b/yvbngnGeN/OGcf73yem74HdHnVQ9H/Yboxo7+le+GgnOe9fKHc/6Nyuu54X8c/Ynq4ai/P7qpo3/le3/BOc9G+cM5/ybl9dzwP4v+XPVw1D8U3drRv/J9qOCcZ5P84Zx/q/L6ezn4vdGD8t89GXhQ/z7N/08+fR6Nbo8+0zGPOXI8WnDOtVX+cJ5nu3IfrOP0fUZz+TqBHxodpno46l+MvlTk7OVf+b5YcM7zjPzhnP8l5fXfUcAfFx0dHR89Wf2oo9+u6J7o3ui+jvnMkWtXwTnfS/KH83x7lB/O8+/VfHA+P/s0/37fh4WbHZ2j+ub6zg8n8t/b4X5cB+hf+dLXnPPQb4z+/t/5OU5fX1/wi6KLo7dEb1U/6ug3IToxuiC6UOenbT5z5JpQcM43Rv5wnm+i8sPdUnALCg7/heJ8fcHfx/NA9XDUfz56dzFPL3+4+/R4uj+c8yyUvzn6Oqff5/hnvrf/7ejQ6BDx3me8Gn0xukPfE1PvfYi5tnsT59ohfzjP8aLy2hf+Vfn6fMHzff8J0WPVB957kDejr3XM07zOiq/2Lc71qvzhPMdrymtf+Dfl6+cVPHuCU1QP573JOx39m/c18dVexnnelL/7wb+jfr5O4NkTnBGdqD7w++1Por/pmAeu7Z7Guej3G/XzHM1x9fPcHMfX5wue/cKnon+sPs19TdOHPcRR0Q/p/PXK87+rlyXaeJydnWfQVsUZhkFeEBULNhQxIChiBxGxIgr2aCwYVEQ/goolxcTYYySjY4sxWLBgS8YZZ2LBZJzYJlEnPxJFUWOJoqCxd7HFFktmkvv6Zrjiztn35c+tnGuf53727O45nP2+c3q1evz3T6TH7tGto71yYBVxE6PbRFs5sIK4PcRzfNVCXvhVlbdPj2/mJ0X3UXs42n8rOrDD/HCTxDs+nP2sqvyOBz9Q8Vw3/NTooWoPR/v1oyM6zA83Vbzjw9nPQOV3PPgRird0/r63+KOiR0e/pzjwxNk8Ojq6cYd+4I4S7zxw9jVC+eFcx8by67zwo5XX/QV/fPSn0R8pDjxxto+Oi27doR+448U7D5x9jVZ+ONextfw6L/w45fW8gj89+nO1h6P9rtHdOswPd7p4x4ezn3HK73jwuymexwn8edHzozMVB544+0cnRffo0A/ceeKdB86+dlN+ONexh/w6L/wk5XV/wc+OXhadpTjwxDks2hU9uEM/cLPFOw+cfU1SfjjXcbD8Oi98l/J6XsFfF/2N2sPR/ujoMR3mh7tOvOPD2U+X8jse/DGK53ECPzd6a/S3igNPnJOjp0SP7dAP3FzxzgNnX8coP5zrOFZ+nRf+FOV1f8HfG70v+kfFgSfOedHzo2d06AfuXvHOA2dfpyg/nOs4Q36dF/585XV/wc+LPhj9q+J0r1uJc2l0dnRWh37g5ol3Hjj7Ol/54VzHLPl1XvjZyut1CP7x6BNqD0f7a6LXdpgf7nHxjg9nP7OV3/Hgr1U8jxP4l6IvRxcoDjxxbonOjV7foR+4l8Q7D5x9Xav8cK7jevl1Xvi5yuv+gl8cfS/6huLAE+eu6N3R2zr0A7dYvPPA2ddc5YdzHbfJr/PC3628nlfwS/X8n/bquWR7ONo/FJ3fYX448sE7Ppz93K38jgc/X/E8TuBXjq4S7aM48MRZGF0UfbRDP3Dkh3ceOPuar/xwruNR+XVe+EXK6/6C3yi6cXQ1xYEnzr+iH0ef79AP3EY6b84DZ1+LlB/OdTwvv84L/7Hyur/gp0QPie6lOPDEWS9/MTw6oHdnfuCm6Lw5D5x9EW+AONcxQH6dF57j5O0bro/4K6NzohdGZyoe7Yg3PXp49MDoHh36g7tS59H54OxvuPKboy77hLtQ/eC67M8c/hhfvcQvVDuO0+4G9WdtPriFOp+OCzdHfpzX86p7vYiuEl1VHPpwClsYXRR9LvpIryXjOs/DBc4+iP+wOPt6RPlL9SyS75aOE/c51eX+gh8U/VZ0cMEfcV6Nvh59o+C7yY85fLxa4OzrOeWHcz2vy3dLx4n7hury/RD8iOgGag9H+w+iHxZ8NuUv5f2gwNnPG8oPZ/8fyq/HCfzm0S2jYxUHnjhfsHAkcc9WZ37M4eOLAmdfxCM/nOvhwBeK53p7aqK7v+C3je4S3VVx4InTO7pStL/6rdaPOXz0LnD21VP54VzPSvLd0nHi9lddnlfw+0b3U3s42q8VHVTw2ZS/lHetAmc//ZUfzv4Hya/HCfx3owdHpygOPHGGRNflfq1DP+bwMaTA2dcg5YdzPevKd0vHibue6nJ/wU+PHhE9UnHgibNJdLPoyILvJj/m8LFJgbOv9ZQfzvVsJt8tHSfuSNXleQV/XPTHag9H+22i2xZ8NuUv5d2mwNnPSOWHs/9t5dfjBP7k6KnR0xQHnjg7RSdGd+7Qjzl87FTg7Gtb5YdzPRPlu6XjxN1Zdbm/4M+Mnh09R3HgibNXdJ/ovgXfTX7M4WOvAmdfOys/nOvZR75bOk7cfVWX5xX8xdFL1B6O9odEpxZ8NuUv5T2kwNnPvsoPZ/9T5dfjBP7K6NXRaxQHnjjTo0dGZ3Toxxw+phc4+5qq/HCu50j5buk4cWeoLvcX/O+iN0VvVhx44hwfPSF6YsF3kx9z+Di+wNnXDOWHcz0nyHdLx4l7ouryvIL/U/TPag9H+7Oj5xR8NuUv5T27wNnPicoPZ//nyK/HCfxfon+L3q848MS5IHpR9OIO/ZjDxwUFzr7OUX4413ORfLd0nLgXqy73F/zD0Uejf1cceOJcEZ0Tvargu8mPOXxcUeDs62Llh3M9c+S7pePEvUp1eV7B/zP6gtrD0f7G6E0Fn035S3lvLHD2c5Xyw9n/TfLrcQL/SvTt6DuKA0+cW6O3R+/o0I85fNxa4OzrJuWHcz23y3dLx4l7h+pyf8F/HP0q+rXiwBPnvuj90QcKvpv8mMPHfQXOvu5QfjjXc798t3ScuA+oLvcX/PJ5Pr1CdMWeS8aBJ85T0aejCwq+m/yYw8dTBc6+HlB+ONfztHzDud4FqsvrEPz60RFqD0f796MfFHw25S/lfb/A2c8C5Yez/w/k1+MEftfoZPZ3FAeeOP3zF+tEh/buzI85fBDfnH0Rj/xwrgef/cW53qGqy/0F/+vo7OhligNPnIOih0W71G+1fszh46ACZ19DlR9udiHeYQWOuF3ivG8Hv0DtOE676xt8lfLBOX6X4pojrvN7XaAe9nVW6/HNnPfHno8+quf1tPO+mrna/Tfi4auU1/6fVzzXA89+zRC1h/M+15uqvzY/XO0+GnHwVcpr/28qnuuGZ39mQ7WH8/7VR6q/Nj9c7f4YcfBVymv/Hyme64Znn2UrtYfzftVSrSXj1+aHq90PIw6+SnntH454rhue/ZPd1B7O+04rq/7a/N3XJ/GlfS3i4KuU1/5XVjzXDc9+yf5qD+f9pLVVf21+uNr9KuLgq5TX/tdWPNcNz77HIWoP5/2j4aq/Nj9c7f4UcfBVymv/wxXPdcOznzFD7eG8DzRK9dfmh6vdZyIOvkp57X+U4rluePYvfqL2cN7f2U711+aHq90/Ig6+SnntfzvFc93w7EP8TO3hvJ+zi+qvzQ9Xu19EHHyV8tr/LornuuHZXzhX7eG8L7Of6q/ND1e770McfJXy2v9+iue64dlPuFTt4bzfcqjqr80PV7ufQxx8lfLa/6GK57rh2Re4Vu3hvL9ylOqvzQ9Xu39DHHyV8tr/UYrnuuF53n+L2nevL/kP9gNOUv21+eFq92GIg69SXvs/SfFcNzzP9+9Rezjvf5yr+mvzw9XurxAHX6W89n+u4rlueJ7TP6D23fM2/8Fz/EtUf21+uNr9FOLgq5TX/i9RPNcNz/P3x9QezvsWV6v+2vxwtfsixMFXKa/9X614rhue5+0vqj2c9yNuVv21+eFq9zuIg69SXvu/WfFcNzzPzd9VezjvP9yp+mvzw9XubxAHX6W89n+n4vm5IjzPv3vk+dRHigPv/YR50XvUH7V+4Gr3LYiDv1Je13GP/Dov/Dzl9TiB53n4Sj2XbN89P/MfPC9/psP8cLX7FPYzT/kdD/4ZxfPvm8Dz/HsDnm9HByke7bxf8GH0reirHfqDq92fsL9nlN8cddknnOt/S/XZH/yH8ufxBc9z84PUHs77D8N6d5YfrnZ/w36IN0yc/Q/TvsMy4ZYWz3P0y6OzojOiRyou7b2/MC16cHRUdKT6q9YvXO2+hn0OU35z1GefcDPUH64Lbpb6z/3gOsxRh/dT4BeoHce97zGtzXxwtfspl8uP8/o6x58douOj60dZNuHnpbBlknDZ6Pv5+wf1/Jf2/JlX4OyD+PPE2Rdx3hfnOji+rOa36+Y4+T0fyUvcb0e3j64bXSm6lNrb7+rcf0Tfzd8/E31IdRHP9Zur7Sf7fEj54VwXx98V537g+NLqd/ff6hpHLR0nLnG6+y1/z/ikDnyuGV0x2ku8630puiA6X35o7/4yV9uv9jVf+eFcxwL5dV74l5TX11XyEndlHUftq/R7io47v8DV1o02/X6k/S9UPNfNfVz/6ArRZcQ9lvbPRp+OPq78tOurdub6F7hnxdnP48rvePBPK57nxzLi14gOjC4r3nlfjL4cfUJ+aG/f5mrrs68nlN/xqOfFQjzX+7Lqcn8tF10vul10k2g/8U8mzmLGXRJ/kv//h/zQnjxPFjj7IP6T4uyLOJ+Icz34XCzO9X6iutxf/cTvFJ0QXV68/fVL3OWjT8kP7V2fudp+sC/ikN/xqKefzmtLx7vjtpbk/e9Hxi3zeEB0rejaUcYt7TzPX4i+En1N47p2vWh3PuGXuPYDt5Z4+4VzvS+qHseDf03x/u/f6VGui8Oi/P43v1fO9aP73+m6zr4T9e+d+/frm65H7V7X2/29/qbfj3c/vFPI6356Vf3g+2PWadbX4dEtomOirD+sa7T3+v5e9Kvo11pfvf4Rz9cLc7XXldp11/W+V4jn+herLjj321fqj5aOE/dr9ZfnAfeH3J+Pjm4a5XwzPmjn+/0vo59qPLR7P9ruvy9qx63reUd+nZd++LKQ1/30qfrB10HWL9anzaKc14Hivc59pvPtcdR0H9Puelo7fl3PZ4V4rvc91eV1g/V8aHTj6KjoSMXFB+1Z99+Ofhz9d/Rz5W/3egI3VLzztttP1Elc+4cbJd71wbmfPlP9jgf/ueJ53WD9Zz4xD3gvBu/b4P0jtPP1gnnj93H4/SS172Vpuj7B1b4XpXbdqH2/iPvpC8VzPzNfOH+cn3HRPaOs+7Tz/OJ89o2R1aJfyV/TPG33elQ7/lwXPs25XvJSD5z7h+PE9XrDeWFd3zG6c5T3l/BeFM4f7b3+L5c8K0b9/hS/J6ZpXNRed+Bq30/jevHteLXvg3F/9VY/wLl/OU5+Xz8ZH6xTfF9iYnSceK9f/g5FX/lpWv/aHaf4I659wE0UX/pehuvsK879xX0Mz0n3itLvo8X7uewAnQ+Ph9r7pNrnv7Xj0PXg0/Fc73Kqy/3FesJ6sXf0oOgY8V531ogOi/r+vem+vN31zb6IN0yc68Gn47neYarL1yXmN/P3gCjv7+G9QLwnqfv3arQeDI4OiZbeo1S7rtS+Z8q+Bxfi1b7nqfY9SO6nIeoHX5eY78x/vhvDd1QmRzl/nG/ae53w92bW0Xn2uGhad2qvt3C13/2p/U5O7fh2P62h+p0Xfh3l9TxgfWG9OTB6WJTzzXijndejodENNB7avW7Wrn/tzgPXN7QQz3UPVl1w7qcN1A+eBzwfYb1jneqK/iB6QpTn1LT3cxXWtw2jY6M7RPvIb+1zmtrn6LXrt+vpI5+O19Vjybocj+P011j1Q0vHyb+D+snXT+YN3zXiu0XTopPF+/tPfN9oow7nJVztd6bsax3lh6v9XpPr3Uic1w3uY9jP/k702Cjzg3lHO+93rxkdo/njedm0b97S8ab7K/tesxCvdt1w/WNUX0vHHW9MoZ9Z7znPnJ9jol1R5g/tfH3gfG6h+eL5VXudabrfanec1s5/172h6nJe+C2U1/3Mes96fnh0epT3HnLfQztfHzaN+r2IQ+Sv6T6q3etRu++dpK7S+xvdD5sW8rqfNlE/uJ/Zd+R6wPp8UfRXUfYdaed9WtbzKdHJ0X7y17Tf29Lxpv3Q2uuZ6+gnn45H/VMK8dw/k1W/7zf4eaTx0TnRG6KXR1m3WA9p758vOzx6XHSa1jevm+Rt+nm12p+bch34cbzadd31r6m64Nxf09QPcO7f49Rvnges/1wPvh89Mcq8Yh7SzteLLaPjNe/avV+tvT61uz64nk3l13nphy0Led1P49UP7meuA9zf8B1GvrP4wyjXCdr5/sffbdyqcN1puo+qvT7B1X5Hs/Y7k653C9XjePBbKZ7XG9Z/zjPn56Qo72/lvbBHqL2vG5zXHaN+f6zfp9t0HWrpOPlL7xuuHbfEaXrfb+37cN1vOxbyul93Un95HnRFOe+c11OiZ0T5dxPtfH/FOJgQ3T06Vv5q79M43vTvudpx6zrGyqfjUf+EQjz3z+6q3/+O5Oevfx/9Q5Trw3jx/Fz2qdHTdN1o9+e7zeHj1AJHnKbrqus4XH7hXPdpqs/jkvsj7mtuj94VvTs6Qe18PzUzemb0rKh/7q3p59ngau/f7G955Xc86ptZiOf6z1R9LR0n/1mq3+sz4511ne/t8r3ZmdFfRBn3tPf67+/08h3aPTU/aq8n7c7L2u8l135f2HVPUD1wMxW39J1eOOLuKc7nh/tQ7jOviv4yelaU+x/ul2jv+9cjogdE946W7qtqn1/U3jfX3s+5ri3lF879sLfqg6Pf/gM47p0GeJydm3n4V1Wdx/npFxcUxRCXXMKp0WoKy8wlJzcsBTU1FUJtcR1FEFk0QNkEbQKRtHLJSRABwSkBhdS0FETtmSZMc2lqMjPNtaKpCdz7w/eL5+HVnOeeO/zzevje9/2cz/tz7z3n3HPPb0a3d/6d2HmHZ4ZBt2vC67ttePzUcLP8vkk4LJwWXhpeEn45vFDn7ZN4nwmPCY8KDwkPVn60RzyO7yPdNOndHjrikGepXfs5RPmis/+j5M/5oT9G+W2a37urXeJODceHY6V3fkeHh4WHKh/iE+dQXYe2dSAO+ZXa5Th+nKfbtY52fV+OC6eEV4SzwsvDSTqvf+IdGX4uHBIOCgcoP9ojHsf7SzdFereHzvkNUPuOh7/PFeLZ9yD5Quc6DVEdfF9ODqeHX1OcKdIPTJwTwpMU33kTnzgcHyjddOndTtt64YO4ztc6x0PXI79zfw4Pua5cj7nh7eFt4ZXhBYqzr+5DruewcEJ4cXhyeJDyJx/ic3xf6SZJ7/bREYe8S+3W3t+uz7CCzvU6Wf7Ruc4TVLeOjhP3YtV18/zO80D/RH9+bXhdeFV4WUg/xvkeJ04LTw9PCY9Vf17bP7Ydn4hDvqV27fe0QjziUAf76eg4dbN/dK7v6aqbx4Np4YxwfniT4uGH8xiPmS+NCIervbbjO7oZ0ru9tnW2vxGFePZ9mnyhc52Gqw5+DugH6RevDueEN4bzQsYZznf/+aVwaHhueN7/c/yq7bfROc8hah8dPonr/NHNkd7+0N0ovf1bR57nSefngP4UP5x3c7gwpJ/kPM8XaGdkOKrQj9bOO2r7+drrYR/DlKfj4X9kIZ7rM0r+XWeeK56bW8JF4b+F9F+c5+dwTDg6PEvjQm1/2Pa5Jw55ltrlOL6cp9ulDmMK7bpOo1UHzz/p73hf/Ul4dzhfer/3XhNeqn6NfIhLO6X35tp+13leU4jn/EcoT3T2e6l8+b6kv1gcLg+XhdznPBecR38yLpwUTtRz4OemqX9Ct1h6t9f2ebWfkcoXHf5p3/6sI+5E6Vxn7neuH9dnVbgi5H7nPI+vXM8rwhl6HsivaZxu+xzW3n/2MUZ5Oh7+ryjEc31myL+ff64v1+WB8P5wofS+D2aFMwvjQ1O/3/Z+c16j1L7j4WdWIZ79zpQv35f0q1wv6rwyvCf8d53nfpjrcnl4WXhBy/68o+O0S5zR0tXeb8TBTyk/juPfftyudbTreTDzDq4z1+fx8NfhL0LeNznf8xWu6+xwUTgvnKB8a+c/te/Dtfet/UxQno5HPWYX4rlei1SHjo7T/jzVyc8Bzxf9+n3hg+F/hjxfnOd+f3r4tfBqPX+140fb5558iet80D0ovfNFZ7+z5Mfx0F+teFvkd56HmSHrMqyn3Bk+H74UvhDeEX5P8QanHa/vXBIuDZeFt4VTwsnyQ17k6fgdHScf4gyWrnYdynW4pKBzHSbLDzrXbYr8o3Pdl6p+HR3nuixTfTs6Tvu3qf5+7uhHV4UPh6vDh0L6Wc7zPOC68NrwSo1Htf02ulXSuz10xCHPUrv2d10hHnHwbz/oXKdrVQePP8y3eD94IvxV+EvFJV/O93vEnPDmcL7at6/a+V7T+0vbetrvnEI8+79OvtC5bjerHh0dJ+581cvPAf3oj8PHwv8KnwzpZzmPfvab4Q3hTeGNhX6bdmjXcWr7d3TkS1zng84+rlae6PBNXPuyjrg3Suc6M89gXCMPzn8qZJ7BeZ5HO++FoechTfPxjo43zX9qx+XaOtvvbPlxPPQLFc/9Df0SzxPPwe/C58Knw5/qfPdnPD9LwsXhLeG3WvaPHR2nfeJcK11t/0AcfJXycz2WFOIRh3rZNzrXdbHq5e+O3wqXhj8P14avhf8T/ne4QHHOSPyLwrnhinBVeE+4IDxf+ZMP8Tl+hnRLpXf76Jzv+Wrf8fA/txDP9Vkhv+j+rl7yj871vUf1QufrsUr19fya/Tr4Jo914Rvhn0LGM8ZJ1iOJd6quF35Whg+Gd2scLK1jNu0nQrdAeueBrnb9tPZ+qJ0vuH5zVAe3S/1XFtr1dblb9UXn6/igrofHO/ptxslnwxfDl0P69fXrwhpHbw1vD5cXxomm8bh2PEFHvsR1PujsY6HyRIdv4tqXdcRdLp3rTH/OvO/18M3wzyH9NOd5XvhA+FD4A/Xj5Nc0v2w7zjjvBwrx7GOJ8kRn3z+QL3Su00Oqg+cVzFO4zlyf/w07Xe/w1fyfdSDO9/yG63pvuDq8P1ykfGvnS7XrVLX3rf0sUp6ORz3uLcRzvVarDh0dp/37VSevvzP/4P7g+v81fEV6z1O4T+4Lv6d8muY5HR2nPeIslq72fiYOPkr52e99iud+g35mTbhx6r1JyHXiunIe/dD3w5+EP9V1LPV/pX4N3Rrp3V7b+w1fxHXe6Oz7Xvmyjrj27zovCXtEv0XYPWR+t1TnjU+8x8LHw4fD0nyR9ojH8fEFHXk9VtDVzlPtZ67yRWf/D8sfOtfrcdXDzz/riZtFv2O4efi89KwzPhr+NvxZ6PXF2vVK5/FoQee8lqp9dPbzW+WNzn5/Jl+uF+uzrJdum/N2CLfv2lDvddynwmfC3yif2nVg2rGuFI88nyrEs49nlC86+/yN/Hj8Z52c70l839k057875HpxH3C+923yXeiR8FldV98vTftAOzretJ5f+32s9n52PR4pxHO9nlUd0Lmej6pO7mcZ/5jPbZfzeoZv5XfGR87z/PDp8MnwR+F9yq9pvEVXOx8lDnmW2rW/pwvxiIN/+0HnOj2pOrjf4PsPzyvPzy7hXdJPThw/18+HU5VP7XcmjtMecSZLV9u/OP+pytPx0D+veNxX6ImHftfw+9K5vRfCaWqfeJw/rVDHWj/OZ5radzz0Lyien8e/hN2i7xXuHG4TdnVteN4PE+8/wl+Evwt/Gf5Y+dEO7ToOOo7TLnF+KB35Etf5oOuSn1J+O6sO9oVuG9XJvl1n5qvMS3uHfcK+YUd19vz2V+Gvw5fC1cqv9v2tdj7t/FarfXT4Iq7zRtdHdbAv62j/Jek8/jO+8R7Me+l7cv4/hluGjHOc730gvM++GP4xfKIwHjbtK+noeNM4XPteb58vFnT2/Yj8oHOdnpB/dK7rH1Uvj0u8d/BesXv4D+Fa6f1+siZ8JVyhfGrX4Wvfg5zXCrXvePhZU4hnv6/Il7/DsH7Muu/7ct5Hw4+EO4Wsl7FeRxyvP/8hfD18LXwuLK3v1X7frl3/rl1XtP8/FOK5Dg/IFzrX7TnVAZ3r/brqh87X4zXV1+MB6/583+mX8/YK3xuu03n+XrQufCP8fbhS+dV+d6r9HuG81xXi2cdK5YnOvn8vX+hcpzdUB48H3Adc5/3Cg8I9Q+bXzNs53/fPRumYNg9fze+l+X3T+nXb+7b2vcK+0L1aaJe64M/tul7EoQ7oXF+OE9fPAesqrId8Ijwk/Bjjvs77u3WYxN8ifDO/L5OP2n1qtes+zo94bxbi4Y98Hc++ibOF6uw6rT/O/Rkd9zHzA+ZvzKc+HH4q/OeQcZ3zva7PPGxtuHXa3SR8Ub6avhOgq53H1M5H7Yd4m6iersfaQjzXC99rpXM9aW/rwvVh3Yz7iOu/b3hEuHe4ra6P91Fy33SlvXeFb+V3r7M17cvs6HjT+l7tc2E/xHurEI96dOk5R+c6Eeddut6uK8e7Cv0U8w++q3885/UPB4bMFzjP3+nfDnumnW1Dzydqv/fXzouc99uFePZBvG1VP/vHz9uK5/oQp2ehzswr2M+yT847LDwy7Kc6e38MLxBbhX3Cdcqvdp9N7XzHeZOH49kH8fqozvaPn27SuT595N915n2G95ADwmO7Nmz3NZ3n95/uif9u5dV2nxK62vet2jrbH/k6nn13ky90rhPHuxfqzLyXee3+4XEhz9EbOs/z5I0Tfyc9Z233/6CrnZfX9hv2R76OZ9/E20l1dp04TlyPn6yLM99lnjogPDzcg+dI46fX05nf9k5724R/yu8/l6+eisvxJ6VrWsdHt5XyLbVbO8/fQ3WwH8ejbvh3PNeVeL0LzwHreB8Kjw4/o3kX8zDOY53vr+H2ib+D5l2epzWtG6IjH/Rur+388Gj5dN7o7Jt4O0jnOnF8+0KdWYfie/yh4QnhgeHuqrO/72+Z+LvSTrhGfmv3CdSujzlv8nA8+yDeZqqffW8mX+hcJ45vWagz66esjx4Tfik8ivFBdfZ6646J/8Fwu/BZ+a1dt639buu8ycPx7IN426l+9r2dfKFznThO+14vZn/J58Mv6P5Yf/9Fz76TPfLD+3Ud2+5fsY48iG9d7f1rH1sqX3T2zfE9Cvcl71G8/wwKTww/TT+i+9LvXX0T/z1hr/AZ+ajdx1H7nuf8iNdLdbE/8nU8++4lX+hcJ473LdyXrOewn+ez4akh6zPovc9n5xz4J63bkE/TPiF0tetKzpP2Hc/5d5xnt//bL8eJ6/kZ+0PoR+gnvhger/zIl/O9r4T+5QPhLmrfvmr3ZzXtZ2lbz9r+0/XAl3Wu087yj8513UX18v3MeMB3vIPD0zVOoPf3wB458GH15+RT+z2xdlxynrTvePZBfj2Un33uKD/uZ9lfQj9F/3Ja+Enqrn7W+9fojz7EdQm9/6NpHxy6pn0vbftZ+yDeptLZ96by5XbRc7xXoc678TyEQ8KzwpOYR6jOL6fBP4fvTfyPhO8L/6K60A7tOg663dQucV6Wbojydz7oPig/pfzOUh3sC91JqpN9+/nfRdfvjPDMkP1E6H0f9Av3DNvuS2p7vzkv4u1ZuH/x00/PGTr73VO+XC++m/G9d2g4LNxP9fL35b1zYJ9wI+VT+3269vud86R9x3P+GylP6xwPncd/vr/wPeTscGJ4jvpz+nfO93ebvcIjwo+pX2+7TwVd7fei2vHHfsnb8ey/h3yhm1iId0QhHnrXydeHdVLep3lfHhF+JTwvPEDXx/tYeM/ePzwu3C/srnyb9sWgq13PrV0fsJ/uytPxqAe+SvGol32j+0ohHnqPg6yvsu/hgvCr4TjiaBz0PoqDEv/4sH+4sfKr3Y9Ru+7rvMnD8exjY+WJ7quFeMdLN051sm/Xme9efK8aHs4Ox4QfVZ39nWzf8JzwwPB1+a3dX1T7Xc75Ee9A6eyPfB1vdkF3TqFd9PbtOrPOzD6RseG88PxwgOrsfSeHhueFnwh7K7/a/Su169/Omzwczz56K0/r8G8/6OYV2kXvfp3v5uwTmR5eGV4fXsr1Vb/u/ScnhCeHZ4bHhF3Kt/a7fe2+F+fZpfYdD7/kXYpHHewH3ZWFeCdLd73q6zr5OWD9n/X9c8Obwkkh+0nWf79Ng3wP+Hg4PBwQbq38avev1H6fcH5bq33Hwx/5Ot5NBd3wQrvo7dt15jsVzyHP27fDWeFg1dnftXge/yUcEu6m/Jq+j6E7XO0SZxvpavuRwfJTyu/bqoN9oZulOtm3+xv2M7AP4eJwTjhT4zbzAs73PohPh0PDwWFp/tC0PxRd7f6L2nmL/ZK3412gOtgPujmFeEOlm6n6uk5+DvieeEo4OVyo547ndf16V+LtHg4MR6n/afv9Eh35oHd7bfuRyfLpvNGdK//2g26h6mX/rjP7SNgnMiVcFE4ID1Odve/kyHB0eHi4lfKr3SdUu8/F+W2l9h0Pf0c2xMO//aBbVIg3ulBnvgOxj298+J3wonCQ6ux9gYeFF4afCvsqv9r9hbXfp5w3eTieffRVnui+U4h3YSEeevt2nVk/4r2C+f6C8HL6H9XZfwfB+8D54aBwb+XX9PcUbde1at+LhspPKb8FqoN9obtcdbJv15l9Uex7Gh3OD78R9ledvY/qk+GI8AthT+VXu4+zdt+W8+up9h0Pf+TrePMLuhGFdtHbt9dveX9g/n+LnhueS/R+zxijfqPt/uK27zO1/cR4+XGe6G6Rf/tyvViPYT1lma7LcaqX120m6r5su9+v7fpQ7f01Wn6cJ7pl8m9ffo5ZT2Q98Lvh1zW+MX6u37+VeKwXfjn8vOYDbfeltl3vrB3XvyufzhvdFPm3H3RfV73s33VmfZ33T94LLwuXhmerzv57DN4bjw0vCvdSfk1/14Gudt2/9v35bPkp5XeZ6mBf6JaqTvbt5595IPO4O0PeT3j/Qe/54iVh6b2oaf9623lp7fvYxfLjPNHdKf/25Xox/jEuLQlZRx2oenmcHB+W1mWb/q6i7Xhcux48XH6cJ7ol8m9ffo753sh3xavC5SHrnay7cp6/T54STgpL67JNf6eIrvZ7aO168FXy6bzRjZV/+0G3XPWyf9eZfV3s27owvCNkffMQ1dn7wA4Op4Sl9dWmv1NEV7vvrHZd1/4OLsS7o6CbIt101cm+vQ7Gvk72bc4Ibw1vCKfio2vD870f9MRwbHh2eHTYdj8zutp9qM5zV7XvePg9sSEedbAfdLcW4o2V7gbV13Vy/3yg/JDPXSHfZdF73zh5Tg33Vz61+85rvyvX1n+EfJTyu0u+7cf3M/to2Lc1MpwbLg7/NWQ/Ded7P9gB4bBwXPjZsJ/yrd1fVrvfxz4OKMSzn37KE93cQrxhhXjUy77RLVZ9XSf36+wbYd/H7eHN4aiQfY6c530mE8KR4d8Az8l5oHicnZt51Fd1ncd5HsAFN0wENREzU9xQ3BUXNMUlBBIhmFwAAUXHJtBQs1wQWUTFMw1qCjUOhqBNNS7UoKIgWCmIW6WkJJZjbmRnJjUtm3Pq/XrOeV72Pffe/Of9yH3fz+fzft/7vd/1d0TnDn/9b59gp79Bh/4tf8OzW9pf7yLehJb2fK4fIN6kQrwjxLu3pX1c4ny9EO/O4MRCPK4TlzjwNwlvo+CA8IYFpwQfIE9wTEv7+7omXq/gwOA1wX7BvVUfecjrOPAGKC9xuhbiUffAQrwx0lOqb6L0Ww+8Bwp54W8a3sbBgeGNCk4Nzg0uCV4cPKul/f3dE3fP4KDg+ODU4DHBPVTvQMXlenfxqA++88JznXsov+Ohd1BFPHywHnhzC/HGi7dE/tontwPeo+HBy4L3BBfreXMf79nOweODXwteqfZBfeQhr+M0fb8vU/2uB9490ud64U2R7pKOxfLJunl/878dhuq94bk/GLy9pT2/p95n3odpwfNVD3HJ4/vh3V7gOV7d9/xB6XGdzgvfOvxenqo6uf+h4OTgaL2XO3b++7qmB/sH91J95CGv48A7VXmJs+M/6PNo6SnVN1n6rQfeQ/LL+u3zYPXX9LdL9b0ZJZ+313iC/niGvtP+3g1WPK5vLx71wHe+pt/ZuuOSpfLBuuBNlU/WjU/wzwjv4eCs4Jkt7Xm75/6ZwWHB3sp/hu7n+u4FHnlnFnhnqq5S3lmFeMMKusfiT/CR4Djp7pP7Tw1eG9xX+cfqfq73EY988B0fnuvZV/nNI67rtO53w9uvNfzg0FbFzf0f8B1JoJ4btecRh7i+r+07VuCV4lEXeR3PdRMHvr8nHybvkeFPDA4LDgpu3dr+vpX0m/mHfsFewe2Cv1R95CMe11eKRz3wnQ/e1qqzlNf6qNfxrHs76Wobf8unXvLBPv8pOo4O/6zgmcEDgtvK58eiY9P8wx7B3sE/5/qvpJd8xOP6Y+JRD3zng7et6izltR54vRXPPqDLee1Tb/ngcdufo+Ow8M8InhPs1tqe/+P80THxdg/2Da6TPuITh+s/Fo/88J0HnusiXl/xrIc6Hc96+0qX/Xo/9e0U/rnBC4Iny6/l+eO3wf0T95BgN9VDXPL4fnhcJx9xlovnOsnveK6/m+qEZ71c37/g13uMM8I/OzgqeLj8WkZ/k3j7BPekDtVDfOJwfZn0kR++87SNt1RXJ+dXPPRQp+NZ757S5fWETuFtpfec9/O84Njgga3t738yul4Iun0cGOwT/Eg+kZc6HA9eJ+UnzpPidZMe19W0XVs3efuIZ5/6SL/zwuc6ed0vfZD357jwTw9+hfYV/JT6pRWpc8v8w27Bo4N/yvU35Qv5iMf1FeJRD3zng/cp1VnKaz3wjlY8+4Au57VPR8sHt4OW8AYHxwS/Gjwt+PngDmoHTzDvSvy9g8cFdwp+Mvgb6W9RXK4/IR71wXdeeDuo3lJe66Vux7P+T0oXPPu1k3xoW0eVv8fJN7eDVvIGTwxeEvxysL/awarofTX4iVw4Nnh4sIvqIw95HQdeq/ISZ5V4J6p+1wPPOrqoTsdDP3ocz/4cLv32+Y9pzyeEPzl4WbBf8CD5/Gj0bJ1/6B88Ptg5+Bf5Qj7icf1R8agHvvPBO0h1lvJaT2fV67z4gC7ntU/Hywd/b7qE94ngkODo4CT19/T/3P9c9LwY3CF59goeoX7f4wTyUofjweui/MR5Trwh0uO6mo5j7Ae6HM9+HSEf2tarCnnhexz5l7xnR4V/YfBL9O8aR/40f2ySeEcGDw2+L7+ITxyu/1Q88sN3Hniui3iHimc91Ol41nuodPm78VHqY17JfPBS9df039z3k9Tpeehn1V+7fycPeR0H3kcaPxHnJ+LVnRfXHX9Y927S5bzwud674POW4R0SPD84gecU3EY+Px89LfmHg4IHBFuDL8mXLRWP68+LRz3wnQ/eNqqzlNf6qNfxrLtVuuDZpwPkg33uEB7rTKwjXaG6DpHPj0eH16VOUj7r6KB4XH9cvlAPfOdr6l/d9TTrP6nAsz8HSb+/s5vQXwbHBb/Gd1rf2Wfyx1aJt29wQPA9+bWJ4nD9GfHID9954Lku4g0Qz3qo0/Gsd4B02a+OfCeCVwdZVzpMfq3OH1sk3inB0vpUR8Xh+mr5RX74ztN0Xcx6qNPxrHd36XI73hT/g18Mjg8yD2EexH3PRs8fg5/Jhf00T/E8iTzkdRx4myovcZ4V74uq3/U0nceNlw/WZZ7j7VfwubO+w3xnr1Q7OF4+r4kef7dPVjtwuyEPeR0HXmflJc4a+Vy3H6nbrq17X+lyXvgnK6/nDduFt2OQdUPW+2YGLw4e29r+/lei+3/QnzysEw4NHhPcXPWSlzo+Fk91kp84r4h3uPS4rqbrpNa9ufQ4Hn4NLcSzn8fIJ7eDjeEF/yU4Nch+Cfsr3Pd0dG+WfzgsOChY2n/ZWPG4/rR8ph74ztd038f6qNfxrHsP6YJnnwbJB7eDHuGxHsV6003B6xinBHdRO1gf/V7HGhUcHtwm+Jb87KG4XF8vHvXBd154u6jeUl7r2kb1Nl2vs2+jCjz7Olx+uR1sHx7zUeaR04Ps67BfxH2/jl7PXz8fLO0nba94XP+1eNQD3/ma7mPVnXdb9/7S5XjwuX5owefNw2N/gv2HGUHOG+wnn3+eOr2fcWqwdH5hc8Xj+s/Fox74zgev7rmJuvsw1r2jdDleG3+j9vE87t4svK8HLw9yzoDzC/B/lj9OSLwTg6VzDZspDtd/1vnv86jjhAKv7nkK6+ineuFZN9fJ7+9zz/D2DjJfYp5zrfqPtv4p978WPe8GPc86Tf2D+x3yUofjweup/MR5Tbx9pMd1NZ0f1u0/7dNh0u+88LlOXn83ttA4ifHNrCDrf6wXct8vosfjqmHB0nriForH9V/IP+qB73xN1zHrjget+0jpcjz4wxTPPncNj/0c9mGuD/5z8HPyeW10e//nC8GDg9uqPvKQ13HgdVVe4qzV86i7H2Ud26pOeNZ9sHQ5L/wvKK993pV+Nci+7zXBacFT5POG6PV+8+DgkGAP1Uce8joOvF2VlzgbxKu7/20dPVSn46EfPY5nf4ZIv33eWc+Z53hRcDjPVT6/ET1+L44K7gxR9e2seFx/Q/5RD3zng3ew6izlrfs+D5d+63E88w4u+Nw9PMb5jLtvCbLvNUQ+vxzdnheMCZb20chDXseB1115ifOynkfdeUrdfT7r3ku6nBf+GOW1z73UnmgH3wyyr3uCfH49et3+zg6W9ol7KR7XX5d/1APf+eDV3Z+u+92w7v7S5Xjwz1Y8jwf7hsd5P87z3Rq8Ocj6LuvB3P9h9Puc4Njg6GBp3biv4nL9Q/lOffCdt+l6dd1zjvZjbIFnn06Rfnj2dbT88vyGcxCcX7gtyPkRzpvA93mJccHSOZR+isP1zuLVPZdR9/yLdRyrep0X/jjl/djvXcMbGBwRvCo4JThS342u+aN7cJfg54IDg59WfeQhr+PAG6C8xOkq3gjV73rgXSV9rhfeSOku6bA/AxXP7yXzeObpNwRnB/kdBXzP90cERwb9u42q32M0XVdwXT2V3/HQM6IQz3pHStfHfn8aHvN55ut3Bv89yD5tWz+YPzz/nxg8L3iS6qv6nUXT/eO66xHWNbHAs96TpAee/TlP+v1esm7K+cW5wUVB1kvh+xzk+OCFwVGqp+45yrrrt65zfCGe6x+lOuFZ74XS5feScy7sA7DO/93gXUHOwXCff0/CvsDk4EVBn6up+l0KvKrzN033Mazjs6rT8dA/uRDP/lwk/R5nsV/L/u2c4Lzgt/Ue8F5wv/d5zwqeE5yg5+z3p2rfGF7V+V94c6THdTV9v+fJF+uDZ5/GS7/jwZ+geG4Hp+v94Ln+MMh8nPk79/n8GO/BVcHS/L7qHDy8uufV6q4r1G0H1j1YuhwP/lWKZ585H8I4mXHwf/J9D54jn32ehHHzxUH/XoP6qn7XBa/q/Aq8qt+JwLOeA1Vv03mDfbpYPPvMPjrnajln+/3g/cGZ8tm/S+Ic7qXBK4JDVV/V75vg1d3frzoXbB66Li3wrHeo9MCzP1dIv8cbrHuwrrFU9VAffK+PzFB81133HHXddZi6flnPjEI8671UutwPsr9Hu+B9Xh5cHGR8zXic+/37LdrBdcErg6Vxe93fg1XtQzZt53XnFdY/QrqcF9+uK+S1r1fKLz8f1kNZ3/9WcGHw9uCNjHv0fLxvcG5wUvD84D8Fj1K9VeuxTfcrLlK9pbzWe24h3o3ywXrg2bdJ8gPe7QXe+YXnw7k0zvEyf2J+tDL4EM9bz8fnqz3vmh2cHvS5uKrz2vCqzs/BqzqX3HS+aN0nS4/j4dfsQjz7OV0+ud8dp/zc90CQfWD2hbnP5xDJc03Q+8bUV3VOGV7dc49V+9VNfbbu06TL8eBfo3j2mfM6nMd5NLgiyDrrBfLZ53uuD94QLK3vVv3eHV7d80R115Wt7/pCPOseK13w7NMN8sE+c+6J/Xf2159UXdPls3+XxH78LcpnHVW/b4JXdR4LXt3zA3V9tv5bCvHsz/XSb585R0I7oR2sCrLeMFU++9wJ7eamYGn9oupcJ7y651zqrpvU/R5Y92Tpcjz4Nyme+0/OL7DvwL7C94I/0jiAcQH3+3e67EdcEpyift7jh6rf/cKrOmfRdB+l7vjGflxSiGefzpV+ePZ1ivzy82Ef4l+D84N3B+8IfoN4ej7sV5wevCD4leCXgmcEB6reqn0QeNQH33nhTVG9pbzzpdv1w/uGfLAeeHfLP/sA7w75a5/8neKcFfNo5slP6bvId5L7fD6fefWt+i76O1p1zh9e1fmvpusAdb/z1n9rIZ79uUX67TP7/Ozjv6B2STu9TD77XMB8tTfvn1Jf1e/k4dU9h1C1b+t46JtfiFf3u2SfLpEP+OR9PObHDwbZ12vbl82NzJ+nBb1/WLUv2HSe73pGKr/jwZ+meNbNueHVQfbZLpduzhHfHCzt51WdQy7lvbnAq7uP6Ponql73L6xns179cnCt3kPeS87TcL/Xwe8K3qH3zOduqLfqXF/T9feq8z5N25l9uauQ137Nlw/w7O8d8q1LeHz/OCfGPif7mMuCLwZfCrL+yHolcfw7GPZBZwUXBO8MltY3q35fA6/qfBu8uvu4dddf7c+sQjz7NUP64dnnBfINnp/DnfLV/Rn7F8xPmX+uD7JOybom9/l8FPPVu4Oldc+qc1Z191Wazq/rrsta93XS5bzw71Ze+8y+54LgfcElwf8Ofls+sy/65eDlwanBq4NN91nhUQ9854Pn+iYoP7z7pNN1w1siH6zLPPJfLZ59Zt7DvOaR4MPBHwQXymfPk64Nzgx+NThJ9dVdT647L1uoOkt5fyBdrtN58eHaQl77NFM+2Oe5eh94Pk/ou7dIPvtcAM9zjr5n/k7WPV9Qdc6m6ftc9ztu/XMK8ezPLOm3z6yHM45iXNWxY3jBlfLZ57EYd60OPhWcrfqqznU1XaevGgfCs47ZqtPx0L+6EM/+PCX9Hn9yXoJ1LdateuT+XYKMmxiPcb/PGbHetT74VrA0bqs6twSv6lxH03W6uuNK+7G+EM8+3SX98OzrW/LL7YD2RX/weHBN8Gm1P+5zf/FvwW8Gb1P7q9vvNG33j6t+1wNvjfS5XnjWO0d6HA/+bYpnn/n+0x88G/xl8JngY/LZ/cW84HeCc4M3Nux34D2svMSZWYhH3fMK8R6TnlJ91j1XuuDZp+/IB39vWAdhnsY87A3Vjx76ce73+gnzt/uUt9TfV63/1l23+UfHGVXPxz7Mk76m8137e594bges2zAPYJzfLd+rrkHWdbjP5ziYF6wLrg02XSeCV3VuBJ7rm6b8Tec71r+uwLM/a6Xf6wqcT2L+y7y1NffvFvwM/Xius75LHJ9vYt67KvhO8HfB0npw1TkzeFXnq5quQ9ddB7APt0qX4+HjqkI8+/yOfIPn5/A7+er2w/iW/on+51fBt4Ivqv14PEx/tSi4OLhA9VWNq5uuo9TtX61rUYFnvQukB579WSz99pn+/fnga8Hfqq6n5TP9/38E/yt4j/I1HU/Aox74ztfUv9ek03XDs+5F0mUeca3f/TX72ZyHaZtvpT3sGmRcy3i5bX07cX1OiXnJhmBpXF11Dgde1b47vKrzU03nZXXnB/ZrQyGe/Vwvn9wOGJ8x/no9+GbwleBzagcez90bvD/43eC3VN8zisf1ueLVHT8+pzpLea3v3kK8V6TfeuDZp/vlg/trzs2wX0o/RL/w6SDjAMYNrJcSx+du3I+9rXGD11epv+rcFLyqcz9113XhVe0bN+2v646v7O86+eW88N9WXrcfxtmMj98Lvqv3jfeP+zwuXxZ8RO9R0/lS03lA3XZhPfeqXufFh2WFvPbpEflgn9eF95vgO8E/BH8ffFU+L8wf3wsuCT4cfCD4fdVHHvI6Drx1ykucheK9o/pdD7xXpadU3x/kg3XB+718sm77TP9PP/928P3g/2mcxX0eL/wwuDy4NOhxW91xR9X4Dt7bqt/1wLOOxarT8dC/vBDP/iyVfvtMO+L9/zD4QXCD+hfuc7tbGVwR/JH64br9VdN2/qbqLOXdIF2u03nxYWUhr31aIR/s84t6zjyfrfJ9Zx73knz2PIPn+UKwNC+s2kduOq+pOx+t+z5b/wsFnv1ZJf32mXbyv8GW3Ncp2Dn4vnymHT0UfCL4ZHCN2hH1kYe8jtO0/VIvcV0PPOtYrjrhoZu41mUecdeI9/9wSEIJeJydm2eUVdUdxRGc0RlkXmw0iUuxIoooAhaULghIVSEaEWwRAaMGkN57R0zggyGiLgt1GHrvimsJMcYlNhCVJC5jiiYxCij5wN6z1v3JyT33+WVfuPvs/97/e095j2dRhRP/nSb8oOIJPCSsXekEXi08W/hf3f+iYnL8i4UncJHwS+F3wgPC7cJVwlMrJOvaB/XM+wD1rfMiePZvXfoyzzrOFfLH/NuRyzz26wD6QH/mfwd/p4unP1Y4orpHhVU0rkR4mvBYxeS4XbrYLdwvfE/4B+Fr8Oc6rksd83zfda2zK6Bn3/sDetZxnpC/05Cfecxjn95DH9jn71T/B2GRxp0lLBYeR5936mKP8I/Cj4TvCN+AP9dxXeqY5/uua52d4NmvdenHPOs4T8ifc1uXucwrRp+Yu0g8rxffYN55vtQQVhf6+R3FerMN64nn2WfCT/Gc+Z65rn1Qz7xvMO+ssw282PUhdh4w/37kYl337bNAXfb1U/SL8+Df8vmtsEDjqgrPFOYqJcdt0cUO4e+Fh4QfCt+HP9dxXeqY5/uua50t4NmvdemHPOc6FOAx7/vIYx778yHycx6cKt4ZwguEFwprwp/9evw+6b4r/EL4V+Fh1GeuU6Hr+/vAsz/zWTdrPy9Abvo3j/kPIZd57Ndh9IE812e/OA88/zxvLhNeLjwP+4vHcb5+Jfxa+Cfsw7H7Vdb1oQQ+Q3WZ76uA3nnIzzzmsU9fow/s85l4zn6O9YWNhNegz5xffu5HTNBEOZpxnpp3TYBHvdj3lLmOBHjM7RxHwGNfjiI3+3yueD8V1hVeJ7xSeD76/LEu/iL8j/AH4TfCz+HPdVyXOuadi7rW+Ri8uvBPP+adjzwhf9ehD8xl3pXoE3N7PS4Q+jl7vaknbIDnbz7Xq2+F3+N9zrrexb5v9eCXPsxrgDz0SR7rmsd9sCL2V883v9ethNcKL8U++CbOFZynJSIe05//Cb+uax/UM68i6lvnTfBizwux6wtzu+6xgJ77VRJY/9jPY+gT32fvDz5HNhbegH3DfJ4vT5FgJeFX8JN2Ps26T9Gn61OP/q1nn+Yxr++fElhnfS735/vrhR3gyz49jt8XVJR+VdRjjtjPAbHfT8T2j/nsl3rMfQpymcc++b51uW5chufSXNhMeKOwDs4jHs/nXVlYbF/Cf+H8FnvOyfqeXQ6/obp1kI8+Wdd9qYx5aN6N6Bvzm8f+FqNvnAde/70fNBW2ELbEOcfjfrRfSP8MYRU/QPhL23eynq+awj/9mMccFeDTvBboA3ORZ90q4LHPDcVrImwjbC9sK7wZfT6ui0Lpnik8V3iW9wP4cx3XpY55DVHXOsfR5zbwTz/m3Yw8IX/t0QfmMq8t+sTc7PM1eM5+Pp2EnbH/ehzP5X6eNYQ1hSXwl3a+z7rvx76nzFUjwGPeEuQxj/2pifw8b3h98Xpzu7CjsDXOG1yHqgmrC3MZ1zHzmqGedYoDevZZLaDXGjlC/pi3OnJxH/S+6n2zh/Bn8GWf3hc8nvtxbeFFqBvaP9LOR1nPAbH7Vmzf2ZfagbrsVzX0wTz29yL0je9zJfHGCMcKL/Y6gfd5r+Ztewl1EP5df/8W5nUl6Pj+3gDPPqxPXiH8heoyh3kd0C/m9n3X5zrr/a+dsIuwO9Ylr1Me5/3xHOF5wgux3oT25dB+a579mM96WdfPLshJ3+Yxdw3kop75F0KP7+VPXNfvsXCScDLeyw/U6IPCvwm7SLgr/FjXdTjevMkBHvUugl/6IM857I885nQ987nOel3wvL9P2Et4t3Wwfns815M6wiuEFwtrYf+K3ReyrmMd4TdUtyvy0Sfrui91AnXvRt+Y3zz29wr0jeuG54ff/97CB4UP4BzicZxPdYX1hFflea7JOn/prybqU8/56gb0mL8e8pnH/lyF/Oyz15m7hD2F98NXd/TZ69AFwsuFV8IXc7iO61Indv0zryf800/WPt+PPjAXedQzr1g897sa9lfvn78Wjhf6exN/H+Pfl1jnEz0A7s/3CTsK+f0Nfy9TDfq+/wl49ms+65uX9juYrOeM2O+j2LeK6APrut/uF+vyeXREf7mP+HtXf27yfPf8GyicIhyHfYTf13KdaCrsJrwdftO+/zVvXIBHvdjPgbHrG/vQNMBjn7ohv3ns4+3oD5+Pzw+XCC9FH6zzG5wzPN7njH/g3xvYv144b9iv69oH9bKecy5FHvrK+u8Mse8F+9Qr8D6yj13QHz4fz1vPt2nCp4SPCv05rQOeD+f7ncLmwuuEoc+Jad9TZ11nYj+fMldt+DWPfbgO+cxj35qjH8xh/p3I4b76c0IP1O/n9VPoc6L5zNEI57ysn7+z9iv23MocdeCXdc1vhLo8T90H/mPC/sKHPH9wnmL964WNhVcLr4C/tHN11ry94DNU9yHkok/WdR+uD9RlnxqjD+yzz10+h/UR9hX+Uvgg+sxzWgNhQ+ENOC/HnveyntP7wD/9mMcc9eDTvL7oA3ORZ90bwOP6/ADqe9wg4WjhROzzHs/PH67XTNhO2Dmw36d9nsl6zojtJ3M2C/CYuynymMd+tUMfzGM/O6NPXJ/9PAcIB8O3c5jv53yLsAV8hN6r0PtinuubzzpZ+zoYueiXPOq1CPTL64vXm6HCIcLH0S+uQ62ELYU3ZlzHzOuPetZpHNCzz1YBvceRI+SPeVsiF+e/9z+fK6YLJ8CXfXq993ieV+4SdkLd0L6Qtv9mPSdl3Y/S+s6+3BWoy361Qh/MY387oW/cB/3eex6MEk4VzsC643GcJ7cJ7xB2x7oTO9+yrnej4J9+zJuKfPRrHvO2Qx7qmd8depwHQ/D8/HzmCp8RzhQOwzzgPPNzvVfYU9hD2DrjvDVvCOpbpyV4se/jMOQK+WM/7g3ozUS/mNs89rUn+sV5MBrPc57wOeEC7Ksex/ejt/BR4SOB/Txtn876PtJfZ9SnnvP1Dugx/6PIZx778wjys8+eN08LnxX+Fr5moM+eVz8XPiz8BXxlnafm2Y/5rJe1f88iJ32bx9y9kYs86zI/+zwQ74Ofz2vC14VT0GeeP/08ZwvnCLvBX+w5NvZ7utj3mTm6wSf1nH92QI/9mYP8Xo99vvO65X33d8LnsZ6Zz/24j7Af1qfY/Tzr+kmffQJ69H8vfJrHvP2Qi++lzzv+/mijcBN82afH8fuoCcKJqMccsd9rxZ7D6HtCQC+2z8zdB7nMY58mog/s81w8lxeELwrnY3/0OD7n/sLHhPfjvBG732Z9r56Bz1Dd+chFn6zrPvQP1GWfHkMf2Gev216fXxUuEi7GPlr+/RPW718JBwgHYh/Nul/E7t+vwj/9mMccj8KneYvQB+Yiz7oDwWOfF4r3knC5cI2wVPgy+txXeo8LhwhHC4cKn4A/13Fd6pi3EHWt0xe85fBPP+a9jDwhf2vQB+YyrxR9Ym72eQGes5/PSuFu7KMex3Ofn+cI4Szso7Hnx6z7d+x7ylwjAjzmnoVc5rEvs5GbffY6s0S4VrhKuBTrkMd5HRokHCMcKXwK63XsukaefQ0K8F6Ez1Bd5hsD/+YtRX7mMY99Gok+sM/eV71vbkY+530efeY+PAm+Q/tH2nko676fdd9Ke27MPwj56M/8SfDHPns9LxOuFq7DPFuMPnu9Hy4cJRyLeRbaZ0L7h3n2Yz7rZV0PViMnfZvH3COQi3rmj4WeoPzzg38/u164Qejfnfh3Kub7d7XjhOOFod+vpP0+lzz7GBfgxf5uhjnug1/zmHs88rFf46HrcT5PT0O/+Hsd64fO+Wn/Hm5e2u+Csn6+iO0X804Az/OQ67Xn/Ras3+Xfz2LdmIx1OOv6H7su0c8Y1Kee+ZOhx9yroOtxW5Gb+411p2Tcp1jXdawzMs/c9D0FPOb2+rNTuAvrUfn37Bo/Qzgzz/XMPNczn/rm0c9Y1Kee+TOhx9wroetxu5Gb67Z1Z6F+7Hofe56MzU3fs8DjeujfYfn7Mn/PtUfo31+Zz9/H+fuwp4W94Cft93Xmxf4eLPb7PfrvBZ/UM/9p6LFfk6DrcW94XUG/+Hs1688V8v9jSPv/DsxL+12cefTVFfWz9ot554LHfnXVxRDhFuFW4dSCJL+W/qGspXB37gRO0Z/vKEr6ob7H1QLPdcyvFdCjT9dvCR79W2cKeMw7Bbm4DvUTbzN82Jd5jaSzK/f/fXqcdRsVnZzHutZtlGd/6N886/I9uUkXE4Sb4Kcf3pMC1esk3Al9+vZ41/F48ujD+gV59ot57LMTeMxrvZ2BfnXQxWjhjFxSZwL6VVX12gl3QJ9+qO9xVQP5zK+K52Iefbp+uzz7xbw7oct5NVy86bnkePsy71bV2Z7i0+Ose2vRyXmsa91b8+wP/e+ALt+T1roYJJyWS+oMx3uSU71mwm0pvqnvcblAH8z3ferRp+s3y7OvzLsduuxXK10M8PqeS+oMQr9KVO8W4dYU39T3uJJAH8wvwXMxjz5d/5Y8+8q826DLfnXSxUPe33JJnQHoVw3Vu1q4JcU39T2uRqAP5tfAczGPPl3/6jz7yrxboct16B7pTva5Jpf0Zd4lqrM5xafHWfeSopPzWNe6l+TZH/rfAl2+J3foYoxwUi6pcw/ek/NVr71wU4pvj3cdjyePPqx/fp79Yh77bA8e824Gn/1qZl3hxFxSZwz6Vax6XYQbU/x4vOsUF52cRx/WL863D8hjn13AY95N4HNe9fW5J4fxmFcNVWdDSn2Ps27DopPzWNe6DfPMTf8boRvKPVE4PpfUYe7OwvXQDemZzzyxuenLdTuDR98bwOf8aG5d4bhcUmci5kdl1esoXJfih/oeVzmQz/zK6HN5Pvh0/Y559ot510OX78nD0h2bw3jMj/qqszbFp8dZt37RyXmsa936efaH/tdBl+9JR6/jXvdzSZ2H8Z5UV72uwjUpvj3edTyePPqwfvU8+8U89tkVPOZdCz771UQXT/jzn/cb71/oV6Hq3SRcneKH+h5XGMhnfiGei3n06fo35dkv5l0DXc4r1x8hHJVL6pR/r6A6bYSroBvSa4P+Zs1NX67bBjz6Xg0+35MGumgrHJlL6ozAe/K9/oH4LNVdmeKH+h5nHeYz3/epR5+uf1ae/WLeVdBlv67VxTA/31xSpy36dUw5WqtuWYpvj3edY6efnEcf1j+Gvsb2i3nsszX7irwrwee86izd4X6fcsl65tVUnRUp9T3Ouh5HHutat2aeuem/DLp8T27TRX8/p1xSpzPek7NVr7GwNMW3x7uOx5NHH9Y/O89+MY99NmZfkXcF+OxXQ108KRzq9ySXrGv+cb3nTVR3eYofj3cdjyePPqx/HPMqtg/MY59NwGPeUvDZr3a66CEckkvqPIl+naN6tYXLUvxQ3+POCeQz3/epR5+uXzvPfjHvcuiyX7P19+/qL44Itwnn5JL8yar3nHCXcKrwNez31reO708uOjnPPqxPHn1Zbyp4zDEVfs1j7l3Ix37NUr19+osfCpI6s9GvSdKZL9wDfeaz/mz0aVKAZx/zA7zYvjLPHvg2j3mfQy7uczNV//2CZJ1ZeE8mavwLqMs81vN4358Y4LnuCwFebB/pfz788j3ZqIvXhQcLkjoz8Z5MkM4c4SvQp++N0PH9CQGefcwJ8GL7xTyvwLd5zPsCcrFfG3SxV1ipMFnPOcwfL515wr3wwXwboOP74wM8+5gX4MX2lTnmwK95zL0X+div9bo4KqzicQXJHOaPk85u4X7oM9966Pj+uADPPnYHeLF9ZY558Gsec+9HPvZrnS52eD8uTOqsR7/GSme68CD0mW8ddHx/bIBnH9MDvNi+Ms9B+DaPeXcjF9ftteJ9Dr/2X/79i8aXoS7zrMV43x8T4LluWYAX20f6nw6/zL1GvM/gYy1yj9b4pdCjzzUY7/ujAzzXXRrgxfaH/svgl/NjtS7eEH4LP/Zn/ijpzBXugD59r4aO748K8OxjboAX2y/m2QHf5jHvUuRiv1bpYr/w9MJkvdXo10jpLBS+DR/Mtwo6vj8ywLOPhQFebF+ZYy78msfcbyMf+7VSF58Kz/C6XZDMYf4I6SwRvgt95lsJHd8fEeDZx5IAL7avzLEQfs1j7neRj+tQmfh/hl/7L//+ReNLocc8ZRjv+8MDPNctDfBi+0j/S+CX78kKXWx3zsKkThnek2HSmSbcB336XgEd3x8W4NnHtAAvtl/Msw++zWPeUuRiv0p18Y6wsDBZbwX6NVQ6C4RvwQfzlULH94cGePaxIMCL7StzTINf85j7LeTjvFou/kfwa//l36No/EvQY57lGO/7QwI8130pwIvtI/0vgF++J8t0cUhYszCpsxzvyWDpLBIehj59L4OO7w8O8OxjUYAX2y/meAl+zWPuw8jHfi3VxZfCswuTOsvQr6eks0Z4APrMtxQ6vv9UgGcfawK82L4yxyL4NY+5DyDfj/4/KvE/gV/7N2+Qxi+GHvMswXjfHxTgue7iAM86/wOVwWp0eJydnXmwleV9x93uhavYK1i1HScuERAmjopbhYCMVgFTzUzGAi6AUVQiO7jgVtMkjY4TUQL3qsgqsms0CtbUREVto0anqRIdY1xiUqeKcU1rmrh1pn4/Z+Z8vE/O68s/v5f7fJ7v7/v9nfecezgPF37Xtt3///rnjk/rvNSdPi3bvZL134m7LbVXuCxvd1su/jN17/ZmndvbmvmLonNH6qvSv1h+bpMO6xcVOHzcUeDQwV+pr3PcJr9wzv2q8uXLDX5j+CfkF/9wF2Z/t/ScZ6P2s35hgaNvd4GrOkf7v0N+fZ9syMXW1L9ub9bZqPvkgugsS/2t9O17g3RYv6DA4WNZgas6L+foll845/6t8nle63PxZuof5XuD5jU3OvemPiJ951svHdbnFjh83Fvgqs7VOZbJL5xzP6J85N4xdZ184pv1Odpv/+u0j/U5Bc7zMNdqbn5dODXcOalrUy/tbOa+mP0Hp87m9UGc9eC/2NEzRx90zNnXbPmAs2/0ZhdyX5L1/86+91KZM9wPwj0QnZ8U5o4e+1n/QWfPHH0fKHD2M0f94eyf9QcK9+tF+cIazZX1kdk3S48z/bwfbmTh8YAfWdCzj9nq3zscj9+QcKenrpYf+p3d1rzvgwj15/t+Z8/+D5I/92M/enD0g2fdevaNj/6FOaN7UGHOVR8Pz2mW5pDfNr4fzMvX/yP8u6l7ZqC7pXL/cV+z//b8/uYI/zj116nP6z71/U9/dFm/vcDhk37mqj7vnBffN2uensevlRPOc3pA+eE81+c1Lz+PL1Zu5sD6TPn2PNg/T3Od2dEz5/maa/U4+PWX58FZqbfqeQHH8+JLqTN035b04P28qvr8s68Z8gFn3+jBO/dFWf+f8G+nXqw8G/P7B6PzL4W5o3exHr+NnT1z9H2wwNnPTPWHs3/WHyzcr2fy/ji6zI31wdk3XY8z/bwfbnDh8YAfXNCzjxnq7/fFp+ViCu9bO5t16Ad/QHSGpE6Tvn2znz4HFDj7QP+AmvNynmnyDee805XL8xqWi5m8r+1s1pmiee0UnaNTp0rffthPn50KnH2gv1PNOTjPVPmGc95pyuV5HZeLb6auUJ+Zmlef6JyYer707Yf99OlT4OwD/T415+A858s3nPNOVS6/jk4Iv1z69IMbmP3fkJ77sw/dgQXOfdEdWDO3/Z8vv37/e0S4canL5Ad/R+n978cR2i+6U9QH/9vJHzr0RcecfdHnY71Ptr/tCvNzPvzuV/Px8Jy+oTn4/qL/7NSl8gOHn2Gp50m3pDeskKdqbvs6Tz7g7HuKfPp16Bjuw9Ql6kNf+N7ROTx1Veef92N99vUu5IPvXdCzT/ofXnNeznuecnlef5OLy3jfKz/na17bR+/41Fs6/7xv9tNn+46eOftAf/ua83IefB4vznnRg/fzalK4xdKnH9yg9FkpPfdnH7qDOnrm3BfdQTVz2z/cyha5Z6XeJD/OPTR1hXRLevDOUzW3fdF3qDj7Rm9F4fkxIhdX8nzobNahL3yv9BuTulz69sN++vTq6JmzD/R71ZyD8+BzjJ9vyrtCvOf1t7k4he9Tnc06V2peu6bfF1KXtfBjffbtWsgHv6seFzj7pP8Xas7LeZdL1/MamYvxfH/rbNY5RfPqSL/9U5e28M1++nR09MzZB/odNeflPPjcX5zzLhPv1yF0p/L9jT+P6fWFPkekLpFuSe8Izffz5rEv+h4hzr6Xivd9MioX3049l/cp/HlD98lu6XdS6s0t/LCfPrt19MzZB/q71ZyD8+DzJHHOu0S85zU6FxekntPZrPNtzatv+o1IXdzCj/XZ17eQD76vHpfG5xPySf8RNeflvDdL1/P6u1xMTJ3c2axzgea1R/odmHpTC9/WZ98ehTnA76HHBc4+6X9gzbk672Lp+nWI/vNSz+5s1oHDz7GpN0q3pHes5vt5c9sXfY8VZ983ifd9cnQuvpZ6VmezzjzdJzuk396pN7TwY3327VDIB7+D5gxnn/Tfu+a8nPdG6XpeQ3PxndSvdzbrfE3z2jH9Tk7tbuGb/fTZsaNnzj7Q37HmvJwHnyeLc94bxHteJ/P+IvXMzmad72hee6XfYaldLfxYn317FfLB76XHpfF+Tz7pf1jNeTlvt3T9OkT/S1IndTbrwOHnuNRF0i3pHaf5ft7c9kXf48TZd5d43ydjc3FN6vfU5xLdJ/um3ympYzUH+2E/ffbt6JmzD/T3rTkH5xkr33DOix6857UqF9v4Qm4kzsE4X2N5Q36/OXo/Sy2du62SDusbChw+0DdX9bzPefC5WfNy3geVy/O6JRfP8z6kvbnfKt1f6+NrdfRelg/nu0U6rK8vcPhA31zVuToPPldrXs67Wbk8r5W5eDJ1Z/4eQFtzDvh1fD+I3i/kw/lWSof1dQUOH+ibqzpX58HnDZqX865WLr9uz0mfV+UX/3Brw92pvs6DHvtZX1vg6IuuuapztH84dH3+Mzv7P+F+yEI//h5wW7NP9q3J7x+P7s9TX1A/56DfHM1jTYHDF33MVZ2f89wpv3CeA7keF+c5vaA5+P6aFT+PtDX3mS2fq/P7+err3OjN1vxWFzj6omuu6rztHw5dvw7NzP7X84W+7c06+IO/Nb/fFL1fSd++0Z+l/LcWOHygb67qvJxjvvzCOTfrmwrzmpE+b/E+t71ZZ6bmdW50fpT6ivSdD/2ZmtO5HT1z+EDfXNW5Oscm+YVz7leUz/Oanj5v5AsftjXrzNC8zonOPak/lb7zoT9DczqnwOHjngJXda7O81P5hnPeHymXX4empf+/tjX3ma7Ha3L2X6e+zoMe+1mfXODoe12BqzpH+79Hfn2fTI3uY/nCX7Q360zTfXJ2dBamPid9+0YfHdbPLnD4WFjgqs7LOa6TXzjnfk75PK8bc/Fy6kdtzTpTNa+zeH+b+qj0ne9G6bB+VoHDx4YCV3WuzrFQfuGc+1Hl8/PqBp6n8ov/xucv2b9Aes5zg/az/vUCR98FBa7qHO1/g/z6PunOxVOpHe3NOviDPzM6S1K3St++u6XD+pkFDh9LClzVeTnHAvmFc+6tyud5deXiv1J3aW/W6da8JkXnrtRnpO98XdJhfVKBw8ddBa7qXJ1jifzCOfczyufn1aLw78sv/hufw2f/Fuk5zyLtZ31igaPvlgJXdY72f5f8+j5ZmIunU/8gP4t0n0yIztLUh6Rv3wulw/qEAoePpQWu6ryc5yH5hnPeLcrleX0/Fz9P7Wxv7rdQ8zojOotTfykfzvd96bB+RoHDx+ICV3WuzvNL+YZz3qXK5XktyMWLqTvw59225hzwp0dnXeqT8uF8C6TD+ukFDh/rClzVuTrHYvmFc+4nlc+vQ9eHf05+8Q93Wvavkp7zXK/9rJ9W4Oi7qsBVnaP9r5Nf3yfX5eJnqdvz/rGt2R/8qdHpSn1C+vZ9nXRYP7XA4aOrwFWdl/M8Id9wzrtKuTyv+bn4Vepf8Tl4W3MO+PHRWZP6G/lwvvnSYX18gcPHmgJXda7O0SW/cM79G+Xz8+ra8P8mv/iHG5f910vPea7VftbHFTj6Xl/gqs7R/tfIr++TifnzyjP5Qq/2Zp1rdZ8sDL8iek9J377RR4f1hZ09c/hYUeCqzss5rpdfOOdmnf7k9s/F83PJv29rXvfPM9+vflV//vn3Ba6kB3+/9Pw5Ozw/t8nPZR6Q2r+9WY99/vlrfo7zzdS3avqDa/Xz3nD2d7/6w1X9+VTnf7PAeT5vKb+fV/AnpU5MnSQdeHT23PnTemDqoJ17ztfKjzl8oG/OvtCjP5zz4HNPcc47SLk+8/em8oWZqbO0H479R6cO1Zyq9i/1PbrA2c8g9W98Di3/Q+X3M+cS4S5KvSL1H6QDj87I1FGpo2v6MYePkQXOvoaqP5zzjJJvOOcdrVy+T+AXpi7Sfjj2T0idWPDZqn+p74QCZz+j1b/x5y75nyi/zg2/MvUW7Ydj/9TUaTX7l/pOLXD2M1H94ex/mvx+5u8PhFufemfqD6UDj87c1EtTL6vpxxw+5hY4+5qm/nDOc6l8wznvZcrl+wT+x6k/0X449n839aqCz1b9S32/W+Ds5zL1h7P/q+TX72vgH0x9JPVx/rwgPfahd03q/NRFqV01/ZnD1zUFzv6uUn8455sv/3DOv0j54DyfLuX3/QX/i9RntL/xOpj9y1NXFPK06l/qu7zA2U+X+sPZ/wr59f0F/0Lqy6mvpb4uPfahtzZ1Q+rdqZtq+jOHr7UFzv5WqD+c822Qfzjnv1v54DyfTcrv+wv+/dQ/aD8c+7ekPlTI06p/qe+WAmc/m9S/8Tm7/D8kv76/4D9O/SS1V74h9u7VrMc+9B5LfTz1qdSna/ozh6/HCpz9PaT+cM73uPzDOf9Tytf4c77m87Ty+/6C75e6u/bDsf+F1BcLeVr1L/V9ocDZz9PqD2f/L8qv30fB75G6T+q+0oFH5+XU11Jfr+nHHD5eLnD29aL6wznPa/IN57yvK5fvE/iBqQdqf+N1I/vfSX234LNV/1Lfdwqc/byu/nD2/678Ojf8kNTDtB+O/R+kflizf6nvBwXOft5Vfzj7/1B+nRt+eOoI7Ydr7N/l09prl3r9S33RNWc/6NEfzv5ZR9e54UenjtF+OPb3Te2n/FX7l/r2LXD200v94ey/n/w6N/zpqWdoPxz7+6cOqNm/1Ld/gbOffuoPZ/8D5Ne54c9JPVf7G5+zZv/BqYfU7F/qe3CBs58B6g9n/4fIr7//wU9LnZM6Vzrw6ByZ+uXU4TX9mMPHkQXOvg5Rfzjn+bJ8wznvcOXyfQJ/Werl2g/H/uNTTyj4bNW/1Pf4Amc/w9Ufzv5PkF/fJ/D/mPq91GulA4/OV1LHpo6r6cccPr5S4OzrBPWHc56x8g3nvOOUq3QexnnMm4XzMM5p7tW5Q9V//7nqeZh93CvOjzc85x4DUv8oHXifN72d6n9Pu6qfxuttezNfOtdq9e94wznHI/LrvvBvq6/nBc85yJmph0oH3udIg1P/VNMPXNXzKvtC70/Scw7WB0vPuVmnr+cFz3nJ7NQp0oH3udKw1CGaX1U/cFXPr+xrsPrDOccQ+XVf+GHq6+878JyfXKn9cD53GlOzP1zVcy37Gab+1oMfIz3fJ/Ccn3Sl/pN04H2uNCn1qzX9ND631fxL51f2NUb94Zzjq/LrvvCT1NfzguecZVXqYuk0/p5LdDiPmZ46uaYfuKrnXvY1Sf3hnGOy/Lov/HT19fMKnnOXu7S/8X4y+zmXubxmf7iq52H2M139rQd/ufT8uSo85yr3p96berf02OdzqatTv5V6RU1/cFXPwezvcvWHc54r5BfO+b+lfPYHf7X8+f6C59zlCe2H87lWd83+cFXPzeznavW3Hny39Pw6BM+5yrOp/y4deJ9HrUy9qaYfuKrnXvbVrf5wznGT/Lov/Er19X0Cz/nJNu2H8/nU5pr94aqef9nPSvW3Hvxm6fk+ged85H9T35AOvM+VHk69p6YfuKrnV/a1Wf3hnOMe+XVf+IfV1/OC5xykI/Uj6cD7vGlr6qM1/cBVPdeyr4fVH845HpVf94Xfqr6eFzznJX+Z2kc68D5Xein12Zp+4KqeX9nXVvWHc45n5dd94V9SX88LnvOT/VL3lg68z5+2pb5a0w9c1XMu+3pJ/eGc41X5dV/4berr1214zl0GaX/jdSj7OZd5r2Z/uKrnXPazTf2tB/+e9HyfwHPucnjqQdKB93nUR6nv1/QDV/Xcy77eU38453hfft0X/iP19bzgOa85hs+5pQPvc6zeqZ/U9ANX9bzMvtD7RHrOwXpv6Tk3670KnzfDc85zYupx0oH3+dfuqX00v6p+4Kqes9lXb/WHc44+8uu+8Lurr+cFz/nQhNS/l07j7z3o3Gpg6j41/cBVPZ+zr93VH8459pFf94UfqL5+3YbnHOk87W98XqxzrUNr9oereo5nPwPV33rwh0rP9wk850MXpE6RDrzP10akDqnpB67qOZ59Har+cM4xRH7dF36E+npe8JwjXZF6oXTgfd42KvWYmn4+77mefY1QfzjnOEZ+3Rd+lPp6XvCck81P/aZ04H0uNz71xJp+4Kqe/9nXKPWHc44T5dd94cerr88J+X/X+H/V3tE5of8/tvt0PlP1/297p8CV9ODvk54/R4Tn34lkYb/U/dub9djn/z+Of1dyW+obNf3Btfr/6uDs7z71h2v172GaI/+2Auf5vKH8fl7BH5N6euoZ0oFHp3fej/VPHbBzz/la+TGHD/TN2Rd6A/z+UXnw2Vuc8w5QLn9/h5/G5+baD8f+I1OP0pyq9i/1PbLA2c8A9W+cO8j/UfLr+wR+Lp+f8/m4dODRGZ56fOoJNf3A/R/VZMn7eJydnHvMlnUZxzF4EbQEDLOpsFJEdLwGKpmkqRBqtjYVagaIDRFR05rFwU4eYKV2mGAeAuygE0QrMMNKgZetMk0FrLaUg4emQOo4hTbBDlt8P/f2fuC35+72ny/j+VzX93v9+N0P7/Nc6pXdu/zvn2uipx6wR4dFu+2RLl/N65+PfjivjxJHn69EP35A575wvE5f+sDvH65N/M3RW6Oz1QeePqOjY6PjCrlb5TFHjtEFzrlGyR/O84xVbjjPO05z+bzg50d/GP2R+sDTZ3J0SvTyQu5WecyRY3KBc65x8ofzPFOUG87zXq658tt78T+N/kz1cNRPi04v5GzlX/KdVuCc53L5wzn/dOX1PYFfGv119DfqA0+f66I3Rmc2zGOOHNcVOOeaLn84z3OjcsN53pmay+cF/7vo49E/qA88fb4XvTU6u5C7VR5z5PhegXOumfKH8zy3Kjec552tufxcwT8b/ZPqq/eJ1M+Lzi/kbOVf8p1X4JxntvzhnH++8vqewD8XfTW6UX3g6XNPdHF0ScM85shxT4Fzrvnyh/M8i5UbzvMu0Vw+L/gd0X9Ed6oPPH2WRZdHVxRyt8pjjhzLCpxzLZE/nOdZrtxwnneF5vJzBd8tB9m2f+d6OOpXRVcXcrbyL/muKnDOs0L+cM6/Wnl9T+APiPaK9lYfePr8Jfp8dG3DPObI8ZcC51yr5Q/neZ5XbjjPu1Zz+bzg3x89ItrP59alc5+/RTdGNxVyt8pjjhx/K3DOtVb+cJ5no3LDed5NmsvPFfyA6NGqr94vU78lurWQs5V/yXdLgXOeTfKHc/6tyut7An9c9EPRIeoDT5+d0bejuxrmMUeOnQXOubbKH87zvK3ccJ53l+byecGfHB0e/aj6wNNnvwPjG207cN+5W+UxRw76m3Mu+uEP53nIuZ84z9umuXxe8COjo6JnuU+Xzn3eE+0V7a1zq5vHHDneU+D2yiV/OM/TS7nhPG9vzeX3IfgLo59VffW+mvojo0cVcrbyL/keWeCcp7f84Zz/KOX1PYGfEJ0YvUR94OkzKDo42t4wjzlyDCpwznWU/OE8z2DlhvO87ZrL9wT+6ugXVA9H/UeipxRytvIv+X6kwDlPu/zhnP8U5fXc8NOjM1QPR/2Z0REN/Uu+ZxY45zlF/nDOP0J5/XzAfyN6c/QW9YGnzznR0dExDfOYI8c5Bc65RsgfzvOMVu7q+1LNO0Zz+Z5M67VHt+VAtkfXtHXmru65Rx+NPhad23Pf/ajn9avF4Qfv/nBrlKvk6/yPqZ/vCfwHcyBHRt/XvXMfePq8Hn0j+pLOo24eOPzh7QNHH/KVfD3HS8prX/g35Ovzgh8XHR/9pPrA0+fo/Bw1MHrIAc3ywOEPbx8456LfIeI8xyHKa194XsfXzxU8+6CrVF99Xkk9+6KTG/rDfV5/Tu4P5zwD5e9+8Cern+8JPHudr0Wnqg+892RnRU9vmAeu7j7OuU6WvznmcU77mhtVuCfw7HXmqL76uTP17H3G67zq+sPV3b99TblKvs4/Xv08Nzz7mh+rvrr3qWefc4Xmr+sPV3ePNke5Sr7Of4X6+fmAZz/z8+j96gPvPdaM6DU6j7p54Oruy36sfCVfz3GN8toXfoZ8fU/g2c88qvrq563Us7+Z1dAfru4ezXlmyN/94GepX49w3cWzd3ki+ttoh/pR5/3VnOh3ozc3zAdXd1/mfLPkb465nBPO839X8zkf/Bzl8/2CZ6/zZ9VX7y+pZ+9zd0N/uLp7NeeZI3/3g79b/Xy/4NnPbIq+GF2vftR57/VQdFF0QcN81ffG+vMo7dmc7275m2Mu54Tz/Is0n/PBP6R8vl/w7HneVD2c92MdDf3h6u7fnOch+bsffIf6+X7Bs9fpHv1Pfv/f6ked92Brok9Gn2iYD67u3s35OuRvjrmcE87zP6n5nA9+jfL5fsGzD+qjejjv1dY19Ieru7dznjXydz/4dernn6Pg2fP0jx6iPvDek22OvtgwD1zdfZxzrZO/OeZxTvua21S4J/DsgwaqvnofTD37om06r7r+1edb/TmV9nH9lavk6/zb1M9zw7PXGap6OO/Hdmv+uv5wdfdvA5Wr5Ov8u9XPc8OznzlV9XDec3U/sHP/uv5wdfdoQ5Wr5Ov8cG2F7zPh2bucrfrqfU57oD6av64/XN192KnKVfJ1/j7q57nh2bOMVX31vqa9zwDNX9cfru6e62zlKvk6/wD189zw7E8mqR7Oe6fjNX9df7i6e62xylXydf7j1c9//8GzP/li9Er1gfdeaXh0mM6jbh64uvurScpX8vUcw5TXvvDD5et7As+e5VrVw3kPNbKhP1zdPZfzDJe/+8GPVD/fE3j2Qd+OXq8+8N4/fTp6bsM8cHX3XM41Uv7mmMc57WtuTOGerOm6R1+MvhR9tmtnbm76LYo+EJ2n+0QdfecWOPvSd64455knfzjnf0B5PTf8a9HXVQ9H/S+jSxv6l3x/WeCc5wH5wzn/UuX13PA7o2+qHo76FdGOhv4l3xUFznmWyr/6/Kv8HcrrueHfld/o2q1zPRz1T0efaehf8n26wDlPh/zhnP8Z5fXc8AdH36t6OOrXRzc09C/5ri9wzvOM/OGcf4Pyem74ftH+qoejflN0c0P/ku+mAuc8G+Rffe5U/s3Ku9fn3vxiYPQY1Vefe1O/Lbq9oX/Jd1uBc57N8q8+Jyr/duX13PBDoyeoHo763dF3GvqXfHcXOOfZLn84539HeT03/KnR01QPV9Xni8v9ezTzL/nS15zz0A9/OOfndfp6bvizomerHo763tE+mr+uf8m3d4Fznv3lD+f8fZR3r8+9+cUF0dGqh6P+iGi/hv4l3yMKnPP0kT+c8/dTXs8NPyF6serhqB8UPbahf8l3UIFznn7yh3P+Y5XXc8NPiV6uejjqT4ie2NC/5HtCgXOeY+UP5/wnKq/nhp8enaH66n019WdGRzT0L/meWeCc50T5wzn/COX13PA3RW9WPRz1F0RHN/Qv+V5Q4JxnhPzhnH+08npu+Nui31d99Tym/qLohIb+Jd+LCpzzjJY/nPNPUF7PDT8/erfq4aifHL2soX/Jd3KBc54J8odz/suU13PD/zT6M9XDUT8tOr2hf8l3WoFznsvkD+f805XXc8P/Ovob1cNRf2N0ZkP/ku+NBc55pssfzvlnKq+/l4NfEX0q+rT6VP+dfPrcFL09ekfDPObIcVOBc66Z8ofzPLcrN5znvUNz+Z7Ar4uuVz0c9fdFFxRytvIv+d5X4JznDvnDOf8C5Y1U88NvjL4e3Rl9U/2oo9+S6NLoimhHw3zmyLWkwDnfAvnDeb6lyg/n+VdoPjifT4fm3+v7sPxiQB64o9s611f3O/VbolsL87TyL/luKXDO0yF/OOffqry+X/BDokOjE6OXqB919NsV3R0dnH/Pvr1ns3zmyLWrwDkf/dr13w14vt3KDzexwA3uuW8Of/zgfL/gp0YXqB6O+tOjX9R51vWHm6o/T/eHc552+Zujr3P677ln9b39y9F10T917cx7n/Fg9L7ofH2fQr33Iebq7k2ca7784TzHfcprX/gH5evzeknf978RfVV94L0HeSS6uGGe6n225r7FuR6UP5znWKy89oV/RL5+ruDZE7ylejjvTVY29K/+Xqu5l3GeR+TvfvAr1c/3BL7aW0R3qw+89yeror9vmAeu7p7GuVbKH85z/F557Qu/Sr4+L3j2C32jB6kPvPcuL0Sfa5gHru5+x7lWyR/OczynvPaFf0G+fq7g2Uscpno472leaegPV3cP5DwvyN/94F9RP88Nz15ikOrhvKfZ0dAfru4eyHlekb/7we9QPz8f8OwlTowOVh9472v+FX2rYR64unsh59ohfzjP8Zby2hf+X/L1ecGzz/hY9BT1gfeep0e0a49meeDq7pOci35dxXmOrsprX3her/ZEXfbNswc5R/Vw3gsd3NAfru7eyXl6yN/94A9WP98TePYgY6Lnqg+890P9o30b5oGru4dyroPlD+c5+iqvfeH7y9fnBc/+5HPRseoD773ScdEBDfPA1d1fOVd/+cN5jgHKa1/44+Tr5wqevcsVqofzHuqkhv5wdfdcznOc/N0P/iT18z2BZ+9ybfRK9YH3PmpkdFjDPHB1917OdZL84TzHMOW1L/xI+fq84NnX3BK9Tn2qfw80fdjrjIl+omEeuLr7MucaKX84z/EJ5bUv/Bj5+rzg2fPcHr1Vfar3rfRhH3RxdGzDPHB192zONUb+cJ5jrPLaF/5i+fp9CJ790A9VD+d92ZSG/nB193HOc7H83Q9+ivr5nsCzH/p59F71gffebEb0qoZ54Oru55xrivzhPMdVymtf+Bny9XnBs0d6NPqw+lT/P6f0Yd80K/r1hnng6u71nGuG/OE8x9eV177ws+Tr5wqevdIzqofzPu7Ohv5wdfd9zjNL/u4Hf6f6+Z7AszfaEF2jPvDety2Mzm2YB67uXs+57pQ/nOeYq7z2hV8oX58XPPuht6Ivqg+893Aro4sa5oGru+9zroXyh/Mci5TXvvAr5bvX96fh2BsNjB7a1rlP9f1p+rBf2hZ9uWEeuLp7PedaKX84z/Gy8toXfpt8I9X7ETz7oUnRC6Pnqh913scdHz0y2rdns3xwdfd/zke/vgWOuZwT7kKdg+dyPnPkq76fFr9Adbzuvdzx/6cfXN193yTlsa+fq1UJuj66IfpCdLX2EnelwYLowuj90R/o+y7q8bmrwDkH/e8S51w/kH/1/qd5Fip39f6uee/XXD4v+I3RzdG/qw88fZZEfxF9uJC7VR5z5FhS4JzrfvnDeZ5fKDec531Yc/nnIfgd0X+oHo76ZdHlhZyt/Eu+ywqc8zwsfzjnX668vifw7/BGkEb7devcB54+j0f/GH2qYR5z5Hi8wDnXcvnDeZ4/Kjec531Kc/m84NuivaN91AeePquja6PrCrlb5TFHjtUFzrmekj+c51mr3HCed53m8nMFf3j0CNXDUf9qdGMhZyv/ku+rBc551skfzvk3Kq/vCfwHogP4uVB94OnzWnRLdGvDPObI8VqBc66N8ofzPFuUG87zbtVcPi/49uiHokPUB54+/4y+Hd1VyN0qjzly/LPAOddW+cN5nreVG87z7tJcfq7gh0c/qno46rvl59K2HvvO2cq/5Etfc85Dvzb9nO381euFz1fwI6Ifj45yny6d+7w7elC0l86jbh5z5Hh3gdsrl/zhPM9Byg3neXtpLp8X/Kei50XPVx94+hwaPSx6eCF3qzzmyHFogXOuXvKH8zyHKTec5z1cc/m5gh8fvUj1cNQPjB5TyNnKv+Q7sMA5z+Hyh3P+Y5TX9wT+kujk6GXqA0+f9uiQ6NCGecyRo73AOdcx8ofzPEOUG87zDtVcPi/4L0WnRqepDzx9ToueHj2jkLtVHnPkOK3AOddQ+cN5ntOVG87znqG5/FzBfzP6LdXDUX9e9PxCzlb+Jd/zCpzznCF/OOc/X3l9T+C/E50dnaM+8PT5THRcdHzDPObI8ZkC51znyx/O84xTbjjPO15z+bzg74rOjc5TH3j6TIxOil5ayN0qjzlyTCxwzjVe/nCeZ5Jyw3neSzWXnyv4B6IPqh6O+i9HpxZytvIv+X65wDnPpfKHc/6pyut7Ar84+kj0V+oDT59ro9dHb2iYxxw5ri1wzjVV/nCe53rlhvO8N2gunxf8yugT0SfVB54+t0TnRG8r5G6Vxxw5bilwznWD/OE8zxzlhvO8t2kunxf8X6PPRZ9XH3j6/CR6T/TeQu5WecyR4ycFzrlukz+c57lHueE8772ay+9D8NujO1QPR/1j0WWFnK38S76PFTjnuVf+cM6/THl9T+D75Dc+yH6nrXMfePr8FxIDpI94nJ2bedTe07mGQxJTaNVUU48M4pABaQmR0GpNNbNqSEol0ioRlJSWKK1T81xBopEgoVQQNUQRraElmqkkiChNFM1UmqSicspZ67ivb63vYq93v+k/d33vvZ/nvp+997P3+3t/mbtWm///36Lg4uDjwXafQJt/5v8sbdf688cKvC+0/wTnFnid2392vMXiEadT+9Y654rH58RdLF9rhtde/P7B44IDFQc+cbqs/QluF+y29mfrbqTHPHQQ3zzrIh754R1XiLddgUfcbuLhu6344zSOzxl3iupSmw+e43dTXPOI6/xrtGnNnxGhrwffCM5s25o3KgvlzuBdwZvWbM1jHHFHFXivF3h3FuKhq5TX+u9SPPuGvyC4UOPhMf6B4IPyX5sf3gLV2/HhvSFdpbzW/6Di2Tf8ZcHlGg+P8ZODT8h/bX54y1Rvx4e3ULpKea3/CcWzb/ir5Q+rt2s9Hh7j/xScKv+1+eGRD77jwyMOukp5rX+q4tk3/C8EN9B4eIyfG3xN/mvzt5xP7VrzHR/e6tJVymv9rymefcPfMvgljYfH+LeD78h/bX54W6rejg9vA+kq5bX+dxTPvuF3DW6j8fAY/27wPfmvzQ+vq+rt+PC+JF2lvNb/nuLZN/wdg700Hh7jPwyulP/a/PB2VL0dH9420lXKa/0rFc++4fcN9tN4eIxvn/vRGmu1jl+bH15f1dvx4fWSrlJe64fXorfNZ/P3Du6j8fAY//ng+vJfmx/e3qq348PrJ12lvNa/vuLZN/zDgodrPDzGbxHcUv5r88M7TPV2fHj7SFcpr/VvqXj2Df/Y4Hc0Hh7j/zu4rfzX5od3rOrt+PAOl65SXuvfVvHsG/73gydqPDzG9wp+Wf5r88P7vurt+PC+I12lvNb/ZcWzb/hnBX+k8S39JeO/FtxT/mvzwztL9XZ8eCdKVymv9e+pePYN/+LgJRoPj/GHBQ+X/9r88C5WvR0f3o+kq5TX+g9XPPuG/4vgdRrfsm8z/pjgsfJfmx/eL1Rvx4d3iXSV8lr/sYpn3/B/GRyt8fAY/73gCfJfmx/eL1Vvx4d3nXSV8lr/CYpn3/DvDk7QeHiMPzN4lvzX5od3t+rt+PBGS1cpr/WfpXj2DX9S8BGNh8f4nwUvkP/a/PAmqd6OD2+CdJXyWv8FiufnivCnBJ8PPqE48IlzXXBE8BLVo1YPvCmqv/PAe0T6Snnt4xLpdV74I5TX6wT+nOCrGt+yPzN+XHD8KuaHN0fz5PjwrGeE8jse/PGKF2jxD5/n38uCi4JvKx7jiMfz8snBh4ITV1EfvKWaD+eDZ33jld88fFknPPt/SP6sD/5k6fP6gs9z8y7tW4+H598flqxifni1v29Yz2Tldzz4SxQvj71b+lFnPUcfFBwQ7BXcUXEZ798Xuge3Dq7M5x+uol54tb9rWOcS5TcPf9YJr5fqYV/wBqh+roN9mNet8HsK/HEax+f+3aN7k/ng1f6eMkh6nNfn3PMRunYCrBP8Z/7+p7at+SMS4MXgrOBjwev1nITx5BlR4FkH8UeIZ13XKz88+3hMeuHZ9yz5834kL3E3ybg1g//I318NTm3berz1/jX45+AjwfHBG6R3asG/ebV1ss4blB+efY2XXniuwyPyZ33U768Ffa7vn1U3r+ep0vlmcE5wmtaz/d4THBe8UXqmFeplXm1dretG5YdnH+Ok13nh36O8PlenKe5rwen6ncm67giOVP7pBT/m1fqeLl2lvNZ/h+LZ9wvhzQ2+EnxRvkdn/O3B24I3Kz/jiDu6wJtb4N0unvXcrPyOB/82xfP+eFH8+cG/BWdpfzjvhOC9wTHSM6ug27xaf9Y1RvkdDz8TCvHs9175cr1mh/ducI0EWpH/fkn1GpsAvw3ODD4ZvEV6GE+esQWedRB/rHjWdYvyw7OfmdINz36flC/Xi7zw18349YIvq17W91Lw5eCt0vNywZ95tXWwrluV3/Hw81Ih3qf8ype/P7Ju2cfzgm8F39G6Zpz3+d3B+4L3a13X9otm99M86bceeG/Jn/XCs98J8uN48O9XvE99T9e5uCT4dvAvOj9avqfrnJ0UnBj8lc7D2vOo2XP9Neks5f2LfFmn81KHSYW8rtNE1cH3Y/o0/fW94EfBj9XH6GuMd39/NPhccIr6lPsf8XxemFd7rtT2Xft9tBDP/n8rX/Bct+dUD3iu6xTVy/uA+yH38/8EP9B6YH0wzvf9Z4NPaT00ex9t9vtF7bq1n0nS67zU4dlCXtfpKdXB5yD9i/70b60P1gt897mnNd9eR43uMc3209r1az9PF+LZ76Py5b5BP18cfD+4Mvih8qKD8fT9h4O/D/4h+IzyN3uewEMffOdttk7vy7f1w1uputgfPNfpafl3PPjPKJ77Bv2f/cQ++N/g0uDf1Td8XrBv/hh8PPgb6Xtb8fh8oniNzid4f5fOUt7avrFU/u3H8cx7qlDn9zR/zM9aCbhx8CPV2fuL+Xwh+IbOhdp92ux5VLv+7OuFAs9+n5MfeK7PG/LvfsO80Nc7ZNzng+0JHGT+GO/+Pzs4Jzgj+LzWe+26qD134BGHP5Ty2u/sQrw2qoP9wHO9ZqgO8FzfOaqbz0/WB32qXcZ9Lsh8w3f/mh58Reugtv81u07RR1zrgId++NZpnvO+UqgX9xiek34xSN2Zd/h+LjtP8+H1UHtPqn3+W7sO7WdeIZ79zpYv14t+Qr/YNNgl+LHq5b4zP7gk6Pt7o3t5s/3NuqYov+PhZ34hnv0ukS+fS+xv9u9WwY7BLYLrt2s9zv1gQXBh8K3gq6vYV9orL3FmFOKhe0Eh3vryU9K3hfzbDzzXaaHq4HOJ/c7+3yi4ebCT5pf5Zrz7xOvBvwUXaZ69Lhr1ndrzFt5G8mNd8DaXX+tudn27TvPl33nhL1Je7wP6C/2mc3A7rS/WG+PcjxYHl2k9NHtu1va/ZveB/S0uxLPvBfIFz3Vapjp4H7yr/kmf6hbcJfhV6qx94Ocq9LflwdV43yI4U3prn9PUPkev7d/2Q7y1/b6J6rG8EM/1wvdy8VxP8sH3+cm+2TbYI9hd+wk++2lp8P3gv1ZxX8IjP3zngWddi5QfXg/5sl7ziPsv8dw3uMfwe/ZmwZ21z9h3jPPv3W8GP9b+8b5s9Ls5vNr7lXW/WYhX2zfs/2P5g+f6LJZ/13lTzTPzs5P2SxfV2ecD8/mR+ob3V+050+i+1ew6rd3/9r1cvpwX/kfK6zrT7+nn2wd7BrcOdlSdfT58EFwR/EdwofQ1ukc1ex51lM5S3q3lyzqdlzp8UMjrOq1QHVznFTpf6M/fDh4VXFd19u+09POuwU7Bl6Sv0e+98Gp/D609z+yDeJ3Es/+uOsfhuT6d5N/3Dd5H4j2w7wZ/EByk/tfSXzPe75dtnzy7BbsHS32z9n212vem7AM9jlfb1+0fXnfV3fXqrjrAc335HJ3eB/R/zoPewa9p37EPGefzgsDrBEv7tNF9tfZ8arY/2A+8dVQ/1wFfzus6EadNoc6cA9xvvhLsE9xV5wTjfP/5T7BtEqwe9LnT6B5Vez7B+4r0Ww+8PvJnvfDsl7yri+f68HnbQr/pqXlmfvYMfp19EdxB/cbnBvPaIbgu+oL/bvIcgtdT+YmzYhXX7Q7yVdK3m+pgP85L3Tpof8NzXddVvbwPuC/tpHn9RvCbQb43Mc73K9bBesEN9b0KfbX3tNrvc7Xr1j5Wk07Hw/962tfwXJ8N5Z/1y/dC3r8+Jzhc5xbnWMv7iwmwV+LtrXOj2fe7zUMH8c2rPVftY3vphWfffL5XYV1yP+Je89Pg/wR/zvxoXfo+tX/woODBQb/31uh9Nni19zfrI97B4tkfeh3P/g+SP3iuz8Hy7/7Meqev7xHcDz3BA7QvGO/+v1Zwg+BGwY21P2rPk2b35R7yY13w9pNf64Zn3+vJD7z9VTf7N4+4G4vn+eEeyj3ze8EjmN8g95+dNT++v+4Q3Cq4qe5JvlfVPr+ovTfX3ufsq4308mfXYVP5g+e6baV62Ad8PseH+1Rv5T8keGCQ85jzm3H2s1lwE53XPt8b3XObrV/tvcJ+OkgvPPvfRP6sD/5m0ufzc0/FPYjzK8h9B771fTH4Od8bFb90b2q2Dl+XvlLeveTHOp3XPPJ6XdKv6DNHB/sHjwxyf2Gc+1vnYJdgx8K9qdF9qNl+an0bKr/j4a9zIZ59d5QveK5TF9XB65J+/q3gAOlBH3z6/H8Ft1b80rlSOi/gkR++8zRbrwHyZb3mOR68dcJjfe6ieWU+hgZ/Ejw3yPMtnocRx/d35rN3cN/gPsHS87NGvwfBq/3+UPvcrnZ9uz69CzzXq6v8w3Od91Xd4Hke9lFdfU+hP9HPjw8ODh4TPFR9jPE+J3oEewa3CW6ufl7bH5s9nw6S3lJe++1RiHeo6mA/8I5R3ewfnuvbU3XzecB5y/3ntOAp0o+flt+ZdJ/qE9xF+Zo93+HV3t9q62x/fQrx7LuHfMFznXZRHbwP6IP0xYHBIcGTg6fqnGG8+2e34E7BnYO7ruL5Vdu34VlnF+WHN1C+rR/eENXF/uCdrLrZv3no3FU874Mj5YdxpwfPUL9lnO8L5Okb7Ffoo7X3jto+Xzsf9tFbOh0P/30L8VyffvLvOrOv2Dc/DA4LnqD+xTjvwz2Cuwd31LlQ2w+b3feDpbOU9wT5sk7npQ57FPK6TrurDr5/0u/4vnpj8EL1Qfj+3jsoeIj6GnpqvzfX9l3rHFSIZ/19pBOe/R4iX16X9Iuzg+cHz9M+YF8wjn7yjeA3g/tpH3jfNOpP8NAD3/ma3a/201d64Z2vetifecTdTzzX+RTNH/NzVfBy7QfG+XxlPo8OHqH9gL5G53Sz+7B2/dnHHtLpePg/uhDP9TlC/r3/T9e8XB28Uv0avtdB/+BRhfOhUd9vdr1ZVz/ldzz89C/Es9+j5MvrcpjmizpfEbwoeKbWpfsw83Jk8NDgV5vs5/CGKS9xdl/F9Xam/JT0XST/9uO85pHX9+ChmmfmZ2zwruD4IN83Ge/7CvN6UnBY8NTgvtJbe/+p/T5cu27tZ1/pdDzqcVIhnus1THWA53qeqjp5H7C/6OuXBa8J3qD9xzj3/W8FBwQHav/Vnh/N7vvLpN964F0jf9YLz377y4/jwR+oeB3CYz/w+x7PZXieckHw/uCDwd8Efxbkdz/i+f0unsscGBwePC94bvCAoH8vbPT+GLza3ylrn0O5DgcWeK7D/vIDz3U7QP7hue7DVT94npfzVF94nrdzVX/vO/oo5/yo4MjgteqzjPM9YHDw+OC3dR7V9m14je4d8K6QzlJe+xtciHet/NsPPNfpeNXB5w/3Lb4f3BL8VfB26UMv4/09Ykjw9OBpym9ftfe9Rt9fmq2n/Q4pxLP/wfIFz3U7XfWA57qepnp5H9BHrw+OCY4L3qo+yzj67HHBE4OnBE8u9G3ykNdxavs7vDHSbz3w7GOgdMIbpzrYl3nEPVk815l7xtXSwfg7g2NVZ9+jrfuMwj2k0X0cXu39p/Zcrq2z/Z4kP44H/wzFc7+hL7Gf2AcTg/cFfx28Sf3G/Yz9c07w7OAPg99tsj/CG6n8xDlevNr+cJN8lfS5HucU4v1a9bJveK7r2aqXf3fk/TTeP7st+GTwmeDjwTuCvLdNHL/fNjR4efCq4EXBHwSbfS8cXu37dda7m/I7Hv6HFuK5PpfLL7xP1Uv+4bm+F6le8DwfV6m+vl8Pkm90PBX8Y/AxnYuckzyPJJ7f48fPFcFrghfqHCw9x2z0PhG82n9PUPv8tHY91N4XXL8hqoPzUv8rCnk9LxeqvvA8j9doPnze0bc5J+8NPhB8SH295bmwztEfB38SPL9wTjQ6j2vPE3j3Sr/1wLOPM6QT3gOqg32ZR9zzxXOd6efc+/4QfDY4Wf2ecb4XXh28Nnix+jj6Gt0vmz1nrPvqQjz7OEc64dn3xfIFz3W6VnXwvWKs5pn5+V1wevDpIM+BGO/7DfN6aXBk8MrgMOmtvS/VPqeqXbf2M0w6HY96XFqI53qNVB3guZ5Xqk5+/s79g/XB/P8++HC71nzfU1gnlwV/2uQ9B959ykecs8WrXc8Py0dJn/1epnjuG/SZR4PTgjM1j8wr4+hDPw/eGLxJ81jqf6W+Bg898J2v2fU2TT6tG559Xypf5hHX/l1n/r3FrODs4Iwg97vhqjP/HmNMcGxwVLB0X2z07zvMQ9eYAq/2nmo/Q6UXnv2Pkj94rtdY1cP7n+eJLwTfDL4Y5DkifJ4zjg7eE7w56OeLtc8rrWN0gWddw5Ufnv3cI93w7Pdm+XK9eD7L89LXg/OD81QvP8f9Px84tFR4nJ2cd9CV1bnFFQEFBESJoojiJJYQE41iF8WGioI1ZjIpc/9JM8ZozKRZomKNBTWa6LUhMYo9YIkx0dBBQFCUoggWuqaAKE3Knbl3/b6Z73ez79nn+s/6OHvt9TzrOe+73733u48jttniv/97PPhYsO3/wBaj8scVwVPSfol47xV4Jb0FwREFPdrfb9s6vxHi0U78x+SnQ3hbBy8J79Lgg8HXgouCM4NXtm3df0B0TwyeH7w7+GTwnuCpype45GE9eJcoPjoDxHtQfpwXvCvlq5Sf63F3Qc/1elJ1gOd63qM6BbZoH3wxvEnBd4NzgpODo9u27ndthG4NPhp8IHhb8NfK70Xp0X6teOQD3/HgjVaepbj292hBb7L82w881+kB1YHrt13wct2v3D9Lg0PatuYPjI7v65HBQcoHXeK4P7zLFQ+dgeLVji/Of5DytB78kdLjuoI/RPxlwavatuY53qjgYMUfov6DC3Ws9eN8Biu+9eCPkp7vx5fCmxJ8K7gkOC84VffjdRG6Pfhg8I/BPwTvUH7EIa514L2kuOhcJ95byt/5wJsqP6X8lqgO9gVvnupk367z38J7NTg/+E7wg+B01fn6CP1n8OHgI8Fngncqv79Jj/brxSMf+I4Hz/ndqfjw5sun84b3jupgX+YR/xnx/Pzn+fZIcFxwefBfwdl6DtKf5+BFwZuCTwf/HBxWeB4SlzysB6/2OTxOfpyXefh8usCz77vlB57rNEz+4bmuf1a9/FwaHt6M4Irg34Nj9Fw6Lzp3BV8IPhe8QfkMlw7t54lHfPiOA8953aD41sPPCwU9+31OvjqGx/gxLLy/BP8Z/DS4Prg4OIH7qW1rnXPzx9XB54MTguODTwWHBi9U/sOkT/u54pEvfMeH97DyLsW1/+cLeq7DUPmC57o9pTrAc70nqH7w/H2MV339PHgovL8G1wY3BP8RHKvnwQURuiY4Njgx+KfgjcqPOMS1DryHFBedCwp65D22oGcfNypPePb9J/mC5zpNVB38PJig77lNBpYOwXX5nPk183b6+/qZFnw9OC5Ymt9PkC7tQ8WrvW5r1xX29ajydVzqMq0Q1/UapzrAc31fV918H4xMx9eDbdOvU3BjPn9G98HFEbo3OD04KzgpeJnyGyk92i8Wj3zgOx4853eZ4lsPf9MLevY9Sb7guU6zVAffB8wPmL8xn1oT7Bqd9kGe6/RnHuF535jgm8FX9fwnX+KSh/Xg1c5jauej9vO08rQe9RhT0HO93lQd4Lmer6pO/n5G6Tri+98y/bcPbsrnC/T9sL/m629q8O3g5KD32YhLHtaDV7u/V3tf2M8I5Wk96jG1oOc6TZZ/eK7r26qXxynmHxODm4Od0797kPkC/Zif3BJ8OTgnuKAwnyAOca0Dr3Ze5LxfLujZx/PKE579z5E/eK7PAvl3nZlXjEcg/boEPxNcqzoz77g5OCU4N/iO5gnIE4e41ml2vuO8pxT07GOs8oRn/3PlD57r8478u86sZ1iHtEu/XVh4tWvth35e/8wILlJe9jtG3yvtXr/Vrrdq62x/Mwp69j1FvlrCqU6LVAfXmXkv89qt0q9nkPtooursefIrwcW6z3z/jpUe7V4P1M7La8cN+3uloGffL8sXPNdpserg5yf74sx3mafukP7dgivz+Vw9P72fzvx2fnBe8C/B4cp3jnRpf0C8Rvv48NAh31Lc2nk+OtTBfqxH3eYX9FzXeaqX7wP28VYHd0q/HkHmVczD6Mc+3+jge8H3Ne/yPK3RviE88oHveM3OD/GFrvOGZ99j5Mt68N+XnuvMPtSs4Lbpt1twm+AK1Zl9qvuDs4PLgjODLyg/4hDXOvBq98ec9+yCnn28oDzh2fdM+YLnOi1THVxn9k/ZH905/foEd+T5oDp7v3Vh8OPgu8EnlV/tvm3te1vnvbCgZx9PKk949v2ufMFznT5WHbxf/EY67p0P9gnyfXG9wL8vOiuDH+l79HWEPjq031fgkcfKAq/2+rWP2coXnn1/JH++LllHsf7pnX67B7djHNF16XXXB8HlwbeCjyu/2nMctes85/e44lsPfx8U9Oz7LfmC5zotVx18XbKfszC4axq+EGR/Bj77PE8ElwQ/CU5XPugSx/3h1e4rOc8lBT3nP115wrPfT+TL8zPOhzCOME58PtirXWtd8qW/z5UwvqwKLlV8+5qp74v2e8RrdJ6l2XrWjp+ux6oCz3VaIv/wXNelqpevZ54HvMfrmIYvBnfW9ez3gW8E12g8J5/a94m1zyXn+UZBzz7WKF949rlQfjzOcr6EcYrxZd/g1tRd46zPrzEerQ6+FvT5j0bn4OA1OvfS7DhrHyOVJzz7fk2+HBf+asV1nT9Mx1XBz6bf/sHPMY9QnZ+N0IvBfwTXB/8ZfEn5EYe41oFHO3HReVY88kXX+cBDBz+l/PCNrn3B+5zqZN++//l++f6+FNyP+bDmWb4O1gbXBZs9l9Ts9ea8Rim+9fCztqBnv+vky/XivRnve/um4eAg78Hg+/3yJi70PCinKZ/a99O17++c56aCnvOfpjzNsx48P/95/8L7kAOCJwUPDDJeM77T3+9tNgS3T6CNGtebPacCr/Z9Ue3zx343FPTsH72N4p1U0Nte38+Bqq/r5O+HfVLW06yXDwueETyU65rxPv19joV19lYJ1DPYJjhDvhqdi4FXu59buz9gP+i1UT1dD3yV9KiXfcM7o6AH389B9lc593B0+p0VPA6ddq37+RxFh+j3CnYOviIftecxavd9nTd5WM8+0Ous+uHber3EO051sm/XmfdevK86JPj94FFBzvvQz+/Jtoz+gcQJTpDf2vNFte/lnF/L+SPVxf7I13rfL/AOlN5RqpN9u87sM3NO5Njg+cHDgzvoeva5k22jfyj5BOfLR+35ldr9b+dNHtazj5a8Ovx7Hv7tB975hbjwPa7z3pxzImcHvx78dvA0vl+N6z5/slsC7BncL7hzcKr81763rz334jzR21l1sl/yLulRB/uB9/WC3p7ifVv1dZ18H7D/z/7+QcEfBk8OtpwnST+/L9gcPCRxdgi+Kb+151dq3084P/R2UF3sb3NB74cF3iHSO1l1sm/XmfdU3Ifcb98Nfi24h+rs91rcj18Ofjb4oXw0ej8Gr5viojPv/zmO7CE/pfy+qzrYF7yvqU727fGG8wycQxgQPDf4VT23mRfQ3+cgtkuAvsE9iFuYPzQ6Hwqv9vxF7bzFfsnbekerDvYD79yCXl/xvqr6uk6+D3ifuFdwYPDHuu+4X1v2uyK0Itg9+kcGS/dzo/eX8MgHvuM1O44MlE/nDe8g+bcfeD9WvezfdWbhzzmRU4IXBU8MdlGdfe7kM9HvF+wWnKv8as8J1Z5zcX7odVP97I98S3r4tx94FxX0+hXqzHsgzvEdH/xp8IRgb9XZ5wK7RL9/sGvwA/moPV9Y+37KeZOH9ewDva6q308Lev3FO0F1sm/Xmf0j1hXM9y8InsP4ozr/r99BRP/wYO/gJvlt9HuKZve1atdFfeWnlB++0bUveOeoTvbtOnMuinNP/YI/Cn4r2Fl19jmqraN/WHCf4Bz5qD3HWXtuy/mht4/qYn/ka70fFXiHSe9bqpN9e/+W9QPz/5/ovuG+hO91xlHB0v3a6Hxxs+uZ2nHiePlxnvB+Iv/25XqxH8N+ymX6XnqqXt63OQndwvfc6Lxfs/tDtddXP/lxnvAuk3/78n3MfiL7gT8LflPPN56fLee3IsR+4THBvYOl52ujc6nN7nfWPtd/Jp/OG94p8m8/8L6petm/68z+OutP1oWnBy8OHqA6+/cYrBt3CZ4Q3CC/jX7XAa923792/XyA/JTyO111sC94F6tO9u37n3kg87grg6xPWP/A93zx1GBpXdTo/Hqz89La9dgA+XGe8K6Uf/tyvXj+8Vz6ZZB91O6ql5+TxwdL+7KNflfR7PO4dj/4EPlxnvB+Kf/25fuY9428V/xG8FdB9jvZd6Wf30/uFTw5WNqXbfQ7RXi170Nr94O/IZ/OG96x8m8/8H6letm/68y5Ls5t9Q9eEWR/s5Pq7HNgHaN/SrC0v9rod4rwas+d1e7r2h/5Wu+KAu8U1fls1cm+Ay3jAOc6Obf5leDPg98LDsIH85T093nQ3RPg2OABwZ2CzZ5nhld7DtV5oreT6mS/5F3Sow72A+/nBb1jxfue6us6eXzeRn7IZ0iQ97LwfW6cPAcFt1I+tefOa98r19b/MPko5TdEvu3H1zPnaDi3dUTwvOAvgmcGOU9Df58Ha5cABwePC+4aXCv/tefLas/72Af5WM9+0NtV9TyvoHeweGeqXvYN7xeqr+vkcZ1zI5z7uDR4YfDIIOcc6edzJidG/4hg++Aa1aX2XGXtuRbnh1571eVS+XTe1sO//cC7UPWyf9eZ84ucEz81eLW+/31VZ5873zH6pwVL11/t+fVG5yqtR97kYb3a++MI+bcfeFcX4p5WGG84D83vRAYHvxO8KnhMkPPO9PfvT3okwP7BwcFOwVXyVXseu/Z3L84TvU6qk/2Sd0mPOtgPvO8U9PYX7yrV13XyfcB1xH40+8mXBC/X900//66C/eYBwYHBHZVfo99nNHt91+6nXyJ/zhfeqfJd8nG56mTfnqf00nXD935N8Aeap/icP9fD6cGDlE+j3wnA+0GBZ73a6/wa+XGejgvfPnxd7qo86X9tkHk+6wP6+XcU6J8RLK0fan+P0ej3Ms3WuXZ901/+7QfetaqX/bvO/E6C5zXP2+s03vRRnf27Cp7HZwZL412j3w3Ca/Q7jmbH2dp5yXWqg33BG6w62bf/v6T8/u/6IOsg1lnw+D3gWdEprbsa/Z6wFBdd82rXe18p6O1e8M18m/nyr4P7ybfn42cH/TuARuf7m53vOx/0zi7w0HWe9j06vPXwQ+jVXrrpPz64JLhU8dFB1/1axrH2/55X0iOvJQU9571UeXo8mcD8LR8cEdw92CM4T+PJ0Oi9xrwz/wPF5fn3+8E/KD/ioUf7UPHIB77jwUOHPEtx7Y98rWff6C0Xz3Varjq4zhOTZ4d88PngPsGNaX9Hdb4luq8zX+Y+D04KPqL8JkqP9lvEIx/4jgcPHfIsxbWfScrXcanDqkJc1+kj1cHztkn5Y6vw9w5+ObhA87Zbo/MK42zw0+AI5YM+OrTfKh7x4TsOPOc1QvGth5+VBT37/VS+XK+x+WNZ8ID0OyTYvX1r/o3RGRXcENwy1/0C5YMucdwfHu3EQ+fGgh55bijoOf8FyrPl/Z780r6hUK8x3J/hfzHYh+eE6nVDdF4NrmG+E5yuvNFHh/YbxCM+fMdpmW8pr+mKbz38rCno2e/H8hVoqdv05Pem7huuz77BLwU38TxK/zsj+Hvdj1zXm4Jrg5OVL3HJw3rwaCc+OneKt0B+nFez97V9T5YfeK7TWvl3XPibFNfPpfGJ2yUf7BU8mvsr7R/quXRz9OYGV/D95/6ZmH8/q/zGS4/2m8UjH/iOBw8d8izFtZ+JytdxqcOKQlzXCZ2WOoTHdTyV9VH4+waPD+4W7BlcpPvgjgguDK4OdkncZfn34uCTypf46NJ+h3jkB99x4aFDvqW49ru6oGf/6C0Tz/Vapjq07KOqvrQT3/fBtPhZHNw+DccGDw92bN+632+j91Tw7eC25BN8Qz6IQ1zrwKOduOj8VjzyRdf5wLOPlrxUP/vHj/VcH3S2LcyDxyW/bvmgf/AEnpvBzRpvbkrcecGO0e8anJHPX1Z+xEOP9pvEIx/4jgcPHfIsxbUfeF1VZ9cBX47rOqED3+PNG8nz7eAu6feF4JF63vP8p/99EXwouCj4SbB94pbmCcQlD+vBo5346NwnHvmj67yance4Hp8U9FwvfH8inuu5RnXyPPLl/LFN+P2Ch/J81zrlN9Gbybw0um2CY5UP+ujQ/hvxiA/fceA5L/Ta6Hq2H/K0nv2is3Vh3Jis9SfrweP0vOb5Tb/bEtfr0M56Xvv5ThziWgce7cRF5zbxatfFtfMP+14hX44Ln3bius5zk9+W+eCg4IF8T8H5Gp+HR29qcHNwY3Ba8GH5IB56tA8Xj3zgOx48dMizFNf+Nhf07HuafMFznTaqDq7zFO1bsY90svLaUtfz7dHzvtQOHf9vH1P0vdJ+u3jkA9/xmq1f7X6a/ePHPNdns/x7nJ2ZP7qGv19wAOO0xtl7ovdmcF1wu+iPUT7oo0P7PeIRH77jwHNeYxTfevhZV9CzX3Tgu16v5I/O4Q8Ksq/EPhT830VnTnCn6Jf2p9BHh/bfiUd8+I7T7L6Y/ZCn9ex3pXz5Pn6d9xDBPdOwv9Y9rIPod290xwX/FVwfLK2TiENc68Cjnbjo3Cse+aLrfJpdx+EbXfsyz3rwXOcZGtcZZwfqPuiqOt+l5w/jcnfdB75viENc68CbofEFnbvEq32O1N7X9r1OvhwXPu3EDbSMA+/HzxLtQ7Lfd1bwmOC2Wjc8HsE/Br3f2CvxOwVnyxdxycN68GgnPjqPi0f+6DqvZvdJ7Zu4nVR31wvf1nM90elVGG9ei49O+eCw4OAg70s66D64O3FnBbeKfo9g6f0L8dCj/W7xyAe+4zX73sf+yNd69o1eD30frhPt6Po+eE/7W+w3/UfwHOYpwb9rv+6xCHofq0/i9Q7Oz+fPyRfx0aX9MfHID77jwkOHfEtx7Qteb9Wzdr/OdcO/ea4r8foU7oOFWt+yjjwjyHsd3hfR74nE9fq1p977+H0S8dCj/QnxyAe+4zX7Hqt23W3f6PUUz3WivU2hzrP1XoT3D2cGOW+wXnUelvh+n7Fr4pTOLxAPPdqHiUc+8B0PXu25idr3MPa9RL6s18Lv2FrP8+5Z+ePE8E8Kcs6A8wvw749Ot+huHyyda0AfHdrvL/DIA33zas9T2Ec75QvPvmnvVhiflybP1Vp/sc45W88Pnif0HxnB0UGvs3bT88HPHeKSh/X+C+PMDnd4nJ2ce9RQZZnFP28gH6KC3ARUBJfKPUFTExW0JpWLIKLimgY1hZrCELSpZrxUVt51zZSmlmtQVETTZd4IarQUJsVLjiikpqB4w0sqghiC84f7x1r86l3nHP1nf3xnn+d59j7nvZ73c+uWT/97bZtPsX2bT/Hpdp/incGtw/swvLXBB3L99+L9U5vN+VzfsXVzHvkODG7Vunl+eMcWeLsqHnnhc52824aXyy1LU992+cXI4MTgwcFt22x+36zU90ywfeLvFmwb/D/pIB/xuD5LPOqB73zwtlWdpbzoIq7rhmfdbaXL8eBzvX3B52ejozW/+ELw+ODng13k8+zUuQQ9ib97sCX4ovSSh7x/F0d1kZc4s8WjXuK6HnjWQbwW8ay7RbqcF/6m6wWf34mej4OH5sLRwXHBbvJ5XupcFGyX+DsHewRXyBfykNdx4HGdvMSZJx71Etf1wLMO4vUQz/rR43j2p4f02+dVem94jocEe7fZ/Ab+eXfq9HuxbXBVfv+IfCEfgbh+t3jUA9/54LWozlLeuu9zb+m3Hsczr6Xg8/Lo2Cm/OC54SnAAz0s+z03evwR7J/7A4Jr8fqXqIw95HQce18lLnLniUS9xXQ+8HtJTqs+64Q1UPPvE9d4Fn99Q+6QdfDU4IthRPt+V/G5/g4Ktweekg3zE4/pd4lEPfOeD11F1lvLW7Tesu1W6HA/+IPWXNJe2wfXR0Tn8/YOnBk8Ojgl2YBzO/QsT8IXgFskzODgg2C24VPrJT1yuLxSP+uA7L7wOqreUF53Edf3m4Qe6zLNP3aQfnn0dIL/wle51m/C+FDwteFiwU5vN+U9Exw6JNyS4XfB5+bCN4nD9CfHID9954HVSfaW81rGd6nVe+FzfodBv7Jgfugb7BEcFRwf7qt94NvUtD74V7JI8XYNvSwd5yOs48HZUXuI8K14f1e964I2SPtcLr690l3TYH3hdCu9lr/COCZ4QnBTcRe/lq8nXK/H6BPsGX1M9vRSH66+KR374zgPPdRGvr3jWQ52OZ719pcvv5W754aDgEcEzgv8aPFLv5Rupc5vE7RQ8KLhvcCfVRx7yOg683ZSXOG/I5yNUv+sxD10HFXjWu5P0wLM/+0q/38tdw/ticEpwZvAkvZevR+f2ife54MHB/qqHuOTx/fB2VT7ivC5fXSf5Hc/191ed8Kz3YOnye7l3fugXHBv8dvDM4OF6L9+PjtXB7ok/InhIsIPqIw95HQfe3spLnPfFG6v6XQ886+igOh0P/ehxPPtziPR7njWQ5xucHJwa/LreA94L7l+bgH8L9kuefYLD9Jz9/pCXOhwP3kDlJ85a8SZLj+tq+n5PlS/WB88+fU76HQ/+MMVzO9hT7wfP9ftB1uOs37nv3ej2+zRK63Wv7/dUPK6/K5+pB77zNd1XqNsOrHtn6XI8+KMUzz7vlR+YJzMP/jf69+A+8vm96Pa8emRwY66vl397KR7X3xOPeuA7H7x9VGcpr/XAG6l4ddcN9mmkePa5f34YFBwe/G7w3OAE+fxB6vww2CbxDw8eGdxF9ZGHvI4Dr7/yEucD8YarftdjHrqo0zzr3UV64NmfI6Xf8w32PdjXuED1UB98748co/iue4DicH2N/Kq7D1PXL+uhTsez3sOly+PgYLUL3udLgucFmV8zH+f+dQno9nRc8CjNwz1vJy91OB68wcpPnHWfsZ3XXVdYfx/pcl58Q7/z2tej5JefD/uh7O9/LTgj+I3gicx79Hz83WBocHhwv+Ae9Fuqt2o/tun3ikNUbymv9Q4txDtRPlgPPPs2XH7A+0aBt1/h+QwjL/1TkPXR5cGf8Lz1fDYk4CdBr7smBccHO6te8lKH48EbpvzE2SDekdLjupquF627s/Q4Hn5NKsSzn+Plk8fdIcrPfT8K8h2Y78Lc91G7f1zv0UF/N6a+IYrH9Y/kM/XAdz54Vd+rm/ps3btKl+PBP1rx7PNQnkfw0uBlQfZZ95fPH0d3z8Q9PnhCsLS/O1TxuP6xfKYe+M7XdF/Z+qjX8ax7sHTBs08nyAf7fEB+4Ps739d/rrrGy+ctmS8F+R5/ivJZB3nI6zjwDlBe4mwpXt3zA3V9tv5TCvHsz/HSb585R0I7oR1cGWS/Yax89rkT2s1JwdL+xYGKx/WtxKt7zqXuvknd/sC6R0iX48E/SfE8fnJ+ge8OfFf4TvAHmgcwL+B+zjn4e8VhwdEa5z1/IC91OB68qnMWTb+j1J3f2I/DCvHs01Dph2dfR8svPx++Q/xzcFrwrODpwa8QT8+H7xV7BvcPHho8ILhXsKvqrfoOAo/64DsvvNGqt5R3mnS7fnhfkQ/WA+8s+Wcf4J0uf+2T+ynOWbGOZp18jfpF+knu4xyW192nql90P0oe8joOvKrzX033Aer289Z/aiGe/TlF+u0z3/n5jn+D2iXt9Evy2ecCpqm9+fsp9Y1QPK63ilf3HELVd1vHQ9+0Qry6/ZJ9Okw+4JO/47E+/nGQ73qbvsvmftbP44L+flj1XbDpOt/19FV+x4M/TvGs+8v5xVVBvrMdId0dc//JwdL3vC/rfq53LPDIe3KBV/c7ous/SPV6fGE/m/3qucHZeg95LzlPw/3eBz8zeLreM5+7od6qc31N99+rzvs0bWf25cxCXvs1TT7As7+ny7fApv6Pc2J85+Q75sXBm4I3B9l/ZL+SOJwr83fSicHpwTOCpf1N6qAux4VXdb4NXt3vuHX3X+3PxEI8+3WM9MOzz9PlGzw/hzPkq8czvl+wPmX9eWuQfUr2NbnP56NYr54VLO17Vp2zqvtdpen6uu6+rHUfJ13OC/8s5bXPfPecHjwneH7wh8Gvy2e+i34heERwbHBMsOl3VnjUA9/54Lm+YcoP7xzpdN3wzpcP1mUe+ceIZ59Z97CuuSh4YfB7wRny2eukY4MTgl8MDld9dfeT667LZqjOUt7vSZfrdF58OLaQ1z5NkA/2eYreB57Pz9TvzZTPPhfA85ys/sz9ZN3zBVXnbJq+z3X7ceufXIhnfyZKv31mP5x5FPOqx4J/Cl4un30ei3nXVcFrgpNUX9W5rqb79FXzQHjWMUl1Oh76ryrEsz/XSL/nn5yXYF+LfasVwbeCzJuYj3G/zxmx33Vr8F7Nrzxvqzq3BK/qXEfTfbq680r7cWshnn06U/rh2dd75ZfbAe2L8eCnwauD16r9cZ/Hi38JfjV4mtpf3XGnabv/qep3PfCulj7XC896J0uP48E/TfHsM/0/48EvgzcGfxG8Qj57vJga/FZwSvDEhuMOvAuVlzgTCvGoe2oh3hXSU6rPuqdIFzz79C354P6GfRDWaazD7lb96GEc537vn7B+O0d5S+N91f5v3X2bzzrPqHo+9mGq9DVd79rfc8RzO2DfhnUA8/wXgs8G2dfhPp/jYF0wJzg72HSfCF7VuRF4rm+c8jdd71j/nALP/syW/sAmvzmfxPqXdeujwXeDf2UcD7K/Sxyfb2Lde2VwfvA3wdJ+cNU5M3hV56ua7kPX3QewD6dKl+Ph45WFePZ5vnyD5+fwG/nq9sP8lvGJ8ecWxvsg+xXc5/kw49XM4HnB6aqval7ddB+l7vhqXTMLPOudLj3w7M950m+fGd+vD94Z/LXqulY+M/5/M/jvwf9QvqbzCXjUA9/5mvp3p3S6bnjWPVO6zCOu9Xu85ns252FYp7C+eCfIvJb58qb97cT1OSXWJfOCpXl11TkceFXf3eFVnZ9qui6ruz6wX/MK8eznrfLJ7YD5GfOvu4L3BG8LXqd24Pnc2cFzg98Ofk31/ULxuD5FvLrzx+tUZymv9Z1diHeb9FsPPPt0rnzweM25Gb6XMg4xLryteQLzBvZLieNzNx7H7tO8wfur1F91bgpe1bmfuvu68Kq+Gzcdr+vOr+zvHPnlvPDvU163H+bZzI9/H3xA7xvvH/d5Xn5x8CK9R03XS03XAXXbhfWcrXqdFx8uLuS1TxfJB/s8Jz/cHpwfvD+4IHiHfJ6ReN8Jnh+8MPij4HdVH3nI6zjw5igvcWaIN1/1ux54d0hPqb775YN1wVsgn6zbPjP+M87fF/xD8H+C98pnzxe+H7wkeEFh3lZ33lE1v4N3n+p3PfCs4zzV6Xjov6QQz/5cIP32mXbE+78w+BDju8YX7nO7uzx4WfAHGofrjldN2/k9qrOUd550uU7nxYfLC3nt02XywT7fpOfM8/lzkHXczfLZ6wye5w3B0rqw6jty03VN3fVo3ffZ+m8o8OzPldJvn2knvwsuDj4efELtiPtoRz8J/iz48+DVakfURx7yOk7T9rtY9bseeNZxieqE97h8sC7ziHu1eF5PsR/HdwK+A3wU/IvGX8Zj7vd5Fb4fPBi8WeOwx+2q8y919w2bfveoO6+w/oulC579ulk+uD74D6o+twP6JfqppcFlwSeDi9QO3I/NCl4fvDZ4RcP+EN5DykucywrxqHtWId4i6SnV96T0Ww88+3S9fLDPD+aHPwafCj4fXBJ8WD5fmnj/Gfxl8KbgdcH/Un3kIa/jwHtQeYlzqXhPqX7XA+9h6SnV97x8sC54S+STdbu/YVymPdFeXg6+pPeF94f7Pa+nnf0qeJues9+zqnVC0/lD3f6hbjuw/lnS5bz49qtCXvt6m/xyO2A8ZzxiHFkefE7jPfd5fsq4Mzd4o8b7uvPcpvOMuuOpdc0t8Kz3BumBZ39ulH63A8bpZ4Krgm8GV6o+6uV+xvP/Dt4dvCd4u/KX5hOleQI86oPvvE39XCXdrh+e9c+VLnj263b5YB757dff/X+G1W7eC74ffEXjC/e5vS4I/jZ4h8bhuuNV0/5hmeos5bW+BYV4r0i/9cCzT7+VD/b5OT1nnuPfIOSFXy+f3b547g8FHwkubNhO4a0v8Byv7ntqXQ8VeNb9iHTBsy8Lpds+v5gfXguuCW4Mrg2+Lp9vSbw7g/cH/xh8IPhr1Uce8joOvBeVlzi3iLdG9bseeK9LT6m+jfLBuuCtlU/W7f/fCc+Z/mZdcIOeP3z3V38I/m/hudft7+q+b+tUr+uAt0F6XKd5zgvP4yD7AYyvtDfe6+1D/Dj/5vs393sfwe10WXBRcL7qrdqXgFf3+3zd+ULd/sW650uP4+HXskI8+7lIPvl9ZnxgHrlFCFsF39P77Pnl4uBjGgfqzk+bjlOuc3EhnutfoDrhWe9j0uV+lnk56/stc1/XtpvHo07u837Bo8HlymcdddcBdfcn6vpnfY8W4ln3YumCZ5+Wywf3Gzw3nkv7YGtw6+BqzUe438/76eCS4OPB32n+Vnee0/Q9Iw71lvJyHX2u03nx5elC3q3lm/XDs79L5JvbAf0/40G73LddsAMPou3m93m8eCr4THCp5jl1x52m8yvqJa7rgWcdj6hOeOgmrnWZR9yl4tnnT+hHwu8Y7BLsFGwrnx9OvD8Fnwu+GHw++KTqIw95HQce18lLnIfFo17iuh54baWnVF8X+WBd8DrJJ+u2z+v13vB8dg72CG4vnz0v53m+HFyp8bfu/L7puF/3PbWulws8610mPfDsz0rp93yD/oX+pluwe3CHtpvz3Q+tCL4U/HPDfgxeq/IRZ0khHnWuKMTbQTpK9VnvS9LlcZBxlXGzT7Cv6qJOxgXu93j8VvBt5S2NH1Xzo6bzgLrjVl3f7ctbhbz2a4V8gGd/35Zvfp/5+6dRuTA6yLk6zuHB5++iurRP/cHS+byqv68yjzqIb17dc4HWAY964Vk318nvfpbxr3OwZ3B39Uv0U9zH+PhC8JXgm+pvSuNyabyFRz3wna9p/9lTOl03POt+WbocD/6biuf3kr8L4Pwa59PGh3iM+ln//QDn2HrmOfbSc6/6+wN45DHP8eqet7MO6jPPOskH3/0s/QLtvl+wf3AP4qj/5n73J6uDHwTfCb6q8avuuNC0H+uuekt5e0mf63RefFldyLuHfLN+ePb3A/nmfmNnvf8DgoODgzQP4T63pzXBdcEPP+O8pmn7dX0rld/x0LemEM/610kfPPvzofTbZ/qZ3sG9gwNV1+7ymX5oVfD94FrVZR3kIa/j1O3/4O2t+l1PU58HygfrMs/x4AU2+b1C4zDj5+Tg2CD7JuzHcL6EOD6vz/jbL9g96P2bpn8nDK/q7wXgVZ2DaTrPqLsfZd/gdVc8+91P8zF4fh7d5a/HEfZdWTfR3ml/hwYnBMdoHPF+rfuJdsm7S7Cb6q3a/4U3psBzvLrrwLr9m31Aj3n2Cb3tVJ997CZ//HyYP3Bun313fCDOScHxej4+3+/9evL313yDeqv+XqDpPKfq7wWbfmeo+17Yp/6F99E+9pQ/fj60W9rbxODI4L5B1mld9Xzc3ncLtg9uzPXSOrFqn7ppP1N3fWpd8DaKZx+43l557Vt7+WEd8LmODnxlndBH+T+veSHzRPjW0ZK4pflj1fq7qV91563WsVr1Oi/8Fr1Xnk/1E/+A4P7BIZoHc5/zbxncIviR1hd159VN9fZXnaW8Q6TLdTovPqDLee3TFvLBPjPvYh42LLhf8EDNl7nP87QNwU+CWyXfOumomu81nacPU/2uB551rFOd8PaTD9ZlHnGJA8/98yDl574RwaOC4zTOc7/XH+RrDXYO9iiM91XrmabzjLp+Wif1mmfd7aQHnv1C9/8DflemvHicnZx3sFXVGcVBQLhwH0elNxURQbGAPLrAo0jvxYZYGEcQhIB0RIoIQqISRWOJGo2KaGyJRmM0mtixV5DeRQSpooAKmQlrvZnzy91zz73558t7e33rW2udvfe53DdJ+Qol/vef0kdLiR5lj9Z+qjW1XllVvy5RRrWZftFOtaNqkWqrsnH8kfJHaznxpVXLq5aCnmbg8bp5jPN84znHuFbQF5rbEb6olzjypQN5tdAvWqp2Vu2k2hp5lRTPMaoVVQusA3rM6znsN64F5pmnZIDPOisG+FrDR0gf/RbAV0o451ao/9JB9QLVvtBlnc3LxvsPax9UEP/JqjUw135KQG8heL1+GPvP+oznXOOaQ29obtLcmYv9cS7zqogcjGO+NZBbOeGOVfW+9znorjpI9UJV3yvu4zmppHqial3cO0nPm3FFmGue8sB1h37qMW4Q/FGvcfRbGX7IZ3xd8PEcdMLz8/MZqnqZ6kWq5+Mc8Jz5uTZQbah6imqU47k1rhPmm6cAuKT78Xz4CuljHg0CfBchL/o2jrk2RF48Bz3wPK9UHak6Au9V93F/NFItVD1XtSb0ZXtP57ofqa8m5pPP/hoF+Oi/EP6MYz7nwj9z9rm5VPVq1eHQdSFy9rk6TbWxahPoyvWcGmc9xnNervldDZ/UbRx9N4Iv4sxL/8y5PfaDn8/vVW9XHYicU9ivfp6XqA5RrQN9nuO55DGuPeaaJwVc0v1MH3Wgk3z2f0mAj/kMgX/fx/5853vL791rVK/FfWY838dNVZvjfkr6Ps/1/qTOpgE+6m8AncbRb3P44r70553BqnNV50GXdbrPn4dOUu2r2g/z6MNzPJc8uX4Oo+6+Ab6kOdN3U/gyjjn1Qw7MeSiey2jVMarD8H50H59zC9WWqmfi80bS922u++oy6AzNHQZf1Mm5zqFFYC5zaokcmLPvbd/P41UnqE7Ee9R9vOfbqrZTbY/3aK7vi6Tv7/HQTz3G0UchdBo3ATnQF3HmbQ8ccx4l3FjVqaqzVKepjkPOzcTXWrWTag/VzqptoM9zPJc8xo3CXPM0A24q9FOPcePgJ6RvFnKgL+OmISf6Zs4j8Jz9fG5QXYj3qPv4uc/Ps6vqxXiPJv38mOv7O+k+pa+uARx9XwxfxjGXS+CbOfuemaQ6W3WG6mTcQ+7zPVSk2lO1m2oH3NdJ7zXirKsogBsDnaG59NcT+o2bDP/0Yxxz6oYcmLPfq35v3gx/9nstcuZ7uD90h94f2T4P5frez/W9le250X8R/FGf8f2hjzn7Pp+uOlP1RpyzicjZ930X1e6qvXDOQu+Z0PvDOOsxnvNyvQ9mwid1G0ffXeGLfMb3Ap9+Xfzvh576xRzVm1Qvd3/ZOL6KeHqr9lE9XbUq9PQEj9erBHDW0TuA6wV9obn0cTr0GkfffeCPefUBr/v8eXow8qqOfMwf+pzfBzxerw6c5xvPObn++yJpXvTbFzifQ97XPvfzcX8Xfz+Le2MA7uFc7/+k9xL19MR88hk/AHz0PQO87lsA33zfmHdgju8pzvUc83TL0zd1DwSOvn3/3Kp6G+6j4u/Z1X+h6kV53mfGeZ7x5DeOenphPvmMvwh89H0DeN23EL55b5v3YsxPet8n/TyZ1Dd1Xwwc78Pe3heq/p7rDtUrcB9WEw+/V7tU9QzoMa/nsN+43phnnmrAJf1+j/rPgE7yGX8p+JhXf/C6b5HvFeRVC7mYf6hqbejpDx6v1wLO843nHOOoqzbm55oX/Q4FjnnV1h++Oqm+HR2tC/TzoFQc/036aC0okG7VgaonFsT1kN995in+3j8Vx3udfNTp+QXAUf+J0Gkc/Q6EL95DzYV7K4r3W5dxJdTfH3zU6T7zlgjgONe8JfLMh/oHQC/3SRnx9lV9M4rzNMc++UTPr4b4DmTRXdyfivcTRx3m/wT7KWle9GOdNYCj3/7wxbyqireH6htRnKcv8lov/ZXF2xf81EN+961HDp5j/Ho8F+Oo0/Mr55kX/R4AL89VF/H+J4r398C5Ok79fZATdbrPvMcFcJxr3uPyzIf6+0Iv90kk3iLVf0dxni7YJyv0/MqLrzf4qZv87luBfeI5xnudfNTp+eXzzJV++8AX86oo3naqr0dxniLk9bV8lBNfL/BTN/nd9zXy8hzjv8ZzMY46Pb9cnrnSb2/4Yl41xHuO6mtRnKcd8tokHwdVe4Kfusnvvk3Iy3OM34TnYhx1ev7BAF+2XOnXOPPyHqov3n9F8X7rMm6X9PQAH3W6z7y70plxnGveXQG+bPlQv3E9AvvkRPH2VH01ivPUxz75VvOqiK87+Knb/Z7jfuKow/zf5pkX/VhnFewT+u0BX8yrvHj7q74SxXl6Iq8vpauW+HZn0eN+z/kynRlHHeb/Ms8c6Mc6awFHv93hi+eqmXj/GcX7++NcHZHuruDjfPeZ90g6M45zzXsE+ST1Tf27wRvy3U/15SjOQ981xdOlIM4b4qsZ8JPUN3V5bk3Mpe6u0MnzUUG8fVT/EcV5+uF8fCVd1cV3Pviph/zu+yrgz/ivkHPx98LQ6fnVgUuaF/12gS/uk8bifSmK9/fB+Tgk/Z3BR53uM++hdGYc55r3EHJMmg/1G9c5sE+qi3eA6otRnKcx9slG6aotvk7gp273e477iaMO828M8GXLi36sszbyot/O8MW8jhVvG9W/R3GeAcjrU+kqI76O4Kce8rvv04A/4z/FczGOOj2/DHBJ86LfTvDFc+X5XVVfiOI8xd8rqP941Q7gDfEdH/CT1Dd1dYAO46i7I3Ryn/yqP/ieIP7nozhPV+yTd/X8Vqt+m0UP+d33LvaJ5xjvdfJRp+evDvBly4t+jTMv8/pF+s4X/9+iOM8JyOsd6YrE2x781O1+z3knnRlHHeZ/J8CXLS/6sc4IedHvt8DzXNUU71+jeL/nGbdZetohJ853n3k3pzPjONe8m/P0Tf3GtQucq0ribaH6XBTnqYl9ska6SoqvLfip2/2e437iqMP8awJ82fKiH+ssibzotx18Ma8j2s/nif/ZKM7TAnktla5jxXce+KnH/Z6zNJ0ZRx3mX5pnDvRjnccCR79t4Yt5VRbvKarPRJiDvNZK/w5/75JFD/ndtxY5eI7xa/FcjKNOz98R4MuW1//5LYjzMq/54n1I9S3V36q+E8Xxh/TzSPHepjpI9Wd8LpgPHq8fCuCsw/zEUZf5BiEH+hgEvcbRt9dHBvbXzcLdo/oedM/H/joofcPEdwf46e9m8Hj9YABnHeYnLmmu9GOdw5AX/Y6EL77n5gn3CPRaf/G/C9U/GnPpZx76vd6vIDPOc81LXNIcqX8Y9HKfzBXudtUl0DMP++QnzRsivuvAT91zweP1n6LMOOsYEsAlzYt+rHMIcPQ7Gr6Y103C3a36EXTPRV4/Sv+V4rsbOujvJvB4/ccAzjrMT1zSXOljCPQaR99evzKQ1xzh3lZdDt03Ia/90rVQfA+Dn/7mgMfr+wM46zA/cUlzpY8rodc4+vb6wkBeNwp3i9/H0D0Hef0gXReIbwn46e9G8Hj9hwDOOsxPXNJc6cc6L0Be9LsQvnhvz/bnf+i1fuP2Sc90zKWf2ej3+r4AznPNS1zSHKnfuOkB37OEexo6ZsP3Xs2ZDD7qnIV+r+8N4DzXvMQlzYf6jZscOB8zhVuk+gb0WJ/xezRvqPhuAT91zwSP1/cEcNZhfuKS5kU/1jkU54N+J8MX85rhe0f1c+ieiby6iWeU6v3QQX8zwOP1bgWZcdZhfuKS5kofQ6HXOPq+H/6Y1w3CPaW6DLpnIK9d0jVJfA+Bn/5uAI/Xd0WZcdYxKYBLmit9jIJe4+jb657Pe2i6cM9Br/Ubt1O6p4GPfqaj3+s7AzjPNS9xSXOkfuOmBfbJ9cL9TvVj6JmOffK95g0W3z3gp+7rweP17wM46zA/cUnzoh/rHIx9Qr/T4It5TRPuQdVPoft65LVDukaI7z7ooL9p4PH6jgDOOsxPXNJc6WMw9BpH314fEThXU4VbDL3Wb9x26RkLPvqZin6vbw/gPNe8xCXNkfqNGxvYJ1OEe1J1M/RMxT75TvMmiO8Z8FP3FPB4/bsAzjrMT1zSvOhjLPQaR99enxDIa7JwL6quge4pyGubdM0S3+Pgp7/J4PH6tgDOOsxPXNJc6WMC9BpH316fFThXk4T7C/Rav3FF6p8IPvqZhH6vFxVkxnmueYlLmiP1z4Je7pOJ3k+qW6BnEvbJVs2bIr5nwU/dE8Hj9a1RZpx1TAngkuZFHxOht/j7Zvj2+pTAPpkg3F3QOxH75Bvpvhx89DMB/V7/JoDzXPMSlzRH6jfu8sA+GS/cA6qboGcC9skWzRsuvqfBT93jweP1LQGcdZifuKR50cfl0GscfXt9eCCv64R7SfVN6B6PvDZL12zx3Qp++rsOPF7fHMBZh/mJS5orfQyHXuPo2+ueb9+lVMdBp3V7vQ36qX8c+rzepiAzjnkQly033gv+O9jZfo/7fEGn/y52QLW1eDcBR74D+Dsfcdn+/kZdnnsAOOo2n/H0/bTWX1Pfq6rO2biNws0Xz7xA7uZzv9c3Rplxnjs/gKOeNphvHPV7fX5gv7YX7jfI1esp9bVSbY157DcuBZx5jU8F+KijNebz///hZ/3iVL8noziP552Zive9rf2yU3V9lFn/j9hXnOf+t4HzPOO9Tj7qto6dwNGHeZhz0ufBnIzzfP26+H3wlH5/nxZeUV2vulLV+8/72v0b9PNVmjNX9UnVR7FPuf8937xe3xDAWafnEZf03NGvdV+FPJnHk/BpHHOaD//GMddHkRfP8ZhUXK9z8HpL6GYeY9Dn9ZYFmXHMl7hsz4H3r89BI9XROBfF37Np3+9XbYF9G+Lbj/Oe6/mjLs/dDxx1m894+n5S66+r72VV52zcOuEWiGdOIHfzud/r66LMOM9dEMBRT0vMN476vb4gsF9PF+5a5Or1fcq1ufpbYB77jdsXeB7G78O+MY46vG5efi6uJ1wT1VHQ43nF349q7s+qzcBP3e73HPcTRx3m/z7Aly0v+rHOn4GjX/M1C+RV2vtHdSTmNEFeH2veMeIrBD/1uN9zPk5nxlGH+T/OMwf6sc5jsL/otxl8Ma+0cN1Vr8GclshrmXRVEl9T8FOP+z1nWTozjjrMvyzPHOjHOisBR7+F8MV79DThRoDf84zbLd3ngo/z3Wfe3enMOM41727kk9Q39RtnXn7+PaxfnCz8cOixPn+gc9970vedahPMsf734cM8nvteAEddnvMecNRnHuZHf9b7XYAv2/NgTsY1Cewvz2+tejX0GFesRzyNwRviKx3wk9Q3dXluacyl7ibQyXuonPeh6p+jOI/nGv+5dP2qeg74qYf87vs84M/4z5GzcdTp+b/mmRf9GndOIK+SwnVWfTiK8zRFXh9IV0XxnQ1+6na/53yQzoyjDvN/EODLlhf9WGdF5EW/58AXz1VD8T4Uxfs741ztlZ6zwMf57jPv3nRmHOead2+evqnfuLOy+G6l+qcozkPfpcRzJnhDfKUCfpL6pi7PLYW51H0WdPJ8lBVvN9UHozhPK5yPz6TrBPE1Aj/1uN9zPktnxlGH+T/LMwf6sc4TgKPfM+GLeRWIt47qA1GcpxvyWi79W1XPAD/1kN99y5GD5xi/HM/FOOr0/K0Bvmx50a9xZwTySom3rur9UZynDvL6Qrq2q54Ofup2v+d8kc6Mow7zfxHgy5YX/VjnduDo13zG8x4yb6HqH6M4j3Gec1i1IXhDfIeRb65+qMtzDwNH3eZrGNgnx4m3l+p9UZynEPtkpeZVFV8D8FOP+z1nZTozjjrMvzLPHOjHOqviOdFvQ/hiXseLt63qvf5+L4rPNX6VdJUV32ngpx7yu29VwJ/xq/BcjKNOzy8LXNK86LcBfDGvKuJtoHpPFOdpi7zWycce1frkhx7yu28d8vIc49fhuRhHnZ6/J8CXLVf6Nc68vIc8v4Pq3VGcxzjrqSCeU8Eb4qsQ8JPUN3V5bgXMpe760Ml9cox4a6n+IYrzdMA++VC6tqjWAz/1kN99Hwb8Gf8hcjaOOj1/S5550a9x9QJ5lRJvb9W7ojhPLeT1kXRVE98p4Kdu93vOR+nMOOow/0cBvmx50Y91VkNe9FsPvphXNfGeq3pnFOfpjbw2SNcvqnXBTz3kd9+GgD/jN+C5GEednv9LnnnRr3Hm5T3k+R1VF0VxHuOsJy2ek8Eb4ksH/CT1TV2em8Zc6q4LndwnJ4l3oOpgzOmIfbJNuuqI7yTwU4/7PWdbOjOOOsy/Lc8c6Mc66wBHvyfDF/N6QrgX1Pe+qv8O5r+vFf/v2/XzDPHdqRr6u5v5zeP1tQGcdZifuKR/76Mf65yBvOh3AXwxryWa86g/h6Ti855AXmv8d0zxPQEd9Gf+J5DTmgDOOsxPXNJc6cc6xyAv+p0BX8zrcd/zwn+Zis9bgrxW6+crxPcgdNCf+Zcgp9UBnHWYn7ikudKPdV6BvOh3DHzx3l6sOc/6fZuK6zdulX6eirn0Y77HkcuqAM5zzUtc0hyp3zjz8u8/j6l/qfCfqK5Oxfms030r9fMi8d6ruhjz6MPzFiOPlQGcdXkOcUnzo5+p0Gscc7CvRcAxp8XIgfvrUem5NRWf8xh0rtDPF2IufZvvMeS3IoDzXPMSlzRv6jfOvLyHHlH/8/73tup/ARtONDh4nJ2ca7BW5XmGMSQc1LAB007+aJIWkM7U1ESxJtImGjSxiUbHVk4bMCAolDExzLRVREajIihpRNNRDuoE8JA6owgoJ9FUFBHDSYMCWm1m2tpUAUnHqLRDZ8p9rZl94du1WP65x1nX89z387K+j29/z2bN7d3t//5b0nFYP3VYur2W/5/x6cO6NDo8uivXPxl+cUfXPlx/rcAt7921vzn6zO39//vu6f3xHHnh8IPnOv49wzH/xHCrov/Su2ufxTqvV/P/N6bfP6q/55uoPlx/tcCRg/7mmp6r55ihvHCem+s3Fs7r8nAro88rN3PA70yumen3E/X3fJerD9d3Fjhy0N9c03P1POScqfPyvDdqrh7duvITwv1YeckP96vkGSFfzzNB9Vz/VYHDl77mmp6j88ONKNwn48PNi76mPBN0n7wSv870+5n6O/d49eH6KwWOHPQ31/S8PMcI5YXz3FzvLJzX98I9HN2o3ON1Xi8n17T0u0P9Pd/31IfrLxc4ctDfXNNz9Rydygvnubk+rfC6uozXqfKSH25H8oxSP89zmeq5vqPA4Utfc03P0fnhRhXuk3HhFpBTeS7TfbI9fhPTb5H6O/c49eH69gJHDvqba3penmOU8sJ5bq5PLJzX2HDLeJ9Q7nE6r23JNT397lN/zzdWfbi+rcCRg/7mmp6r55iovHCem+vTC6+rMeGeUV7yw21Nnjnq53nGqJ7rWwscvvQ11/QcnR9uTuE+6Qy3MPoL5Rmj+2RL/Cal323q79yd6sP1LQWOHPQ31/S8PA85J+k+8bxzNJfPa3S4e6K7lLtT5/XL5JqQfouVw/ONVh+u/7LAkYP+5pqeq+ch5wSdl+edpLl8XqPCPRh9SblH67xeSq6r0+8flMPzjVIfrr9U4MhBf3NNz9VzTFBeOM/Ndfz9PjSSz03KS364zckzVf08z0jVc31zgcOXvuaanqPzw00t3Ccjwt0V3aw8I3WfvBi/sen3U/V37hHqw/UXCxw56G+u6Xl5HnKO1X3ieadqLp/X8HBLo79W7hE6r03J9f30e0Q5PN9w9eH6pgJHDvqba3qunmOs8sJ5bq5/v/C6ujTc3ysv+eFeSJ6R6ud5LlU9118ocPjS11zTc3R+uJGF+2Re6u/jc5ryXKr7ZGP4yem3QP2dm/704frGAkcO+ptrel6eY6TywnluruPP3N2jV4dbF32qd9frZ6Xulugs+VFHH/NwTxW4Uj/4WerXK1wP8euju6PvRveqH3X0uzW6JPpkdFXLfHDrda72g3O+WfKH2605ndsc8z9Z4Hw+qzS/X1fwv3/sYT05OvjYrn3g6fNW9L3ogcJ8dXnMkeOtAudcq+QP53neU244z3tAc/n9F/7M6FdUD0f9J/oc1u59Pj5nnX/Jl77mnId++MM5P9fp6/sE/mvR86LfVB94+vSO9o3203k0zWOOHL0LnHN1lz+c5+mr3HCet5/m8n0C3xkdo3o46gdFTy7krPMv+Q4qcM7TT/7Vz13Kf7Lyem74KdG/Vj0c9adHh7T0L/meXuCc52T5wzn/EOX16wP+h9FroteqDzx9hka/ER3WMo85cgwtcM41RP5wnucbyg3neYdpLt8n8DdHb1E9HPXfjV5UyFnnX/L9boFznmHyh3P+i5TXn2vgZ0fnRu+M3qV+1NHvkujw6Jjo2Jb5zJHrkgLnfBfJH87zDVd+OM8/RvPB+XzGan7fX/D3Ru9TffU+mPoro5ML89T5l3yvLHDOM1b+cM4/WXl9f8E/EH04+nh0ufpRR78fRKdFr4vOaJnPHLl+UOCcb7L84TzfNOWH8/zXaT44n88Mze/7C/6Z6C9UD0f9nOhthXnq/Eu+cwqc88yQf/U9u/Lfpry+v+BfiG6Kbo/uUD/q6Dcvemd0QXRhy3zmyDWvwDnfbfKvvj/RfHcqP5znX6D5qp/zdT4LNb/vL/jXo2+oHo76B6IPFuap8y/5PlDgnGeh/OGc/0Hl9eco+Dejb0f/Q33g6fNw9PHo8pZ5zJHj4QLnXA/KH87zPK7ccJ53uebyfQK/P/qe6qv3jdSvia4t5KzzL/muKXDOs1z+cM6/Vnk9N/zB6H+rHo7656LPt/Qv+T5X4JxnrfzhnP955fXc8D2OO6w9j+taD0f9tuj2lv4l320Fznmelz+c829XXs8N3y/aX/Vw1O+Jvt7Sv+S7p8A5z3b5wzn/68rrueEHRAeqHo76vdF9Lf1LvnsLnPO8Ln8459+nvJ4b/ovRP1F99T1r6j+IftjSv+T7QYFznn3yh3P+D5XXf//BD4meFR2qPvD0ORT9VPYLPTra5TFHjkMFzrno10N7Ds9DzkPq53npU83V7eP5YdFzVQ9HfZ9oh86pqX/Jt0+Bc54e8odz/g7l9X0C/xfRv4peqj7w9PlM9HPRz7fMY44cnylwztUhfzjP8znlhvO8n9dcpX0Y+5gnC/sw9jQ36Hv1q1XH9bPENd2HOccN4vzn/ZT2KPuiz6oPvPdNq6NzC/PV5anebxvutZ5UvpKv55irvPaFXy1fnxc8e5A/in6kPvDeI/02uqFlHrim+yrnWi1/OM+xQXntC/9b+fq84NmXfDX6JfWBP2KvFD3YMg9c0/2Vc9HvoPp5juq6+nlurncvfD6BZ3/yLdd361rPfqW/zqupP1zTvdYReeyvfvD91c/3CTz7k7HRC9UH3nulwdHPtsxTfW+r8y/tr5yrv/zhPMdnlde+8IPl6/OCZ88yNTpBfeC9jzojekrLPHBN917ONVj+cJ7jFOW1L/wZ8vXrCp69y3TVV58nU89e5tyW/nBN92HOc4b83Q/+XPXz96rw7FVmRW+IXqd+1HkvdXH029HzWuaDa7oHc75z5Q/nec5TXjjP/23N53zwFyuf7y949i4/VT2c91rjWvrDNd2bOc/F8nc/+HHq5/chePYq90fvVp/q312kD/uXKdHxLfPANd17Odc4+cN5jvHKa1/4KfL1fQLP/mSF6uG8n7q+pT9c0/2X80yRv/vBX69+vk/g2Y/8U3Sl+sB7r3R7dGbLPHBN91fOdb384TzHTOW1L/zt8vV5wbMHeTm6UX3gvW9aFL2jZR64pnst57pd/nCe4w7ltS/8Ivn6vODZl/xzdKf6wHuv9FD0/pZ54Jrur5xrkfzhPMf9ymtf+Ifk6/OCZ3/ym+i/qg+8908roo+2zAPXdM/lXA/JH85zPKq89oVfIV+/b8Ozdzmg+up9KPXsZda19IdruudynhXydz/4dern+wSevcv/RN9XH3jvozZGn2mZB67p3su51skfznM8o7z2hd8oX58XPPuaXnzPrT7w3mPtiG5qmQeu6b7MuTbKH85zbFJe+8LvkK/PC549zwnR49Wn+nfL6cM+6I3ozpZ54Jru2Zxrh/zhPMdO5bUv/Bvy9XnBsx8aFD1Jfarfe0gf9kj7o2+3zAPXdD/nXG/IH85zvK289oXfL1+/b8OzRzpV9dX3xalnz/RRS3+4pns859kvf/eD/0j9fJ/Asx/6s+iX1Afe+7We0YMt88A13eM5F/0Oqp/n4HpP9fPcPbWX83nBs0c6L/rn6gPvfVvfaC+dX9M8R7vXc66e8ofzHL2U177wfeXr84JnTzY8er76wHsv94XoCS3zwDXd/zlXX/nDeY4TlNe+8F+Qr/eEV7F/ia7RnvDM7A9+FL1J+wbq6GMebk2BK/WDv0n9/D0i/NP8e9fob6L/qX7U0W929K7oiujKlvngnta52g/O+W6SP9yLmtO5zTH/igLn81mp+f26gu+Vz1kDogOP7doHnj47onuj+wrz1eUxR44dBc65Vsq/+vyoefYqN5zn3ae5jvh9nnBDomeoHo76Q9Fu/L19lP4l30MFznn2yb/aOyg/1w8V7hP4odFh0XPVB54+PdK3T7SjT7s85sjRo8A5Vzf5V39Pah5y9hDneTs01xG/1xLukuio6Gj1qX6vJX1OjA6IDizkrstjjhwnFjjn6pA/nOcZoNxwnneg5jriuSThJkWvjE5WH3j6nBr9cvS0Qu66PObIcWqBc66B8ofzPF9WbjjPe5rm8vsQ/N9E/1b1cNR/PXp2IWedf8n36wXOeU6TP5zzn628vk/gZ0ZvjP5IfeDpc370O9ELWuYxR47zC5xznS1/OM/zHeWG87wXaC6fF/yPoz+J3qE+8PQZER0VHV3IXZfHHDlGFDjnukD+cJ5nlHLDed7RmsuvK/gF0YWqr94nUj8xOqmQs86/5DuxwDnPaPnDOf8k5fV9Av+z6KPRx9Snel5t+kyNXhO9tmUec+SYWuCca5L84TzPNcoN53mv1Vw+L/h10aei69UHnj63RGdFby3krstjjhy3FDjnulb+cJ5nlnLDed5bNZdfV/BboltVD0f93dF7Cjnr/Eu+dxc457lV/nDOf4/y+j6BfyW6K7pbfeDpc290cXRJyzzmyHFvgXOue+QP53kWKzec512iuXxe8L+O/lv039UHnj6PRB+LLivkrstjjhyPFDjnWiJ/OM/zmHLDed5lmsuvK/i90X2qr94vU78qurqQs86/5LuqwDnPMvlXPy8r/2rlPeL3osP9V/TD6EfqU/1edPqsjz4b3dAyjzlyrC9wzrVa/nCe51nlhvO8GzSXzwv+GPYQfF/K9986L/psjm6Jbi3krstjjhybC5xzbZA/nOfZotxwnner5vJ5wX+a73n5Hld9qvf59Hk1uiu6u5C7Lo85crxa4Jxrq/zhPM8u5YbzvLs1l9+H4P8g+oeqr95XU/9O9N1Czjr/ku87Bc55dssfzvnfVV7fJ/CDo38cPUV94OlzIPp+9Hct85gjx4EC51zvyh/O87yv3HCe93eay/cJ/JnRr6gejvpPZL/RvePjc9b5l3zpa8556Ndd+xjn5zp9PTf82dFzVA9H/XHR4zV/U/+S73EFznm6yx/O+Y9XXr8+4L8VvST6l+oDT5/+0ROjJ7XMY44c/Quccx0vfzjPc6JyV9+Xat6TNJfvk6u0N1obna/nonrPdnP0cn0PfpXquX6muKZ7vPnKVfJ1/pvVz/fJGu2J3om+pT7w3qc9Ef25zqNpHrime7u1ylfy9Rw/V177wj8hX58XPHueQdHfO7ZrH3jvyfZH32yZB67pPs65npA/nOd4U3ntC79fvkf8Xk449kF/qvrq5xXty47p084fruk+znnod4w45+d6t8LnE3j2OjxX8WvqA+89mZ8jebR54Jru45zrGPmbq3vepefuK1/fJ/DsdTpVX33uTD17n9LzKev84Zru35o+F9P5B6mf54ZnXzNF9dV9n3r2OaXnU9b5wzXdozV9Lqbzn65+fn3As5/5u+gP1Qfee6xzoqXnU9blgWu6L2v6XEzPMVR57Qt/jnx9n8Czn7lJ9dXnrdSzv7mwpT9c0z2a85wjf/eDv1D9/PtI8Oxd5kV57uNs9aPO+6vOqJ8jebT54Jruy5zvQvmbq3vepecfrvmcD75T+Xx/wbPXWaT66v0l9ex9rmjpD9d0r+Y8nfJ3P/gr1M/3Fzz7mWVRnvv4gPpR573X9KifI3m0+arvjfXnUdqzOd8V8jdX97xLzz9N8zkf/HTl8/0Fz57nadXDeT82u6U/XNP9m/NMl7/7wc9WP99f8Ox1tkV57uML6ked92Dzo36O5NHmg2u6d3O+2fI3V/e8S89/p+ZzPvj5yuf7C5590B7Vw3mvtrSlP1zTvZ3zzJe/+8EvVT9/joJnz8NzFd9UH3jvyfwcyaPNA9d0H+dcS+Vvru55l577cfn6PoFnH7Rf9dX7YOrZF5WeT1nnX/18qz+n0j6u6XMxnX+N+nluePY6B1UP5/1Y6fmUdf5wTfdvTZ+L6fzPqZ/nrvZr/DsL/j2J5vaeq/R8yjp/uKZ7tKbPxXT+bernueHZu/RTffU+l3r2MqXnU9b5wzXdhzV9Lqbz71E/zw3PnmWA6qv3tdSzhyk9n7LOH67pnqvpczGdf6/6eW549idfVD2c906l51PW+cM13Ws1fS6m83+gfv77D579yVejQ9QH/oi9kp4j6edT1uWBa7q/avpcTM9RPedS/Tz3J7W34j75X1BPKdx4nJ2dWaxW5RWGTbQyyKm2mjQRpMZSBQcE2sZoUhG4MHVKS2kvWhlFnBgVUXFAkNlZmcRqRQWnVtEqs4oVsIOK2sYqUFITFdMqoqBYcOgF6/mT8+Dq/s7uzZvAs9b7rt3975+zV8/X/fbZ858TD9ijvUP7hJ4Uul9w+x64R9uFNoXud2Bz7kTXH9i8Hg4/ePeH2yuP/dUPvkn9WgX3DfH9Qn8Repr6wNOnY+h3Qw+pmQeun66/feCcq0n+5pjHOe1rDt/992nOz48L+FDow6F3tmrODY1+l4SODT1X9xN19B2acPal71BxznOu/OGcf6zyem74J0OfUj0c9deETqjpn/lek3DOM1b+cM4/QXk9N/wzoc+qHo766aEzavpnvtMTznkmyB/O+Wcor+eGfzH0JdXDUT8ndG5N/8x3TsI5zwz5wzn/XOX13PCbQv+pejjqF4U+UNM/812UcM4zV/5wzv+A8npu+C2h76kejvrHQ5+o6Z/5Pp5wzvOA/OGc/wnl9dzw20I/Uj0c9StCV9b0z3xXJJzzPCF/OOdfqbyeG3536Oeqh6N+bei6mv6Z79qEc56V8odz/nXK67nh92+9R1u1bl4PR/0roa/W9M98X0k451knfzjnf1V5PTf8QaHfUj0c9RtCN9b0z3w3JJzzvCp/OOffqLyeG75D6GGqh6P+3dAtNf0z33cTznk2yh/O+bcor+eG7xzaRfVw1H8cur2mf+b7ccI5zxb5wzn/duX13PA9Qn+gejjqPw/9oqZ/5vt5wjnPdvnDOf8Xyuu54XuF9lZ947ka9Qe026Pt2tXzz3zpa8556Ic/nPPz9/T13PB9Q3+uejjqO4QepvlL/TPfDgnnPO3kD+f8hymv54bvHzpA9Y3PY9QfFdq5pn/me1TCOc9h8odz/s7K67nhh4Wep3o46ruFdq/pn/l2Szjn6Sx/OOfvrryeG35c6GWqh6P+lNBeNf0z31MSznm6yx/O+Xspr+eGnxR6nerhqD8j9Mya/pnvGQnnPL3kD+f8Zyqv38vBTw+dHTpHfeDp0zd0YOigmnnMkaNvwjnXmfJvvC/QPAOVG87zDtJcvk/gF4YuUj0c9aNCRyc5q/wz31EJ5zyD5A/n/KOVN/64MT/84tCnQp8JfVb9qKPf+NAJodNDZ9TMZ45c4xPO+UbLH87zTVD+xntAzT9d8zXeM+n6zND8e70PC25r6Ieqb9zfUb8sdHkyT5V/5rss4ZxnhvzhnH+58vr+gt8Vujv02DZ79Lg2zftRR781oWtDPw3dWTOfOXKtSTjnWy5/OM+3VvnhmNvcpwmH/05xvr/ge4aOVj0c9W2a9uhJTV8/T5U/HH7w7g/nPPQ7KeHo65z+nmNPwXv7R0IXhv6mVXPe+4xLQ0eFDtPPHdR7H2KudG/iXMPkD+c5RimvfeEvla+vFzzv+5eEPqY+8N6DXBt6Rc08jeds4b7FuS6VP5znuEJ57Qt/rXz9uYJnT7Ba9XDem8ys6d/4XivcyzjPtfJ3P/iZ6uf7BJ49wcu8b1UfeO9P5oXeXDMPXOmexrlmyh/Oc9ysvPaFnydfXy949gubQ99QH3jvXR4MvbdmHrjS/Y5zzZM/nOe4V3ntC/+gfP25gmcv8bbq4bynebSmP1zpHsh5HpS/+8E/qn6eG569xMeqh/OeZlVNf7jSPZDzPCp/94NfpX7+fMCzl/gi9FP1gfe+5oXQ1TXzNN5fFu6FnGuV/OE8x2rltS/8C/L19YJnn9E6dN/WzfvAe8/zWuhLNfPAle6TnOsF+cN5jpeU177wr8nXnyt49iDfVj2c90KbavrDle6dnOc1+bsf/Cb1830Czx6kY+gh6gPv/dB7oZtr5oEr3UM51yb5w3mOzcprX/j35OvrBc/+5OjQTuoD773SjtCtNfPAle6vnOs9+cN5jq3Ka1/4HfL15wqevcsPVQ/nPdSXNf3hSvdczrND/u4H/6X6+T6BZ+/SJ/RH6gPvfVRT6Fc188CV7r2ci35fqZ/n4O+b1M9z8/f4+nrBs6/pF/oT9Wn870CjD3udjqEH6/qV5oEr3Zc5V5P84TzHwcprX/iO8vX1gmfPMzD0V+rTeG5FH/ZBXUI71cwDV7pnc66O8m+8H9ccnZTXvvBd5OvnEDz7ofNVD+d9WY+a/nCl+zjn6SJ/94PvoX6+T+DZD10eOkJ94L036x16Qs08cKX7OefqIX84z3GC8toXvrd8fb3g2SNNDr1afeC9bzsr9NSaeeBK93rO1Vv+cJ7jVOW1L/xZ8vXnCp690lzVw3kfN7imP1zpvs95zpK/+8EPVj/fJ/DsjR4Ina8+8N63jQkdWjMPXOlez7kGy7/xvkRzDFVe+8KPka+vFzz7odWhD6kPvPdwM0MvqZmn8d5Q/71l+z7nGiP/xvtTzXGJ8toXfqZ893p/Ghx7o22hb6lP4/1p9GG/tCL0kZp54Er3es41U/5wnuMR5bUv/Ar5xh83nkfw7Ie6hh4Rekib5v2o8z7us9D3QzfXzAdXuv9zvhXyN8dczgl3hK6D53I+c+Tj/tpX/GjV8ffey33WQj+40n1fV+Wxrz9X8+IPFvH+lvezoXfovdiQ+PlmdOiY0ItDz9HPR9TjMyThnIP+Q8Q51znybzz/NM8Y5W483zXvxZrL1wt+Me99Q/+gPvD0GR96VejVSe6qPObIMT7hnOti+cN5nquUu/EeXfNerbn87yH4VaFPqx6O+qmh05KcVf6Z79SEc56r5Q/n/NOU1/cJ/LrQv4T+VX3g6XNL6KzQ2TXzmCPHLQnnXNPkD+d5Zik3nOedrbl8veDXh24I3ag+8PS5I/T+0IVJ7qo85shxR8I512z5w3me+5UbzvMu1Fz+XMG/E/qu6uGofyx0cZKzyj/zfSzhnGeh/OGcf7Hy+j6B/3fo1tAP1QeePk+GLgtdXjOPOXI8mXDOtVj+cJ5nmXLDed7lmsvXC35n6H9Dd6kPPH2eC30+dE2SuyqPOXI8l3DOtVz+cJ7neeWG87xrNJc/V/D78fNE6+b1cNS/HLo+yVnln/m+nHDOs0b+cM6/Xnl9n8C3C/1m6IHqA0+f10PfCH2zZh5z5Hg94ZxrvfzhPM8byg3ned/UXL5e8N8JPTS0va/bPs37vBX6dug7Se6qPObI8VbCOdeb8ofzPG8rN5znfUdz+XMFf2ToUaqHo35b6EdJzir/zHdbwjnPO/KHc/6PlNf3Cfxxod1Cu6sPPH12hu4K3V0zjzly7Ew45/pI/o2fWzXPLuWG87y7NZevF/yPQ3uGnqI+8I0+8fNtm9C27b4+d1Uec+Sgvznnol9b/fzuecjZSpznbau5/LmC/2noz1QPR/2hoe11nUr9M99DE8552sofzvnbK6/vE/hfhv469Gz1gafP4aHfDz2yZh5z5Dg84ZyrvfzhPM/3lRvO8x6puXy94IeEDg09V33g6XNsaNfQ45PcVXnMkePYhHOuI+UP53m6Kjec5z1ec/lzBT829FLVN963Rf3JoT2TnFX+me/JCec8x8sfzvl7Kq/vE/grQq8Nnag+8PTpE3pa6Ok185gjR5+Ec66e8ofzPKcpN5znPV1z+XrBzwy9LfR29YGnT7/Qs0P7J7mr8pgjR7+Ec67T5Q/nec5WbjjP219z+XrBLwi9N/Q+9YGnz0Whw0NHJLmr8pgjx0UJ51z95Q/neYYrN5znHaG5/ByCXxm6SvVw1E8JnZrkrPLPfKcknPOMkD+c809VXt8n8BtD/xP6vvrA02dh6FOhS2rmMUeOhQnnXFPl33hvqHmeUm44z7tEc/l6wX8v9j5dQo9mL6brRZ8PQreH7khyV+UxR44PEs65lsgfjjncb3vC0XeHOO/t4Eeojr+n7oSm/58r84Nz/x3qa46+9vdzgf0Xex3O5Zuv91Tej2XnFFLnvZq50v1b6fmIzn+J+nluePY1T6oeznuu7JzCKn+40j1a6fmIzn+N+nluePYzz6gezvur7JzCKn+40v1Y6fmIzj9d/Tw3PHuWF1UP531Vdk5hlT9c6T6s9HxE55+jfp4bnv3JJtXDee+UnVNY5d/4firca5Wej+j8i9TPc8OzL9miejjvk7JzCqv84Ur3VaXnIzr/4+rnueHZe2xTPZz3R9k5hVX+cKX7qdLzEZ1/hfp5bnj2GbtVD+c9UHZOYZU/XOmeqfR8ROdfq36eG579Befy7dbc3u9k5xRW+cOV7o9Kz0d0/lfUz3PDs4c4SPVw3udk5xRW+cOV7otKz0d0/g3q57nh2S90UD2c9zLZOYVV/nCle5/S8xGd/13189zw7BM6qx7O+5bsnMIqf7jSfU7p+YjO/7H6eW549gI9VA/n/Up2TmGVP1zp/qb0fETn/1z9PDc87/t7qb7xfIl69gHZOYVV/nCle5jS8xGdH45+nhue9/t9VQ/n/Ud2TmGVP1zpfqX0fETn76B+nhue9/T9Vd/43EY97/Gzcwqr/OFK9yml5yM6/1Hq57nhef8+TPVw3ltk5xRW+cOV7kVKz0d0/m7q57nhed8+TvVw3kdk5xRW+cOV7jtKz0d0/lPUz3PD8958kurhvH/Izims8ocr3W+Uno/o/Geon98rwvP+e1bodPWB9z5hQGh2TmFVHrjSvUXp+Yieo6/y2hd+gHx9n8DzPvx+1Tc+n3o/P7KmP1zpnsJ5Bsjf/eBHql/8cWN+eN5/Px3K+X+L1Y867wumhfo8wZbmgyvdTzjfSPmbqzr30PNP0HzOBz9N+Xx/wfPe/APVw3n/sLSmP1zpfsN5psnf/eCXql+8Bm88j+B5j35MaCed/7dLfan3fuGT0K2hPl+wpXnhSvcazrlU/uaqzkHk77kenguuk66fr4PnMMcc3qccrX3KMdqneO/xSQv94Er3Kccoj339PTcr/uBvoX8PXcl70FbN+QHx88JdoXeHTgkdqJ8nqMdnQMI5B/0HiHOugfKH8xxTlBfOc9+t+fx5nK2+/wp9NXRZ6P28b27VvN55Hw69M3RS6MjQQco7J5nfXOl1cs5B8ofzXCOVF87XYZLmcz6u38NJPl/fO3XdfD/PUc7fh94XOlf3s+e9LHRE6GDlmZtcL3Ol19W5BssfznOMUF77wl8mX3+vzlVffn+Q30uEc67s9xTnJfOYK5279PcjnX+0+nluzk3lXNR7ee5obp+rOjz0PPlT5/NYzZWe2+o858nf/eCHq58/H3eJ/13oozz/9Pmw77jQy0PPV567k9zmSudzrvPl737MMy7p53kv11y+Xr9lTxL6Suhzoffoel0Qfa4LnR96feiFykM9PhcknHPQ/wJxznWh/OE8z3zlhvO812suX697xL8e+o/QBbpezndP6ILQi5RnQTKfudLr4FwXyd/9mOeepN9e82ou//zIfcvnmPOJOX/4cd3X1Plz7vOMr9R9Xfq8aOnnqfR86dLzlz3vOM3jfvBXqt9eP6fre3FpKL//ze+VL9J19vfsxFD/3rl/v77q+6il3+st/b3+qt+P93WYmPj6Oo3XdfC/j3lO83xdEfqn0D/rOcZzjXo/3yeH3hZ6u55Tfv7Rz98X5kq/V0qfu553ctLP81+nueB83W7T9YDzdb1d18ufA/59yL/PXwj9o+4H7g/q/O/9W0Nv0P3Q0n+PtvTni9L71vNMVF77ch1uTXx9nW7QdfD3IM8vnk/P6/7gfoH3c+5G/fft+6jq3zEtfZ6W3r+e58akn+edrLn83OB5zvnznL/O+epr5EsO6n1Ov89tv0n+Lf0+gSv9/wcovU6l5+6Xnkvv63Sj5nc/+JvUz88Nnv98nvgccC4G521w/gh1/r7gc+PzOHw+Sem5LFXfT3Cl56KUPjdKzxfxdbpF/bjO/wMS9EmqeJydm3v01/MdxyOqjW3WWNg5c51zFm07k7u2c0aW3TCK2Nkll7nsSqgpLBlTKhUq3WVKRVQ0EZVVKmwIM9ewEbWbCmfYH54P53jsvPd9f/jn6fR9fl7v1/P5eb9f79fn/fn82rV5778727+HS4P3BR8OPhtcEWyX6wZv8x5eGRwWHB+cERwZ3KrNB8dhXMeBd6fGJc5g8e5T/s7HPHSNL/Csd6T0wLM/M6T/I+Hl5zZL8j/Lg2uCfw4+FFwZXNb+g9cPTdyrgpOC1wfHBkcHRyjfJYrL70PFIz/4HhfeMuVbGtd6JxXirZQP1gPPfo2VD/Ds7/XyDV+3DjI//hB8MPiE5g185s3w4JjgVM0D8iEu4/j6pvP0QeXrPOA9IT3O0zyPO7Xg14L8w5+Cz+s+L5dfgxLnuuBM3Q/PhwWKw++DxGN8+B6n6Ty0npmFeNY7SbrsF/WEerE2uD54v/xy3ZkVvCM4SvmsUBx+d/2trW/Oa5TGdzz0zCrEs947pKtDeOwvrG/W7yvBdcGXgk9qX3I9mBucF7wlOO1D1pWHNC5xxhbikffcQrwnpaeU30vSbz3w7NM8+eB9ifXO+n8m+GLwVd1f7jfXu05MD94cnK/77HnRqu7U7rfwnpEe5wXvRel13k3nt32aJf0eF/58jet1QH2h3rwW/LfmF/ON61yPbg/erfnQdN+srX9N14H13V6IZ91zpQuefbpbPngd/F71jjr1enCL3JiPBP+odXCJ6jL1bVFwVfCR4Djly7jk4Xjw+J3xiXOJeLX123rGKU/Hw49FhXj2a5V8gGc/H5FP3j9ZN/8Kbgpu1HqCz3q6K3hv8J4PuS7hMT58jwPPec3X+PA2SZfzNY+494jnukEf81zwheC7WmesO66jz7kpODt4v9aP1yXjMK7jNO2vnPfsQrzaumH990sfPPtzu/Tb57W6z9yfd7Re1stn7w/czxVaL15ftftMq36r6TytXf/WvUi6PC78FRrXPlPvqedvBDcHN6jv4TrvD0uCi4MLgvOUX6s+qul+tE55lsbdIF3O0+Piw5LCuPZpsXywz4u1v1CfPxfcLfiYfB6ifY16/vfgq8HJyo9xGNdx4PE74xJniHi1+5l1TFaejof+vxfi2Z9Xpd/9xsL8z6PBL+S6g4N7B6lb1EOuvzRxJwbfIJ9t38ONqm+um4xLHo4Hb6HGJ86lhXjoeKMQr7auWz+8jeLZr43yAZ795Xfy9Dqg/rMfQPgo+lSPuM77xcrgo1p3TfvV2v2paX2wniXK1+Piw8rCuPbpUflgn9kH6G/eDrYNccvgO/LZ/c/y4APB1YV9p1UfVbs/wXtb+TsfeOiB73zhWe8K6XE8+KsVz/Vms+YD92eb4Lasi+CbqjfeN7iva4KPBR8MLm24D8Hjd8YnzmLxauctcdBVym8r+WA9Hhff1hTGta+PyS+vg9c137ivHwt+KshzE9e5v2IePB58OrhK+dX2abXPc7Xz1jpWKU/HQ//jhXj252npZ/7yXPhI/uHw8LsH2R/Yx+BPSJyPZ7/4hPYN72/EJw6/TyjwyIP45tXuq9YB7xPaB62b3xnf85L+iL7mG8FvB7/D/VF9dj+1feJ3Cu4YnCIdjEc8fnffWNu/OT/i7ShfrI98Hc/60bO94tmfHaXf9Zn5Tl3vEOxIPsEdtC643vX/4eBTwWeCz2p91O4nTddlB+lxXvA6Sq/zhmfdj0sPvO3lm/WbR9xnxfP9oQ+lz/xicBfuLwsn+K72T/evbwZfCa5Vn+S+qvb8orZvru3nrGul8oVnH9ZKHzz79or8sA74b0qH61Qbjb9T8NPar9m/uc56Xgg+p/3a+3urPrepf7V9hfWsUb7wrP856XN+8F9Qft4/t1HcTuxf6nfgO7/ng0+ob6ztm5r6sK3yK437celxnh7XPMb1vKReUWd2D+4R3FX9C9e5vr0WXB9cV+ibWvVDTeup83ta4zse+l4rxLPuddIFzz6tlw+el9Tzzwb3VD7kB586/3Jwg+KX9pXSfgGP8eF7nKZ+7Sldztc8x4P30fCYn1vovnI/9g9+PXhEkPMtzsOI4/6d+9kmfc4ng9sFS+dnrd4HNX1+qD23q53f9gd95tkvxt1O/aF9xqc24vk+bCdf3adQn6jn+wS7BPcK7qw6xvXeJzYFNwf/EXxR9by2Pjbdnzop39K41rupEG9n+WA98PaSb9YPz/5ulm/eD9hv6X8OCh6g/NHz/nsm9VNtc/+32Pb/6221vzft32p9tj7ydTzrJt4WWgf2id/bFtYBdZC62DnYNbhf8EDtM1zv+vl68J3gu8EtM37T/au2bsNznus1PrzO0u384XWVL9YHbz/5Zv3mkSd5vVtYB7tKD9cdEjxU9Zbr3C8wztbBdoU6Wtt31Nb52vthHW2Up+OhHz2OZ3/aSb99Zl2xbr4S7Bb8kuoX13kddgi2D76lfaG2HjZd912UZ2ncL0mX8/S4+NBBdRSefWpvH8Kjn6Te8bz6o+BRqoPw/dy7d+LupLrW9Lm5tu46T8Z3POffVnnCs15+37swL6kXhwWPDPbQOmBdcB315GOJ+6lgR60Dr5tW9Qke+cD3eE3Xq/VsrXzhHSk/rM884nYUzz4foPvH/Tkh2FPrgeu8v3I/dw/uovVQu083XYe18886OihPx0P/7oV49mcX6ff6P0T3pXfweNVr+J4HewR3K+wPrep+0/nmvNppfMdDzx6FeNa7m3R5XnbT/cLnXsGjg1/VvHQd5r7sGtyZfrBhPYfXTeMSp/2HnG9flZ5SfkdLv/V4XPMY133w/rrP3J8zgucEfxbkeZPr3a9wX/cNdgseqOfSpv1P7fNw7by1nk8qT8fDj30L8exXN/kAz34eKJ+8Dlhf1PXjgicGf6j1x3Wu+58N7hnsrPVXu380XffHKX/nA+9E6XO+8Kx3D+lxPPidFY9jKNYD7/c4l+E85VvBC4IXBgcEvxnkvR/x/H0X5zKfDnYP9ggeEdwh6PeFrb4fa/qesvYcyj6Qv3n2YXvpgWffdpB+ePa9u/yD5/vSQ/7C8307Qv573VFH2edPDvYJnqQ6y3XuA7oE9wl+TvtRbd2u7Tvg9VKepXGtr0sh3knSbz3w7NM+8sH7D/0WzwdnBn8Z/LnyI1+u93NE1+AhwYM0vnXV9nutnl+a+mm9XQvxrL+LdMGzb4fID3j29SD55XVAHf1B8PTgT4Nnqc5yHXX288EvBw8I7leo24zDuI5TW9/hna78nQ886+isPOH9VD5Yl3nE3U88+0yf0Vt5cP3Z6kO4zn208z600Ie06seb9j+1+3Ktz9a7r/Q4HvxDFc/1hrrEemId/CrYP9g3eIrqjesZ6+fw4GHBrwS/0LA+wuuj8Ymzj3i19eEU6SrlZz8OL8TrK7+sG559PUx++b0j36fx/dlPgkOCw4K/Cf4iyHfbxPH3bfsHewZPCB4dPJi8lX+r78Lh1X5f53y38viKh37ydzz701N64f2PX9IPz/4eLb/g+X6cIH/dX+8t3eQxNDgieKn2RfZJziOJ5+/40dMreGLwKO2DpXPMVt8Twav9e4La89Pa+VDbL9i/rvLB4+J/r8K4vi9HyV94vo8n6n54v6Nus0/2Cw4MXqS6/v65sPbRrwW/HjyysE+02o9r9xN4/ZS/84FnHYcqT3gD5YN1mUfcI8Wzz9Rz+r7hwauCl6nec537wt7Bk4LHqI7X9pdN9xnn3bsQzzoOV57wrPsY6YJnn06SD+4rztB95v78NjgmeGWQcyCud3/DfT022Cd4fLCb8q3tl2rPqWrnrfV0U56Ohx/HFuLZrz7yAZ79PF4++fy9v+YH9/+K4MUdPsh3n8I8OS74jYZ9Drz+Go84h4lXO58vlo5SftZ7nOK5blBnBgevDY7TfeS+ch116DvBHwVP0X0s1b9SXYNHPvA9XtP5dq10Om941n2sdJlHXOu3z/y9xcTgpODYIP1dd/nM32OcHjwjeLL6v6Z/32EeeZ1e4NX2qdazv/KFZ/0nSx88+3WG/PD65zxxfHB2cEKQc0T4nDOeFjw/+OOgzxdrzyudx2kFnvPqrvHhWc/5yhue9f5YuuwX57Ocl04PzgrOlF8+xz07eF7wXOVTew48s8ArxSPPswvxrOM85QvPOs+VHu//nJPzPon3O9cFb9Z9Zx5wvb/b5L3QqcF+uq+eL62+A216nl/7fqx2PtuPUwvx7Fc/+QDPfp4mn1xn2f/o524KTgmO1P7Ide4P+wbPCn5PfUHtftu0H71CeZbGtb6+hXgjpd964Nmns+SD6wbvf1ivrJ9bg/zd2/vf5SeO1/UFwU7Kp/Y9U+3f4dXWF+ffSXk6HvwLFI95Bf/b4t9GH9LhgzyPNyDov1Ns9XeFTfU4nx01vuPBH6B4Xo+X5x9GB6cF5wRvCF6t9fjdxPt+8GfBXwV/HvyB8mMcxnUceJdrXOJ8V7xpyt/5wLtaekr5zZEP1gXvBvlk3faZfpW+9MbgjOC84Bj57P72l8FzghcG+yi/2ue32n7a+fXR+PBulE7nDW+GfLAu8xj/QvG8/7O/8RzMc+nc4O+Dk7UPcr2/A+F5dmDwkuCZhf2w1XclTffh2ud66xxY4Fn3qdIDzz6dKf3w7Osl8sv7Es8dPFfcGbw9yHk2fD+fDA5eHOypfGrP4Wufg5xXT43veOgZXIhnvRdLl9/DcH7Mue+C4B+C9wVvCXJexnkdcXz+PCg4PDgs2D9YOt+rfb9de/5de65o/YMK8exDb+mCZ9/6ywd49nu4/IPn+zFM/no/4Nyf9ztLgsuCdwQ5z+c6vy8aGhwR/HWwl/Krfe9U+z7CeQ8txLOOXsoTnnX/Wrrg2acR8sH7wXDd59XBR4JLg/TX9O1c7/lzTXBC8Mpgqb9vdX7ddN7WPldYV1/l63Hx5ZrCuPbrSvkAz/5OkG9eB5yrcB7yYHBNcDn7vtaBz2HGBCcFrwr2UH6136nVnvs4vx4a3/HQN6YQz7qvki549mmSfPA6oD+gf6OfWhz8c/CP6iO43uf69GFDgtcHx2n/r31P0LSPqe1HrWeg8nQ8/BhSiGe/rpcP8OznOPnk+zNA84j7vyr4VHBFcLruj7+jZN5cHfxdcGTQ52ytvstser5Xuy6s52zl6Xj4cXUhnn0aKf3w7Ovv5JfrFP0H79XvDz4efEb9Cdf5Pf2o4JTg9EI/Ufu+v7Yvct6jCvGsY5DyhGf9U6QPnv2ZLv32mb6C71lWBp8IPqu+g+v8fczo4NTgDPUJ5Ff7nU1tv+O8RxfiWcdQ5QnP+qdKHzz7M0P67TPPMzyHPBR8UfkPk89+/hkbvFl5Nf1OqenzVq3P1je2EM+6R0sXPPt0s3ywz/S99LUPBF/Sehwhn90nXxu8Reus6fc/Tfvy2rphfdcW4ln3KOmCZ59ukQ/ePzkXp9+lT306+JfgQtaR9k+fp9Pf3hi8IXhp8CfKd4ri8vtZ4rU6x4c3VfmWxq3t8xfKB+txPHy7sRDPvt4gv7wOOMe7N/h8cK36LvowruOc74rgzOAs9V3u01qdG8IjH/ger2l/+Lx0Om941j1EuhwP/izFs8+cQ/E+/rHgy8GHg3fKZ7/fnxy8LTg+OFj51X4nUHs+5rwnF+JZx2DlCc+6x0sXPPt0m3ywz5yfcj76QvD14HPsD/LZ562zg4uCNwX7Kb/ac9va97bOe3YhnnX0U57wrPsm6YJnnxbJB58X833JP4P/0vxgvsDnu5OFwbt0H5t+v2IeeSws8Grnr3VMVr7wrPsu6fO85DmK5591wVeCT1JHNC/93DUvODc4LXie8qv9jqP2Oc/5nafxHQ998wrxrHuadMGzT3Plg+cl5zl8z/PX4MYg5zPw/Z3PnOA9wTHKp9V3Qk3PlZznnEI85z9GecKz3nuky/0Z34dQR6gT/w7+TfmRL9f7uxLqy93BWzW+ddV+n9Xqe5amftbWT/txd4Fnn+ZIPzz7eqv88nxmP+A93qPBzdon4Pt94MTgYtXzpu8Ta/cl5zmxEM86FitfeNY5W3pcZ/m+hDpFfdkU/BO+q876+zXq0b3B64L+/qPVd3DwWn330rTOWscFyhOedV8nXR4X/r0a1z7PZz0E1wffCm6gj5DPFyXeZcE7gvcFFwQvV36Mw7iOA2++xiXOReKtV/7OB94i6Snl95Z8sC54G+STdXv936r790bwzSDfE8H3PFgSXBps+l1S0/nmvAZofMdDz5JCPOtdKl32i/dmvO99h4mbjW+1/PL75RXBlcFrlE/t++na93fOc0UhnvO/Rnma53jwvP/z/oX3If8JdgzxbdVz6jvX+73NsuBTweWq602/U2n6vqh2/7HeZYV41j9RuuDhl+M9VYgH3z75/nBOyvM0z8ttQ/xMcMvgQ7o//o6F5+wHgi8FVwfHKt9W38U0Pc+tPR+wnrHK0/Hw44EW8fDLuuHxu+PBp5z8FwnTNBZ4nJ2dZ/iW5XnGwYGADJUhCA40iamxMZpYqCZm2LTRLE3rylTT7MQmqUnapkkaP4aNICCKqCBLFNCMuhmy995bGWpFAygI2A+cv/9x+DP38Txv/HIe//c+n/O6zut97ue51ystmh37748tj+GsYKtWx/DMYNvgwrS3yHW/bXMMBwSXB3cGVweHBk9o9s44xLUOPNqJi85vC3rkvbygZx9DlSc8fFtvZ0EPvn0nTFO9/pQPNgWb57oPB1sGn1ed74jeuOD84JHgsmB/5Uc89Gi/Qzzyge948Jxff8W3Hv7mF/Twbd6RQlz49u06z8gHy4Ntcl2v4AnBjapz3+jdG1wVPK7tMVyUv8cqP+IQ1zrwaCcuOn0LeuS9qqBnH+gtKvDwbz/waHdc+Pm42UnB2Ym7MnhWCO8NXhTsyvfb8p3XD4z+fcFdwVeDB4Pbg0OUL/HRpX2geOQH33HhOc8him89/O6q0KMO9gOPelnvVfEuUn1dJ/eDaflgW/DtYM9c3yG4Vv2gd/QeDs4NNs99sDF/P6j8iIce7b3FIx/4jgfP+aG3saCHv7kFPXybhy/HhW/frvN69Wv628XB84Ivqc5j9DyhP74VfCX4e/kgDnGtA4924qIzRry6zxF08FPK72I9Z+0L3nmqk337eTM98TcHTwnhI8Eeem/P0vOmT3THB9cFjwZf0jjG4wfio0t7H/HID77jNjpusd91Bb1WqoP9wKNe1jsqXg/V13VyP9iaD/YGO+a6j6rfbVM/mBi9J4Kbgi3S70r9eav0aJ8oHvnAd7xGnyP4Qtd5w0MH//YDj3Z07d91npcPdgQ75bqPBU8NrlGdB0f3keDm4EmJsz5/P6D8iIce7YPFIx/4jgfP+aG3vqCHv80Vevi3H3i0W++kQp3n5IMNwXa57hPB9sE9qvOg6D4UXBNsnThr8/fjyo84xLUOPNqJi86ggh55ryno2Qd6a8XDt/Va6/3ZXnWyb9d5puYpjPcvC57D80d17he9d81DEm9P/p4jH8QhrnXg0U5cdPqJV3dehA5+SvnhG137gneO6mTfrvPcfPAC78Vc9/fB9wdXq853Ru/R4NLg8Yn3ev6+Xz6Ihx7td4pHPvAdD57zQ+/1gh7+lhb08G3e8arz+1Un+2Z8cWKQ+QPj/yvUb+iX8D3PaBn9Un+dr+cS7Z4f1Z3P1H1OtJMf5wmPduLbl+vFegzrKZ/R9/KC6uV1m9OiW/qeF+q+p93rUnXXh+reXyfJj/OERzvx7cv9eHE+OC78TwbP1/tth/rx8OguCJ4c/dc0HvD7lXjo0T5cPPKB73iNvtfxha7zhtdJ/u0H3vmql/27zks0n2VeeEbw08HDqvPd0fN8dUewfeLOUn7EIa514NFOXHTuFq/u/Bkd/JTywze69gXv06qTfbv/Mw5kHPc5zU82q/97vNg5uqV5Efro0O5xbt1xad352Cny4zzh0U58+3K9eP/xXvoH1imYP6hefk+2i25pXRZ9dGj3+73u+7juenBz+XGe8Ggnvn25Hy/gg/DfF7wqyHrncvXju6I7L7g32CFxSuuyxEOP9rvEIx/4jtfoejC+0HXe8NrIv/3Au0r1sn/XeVE+2B9snes+G2R9c6XqPCy6zwZXBDslTml9lXjo0T5MPPKB73iNruva34qCHr7N66T311mqk33ztfIcWJX4rwfPDuFTwUuCp+ND62CjovtUcHewTeIezt9bg1Pli/jo0j5KPPKD77jwnOdUxbcefndX6FEH+4FHvazXRt/PJaqv6+Tn8zLVh3w+z/yh1Tv596jO5Hl64ixU3ugSx9fDo5146NzzV9b/ePko5fd53X/24/t5aeIe4L0Vwt8Frwx2D76p+3lE9J8LLg42S7y2wRfz+XTlS1zysB482omPzoiCHj4WF/TsB70XxaMO1mum+7S76mXf8K5UfV0nP9dX5IMjwX/KdZcHWwTf0HN9ZOLODp4a/RODS/L5NOVHPPRoHyke+cB3PHjOD70l4uELXedtPfzbDzza0bV/13ldPtgd7Jzrvqjv/4DqPDpxHwtuCXZNnNL9RxziWgfeOvVPdEYX9Mh7S0Gvbv84Uf7tBx7tjtu18LzZkvj7gl1C+FDwC8wPg3/W82ZC9J8JbgseCnZJ3JX5+2nlS3x0aZ8gHvnBd1x4zhO9lQU9/G6r0KMO9gOPelnvkHhfUH1dJ/cD7iPWo1lP/sfg1fq+uY77zOvXpyROx+AW5bdH/c46jd7fddfT8QPf+cLrLN8lH1erTvbtccpO3Yd879cEL9U4ZYruZ+6HM6L/tvJBlzi+Hh5xzLNe3fv8GvVf5+m48O3D9+WL8sP112qcv1/P58lt/rKvbolXmj8Qh7jWgUc7cdGZ/FfWue78prX82w+8a3U/2b/rvF3vf963X9LzZp/qPEnjCd7H3ROn9LwjHnq0TxKPfOA7XqPP2brjEnzDty94XVQn+6ZO8F9L3H/WPIh5Frwnc/2Z0SvNu9DjetqfLPCIi655ded7tFtvd8E3423Gy/8SPCjfHo+fFd0Zio8e19PueUbd8b7zmaH45qHrPO37d+HNhM/zoJV0c32/4OTgFMVHB11f1/QcK/BKeuQ1uaDnvKcoTz9P+oe3lPdZ62O4m/4ZHKPxxE3RG8G4M/hY8OHgvym//tKj/SbxyAe+48EbozxLce1vcUHPvh+Wr6bxt+r0mOrgOg8IbznvaZ4nwdnB8arzl6N3b/Dp4FPBgcF/V34DpEf7l8UjH/iOB2+88izFtZ+BytdxqcPThbiu01Oqg8dtA8NbGHwt+FZwnMZtX4nOUJ6zweeDP1U+A6VD+1fEIz58x4HnvH6q+NbDz5MFPft9Xr5crz7hTQ0eDjbP/b1J9bo+Ov/NPhj7EMFxygdd4vh6eLQTD53rC3rkOaug5/zHKc+m/T35nS9frldvxhmMTxhvBRepXtdF5+7gtOAzwWHKp7d0aL9OPOLDd5ym8VZB75mCHn6mFfTs9xn5ysdNdRuWDx7Ufc79eZT3fnBOq3def2t0f6T+yH09Jzg9OEj5Epc8rAdvmOKjc6t44+THeTXar+17kPzAc52my7/jwp+juH4v9QtvTXBvsFX6w6z8/Xu9l26M3gPBJ4LLgwOCv1F+/aRH+43ikQ98x4OHDnmW4trPAOXruNThiUJc12m56uB+MCQfbA8eCLbL9bvy9wvBR9QPvhHdScHngmuCU4OPBv9D+Q6RLu3fEI/84DsuvEeUbymu/T5X0LP/R+ULnus1VXVoWkdVfdeobu4HdxE3uCHYJtefEFyhfnBz9P4z+FBwVXBRcKTyIw5xrQOPduKic7N4G5S/84FnHyOVp/Xwv6qg5/oskn/XuW901wdb57r2jMvz+VzV+YbojQmuCK4NDg/eqfz6So/2G8QjH/iOBw8d8izFtZ/hytdxqcOKQlzXaa3q4OfNyHzwUHBHcD91jQ7vc97/XP/d6P44+Ejw2eASvfc9TiAueVgP3kjFR+e74u2QH+fV6DjG9Xi2oOd6LVEdmtarVM9pqpPHkXeGt4y65/rjgtM1jvxadO4JLg0uCPZRPuijQ/vXxCM+fMeB57z6KL718LO0oGe/C+TLz41B0WVeyXywba7nfcz7m+u+Gj3PQ1frfe33O3GIax14gxQXna+KV3deXHf8Yd9PyJfjwl+tuK7zA+HPD74dPBJcEByrOv8wekOCc4Ozg3cFf6L8HpAe7T8Uj3zgOx68scqzFNf+5hb07Psu+YLnOs1WHVznweGxzsQ6UofW79Sbrzp/PXpel9qoePYxWHq0f1088oHveI3Wr+56mv1vLPBcn7ny7+fsPeGtDR4MnhKdaXrOfjs6DwZnBNcFeysf9NGh/dviER++48BzXr0V33r4mVHQs9918uV6DQ1/dfD0XMe60kLV65bo3B/cGiytTw2VDu23iEd8+I7T6LqY/Wwt6Nnvk/Llfnwv9Q++GjwUZB7CPIjrvhO9vsH/Dc4MluZJxCGudeDdq7jofEe8V5W/82l0HndIdbAv86w3s1Dn4XoO85ztmO+L+3yt6vxNvX94Lm9SP3C/IQ5xrQNvuOKi803x6r5H6vZr+54hX44Lf5PiprnpOfBwPpgcZN2Q9b4zo3My80LGe7n+59H9r6DXG3cGVwZHKV/ikof14NFOfHR+Lt4i+XFeja6T2vco+bEe9dpZ0HM9V6pO7gcjorsyeHyu6xJkv2S5+sG3ondfcGFwW7C0/zJCerR/Szzyge94je772N/Cgp59Py1f8FynbaqD+8HEfMB6FOtNF+T6cxin5PM/qB/8LLpex9oX3BMcG/wf5TtRurT/TDzyg++48NAh31Jc+xqrfBtdr3Pd9hV4ruse1cv9YFJ0mY8yj+wWZF9nqvrBL6Ln+esLwdJ+0iTp0f4L8cgHvuM1uo9Vd95t37Pky3rwX5Ce6zwqeuxPsP/QPddz3mCm6vz96Hk/48Vg6fzCKOnR/n3xyAe+48Gre26i7j6MfU+WL+s18aXncfd94Z8a/mlBzhk0nV8I/3vRWR/cECyda0AfHdq/V+CRx/oCr+55CvtYrHzh2fcG+fPzeQrjxiDzJeY5ZwV57vM+4fpfRvd3Qc+zdun94PcOccnDevBoJz46vxRvmvw4r0bnh3Xfn67TQvl3XPi7FNfPjfs1PmN8c3aQ9b9lem78QONCxkO7g6X1ROKhR/sPxCMf+I7X6Dpm3fGgfS+VL+vB3y0913l04rKfwz5Mj2Cz4GbV+bboef/npeC84HjlRxziWgce7cRF5zbx6u5H2cd45QnPvufJl+PCf0lxXec/8V4Nsu/bNXgG6wqq8x3R837z9uCO4ETlRxziWgce7cRF5w7x6u5/28dE5Wk9/G8v6Lk+O+TfdX5c9w3fY0vGhfl8nur8a92vfO/Lgo8HByu/x6VH+6/FIx/4jgcPHfIsxa17P6ODf/uxnnnzCnWeoPkA4+4Lg+x77VCdb4+e5wUHgqV9NOIQ1zrwJiguOreLV3eeUnefz76flS/HhX9AcV3nx9Q/6Qd/G2Rfd73q/Cs9F+g3bwRL+8TEQ4/2X4lHPvAdD17d/em6zw37XiFf1oP/hvQ8Hnw+H3Dej/N8Hwx+IMj6LuvBXN8/uj4n+GZwf7C0bkx8dGnvLx75wXfcRter655zdD3eLPBcp63yD8913a96eX7DOQjOL1wU5PwI503g+7zEwWDpHAr66NA+XLy65zLqnn+xj1XK13HhH1Tcd/3eNfG3BF8Odsr1nYOv6LkxOnoTgn8Ibg5uCf5R+RGHuNaBRztx0Rkt3svK3/nAww985wuPePgu+XB9tkjP9yXzeObp5wbPY91W96Xn+y8HXwn6dxtVv8dodF3BeU1RfOvh5+WCnv2+Il/v+v2p5v3M1y8PfiTIPm3TezB6nv+f2O4YHs3fG5Vf1e8sGt0/rrseYV/kaZ79EveoeK7PUfn3fcm6KecXPxT8WJD1Uvg+B3mI+z36+5RP3XOUdddvneehgp7z36c84dkv7YcK9yXnXNgHYJ3/E8Ergm11X/r3JOwLtE68lkGfq6n6XQq8qvM3je5j2Ad6LVU/+8eP9VwfdOB7nMV+Lfu3f5PrLg5+WPcB9wXXe5/3z8G3gkd0//r+qdo3hld1/hce+aPrvBq9v6kDuvYHz3U6JP/Wg39Eeu4He3W/8b1+VvP1VuoHPj/GfdApWJrfV52Dh1f3vFrddYW6/cC+0esknuvUSf3fdeZ8CONkxsGf5Pmedn6PwXU+T8K4+eTE8e81yK/qd13wqs6vwKv6nQg8+5mjfBudN7hO6Owv3M/so3OulnO2VwavCp6p+9m/S+IcbtvE6xDcqfyqft8Er+7+ftW5YPPwRZ7m2S9xO+j7cH1ob1sYb7DuwbrGl5QP+cH3+kh36Tvvuueo667D1K2X/ZCn9ewXve6F9+Cb6j/cz9cHr9b4m/F407/XkPjuT+cEOwZL4/a6vwer2odstJ/XnVfYP7yO0nPd8O+4rmtH1cvfD+uhrO9fEvxo8NLgexj36PvxvsHhYIvEezt//19wmfxXrcc2ul/RUvmW4trv4YLee1QH+4HnuuH/sHiXFnjo+vvhXBrneJk/MT+6KXgt37e+H5+v9rzrvMTvFvS5uKrz2vCqzs/BqzqX3Oh80b6J2039wvXCt/VcT3Tg+717UL657otB9oHZF27698Xa/uV8uwa9b0x+VeeU4dU991i1X91one0bva7iuU5ddR+6zpzX4TzODcEbg6yzNledfb6nR/TPDZbWd6t+7w6v7nmiuuvK9ke+1rNv9M5VnV0n2nsU6sy5J/bf2V+/VXl1U539uyT24y9UPPuo+n0TvKrzWPDqnh+oW2f7x4/1XB/0LizUmXMk9BP6wc1B1hu6qM4+d0K/uUDrEY2e64RX95xL3XWTus8D+24tX9aDf4Gem35/cn6BfQf2FT4V/JzGAYwLuN6/02U/ok3iddZ73uOHqt/9wqs6Z9HoPkrd8Y3rgS/ruU7oddb347rS3qbw/bAP8b5gz+DHg72C56On74f9ir3B5sQJHhd8Le1b5KtqHwQe+cF3XHidlW8pbk/5dv7wzlcd7Afex1U/1wFeL9XXdfJzinNWzKOZJ/+rnos8J7nO5/OZV39Qz0U/R6vO+cOrOv/V6DpA3ee8/ePHeq7PhfLvOrPPzz7+j9Qv6aftVWefC+ip/ub9U/Kr+p08vLrnEKr2ba2HP/K1Xt3nkusED13//+uYZzM/vibIvl7Tvqzm4WcEvX9YtS/Y6Dzf+aB3hnjOn/aOBd+cG74lyD7bafLNOeIPRKe0n1d1DrkUF13z6u4jOn946Pr9wno269W3B2/Tfch9yXkarvc6+BXBXrrPfO6GfKvO9TW6/l513qfRfua64M9xXa+eqgM817eX6paPm55/nBNjn5N9zOuCPw7+JMj6I+uV6Ph3MOyDnh28LHi51iu9vln1+xp4Vefb4NXdx627/ur64M96rld3+YfnOl+musHz94DO/wPPoMh4eJydnHnQVmUZxtE0BQRT9k0EVDTNwMwMBRdMFrcUEbBSSC0nN8AlHStTQdMUSUVNR1RKy70al1JxwUxcp8IFp3JB0JJqcsFMBZuJ6/fN8LNn3ufAP9fM917nvu/rOud57vuc97wM69zuf/82XA3tPhnctsNq3D44MXhy8JDgHh3WPG5Fp9X4XnBg4u8W3DzYMbheuzXzkNdx4G2rvMRZId5E1e964O0hPaX6rHtz6XJe+Hw+sODz0PCGBUcH9w/uF/ycfP4AvYm7abBnsEdwpXwZqnh8/oF41APf+eC5PuL1EG+0dLpuePvLB+syj/w9xLPPO4S3b/Dg4LjgXsFd5fOH0dM9cTcL9gt2Dn5S9e3geJ3XjAePeuA7H7xdVWcp717S5TqdFx/Q5bz2qZ98sM9DdD1wfg4Pjg8Ol8/vR7evn22C/YMbqD7ykNdx4A1RXuK8L17t9Txcekr1WT96HM/+9Jd++7xjeLsEpwQvD14ZnCSfV0XP+om7bXBK8MjgINVHHvI6DrwdlZc4q+TzFNXveuBZxyDV6Xjon1KIZ3+OlP724W0QHBHe7sHJwZuDdwVPCu7D9ZXjN0zcDsFPB08Ofj84IthN9ZKXOhwP3gjlJ86G4k2WHtcFbx/pKtVnP04uxLNPI6Qfnn39vvzyOmB90Q8OCx4RPErrj+PcL7YOfib4Wa2/2r7TdN0fpvpdD7wjpM/1wrPebaTH8eB/VvHsM/s//eCbwROC3wgeKp/dL4YGvxgcEtyiYd+BN055idOvEI+6hxbiHSo9pfqse4h0wbNPX5QP3m/2DO+44PHB76l+9NDHOX6jxP1CcOfgaOUt9fs9FZfPNxKP+uA779rOGa3Oj30YKn3wjpeP9sM84o4Wz+tgbHjcBzDn/zz40+CXtQ66Jp7vG6YHjw/2Vn1jFY/Pu4pHPfCdD57r6638Te93rH96gWd/jpf+/LnN75H5w0HBacHLgvcEf0MfD46hTyZOp8TvG9wlODk4I3h2cPtgF9U/UvH5vJN41Avf+eGNUd2lvNPkg3XAsw/bS5fj4ePkQjz7PEO+wfN5OFu+ev0w39Kf6D8n0u+DU7V+PA/Tr4YHxwaHqb5WczW88cpLnP7i1fZX6xpe4FnvMOmBZ3/GSr99pr8fGzw9+B3VdZR8pv/vFPxScG/lazpPwKMe+M7X1L/TpdN1w7Pu4dJlHnGt3/36gPAODHKfwv3Fr4PMtczLHN8rcfsEfX9zVrA0V5OXOhwP3gHKT5xe4k2SHtfV9L6s9v7Afp1ViGc/T5ZPXgfMZ8xf3w2eETwleLTWgee5UcExwd2DO6i+bygenw8Rr3Z+PFp1lvJa36hCvFOk33rg2acx8sH9ekL+8HX1IfrC3ZoTmBt4XkqcAYm/nfoxfeVMzQ1+vkr9ExSfzweIR73wnb/2uS68I+WDdTTt17Xzlf2dLr+cF/6Zyuv1w5zNfPzD4Pm63rj+2p5rJx7z9PjgwbqOmt4vNb0PqF0X1jNK9TovPowv5LVPB8sH+zw9vFOZt4LnBWcGT5PPuybensH9g+OCBwRHqj7ykNdx4E1XXuLsKt4M1e964J0mPaX6zpMP1gVvpnyybvtM/6fPnxm8IPgDzVkc53lhn+AhwYMKc1vt3NFqvoN3pup3PfCsY6zqdDz0H1KIZ38Okn77zDri+r8oOIv+rv7CcV53k4ITg/uqD9f2q6br/AzVWcp7lnS5TufFh0mFvPZponywz1N1njk/PwlyHzdNPvs+g/N5XLB0XzhV8fjc91219zW196O117P1H1fg2Z/J0m+fWSfnBucErwj+WOuI41hHBwYPD349eITWEfWRh7yO03T9zlH9rgeedRyiOuFdIR+syzziHiGe76d4Hsf3BHwP8HDwZ+q/9GOO57mdv1+4MDhNfdh9m7zU4Xi1zw2bfu9RO1dY/3jpgme/pskH1wf/QtXndcC+xD51XXBe8KrgbK0D72PHBI8NHhU8tOF+CG+W8hJnYiEedR9TiDdbekr1XSX91gPPPh0rH+zzheFdHLw6eENwbvAS+Twh8b4a/GZwavDo4NdUH3nI6zjwLlRe4kwQ72rV73rgXSI9pfpukA/WBW+ufLJu7zf0ZdYT6+XW4C26Xrh+ON5zPevs28FTdJ59nbW6T2g6P9TuD7XrwPqPkS7nxbdvF/La11Pkl9cB/Zx+RB+5KXi9+j3HeT6l75wUPEH9vnbObTpn1PZT6zqpwLPe46QHnv05Qfq9DujT1wbvCN4ZvE31US/H08+/Ffxe8IzgqcpfmidKcwI86oPvvE39vEO6XT886z9JuuDZr1Plg3nkt19eB6w/1s29wfuCt6u/cJzX68zgOcHT1Idr+1XT/WGe6izltb6ZhXi3S7/1wLNP58gH+3y9zjPn8bfBx4OPyGevL877rOClwYsarlN4jxR4jld7nVrXrALPui+VLnj25SLpts83hvfL4APBhcEHg7+Szycm3unB84IXB88Pfkf1kYe8jgPvRuUlzoniPaD6XQ+8X0lPqb6F8sG64D0on6yb/Xj9IOeZ/WZB8FGdf/jery4I/qhw3mv3u9rrbYHqdR3wHpUe12me88JzH+R5AP2V9cZ1vTj4uyDff3O8nyN4nc4Lzg7OUL2tnkvAq/1+vnZeqN1frHuG9Dgefs0rxLOfs+WTr2f6A3PkE8Gn1Dfge76cE7xcfaB2Pm3ap1znnEI81z9TdcKz3suly/ssczn3908GX1Zd1Mlxfl5wWfAm5bOO2vuA2ucTtf5Z32WFeNY9R7rg2aeb5IP3jXt1Xp4NPhN8Ojhf8wjH+3xfE5wbvCJ4rua32jmn6XV2n+ot5Z0vfa7TefHlmkLep+Wb9cOzv3Plm9cB+z/9YFHwueDzmnPannepX1wdvDZ4neac2r7TdL5apPpdDzzruFR1wntOPliXecS9Tjz7/Fh4vw/+KfhS8M/BP8jnSxLvyuD1wRuDNwSvUn3kIa/jwHtMeYlziXh/Uv2uB94fpKdU30vywbrg/Vk+Wbd9fkTnmfPzanCp+i/HeS7nfN4avE39t3a+b9r3a69T67q1wLPeedIDz/7cJv2eN9hf2G9eCS4JvqB5w/vQzcFbgj9puI/Be0b5iDO3EI86by7Ee0E6SvVZ7y3S5T5IX6Vv/j34D9VFnfQFjnc/vit4t/KW+ker+ajpHFDbt2p9ty93FfLar5vlAzz7e7d88/XM75+6dVyN3YO8V8d7ePD5XdRLwZeDpffzWv2+yjzqeKnAq30v0DrOUr3wrPtl6fM+S/97MbgsuFz7EvsUx9Effx68PXin9ptSXy71W3jUA9/5mu6fy6TTdcOz7luly/Hg36l4vi75XQDvr/F+Wp+cp74d1+T79wO8x7Ys+JrqafX7A3jkMc/xat+3s45lBZ51viY93mfZF1j3bwffCf4z+Jr2b473fjI/eH/w18FfqH/V9oWm+9gS1VvK+5r0uU7nxZf5hbz/lG/WD8/+3i/fvG+8qut/RfC94L81h3Cc19MDwQXBh9Zyrmm6fl3fbcrveOh7oBDP+hdIHzz785D022f2mTeCbwXfVV3L5TP70B3B+4IPqi7rIA95Had2/4P3lup3PU19flc+WJd5jgcvf27zm/ft6a/0z22CPYM8N+F5DO+XEMfv69N/3w4uCfr5TdPfCcNr9XsBeK3eg2k6Z9Q+j7Jvl8kH58Xvtwt5fT6WyF/3EZ67ct/Eemf9tU+cfsEeHdc83s9rvU8sCr4efEX1tnr+C4+85jle7X1g7f5mHxYVePbpdemHZx9fkT8+P8wPvLd/j/wizqeDfXR+/H6/n9eT/x3NG9Tb6vcCTeecVr8XbPo9Q+11YZ/eKfDs4zL54/PDumW99Q92DK7K59yncf/X9n2T1vvfgs8GFwZL94mtnlM33Wdq70+t6y7VC88+LJQ+ePbtWflhHfD/Jh2+r6A+8rdj/9T8DN86Htec1/T+u6lftXOrdcxXvc4L/3Hl9Tz1tvjrBtcJ/kdzMMc5/5PBJ4IPB+9Xfa3m6qZ6iUOdpbx8ji7X6bz48GQhr316Qj7YZ+Yu5rCVwY+Cn0ic9+Sz57RHg48FnwouUH2t5r2mc/pK1e964FnHAtUJ7yP5YF3mEfcp8bw//1t6OK5DsGuwd5A+zvG+/yDfM8EXg0sL/b7V/UzTOaPWT+t8psCz7kXSA89+vSgf4NnPpfLJ+zPnc8PwN1Ld6IDPef5j8DnVUbquStcLPPLDd56mvqKDuK7XPMd7ruAX+wv7Tedgp+B68sv70OLg88GnG+5j8NZRPuI8UYhHnYsL8daTjlJ91vu8dHn90/+YKzYP9lJd1Ml+z/GeV94Ivqq8pb7Qqv82nZOa9qNWvtuXNwp57ddi+QDP/r4q39wHue5ZB12CmwUHaN/hOK+TvwT/Glyufad2vTXd77qoftcDbzPpc73wrPdF6XE8+MsVz+ugk84f52dwcOvgwODGWgdeZ5zXN4NvBf8efKHhuoXXSfmJ87x4tdfjxtJVqs9+vFmIN1B+WTc8+/qW/PI66KrzuW1wx+AO6qsc5+tjRXBV8MNCP2/Vp5tej65vqfI7HvpWFOJZ/yrpg2d/PpR++8y62So4JDhUdQ2Qz6yrfwXfD36gupquU3jUA9/5mvo3RDpdNzzrXiFd5hHX+u1ze10PnJ9Dg18J9pPPnj85n1tsvBq3DL6u+mrn2NrndLXXs3UQjzodD/3ocTz7Qxz4ngfZt+i7nwvupP0MvvvxymC7xH9T9bTq5033T9e5shDP9b+pOuFZL5+vLFyXzDs8Pzog+GXVRZ0c5+dRvZKnt/JZR+1zrdo5zHVTh+PV+mzd8HrLZ/vE5+S3z4N1Xr4Q3Dm4nfojx/k8rxNcN/iu5o3aftv0utpadZbybiddrtN58QFdzmuf1pUPH/t/+tW/hgdHBHdTH217/qT9ewPiB9sHV0lHbb+o7d/DVb/rgWcdxGsv3gj5YF3mEbe9ePb58+ENC44Mjg3uFdxFPn+E3sTtFOwa7BxcX/WRh7yOA+/zykucj+TzSNXveuDtIj2l+sbKB+uCt5d8su6P/T/9Os+cn1HBSeqjbf9Pv+Y+zucmwUHqo7XzY9P+XXudWtcmG/9/nnUPki549mUL6bbP7DO7B/cJjg7uoX2I49iHOgS7BTcNdtR+XbuvmUddHQq8nVVnKa/1dVP98PaQfuuBZ582lQ/2mb5K3zxQ+tC7k3x2H+6jukv9o9U81LTvN+1brc6b9XeQPtcHn897F3xmP987OCa4r9bZbvKZ/f5TwS7B7lpnpT5T6h/wqAe+8zXdD8ZIp+uGZ92bSJfjwe+ueL7f4P3Z/YL7B3nvhPdU4PNebY/E6xksvb/S6v1c86iD+ObVvjdjHfB6yi/r5vMeBb96Ki7HMU/3l19+X4f4pTm/1ffh8Fq9F9T0/qLWL+vtpeuAdej9mnV/kPbvtuez2jf6ah9uuv/X7kuup5vyOx78vopn3aMVl+PGSbf7DXH7NexTzkse4my6lrpddz/xrJv9Z0JwovajtufsOX5AcOBa7mfwyAff8eG5nu7K73jwByqedY9SXI6bJN3et4k7SPlr9/vaebJWt+seJJ73Q97D4nkZz7m+GuT9K/h+P47nYVsF39G+1Or9Oni174PVPt9z/cTbSjzr5fMtC371UVyO+xr7ivzy+2rEHxz07xha/e4AXqv34uC5LuINXku/rHewzv9/AR/E+bU=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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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 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 10/31] 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 11/31] 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 12/31] 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 13/31] 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 14/31] 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 15/31] 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 16/31] 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 17/31] 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 18/31] 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 19/31] 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 20/31] 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 21/31] 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 22/31] 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 23/31] 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 24/31] 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 25/31] 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 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 26/31] 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 27/31] 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 96f22373ea0f1f936310c33cf4bac007face9e4c Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 11:51:50 +0200 Subject: [PATCH 28/31] 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 29/31] 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 30/31] 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 31/31] 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,