From a8a6b9bebb9648bb3e546647f53422967bfb7862 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Mon, 16 Jun 2025 09:07:59 +0200 Subject: [PATCH 01/58] add the VTK filter to fill a partial arrays of an input mesh --- .../geos/mesh/processing/FillPartialArrays.py | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 geos-mesh/src/geos/mesh/processing/FillPartialArrays.py diff --git a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py new file mode 100644 index 00000000..01e25e52 --- /dev/null +++ b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py @@ -0,0 +1,133 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Romain Baville, Martin Lemay + +from typing_extensions import Self +from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase + +from geos.utils.Logger import Logger, getLogger +from geos.mesh.utils.arrayModifiers import fillPartialAttributes +from geos.mesh.utils.arrayHelpers import ( + getNumberOfComponents, + isAttributeInObject, +) + +from vtkmodules.vtkCommonCore import ( + vtkInformation, + vtkInformationVector, +) + +from vtkmodules.vtkCommonDataModel import ( + vtkMultiBlockDataSet, +) + +__doc__=""" +Fill partial arrays of input mesh. + +Input and output are vtkMultiBlockDataSet. + +To use it: + +* TODO + +""" + +class FillPartialArrays( VTKPythonAlgorithmBase ): + + def __init__( self: Self ) -> None: + """Map the properties of a server mesh to a client mesh.""" + super().__init__( nInputPorts=1, nOutputPorts=1, inputType="vtkMultiBlockDataSet", outputType="vtkMultiBlockDataSet" ) + + self._clearSelectedAttributeMulti: bool = True + self._selectedAttributeMulti: list[ str ] = [] + + # logger + self.m_logger: Logger = getLogger( "Fill Partial Attributes" ) + + def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestInformation. + + Args: + port (int): input port + info (vtkInformationVector): info + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + if port == 0: + info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkMultiBlockDataSet" ) + return 1 + + def RequestDataObject( + self: Self, + request: vtkInformation, + inInfoVec: list[ vtkInformationVector ], + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestDataObject. + + Args: + request (vtkInformation): Request + inInfoVec (list[vtkInformationVector]): Input objects + outInfoVec (vtkInformationVector): Output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + print( "RequestDataObject" ) + inData1 = self.GetInputData( inInfoVec, 0, 0 ) + outData = self.GetOutputData( outInfoVec, 0 ) + assert inData1 is not None + if outData is None or ( not outData.IsA( inData1.GetClassName() ) ): + outData = inData1.NewInstance() + outInfoVec.GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), outData ) + return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] + + def RequestData( + self: Self, + request: vtkInformation, # noqa: F841 + inInfoVec: list[ vtkInformationVector ], + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestData. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + self.m_logger.info( f"Apply filter {__name__}" ) + try: + inputMesh: vtkMultiBlockDataSet = self.GetInputData( inInfoVec, 0, 0 ) + outData: vtkMultiBlockDataSet = self.GetOutputData( outInfoVec, 0 ) + + assert inputMesh is not None, "Input mesh is null." + assert outData is not None, "Output pipeline is null." + + outData.ShallowCopy( inputMesh ) + for attributeName in self._selectedAttributeMulti: + # cell and point arrays + for onPoints in (False, True): + if isAttributeInObject(outData, attributeName, onPoints): + nbComponents = getNumberOfComponents( outData, attributeName, onPoints ) + fillPartialAttributes( outData, attributeName, nbComponents, onPoints ) + outData.Modified() + + mess: str = "Partial arrays were successfully completed ." + self.m_logger.info( mess ) + except AssertionError as e: + mess1: str = "Partial arrays filling failed due to:" + self.m_logger.error( mess1 ) + self.m_logger.error( e, exc_info=True ) + return 0 + except Exception as e: + mess0: str = "Partial arrays filling failed due to:" + self.m_logger.critical( mess0 ) + self.m_logger.critical( e, exc_info=True ) + return 0 + + self._clearSelectedAttributeMulti = True + return 1 \ No newline at end of file From e948b20cb1cf5da3700de5ac1dbd978e52f87448 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Mon, 16 Jun 2025 09:10:02 +0200 Subject: [PATCH 02/58] add the pv plugin to fill a partial arrays of an input mesh --- geos-pv/src/PVplugins/PVFillPartialArrays.py | 113 +++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 geos-pv/src/PVplugins/PVFillPartialArrays.py diff --git a/geos-pv/src/PVplugins/PVFillPartialArrays.py b/geos-pv/src/PVplugins/PVFillPartialArrays.py new file mode 100644 index 00000000..56d48f7d --- /dev/null +++ b/geos-pv/src/PVplugins/PVFillPartialArrays.py @@ -0,0 +1,113 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Martin Lemay, Romain Baville +# ruff: noqa: E402 # disable Module level import not at top of file +import sys +from pathlib import Path +from typing_extensions import Self + + +from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] + smdomain, smhint, smproperty, smproxy, +) + +from vtkmodules.vtkCommonDataModel import ( + vtkMultiBlockDataSet, +) + +# update sys.path to load all GEOS Python Package dependencies +geos_pv_path: Path = Path( __file__ ).parent.parent.parent +sys.path.insert( 0, str( geos_pv_path / "src" ) ) +from geos.pv.utils.config import update_paths + +update_paths() + +from geos.mesh.processing.FillPartialArrays import FillPartialArrays +from geos.pv.utils.AbstractPVPluginVtkWrapper import AbstractPVPluginVtkWrapper + +__doc__ = """ +Fill partial arrays of input mesh. + +Input and output are vtkMultiBlockDataSet. + +To use it: + +* Load the module in Paraview: Tools>Manage Plugins...>Load new>PVFillPartialArrays. +* Select the input mesh. +* Select the partial arrays to fill. +* Apply. + +""" + + +@smproxy.filter( name="PVFillPartialArrays", label="Fill Partial Arrays" ) +@smhint.xml( '' ) +@smproperty.input( name="Input", port_index=0 ) +@smdomain.datatype( + dataTypes=[ "vtkMultiBlockDataSet" ], + composite_data_supported=True, +) +class PVFillPartialArrays( AbstractPVPluginVtkWrapper ): + + def __init__( self: Self ) -> None: + """Map the properties of a server mesh to a client mesh.""" + super().__init__() + + self._clearSelectedAttributeMulti: bool = True + self._selectedAttributeMulti: list[ str ] = [] + + @smproperty.stringvector( + name="SelectMultipleAttribute", + label="Select Multiple Attribute", + repeat_command=1, + number_of_elements_per_command="1", + element_types="2", + default_values="", + panel_visibility="default", + ) + @smdomain.xml( """ + + + + + + + Select a unique attribute from all the scalars cell attributes from input object. + Input object is defined by its name Input that must corresponds to the name in @smproperty.input + Attribute support is defined by input_domain_name: inputs_array (all arrays) or user defined + function from tag from filter @smdomain.xml. + Attribute type is defined by keyword `attribute_type`: Scalars or Vectors + + """ ) + + def a02SelectMultipleAttribute( self: Self, name: str ) -> None: + """Set selected attribute name. + + Args: + name (str): Input value + """ + if self._clearSelectedAttributeMulti: + self._selectedAttributeMulti.clear() + self._clearSelectedAttributeMulti = False + self._selectedAttributeMulti.append( name ) + self.Modified() + + def applyVtkFilter( + self: Self, + input: vtkMultiBlockDataSet, + ) -> vtkMultiBlockDataSet: + """Apply vtk filter. + + Args: + input (vtkMultiBlockDataSet): input mesh + + Returns: + vtkMultiBlockDataSet: output mesh + """ + filter: FillPartialArrays = FillPartialArrays() + filter.SetInputDataObject( input ) + filter.Update() + return filter.GetOutputDataObject( 0 ) From 35c3b74273fe665856424360c3d3c929fdabf35a Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Mon, 16 Jun 2025 17:20:33 +0200 Subject: [PATCH 03/58] fixing bugs by removing the use of the abstract class --- geos-pv/src/PVplugins/PVFillPartialArrays.py | 67 ++++++++++++++++---- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/geos-pv/src/PVplugins/PVFillPartialArrays.py b/geos-pv/src/PVplugins/PVFillPartialArrays.py index 56d48f7d..b4481e85 100644 --- a/geos-pv/src/PVplugins/PVFillPartialArrays.py +++ b/geos-pv/src/PVplugins/PVFillPartialArrays.py @@ -8,13 +8,18 @@ from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] - smdomain, smhint, smproperty, smproxy, + VTKPythonAlgorithmBase, smdomain, smhint, smproperty, smproxy, ) from vtkmodules.vtkCommonDataModel import ( vtkMultiBlockDataSet, ) +from vtkmodules.vtkCommonCore import ( + vtkInformation, + vtkInformationVector, +) + # update sys.path to load all GEOS Python Package dependencies geos_pv_path: Path = Path( __file__ ).parent.parent.parent sys.path.insert( 0, str( geos_pv_path / "src" ) ) @@ -23,7 +28,6 @@ update_paths() from geos.mesh.processing.FillPartialArrays import FillPartialArrays -from geos.pv.utils.AbstractPVPluginVtkWrapper import AbstractPVPluginVtkWrapper __doc__ = """ Fill partial arrays of input mesh. @@ -47,11 +51,11 @@ dataTypes=[ "vtkMultiBlockDataSet" ], composite_data_supported=True, ) -class PVFillPartialArrays( AbstractPVPluginVtkWrapper ): +class PVFillPartialArrays( VTKPythonAlgorithmBase ): - def __init__( self: Self ) -> None: + def __init__( self: Self,) -> None: """Map the properties of a server mesh to a client mesh.""" - super().__init__() + super().__init__(nInputPorts=1, nOutputPorts=1, inputType="vtkMultiBlockDataSet", outputType="vtkMultiBlockDataSet") self._clearSelectedAttributeMulti: bool = True self._selectedAttributeMulti: list[ str ] = [] @@ -95,19 +99,56 @@ def a02SelectMultipleAttribute( self: Self, name: str ) -> None: self._selectedAttributeMulti.append( name ) self.Modified() - def applyVtkFilter( + def RequestDataObject( self: Self, - input: vtkMultiBlockDataSet, - ) -> vtkMultiBlockDataSet: - """Apply vtk filter. + request: vtkInformation, + inInfoVec: list[ vtkInformationVector ], + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestDataObject. Args: - input (vtkMultiBlockDataSet): input mesh + request (vtkInformation): Request + inInfoVec (list[vtkInformationVector]): Input objects + outInfoVec (vtkInformationVector): Output objects Returns: - vtkMultiBlockDataSet: output mesh + int: 1 if calculation successfully ended, 0 otherwise. """ + inData = self.GetInputData( inInfoVec, 0, 0 ) + outData = self.GetOutputData( outInfoVec, 0 ) + assert inData is not None + if outData is None or ( not outData.IsA( inData.GetClassName() ) ): + outData = inData.NewInstance() + outInfoVec.GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), outData ) + return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] + + def RequestData( + self: Self, + request: vtkInformation, # noqa: F841 + inInfoVec: list[ vtkInformationVector ], + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestData. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + inputMesh: vtkMultiBlockDataSet = self.GetInputData( inInfoVec, 0, 0 ) + outputMesh: vtkMultiBlockDataSet = self.GetOutputData( outInfoVec, 0 ) + assert inputMesh is not None, "Input server mesh is null." + assert outputMesh is not None, "Output pipeline is null." + filter: FillPartialArrays = FillPartialArrays() - filter.SetInputDataObject( input ) + filter.SetSelectedAttributeMulti( self._selectedAttributeMulti ) + filter.SetInputDataObject( inputMesh ) filter.Update() - return filter.GetOutputDataObject( 0 ) + outputMesh.ShallowCopy( filter.GetOutputDataObject( 0 ) ) + + self._clearSelectedAttributeMulti = True + return 1 From 5633fceca093f44d0a8ce7d18c09c8061d63e208 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Mon, 16 Jun 2025 17:21:58 +0200 Subject: [PATCH 04/58] Add the possibility to choose the atribute to fill without using paraview --- .../geos/mesh/processing/FillPartialArrays.py | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py index 01e25e52..2586bbbf 100644 --- a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py +++ b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py @@ -38,25 +38,21 @@ def __init__( self: Self ) -> None: """Map the properties of a server mesh to a client mesh.""" super().__init__( nInputPorts=1, nOutputPorts=1, inputType="vtkMultiBlockDataSet", outputType="vtkMultiBlockDataSet" ) - self._clearSelectedAttributeMulti: bool = True - self._selectedAttributeMulti: list[ str ] = [] + # Initialisation of the empty list of the selected attribute name + self.SetSelectedAttributeMulti() # logger self.m_logger: Logger = getLogger( "Fill Partial Attributes" ) - def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestInformation. + def SetSelectedAttributeMulti( self: Self, selectedAttributeMulti: list[ str ] = []) -> None: + """Set the list of the attribute name. Args: - port (int): input port - info (vtkInformationVector): info - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. + selectesAttributeMulti (list[str]): list of all the attribute name. + """ - if port == 0: - info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkMultiBlockDataSet" ) - return 1 + self._selectedAttributeMulti: list[ str ] = selectedAttributeMulti + def RequestDataObject( self: Self, @@ -74,12 +70,11 @@ def RequestDataObject( Returns: int: 1 if calculation successfully ended, 0 otherwise. """ - print( "RequestDataObject" ) - inData1 = self.GetInputData( inInfoVec, 0, 0 ) + inData = self.GetInputData( inInfoVec, 0, 0 ) outData = self.GetOutputData( outInfoVec, 0 ) - assert inData1 is not None - if outData is None or ( not outData.IsA( inData1.GetClassName() ) ): - outData = inData1.NewInstance() + assert inData is not None + if outData is None or ( not outData.IsA( inData.GetClassName() ) ): + outData = inData.NewInstance() outInfoVec.GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), outData ) return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] @@ -115,8 +110,8 @@ def RequestData( nbComponents = getNumberOfComponents( outData, attributeName, onPoints ) fillPartialAttributes( outData, attributeName, nbComponents, onPoints ) outData.Modified() - - mess: str = "Partial arrays were successfully completed ." + + mess: str = "Fill Partial arrays were successfully completed ." self.m_logger.info( mess ) except AssertionError as e: mess1: str = "Partial arrays filling failed due to:" @@ -129,5 +124,4 @@ def RequestData( self.m_logger.critical( e, exc_info=True ) return 0 - self._clearSelectedAttributeMulti = True return 1 \ No newline at end of file From 919351b347c5215a2f7d7dd3aa1efc15d47a87c5 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 18 Jun 2025 08:34:17 +0200 Subject: [PATCH 05/58] add the possibility to choose the value to fill in the function fillpartialattribute, it is still nan by default --- .../src/geos/mesh/utils/arrayModifiers.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index 6d9a738c..f5c112ab 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -44,26 +44,27 @@ def fillPartialAttributes( attributeName: str, nbComponents: int, onPoints: bool = False, + value: float = np.nan ) -> bool: - """Fill input partial attribute of multiBlockMesh with nan values. + """Fill input partial attribute of multiBlockMesh with values. 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 (False) or on Cells (True). 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 From 10bfc75f6a555f0a92fb17ad9784914bc04c0f7c Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 18 Jun 2025 08:35:42 +0200 Subject: [PATCH 06/58] add the possibility to choose a value to fill the partial array --- .../geos/mesh/processing/FillPartialArrays.py | 62 ++++++++++++++----- geos-pv/src/PVplugins/PVFillPartialArrays.py | 42 +++++++++++-- 2 files changed, 82 insertions(+), 22 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py index 2586bbbf..bfb8d590 100644 --- a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py +++ b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py @@ -21,6 +21,8 @@ vtkMultiBlockDataSet, ) +import numpy as np + __doc__=""" Fill partial arrays of input mesh. @@ -28,8 +30,25 @@ To use it: -* TODO +.. code-block:: python + + from geos.mesh.processing.FillPartialArrays import FillPartialArrays + + # filter inputs + input_mesh: vtkMultiBlockDataSet + input_attribute: list[str] + # instanciate the filter + filter: FillPartialArrays = FillPartialArrays() + # set the list of the selected atribute to fill + filter.SetSelectedAttributeMulti( input_attribute ) + # set the mesh + filter.SetInputDataObject( input_mesh ) + # do calculations + filter.Update() + + # get output object + output: vtkMultiBlockDataSet = filter.GetOutputDataObject( 0 ) ) """ class FillPartialArrays( VTKPythonAlgorithmBase ): @@ -38,21 +57,14 @@ def __init__( self: Self ) -> None: """Map the properties of a server mesh to a client mesh.""" super().__init__( nInputPorts=1, nOutputPorts=1, inputType="vtkMultiBlockDataSet", outputType="vtkMultiBlockDataSet" ) - # Initialisation of the empty list of the selected attribute name - self.SetSelectedAttributeMulti() - - # logger - self.m_logger: Logger = getLogger( "Fill Partial Attributes" ) + # initialisation of empty list of selected attribute name + self._SetSelectedAttributeMulti() - def SetSelectedAttributeMulti( self: Self, selectedAttributeMulti: list[ str ] = []) -> None: - """Set the list of the attribute name. + # initialisation of the value to fill in the partial attribute + self._SetValueToFill() - Args: - selectesAttributeMulti (list[str]): list of all the attribute name. - - """ - self._selectedAttributeMulti: list[ str ] = selectedAttributeMulti - + # logger + self.m_logger: Logger = getLogger( "Fill Partial Attributes" ) def RequestDataObject( self: Self, @@ -108,7 +120,7 @@ def RequestData( for onPoints in (False, True): if isAttributeInObject(outData, attributeName, onPoints): nbComponents = getNumberOfComponents( outData, attributeName, onPoints ) - fillPartialAttributes( outData, attributeName, nbComponents, onPoints ) + fillPartialAttributes( outData, attributeName, nbComponents, onPoints, self._value ) outData.Modified() mess: str = "Fill Partial arrays were successfully completed ." @@ -124,4 +136,22 @@ def RequestData( self.m_logger.critical( e, exc_info=True ) return 0 - return 1 \ No newline at end of file + return 1 + + def _SetSelectedAttributeMulti( self: Self, selectedAttributeMulti: list[ str ] = []) -> None: + """Set the list of the attribute name. + + Args: + selectesAttributeMulti (list[str], optional): list of all the attribute name. + Defaults to an empty list. + """ + self._selectedAttributeMulti: list[ str ] = selectedAttributeMulti + + def _SetValueToFill( self: Self, value: float = np.nan ) -> None: + """Set the value to fill in the partial attribute. + + Args: + value (float, optional): value to fill in the partial attribute. + Defaults to nan. + """ + self._value: float = value \ No newline at end of file diff --git a/geos-pv/src/PVplugins/PVFillPartialArrays.py b/geos-pv/src/PVplugins/PVFillPartialArrays.py index b4481e85..a697e75c 100644 --- a/geos-pv/src/PVplugins/PVFillPartialArrays.py +++ b/geos-pv/src/PVplugins/PVFillPartialArrays.py @@ -6,6 +6,7 @@ from pathlib import Path from typing_extensions import Self +import numpy as np from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] VTKPythonAlgorithmBase, smdomain, smhint, smproperty, smproxy, @@ -60,13 +61,15 @@ def __init__( self: Self,) -> None: self._clearSelectedAttributeMulti: bool = True self._selectedAttributeMulti: list[ str ] = [] + self._doubleSingle: float = np.nan + @smproperty.stringvector( name="SelectMultipleAttribute", label="Select Multiple Attribute", repeat_command=1, number_of_elements_per_command="1", element_types="2", - default_values="", + default_values="N/A", panel_visibility="default", ) @smdomain.xml( """ @@ -85,8 +88,10 @@ def __init__( self: Self,) -> None: function from tag from filter @smdomain.xml. Attribute type is defined by keyword `attribute_type`: Scalars or Vectors + + + """ ) - def a02SelectMultipleAttribute( self: Self, name: str ) -> None: """Set selected attribute name. @@ -95,9 +100,33 @@ def a02SelectMultipleAttribute( self: Self, name: str ) -> None: """ if self._clearSelectedAttributeMulti: self._selectedAttributeMulti.clear() - self._clearSelectedAttributeMulti = False - self._selectedAttributeMulti.append( name ) - self.Modified() + self._clearSelectedAttributeMulti = False + + if name != "N/A": + self._selectedAttributeMulti.append( name ) + self.Modified() + + @smproperty.stringvector( + name="StringSingle", + label="Value to fill", + number_of_elements="1", + default_values="nan", + panel_visibility="default", + ) + def a01StringSingle( self: Self, value: str, ) -> None: + """Define an input string field. + + Args: + value (str): Input + """ + if value == "nan": + value = np.nan + else: + value = float( value ) + + if value != self._doubleSingle: + self._doubleSingle = value + self.Modified() def RequestDataObject( self: Self, @@ -145,7 +174,8 @@ def RequestData( assert outputMesh is not None, "Output pipeline is null." filter: FillPartialArrays = FillPartialArrays() - filter.SetSelectedAttributeMulti( self._selectedAttributeMulti ) + filter._SetSelectedAttributeMulti( self._selectedAttributeMulti ) + filter._SetValueToFill( self._doubleSingle ) filter.SetInputDataObject( inputMesh ) filter.Update() outputMesh.ShallowCopy( filter.GetOutputDataObject( 0 ) ) From 839821a977903b91db856882395d7f6488fbcb47 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 18 Jun 2025 10:14:18 +0200 Subject: [PATCH 07/58] add the possibility to choose the value to fill in the partial attributes in the fillallpartialattributes function --- geos-mesh/src/geos/mesh/utils/arrayModifiers.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index f5c112ab..dfd0e091 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -46,7 +46,7 @@ def fillPartialAttributes( onPoints: bool = False, value: float = np.nan ) -> bool: - """Fill input partial attribute of multiBlockMesh with values. + """Fill input partial attribute of multiBlockMesh with values (defaults to nan). Args: multiBlockMesh (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): multiBlock @@ -73,23 +73,24 @@ def fillPartialAttributes( def fillAllPartialAttributes( multiBlockMesh: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], onPoints: bool = False, + value: float = np.nan ) -> bool: - """Fill all the partial attributes of multiBlockMesh with nan values. + """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 (False) or on Cells (True). 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 From 870fb2546fc789c993ed1341db31e9171726d5b2 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 18 Jun 2025 10:40:05 +0200 Subject: [PATCH 08/58] Update the documentation and upgrade the name of the variables --- .../geos/mesh/processing/FillPartialArrays.py | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py index bfb8d590..2bdffaf4 100644 --- a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py +++ b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py @@ -24,9 +24,10 @@ import numpy as np __doc__=""" -Fill partial arrays of input mesh. +Fill partial arrays of input mesh with values (defaults to nan). +Several attributes can be fill in the same time but with the same value. -Input and output are vtkMultiBlockDataSet. +Input and output mesh are vtkMultiBlockDataSet. To use it: @@ -36,15 +37,18 @@ # filter inputs input_mesh: vtkMultiBlockDataSet - input_attribute: list[str] + input_attributesNameList: list[str] + input_valueToFill: float, optional defaults to nan - # instanciate the filter + # Instanciate the filter filter: FillPartialArrays = FillPartialArrays() - # set the list of the selected atribute to fill - filter.SetSelectedAttributeMulti( input_attribute ) - # set the mesh + # Set the list of the partial atributes to fill + filter._SetAttributesNameList( input_attribute ) + # Set the value to fill in the partial attributes if not nan + filter._SetValueToFill( input_valueToFill ) + # Set the mesh filter.SetInputDataObject( input_mesh ) - # do calculations + # Do calculations filter.Update() # get output object @@ -57,13 +61,13 @@ def __init__( self: Self ) -> None: """Map the properties of a server mesh to a client mesh.""" super().__init__( nInputPorts=1, nOutputPorts=1, inputType="vtkMultiBlockDataSet", outputType="vtkMultiBlockDataSet" ) - # initialisation of empty list of selected attribute name - self._SetSelectedAttributeMulti() + # Initialisation of an empty list of the attribute's name + self._SetAttributesNameList() - # initialisation of the value to fill in the partial attribute + # Initialisation of the value (nan) to fill in the partial attributes self._SetValueToFill() - # logger + # Logger self.m_logger: Logger = getLogger( "Fill Partial Attributes" ) def RequestDataObject( @@ -115,12 +119,12 @@ def RequestData( assert outData is not None, "Output pipeline is null." outData.ShallowCopy( inputMesh ) - for attributeName in self._selectedAttributeMulti: + for attributeName in self._attributesNameList: # cell and point arrays for onPoints in (False, True): if isAttributeInObject(outData, attributeName, onPoints): nbComponents = getNumberOfComponents( outData, attributeName, onPoints ) - fillPartialAttributes( outData, attributeName, nbComponents, onPoints, self._value ) + fillPartialAttributes( outData, attributeName, nbComponents, onPoints, self._valueToFill ) outData.Modified() mess: str = "Fill Partial arrays were successfully completed ." @@ -138,20 +142,21 @@ def RequestData( return 1 - def _SetSelectedAttributeMulti( self: Self, selectedAttributeMulti: list[ str ] = []) -> None: - """Set the list of the attribute name. + def _SetAttributesNameList( self: Self, attributesNameList: list[ str ] = []) -> None: + """Set the list of the partial attributes to fill. Args: - selectesAttributeMulti (list[str], optional): list of all the attribute name. + attributesNameList (list[str], optional): list of all the attributes name. Defaults to an empty list. """ - self._selectedAttributeMulti: list[ str ] = selectedAttributeMulti + self._attributesNameList: list[ str ] = attributesNameList - def _SetValueToFill( self: Self, value: float = np.nan ) -> None: + def _SetValueToFill( self: Self, valueToFill: float = np.nan ) -> None: """Set the value to fill in the partial attribute. Args: - value (float, optional): value to fill in the partial attribute. + valueToFill (float, optional): value to fill in the partial attribute. Defaults to nan. """ - self._value: float = value \ No newline at end of file + self._valueToFill: float = valueToFill + From 798003c2e0cc73e55fc8b610f59e86730be0148e Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 20 Jun 2025 14:39:13 +0200 Subject: [PATCH 09/58] Small modifications of the doc --- .../geos/mesh/processing/FillPartialArrays.py | 2 +- geos-pv/src/PVplugins/PVFillPartialArrays.py | 41 +++++++++++-------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py index 2bdffaf4..e4492492 100644 --- a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py +++ b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py @@ -127,7 +127,7 @@ def RequestData( fillPartialAttributes( outData, attributeName, nbComponents, onPoints, self._valueToFill ) outData.Modified() - mess: str = "Fill Partial arrays were successfully completed ." + mess: str = "Fill Partial arrays were successfully completed. " + str(self._attributesNameList) + " filled with value " + str(self._valueToFill) self.m_logger.info( mess ) except AssertionError as e: mess1: str = "Partial arrays filling failed due to:" diff --git a/geos-pv/src/PVplugins/PVFillPartialArrays.py b/geos-pv/src/PVplugins/PVFillPartialArrays.py index a697e75c..74058158 100644 --- a/geos-pv/src/PVplugins/PVFillPartialArrays.py +++ b/geos-pv/src/PVplugins/PVFillPartialArrays.py @@ -40,6 +40,7 @@ * Load the module in Paraview: Tools>Manage Plugins...>Load new>PVFillPartialArrays. * Select the input mesh. * Select the partial arrays to fill. +* Set the value to fill (optinal defaults to nan). * Apply. """ @@ -58,14 +59,16 @@ def __init__( self: Self,) -> None: """Map the properties of a server mesh to a client mesh.""" super().__init__(nInputPorts=1, nOutputPorts=1, inputType="vtkMultiBlockDataSet", outputType="vtkMultiBlockDataSet") + # Initialisation of an empty list of the attribute's name self._clearSelectedAttributeMulti: bool = True - self._selectedAttributeMulti: list[ str ] = [] + self._attributesNameList: list[ str ] = [] - self._doubleSingle: float = np.nan + # Initialisation of the value (nan) to fill in the partial attributes + self._valueToFill: float = np.nan @smproperty.stringvector( name="SelectMultipleAttribute", - label="Select Multiple Attribute", + label="Select Attributes to fill", repeat_command=1, number_of_elements_per_command="1", element_types="2", @@ -82,28 +85,25 @@ def __init__( self: Self,) -> None: - Select a unique attribute from all the scalars cell attributes from input object. - Input object is defined by its name Input that must corresponds to the name in @smproperty.input - Attribute support is defined by input_domain_name: inputs_array (all arrays) or user defined - function from tag from filter @smdomain.xml. - Attribute type is defined by keyword `attribute_type`: Scalars or Vectors + Select all the attributes to fill. If several attributes + are selected, they will be fill with the same value. """ ) def a02SelectMultipleAttribute( self: Self, name: str ) -> None: - """Set selected attribute name. + """Set the list of the names of the selected attributes to fill. Args: name (str): Input value """ if self._clearSelectedAttributeMulti: - self._selectedAttributeMulti.clear() + self._attributesNameList.clear() self._clearSelectedAttributeMulti = False if name != "N/A": - self._selectedAttributeMulti.append( name ) + self._attributesNameList.append( name ) self.Modified() @smproperty.stringvector( @@ -113,19 +113,28 @@ def a02SelectMultipleAttribute( self: Self, name: str ) -> None: default_values="nan", panel_visibility="default", ) + @smdomain.xml( """ + + Enter the value to fill in the partial attributes. The + default value is nan + + """ ) def a01StringSingle( self: Self, value: str, ) -> None: - """Define an input string field. + """Set the value to fill in the attributes. Args: value (str): Input """ + assert value is not None, "Enter a number or nan" + assert "," not in value, "Use '.' not ',' for decimal numbers" + if value == "nan": value = np.nan else: value = float( value ) - if value != self._doubleSingle: - self._doubleSingle = value + if value != self._valueToFill: + self._valueToFill = value self.Modified() def RequestDataObject( @@ -174,8 +183,8 @@ def RequestData( assert outputMesh is not None, "Output pipeline is null." filter: FillPartialArrays = FillPartialArrays() - filter._SetSelectedAttributeMulti( self._selectedAttributeMulti ) - filter._SetValueToFill( self._doubleSingle ) + filter._SetAttributesNameList( self._attributesNameList ) + filter._SetValueToFill( self._valueToFill ) filter.SetInputDataObject( inputMesh ) filter.Update() outputMesh.ShallowCopy( filter.GetOutputDataObject( 0 ) ) From 1494d5b6d6ec953db4e8032b00aadf5992870955 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 20 Jun 2025 14:39:43 +0200 Subject: [PATCH 10/58] Update of the tests --- .../src/geos/mesh/utils/arrayModifiers.py | 4 +- geos-mesh/tests/test_arrayModifiers.py | 109 ++++++++++++------ 2 files changed, 77 insertions(+), 36 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index dfd0e091..ffb030b1 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -53,7 +53,7 @@ def fillPartialAttributes( 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 (True). + 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. @@ -80,7 +80,7 @@ def fillAllPartialAttributes( Args: multiBlockMesh (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): multiBlockMesh where to fill the attribute - onPoints (bool, optional): Attribute is on Points (False) or on Cells (True). + 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. diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index 5f90bb13..1e5abc14 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -22,61 +22,102 @@ 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 ), + ( "CellAttribute", 3, False, 2. ), + ( "PointAttribute", 3, True, 2. ), + ( "CELL_MARKERS", 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 = vtkMultiBlockDataSetTestRef.GetBlock( block_id ) + dataset: 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" ) ), + 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 : + if np.isnan(value_test): + assert np.all(np.isnan(array) == np.isnan(expected_array)) + else: + assert (array == expected_array).all() + + +@pytest.mark.parametrize( "onpoints, attributesList, value_test", [ + ( True, ( (0, "PointAttribute", 3), (1, "collocated_nodes", 2) ), np.nan ), + ( False, ( (0, "CELL_MARKERS", 1), (0, "CellAttribute", 3), (0, "FAULT", 1), (0, "PERM", 3), (0, "PORO", 1) ), np.nan ), + ( True, ( (0, "PointAttribute", 3), (1, "collocated_nodes", 2) ), 2. ), + ( False, ( (0, "CELL_MARKERS", 1), (0, "CellAttribute", 3), (0, "FAULT", 1), (0, "PERM", 3), (0, "PORO", 1) ), 2. ), ] ) def test_fillAllPartialAttributes( dataSetTest: vtkMultiBlockDataSet, onpoints: bool, - expectedArrays: tuple[ str, ...], + attributesList: tuple[ (int, str, int), ...], + value_test: float, ) -> None: - """Test filling all partial attributes from a multiblock with nan values.""" + """Test filling all partial attributes from a multiblock with values.""" + vtkMultiBlockDataSetTestRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) 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() ) + nbBlock: int = vtkMultiBlockDataSetTestRef.GetNumberOfBlocks() + for block_id in range( nbBlock ): + datasetRef: vtkDataSet = vtkMultiBlockDataSetTestRef.GetBlock( block_id ) + dataset: vtkDataSet = vtkMultiBlockDataSetTest.GetBlock( block_id ) + expected_array: npt.NDArray[ np.float64 ] + array: npt.NDArray[ np.float64 ] + dataRef: Union[ vtkPointData, vtkCellData ] data: Union[ vtkPointData, vtkCellData ] + nbElements: list[ int, int] if onpoints: + dataRef = datasetRef.GetPointData() data = dataset.GetPointData() + nbElements = [212, 4092] else: + dataRef = datasetRef.GetCellData() data = dataset.GetCellData() - - for attribute in expectedArrays: - assert data.HasArray( attribute ) == 1 - - iter.GoToNextItem() + nbElements = [156, 1740] + + for inBlock, attribute, nbComponents in attributesList: + array = vnp.vtk_to_numpy( data.GetArray( attribute ) ) + print(block_id) + if block_id == inBlock : + expected_array = vnp.vtk_to_numpy( dataRef.GetArray( attribute ) ) + assert (array == expected_array).all() + else: + expected_array = np.array([[value_test for i in range( nbComponents )] for _ in range(nbElements[inBlock])]) + + if np.isnan(value_test): + assert np.all(np.isnan(array) == np.isnan(expected_array)) + else: + assert (array == expected_array).all() @pytest.mark.parametrize( "attributeName, dataType, expectedDatatypeArray", [ From f69e853e3a76b40c358f2b432623fd60dc4546d8 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 20 Jun 2025 15:29:29 +0200 Subject: [PATCH 11/58] Formatting for the ci --- .../geos/mesh/processing/FillPartialArrays.py | 19 +++++++------- geos-mesh/tests/conftest.py | 2 +- geos-mesh/tests/test_arrayModifiers.py | 25 +++++++++---------- geos-pv/src/PVplugins/PVFillPartialArrays.py | 20 +++++++-------- 4 files changed, 32 insertions(+), 34 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py index e4492492..48fe28a9 100644 --- a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py +++ b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py @@ -3,6 +3,7 @@ # SPDX-FileContributor: Romain Baville, Martin Lemay from typing_extensions import Self +from typing import Union from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from geos.utils.Logger import Logger, getLogger @@ -68,7 +69,7 @@ def __init__( self: Self ) -> None: self._SetValueToFill() # Logger - self.m_logger: Logger = getLogger( "Fill Partial Attributes" ) + self.m_logger: Logger = getLogger( "Fill Partial Attributes" ) def RequestDataObject( self: Self, @@ -126,8 +127,8 @@ def RequestData( nbComponents = getNumberOfComponents( outData, attributeName, onPoints ) fillPartialAttributes( outData, attributeName, nbComponents, onPoints, self._valueToFill ) outData.Modified() - - mess: str = "Fill Partial arrays were successfully completed. " + str(self._attributesNameList) + " filled with value " + str(self._valueToFill) + + mess: str = "Fill Partial arrays were successfully completed. " + str(self._attributesNameList) + " filled with value " + str(self._valueToFill) self.m_logger.info( mess ) except AssertionError as e: mess1: str = "Partial arrays filling failed due to:" @@ -141,16 +142,16 @@ def RequestData( return 0 return 1 - - def _SetAttributesNameList( self: Self, attributesNameList: list[ str ] = []) -> None: + + def _SetAttributesNameList( self: Self, attributesNameList: Union[ list[ str ], tuple ] = () ) -> None: """Set the list of the partial attributes to fill. Args: - attributesNameList (list[str], optional): list of all the attributes name. - Defaults to an empty list. + attributesNameList (Union[list[str], tuple], optional): list of all the attributes name. + Defaults to a empty list """ - self._attributesNameList: list[ str ] = attributesNameList - + self._attributesNameList: Union[ list[ str ], tuple ] = attributesNameList + def _SetValueToFill( self: Self, valueToFill: float = np.nan ) -> None: """Set the value to fill in the partial attribute. diff --git a/geos-mesh/tests/conftest.py b/geos-mesh/tests/conftest.py index 56a1de08..dd2bdc6e 100644 --- a/geos-mesh/tests/conftest.py +++ b/geos-mesh/tests/conftest.py @@ -51,4 +51,4 @@ def _get_dataset( datasetType: str ): return reader.GetOutput() - return _get_dataset \ No newline at end of file + return _get_dataset diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index 1e5abc14..7aba09ba 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -22,11 +22,11 @@ from geos.mesh.utils import arrayModifiers -@pytest.mark.parametrize( "attributeName, nbComponents, onpoints, value_test", [ - ( "CellAttribute", 3, False, np.nan ), +@pytest.mark.parametrize( "attributeName, nbComponents, onpoints, value_test", [ + ( "CellAttribute", 3, False, np.nan ), ( "PointAttribute", 3, True, np.nan ), - ( "CELL_MARKERS", 1, False, np.nan ), - ( "CellAttribute", 3, False, 2. ), + ( "CELL_MARKERS", 1, False, np.nan ), + ( "CellAttribute", 3, False, 2. ), ( "PointAttribute", 3, True, 2. ), ( "CELL_MARKERS", 1, False, 2. ), ] ) @@ -44,8 +44,8 @@ def test_fillPartialAttributes( nbBlock: int = vtkMultiBlockDataSetTestRef.GetNumberOfBlocks() for block_id in range( nbBlock ): - datasetRef: vtkDataSet = vtkMultiBlockDataSetTestRef.GetBlock( block_id ) - dataset: vtkDataSet = vtkMultiBlockDataSetTest.GetBlock( block_id ) + 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: @@ -60,7 +60,7 @@ def test_fillPartialAttributes( 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 : @@ -79,7 +79,7 @@ def test_fillPartialAttributes( def test_fillAllPartialAttributes( dataSetTest: vtkMultiBlockDataSet, onpoints: bool, - attributesList: tuple[ (int, str, int), ...], + attributesList: tuple[ tuple[ int, str, int ], ...], value_test: float, ) -> None: """Test filling all partial attributes from a multiblock with values.""" @@ -89,13 +89,13 @@ def test_fillAllPartialAttributes( nbBlock: int = vtkMultiBlockDataSetTestRef.GetNumberOfBlocks() for block_id in range( nbBlock ): - datasetRef: vtkDataSet = vtkMultiBlockDataSetTestRef.GetBlock( block_id ) - dataset: vtkDataSet = vtkMultiBlockDataSetTest.GetBlock( block_id ) + 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 ] dataRef: Union[ vtkPointData, vtkCellData ] data: Union[ vtkPointData, vtkCellData ] - nbElements: list[ int, int] + nbElements: list[ int ] if onpoints: dataRef = datasetRef.GetPointData() data = dataset.GetPointData() @@ -107,13 +107,12 @@ def test_fillAllPartialAttributes( for inBlock, attribute, nbComponents in attributesList: array = vnp.vtk_to_numpy( data.GetArray( attribute ) ) - print(block_id) if block_id == inBlock : expected_array = vnp.vtk_to_numpy( dataRef.GetArray( attribute ) ) assert (array == expected_array).all() else: expected_array = np.array([[value_test for i in range( nbComponents )] for _ in range(nbElements[inBlock])]) - + if np.isnan(value_test): assert np.all(np.isnan(array) == np.isnan(expected_array)) else: diff --git a/geos-pv/src/PVplugins/PVFillPartialArrays.py b/geos-pv/src/PVplugins/PVFillPartialArrays.py index 74058158..468fdf8b 100644 --- a/geos-pv/src/PVplugins/PVFillPartialArrays.py +++ b/geos-pv/src/PVplugins/PVFillPartialArrays.py @@ -59,7 +59,7 @@ def __init__( self: Self,) -> None: """Map the properties of a server mesh to a client mesh.""" super().__init__(nInputPorts=1, nOutputPorts=1, inputType="vtkMultiBlockDataSet", outputType="vtkMultiBlockDataSet") - # Initialisation of an empty list of the attribute's name + # Initialisation of an empty list of the attribute's name self._clearSelectedAttributeMulti: bool = True self._attributesNameList: list[ str ] = [] @@ -85,7 +85,7 @@ def __init__( self: Self,) -> None: - Select all the attributes to fill. If several attributes + Select all the attributes to fill. If several attributes are selected, they will be fill with the same value. @@ -119,7 +119,7 @@ def a02SelectMultipleAttribute( self: Self, name: str ) -> None: default value is nan """ ) - def a01StringSingle( self: Self, value: str, ) -> None: + def a01StringSingle( self: Self, value: str ) -> None: """Set the value to fill in the attributes. Args: @@ -128,13 +128,11 @@ def a01StringSingle( self: Self, value: str, ) -> None: assert value is not None, "Enter a number or nan" assert "," not in value, "Use '.' not ',' for decimal numbers" - if value == "nan": - value = np.nan - else: - value = float( value ) - - if value != self._valueToFill: - self._valueToFill = value + value_float: float + value_float = np.nan if value == "nan" else float(value) + + if value_float != self._valueToFill: + self._valueToFill = value_float self.Modified() def RequestDataObject( @@ -181,7 +179,7 @@ def RequestData( outputMesh: vtkMultiBlockDataSet = self.GetOutputData( outInfoVec, 0 ) assert inputMesh is not None, "Input server mesh is null." assert outputMesh is not None, "Output pipeline is null." - + filter: FillPartialArrays = FillPartialArrays() filter._SetAttributesNameList( self._attributesNameList ) filter._SetValueToFill( self._valueToFill ) From 4d9468e6348a6741468bb7442bd6ebc3c7131c9c Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 20 Jun 2025 17:13:58 +0200 Subject: [PATCH 12/58] Add the test file for the FillPartialArrays vtk filter --- geos-mesh/tests/test_FillPartialArrays.py | 74 +++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 geos-mesh/tests/test_FillPartialArrays.py diff --git a/geos-mesh/tests/test_FillPartialArrays.py b/geos-mesh/tests/test_FillPartialArrays.py new file mode 100644 index 00000000..174e0ec0 --- /dev/null +++ b/geos-mesh/tests/test_FillPartialArrays.py @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Romain Baville +# SPDX-License-Identifier: Apache 2.0 +# ruff: noqa: E402 # disable Module level import not at top of file +# mypy: disable-error-code="operator" +import pytest +from typing import Union, Tuple, cast + +import numpy as np +import numpy.typing as npt + +import vtkmodules.util.numpy_support as vnp +from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet, vtkPointData, + vtkCellData ) + +from geos.mesh.processing.FillPartialArrays import FillPartialArrays + + +@pytest.mark.parametrize( "onpoints, attributesList, value_test", [ + ( False, ( (0, "PORO", 1), ), np.nan ), + ( True, ( (0, "PointAttribute", 3), (1, "collocated_nodes", 2) ), 2. ), + ( False, ( (0, "CELL_MARKERS", 1), (0, "CellAttribute", 3), (0, "FAULT", 1), (0, "PERM", 3), (0, "PORO", 1) ), 2. ), + ( False, ( (0, "PORO", 1), ), 2.0 ), + ( True, ( (0, "PointAttribute", 3), (1, "collocated_nodes", 2) ), np.nan ), + ( False, ( (0, "CELL_MARKERS", 1), (0, "CellAttribute", 3), (0, "FAULT", 1), (0, "PERM", 3), (0, "PORO", 1) ), np.nan ), +] ) +def test_FillPartialArrays( + dataSetTest: vtkMultiBlockDataSet, + onpoints: bool, + attributesList: Tuple[ Tuple[ int, str, int ], ...], + value_test: float, +) -> None: + """Test FillPartialArrays vtk filter.""" + vtkMultiBlockDataSetTestRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + vtkMultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + attributesNameList: list[ str ] = [ attributesList[ i ][ 1 ] for i in range( len( attributesList ) ) ] + + filter: FillPartialArrays = FillPartialArrays() + filter._SetAttributesNameList( attributesNameList ) + filter._SetValueToFill( value_test ) + filter.SetInputDataObject( vtkMultiBlockDataSetTest ) + filter.Update() + + nbBlock: int = vtkMultiBlockDataSetTestRef.GetNumberOfBlocks() + for block_id in range( nbBlock ): + datasetRef: vtkDataSet = cast( vtkDataSet, vtkMultiBlockDataSetTestRef.GetBlock( block_id ) ) + dataset: vtkDataSet = cast( vtkDataSet, filter.GetOutputDataObject( 0 ).GetBlock( block_id ) ) + expected_array: npt.NDArray[ np.float64 ] + array: npt.NDArray[ np.float64 ] + dataRef: Union[ vtkPointData, vtkCellData ] + data: Union[ vtkPointData, vtkCellData ] + nbElements: list[ int ] + if onpoints: + dataRef = datasetRef.GetPointData() + data = dataset.GetPointData() + nbElements = [212, 4092] + else: + dataRef = datasetRef.GetCellData() + data = dataset.GetCellData() + nbElements = [156, 1740] + + for inBlock, attribute, nbComponents in attributesList: + array = vnp.vtk_to_numpy( data.GetArray( attribute ) ) + if block_id == inBlock : + expected_array = vnp.vtk_to_numpy( dataRef.GetArray( attribute ) ) + assert (array == expected_array).all() + else: + expected_array = np.array([[value_test for i in range( nbComponents )] for _ in range(nbElements[inBlock])]) + if np.isnan(value_test): + assert np.all(np.isnan(array) == np.isnan(expected_array)) + else: + assert (array == expected_array).all() + From d44bc56f88b2b45ccb3c017acff8f378244855cd Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 20 Jun 2025 17:23:45 +0200 Subject: [PATCH 13/58] Formatting for the ci --- .../geos/mesh/processing/FillPartialArrays.py | 29 +++++---- geos-mesh/tests/test_FillPartialArrays.py | 35 +++++------ geos-mesh/tests/test_arrayModifiers.py | 60 +++++++++++-------- geos-pv/src/PVplugins/PVFillPartialArrays.py | 16 ++--- 4 files changed, 77 insertions(+), 63 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py index 48fe28a9..e43ffc47 100644 --- a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py +++ b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py @@ -3,7 +3,7 @@ # SPDX-FileContributor: Romain Baville, Martin Lemay from typing_extensions import Self -from typing import Union +from typing import Union, Tuple from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase from geos.utils.Logger import Logger, getLogger @@ -19,12 +19,11 @@ ) from vtkmodules.vtkCommonDataModel import ( - vtkMultiBlockDataSet, -) + vtkMultiBlockDataSet, ) import numpy as np -__doc__=""" +__doc__ = """ Fill partial arrays of input mesh with values (defaults to nan). Several attributes can be fill in the same time but with the same value. @@ -44,7 +43,7 @@ # Instanciate the filter filter: FillPartialArrays = FillPartialArrays() # Set the list of the partial atributes to fill - filter._SetAttributesNameList( input_attribute ) + filter._SetAttributesNameList( input_attributesNameList ) # Set the value to fill in the partial attributes if not nan filter._SetValueToFill( input_valueToFill ) # Set the mesh @@ -56,11 +55,15 @@ output: vtkMultiBlockDataSet = filter.GetOutputDataObject( 0 ) ) """ + class FillPartialArrays( VTKPythonAlgorithmBase ): def __init__( self: Self ) -> None: """Map the properties of a server mesh to a client mesh.""" - super().__init__( nInputPorts=1, nOutputPorts=1, inputType="vtkMultiBlockDataSet", outputType="vtkMultiBlockDataSet" ) + super().__init__( nInputPorts=1, + nOutputPorts=1, + inputType="vtkMultiBlockDataSet", + outputType="vtkMultiBlockDataSet" ) # Initialisation of an empty list of the attribute's name self._SetAttributesNameList() @@ -122,13 +125,14 @@ def RequestData( outData.ShallowCopy( inputMesh ) for attributeName in self._attributesNameList: # cell and point arrays - for onPoints in (False, True): - if isAttributeInObject(outData, attributeName, onPoints): + for onPoints in ( False, True ): + if isAttributeInObject( outData, attributeName, onPoints ): nbComponents = getNumberOfComponents( outData, attributeName, onPoints ) fillPartialAttributes( outData, attributeName, nbComponents, onPoints, self._valueToFill ) outData.Modified() - mess: str = "Fill Partial arrays were successfully completed. " + str(self._attributesNameList) + " filled with value " + str(self._valueToFill) + mess: str = "Fill Partial arrays were successfully completed. " + str( + self._attributesNameList ) + " filled with value " + str( self._valueToFill ) self.m_logger.info( mess ) except AssertionError as e: mess1: str = "Partial arrays filling failed due to:" @@ -143,14 +147,14 @@ def RequestData( return 1 - def _SetAttributesNameList( self: Self, attributesNameList: Union[ list[ str ], tuple ] = () ) -> None: + def _SetAttributesNameList( self: Self, attributesNameList: Union[ list[ str ], Tuple ] = () ) -> None: """Set the list of the partial attributes to fill. Args: - attributesNameList (Union[list[str], tuple], optional): list of all the attributes name. + attributesNameList (Union[list[str], Tuple], optional): list of all the attributes name. Defaults to a empty list """ - self._attributesNameList: Union[ list[ str ], tuple ] = attributesNameList + self._attributesNameList: Union[ list[ str ], Tuple ] = attributesNameList def _SetValueToFill( self: Self, valueToFill: float = np.nan ) -> None: """Set the value to fill in the partial attribute. @@ -160,4 +164,3 @@ def _SetValueToFill( self: Self, valueToFill: float = np.nan ) -> None: Defaults to nan. """ self._valueToFill: float = valueToFill - diff --git a/geos-mesh/tests/test_FillPartialArrays.py b/geos-mesh/tests/test_FillPartialArrays.py index 174e0ec0..31af5702 100644 --- a/geos-mesh/tests/test_FillPartialArrays.py +++ b/geos-mesh/tests/test_FillPartialArrays.py @@ -11,19 +11,20 @@ import numpy.typing as npt import vtkmodules.util.numpy_support as vnp -from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet, vtkPointData, - vtkCellData ) +from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet, vtkPointData, vtkCellData ) from geos.mesh.processing.FillPartialArrays import FillPartialArrays @pytest.mark.parametrize( "onpoints, attributesList, value_test", [ - ( False, ( (0, "PORO", 1), ), np.nan ), - ( True, ( (0, "PointAttribute", 3), (1, "collocated_nodes", 2) ), 2. ), - ( False, ( (0, "CELL_MARKERS", 1), (0, "CellAttribute", 3), (0, "FAULT", 1), (0, "PERM", 3), (0, "PORO", 1) ), 2. ), - ( False, ( (0, "PORO", 1), ), 2.0 ), - ( True, ( (0, "PointAttribute", 3), (1, "collocated_nodes", 2) ), np.nan ), - ( False, ( (0, "CELL_MARKERS", 1), (0, "CellAttribute", 3), (0, "FAULT", 1), (0, "PERM", 3), (0, "PORO", 1) ), np.nan ), + ( False, ( ( 0, "PORO", 1 ), ), np.nan ), + ( True, ( ( 0, "PointAttribute", 3 ), ( 1, "collocated_nodes", 2 ) ), 2. ), + ( False, ( ( 0, "CELL_MARKERS", 1 ), ( 0, "CellAttribute", 3 ), ( 0, "FAULT", 1 ), ( 0, "PERM", 3 ), + ( 0, "PORO", 1 ) ), 2. ), + ( False, ( ( 0, "PORO", 1 ), ), 2.0 ), + ( True, ( ( 0, "PointAttribute", 3 ), ( 1, "collocated_nodes", 2 ) ), np.nan ), + ( False, ( ( 0, "CELL_MARKERS", 1 ), ( 0, "CellAttribute", 3 ), ( 0, "FAULT", 1 ), ( 0, "PERM", 3 ), + ( 0, "PORO", 1 ) ), np.nan ), ] ) def test_FillPartialArrays( dataSetTest: vtkMultiBlockDataSet, @@ -54,21 +55,21 @@ def test_FillPartialArrays( if onpoints: dataRef = datasetRef.GetPointData() data = dataset.GetPointData() - nbElements = [212, 4092] + nbElements = [ 212, 4092 ] else: dataRef = datasetRef.GetCellData() data = dataset.GetCellData() - nbElements = [156, 1740] + nbElements = [ 156, 1740 ] for inBlock, attribute, nbComponents in attributesList: array = vnp.vtk_to_numpy( data.GetArray( attribute ) ) - if block_id == inBlock : + if block_id == inBlock: expected_array = vnp.vtk_to_numpy( dataRef.GetArray( attribute ) ) - assert (array == expected_array).all() + assert ( array == expected_array ).all() else: - expected_array = np.array([[value_test for i in range( nbComponents )] for _ in range(nbElements[inBlock])]) - if np.isnan(value_test): - assert np.all(np.isnan(array) == np.isnan(expected_array)) + expected_array = np.array( [ [ value_test for i in range( nbComponents ) ] + for _ in range( nbElements[ inBlock ] ) ] ) + if np.isnan( value_test ): + assert np.all( np.isnan( array ) == np.isnan( expected_array ) ) else: - assert (array == expected_array).all() - + assert ( array == expected_array ).all() diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index 7aba09ba..e82cb263 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -26,10 +26,12 @@ ( "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, @@ -40,7 +42,11 @@ def test_fillPartialAttributes( """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 ) + arrayModifiers.fillPartialAttributes( vtkMultiBlockDataSetTest, + attributeName, + nbComponents, + onPoints=onpoints, + value=value_test ) nbBlock: int = vtkMultiBlockDataSetTestRef.GetNumberOfBlocks() for block_id in range( nbBlock ): @@ -50,42 +56,44 @@ def test_fillPartialAttributes( array: npt.NDArray[ np.float64 ] if onpoints: array = vnp.vtk_to_numpy( dataset.GetPointData().GetArray( attributeName ) ) - if block_id == 0 : + 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)]) + 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 : + 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)]) + 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 : - if np.isnan(value_test): - assert np.all(np.isnan(array) == np.isnan(expected_array)) + 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() + assert ( array == expected_array ).all() @pytest.mark.parametrize( "onpoints, attributesList, value_test", [ - ( True, ( (0, "PointAttribute", 3), (1, "collocated_nodes", 2) ), np.nan ), - ( False, ( (0, "CELL_MARKERS", 1), (0, "CellAttribute", 3), (0, "FAULT", 1), (0, "PERM", 3), (0, "PORO", 1) ), np.nan ), - ( True, ( (0, "PointAttribute", 3), (1, "collocated_nodes", 2) ), 2. ), - ( False, ( (0, "CELL_MARKERS", 1), (0, "CellAttribute", 3), (0, "FAULT", 1), (0, "PERM", 3), (0, "PORO", 1) ), 2. ), + ( True, ( ( 0, "PointAttribute", 3 ), ( 1, "collocated_nodes", 2 ) ), 2. ), + ( False, ( ( 0, "CELL_MARKERS", 1 ), ( 0, "CellAttribute", 3 ), ( 0, "FAULT", 1 ), ( 0, "PERM", 3 ), + ( 0, "PORO", 1 ) ), 2. ), + ( True, ( ( 0, "PointAttribute", 3 ), ( 1, "collocated_nodes", 2 ) ), np.nan ), + ( False, ( ( 0, "CELL_MARKERS", 1 ), ( 0, "CellAttribute", 3 ), ( 0, "FAULT", 1 ), ( 0, "PERM", 3 ), + ( 0, "PORO", 1 ) ), np.nan ), ] ) def test_fillAllPartialAttributes( dataSetTest: vtkMultiBlockDataSet, onpoints: bool, - attributesList: tuple[ tuple[ int, str, int ], ...], + attributesList: Tuple[ Tuple[ int, str, int ], ...], value_test: float, ) -> None: """Test filling all partial attributes from a multiblock with values.""" vtkMultiBlockDataSetTestRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) vtkMultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - arrayModifiers.fillAllPartialAttributes( vtkMultiBlockDataSetTest, onpoints ) + arrayModifiers.fillAllPartialAttributes( vtkMultiBlockDataSetTest, onpoints, value_test ) nbBlock: int = vtkMultiBlockDataSetTestRef.GetNumberOfBlocks() for block_id in range( nbBlock ): @@ -99,24 +107,24 @@ def test_fillAllPartialAttributes( if onpoints: dataRef = datasetRef.GetPointData() data = dataset.GetPointData() - nbElements = [212, 4092] + nbElements = [ 212, 4092 ] else: dataRef = datasetRef.GetCellData() data = dataset.GetCellData() - nbElements = [156, 1740] + nbElements = [ 156, 1740 ] for inBlock, attribute, nbComponents in attributesList: array = vnp.vtk_to_numpy( data.GetArray( attribute ) ) - if block_id == inBlock : + if block_id == inBlock: expected_array = vnp.vtk_to_numpy( dataRef.GetArray( attribute ) ) - assert (array == expected_array).all() + assert ( array == expected_array ).all() else: - expected_array = np.array([[value_test for i in range( nbComponents )] for _ in range(nbElements[inBlock])]) - - if np.isnan(value_test): - assert np.all(np.isnan(array) == np.isnan(expected_array)) + expected_array = np.array( [ [ value_test for i in range( nbComponents ) ] + for _ in range( nbElements[ inBlock ] ) ] ) + if np.isnan( value_test ): + assert np.all( np.isnan( array ) == np.isnan( expected_array ) ) else: - assert (array == expected_array).all() + assert ( array == expected_array ).all() @pytest.mark.parametrize( "attributeName, dataType, expectedDatatypeArray", [ diff --git a/geos-pv/src/PVplugins/PVFillPartialArrays.py b/geos-pv/src/PVplugins/PVFillPartialArrays.py index 468fdf8b..de4475a1 100644 --- a/geos-pv/src/PVplugins/PVFillPartialArrays.py +++ b/geos-pv/src/PVplugins/PVFillPartialArrays.py @@ -13,8 +13,7 @@ ) from vtkmodules.vtkCommonDataModel import ( - vtkMultiBlockDataSet, -) + vtkMultiBlockDataSet, ) from vtkmodules.vtkCommonCore import ( vtkInformation, @@ -55,9 +54,12 @@ ) class PVFillPartialArrays( VTKPythonAlgorithmBase ): - def __init__( self: Self,) -> None: + def __init__( self: Self, ) -> None: """Map the properties of a server mesh to a client mesh.""" - super().__init__(nInputPorts=1, nOutputPorts=1, inputType="vtkMultiBlockDataSet", outputType="vtkMultiBlockDataSet") + super().__init__( nInputPorts=1, + nOutputPorts=1, + inputType="vtkMultiBlockDataSet", + outputType="vtkMultiBlockDataSet" ) # Initialisation of an empty list of the attribute's name self._clearSelectedAttributeMulti: bool = True @@ -125,15 +127,15 @@ def a01StringSingle( self: Self, value: str ) -> None: Args: value (str): Input """ - assert value is not None, "Enter a number or nan" + assert value is not None, "Enter a number or nan" assert "," not in value, "Use '.' not ',' for decimal numbers" value_float: float - value_float = np.nan if value == "nan" else float(value) + value_float = np.nan if value == "nan" else float( value ) if value_float != self._valueToFill: self._valueToFill = value_float - self.Modified() + self.Modified() def RequestDataObject( self: Self, From 5e0da352b1f1dd1c5fad70cab5a54f88522138ba Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 20 Jun 2025 17:48:55 +0200 Subject: [PATCH 14/58] Fix the lin with yapft issue --- .../src/geos/mesh/utils/arrayModifiers.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index ffb030b1..15c69e7a 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -39,13 +39,11 @@ """ -def fillPartialAttributes( - multiBlockMesh: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], - attributeName: str, - nbComponents: int, - onPoints: bool = False, - value: float = np.nan -) -> bool: +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: @@ -70,11 +68,9 @@ def fillPartialAttributes( return True -def fillAllPartialAttributes( - multiBlockMesh: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], - onPoints: bool = False, - value: float = np.nan -) -> bool: +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: From 0ca3fe52c08a988494295ab331e0348e38199a47 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 24 Jun 2025 16:48:09 +0200 Subject: [PATCH 15/58] 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 16/58] 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=eJxt2nmcTvX7+PGxZGyfEhKFJHtJNFQq6Zz3lOaTj/rayj6WyBKSxhLGOkKUZQhDtsuS3diX4T5nwlgjpFLGnn0Lifzu67re1znXH795PD6Pz+txP8597nPe533e5+72jPmuYGYM/g//ov+/bPBxX7rvx22C/iQ+3ZOu3bpR0DExxXRHwk51pOetinGlD1UZFvTzlW8H/XWV7kb6oTKngo7+Lx6z+cn8wXvbf54/OJ6jN/IGx9lvUt7Mc9Mq0jYDm+XNnFY/iY8tJjbz/dxdgg6OmTsStj3maLdNfIL281nLPJn+oFn8uVXzZF56di+99+VZD2Wu6LaHXv/LzZl5+fXnqTv/liOz5N0+tM2UATkyb6Ul8Tm2yZHZd0VPTzo4Bu5I2PYYsN8vTe99rlKOzGqDNtD2WyY/8Memd6LzTW35j5+9qw+N1YiLd/wmg07S9seTb/vBdYx2icEfedLB53JHwrafG+0tQx6l/fSuc9uXa/Hf+rf878Zm0nsbmr/8x+6Nom26OTf9fn+kUFccesP/9PHnqB89fd3v98sg2t58f92f//ZP1A2KXPOvDM9F+3yt0CX/z1+f4Gtd96L/vunuSQfHyR0J2x5ntF8dX4g+6+y0M35kOH/WexPP+AuOjKF9xuU746/rnkzjcK3Iab/wt6Np++Ryp3yZ89hn7tygbZatPOm/2ngn7adgl5N+xdO3jLTMw44fn/QbnOVrnevZE/5T41bZuZTt3+zQPOhwjmWHx08tcyzb//hIGdpPk0HH/aQuM6lL5j7unyo9nI7n4epH/QsvXKf9DO1yxD+WOZu2eb/FYX9V7mG0zdlth/xaucrzHNt6yF8ba+d5tINj4I6EzccwLOmg/8N742ibyAcH/auTk6mXFTvoj57C1zf+w12+X/9Xev3alSx/7vq7/FnJWX6ZFs086XBeZal5laXmVZafZ9Qj9N4GZbP8RS9tpO3XmB1+99R79PoNk+lvvtePen6i7y/K/SLP5w+3+gtj+9L2N2ts9W80nG7nRob/8tZPgg7nTIaaMxlqzmT4To7itM/zczf5xfanUde5vNbfPiIfjec/B9b4jVvlpV5qVvsZrUvRHEgpstpfkSeVtp84KN3vuoDXmZiYdL/ARLnu6eq6p6vrnq6ue7q/o2w5em/jbav89RtG0P5/PLfSvzNsNb23Va1lfsKcRdRnui31F075grrle0v8qw1m2Wu9wL+Rr40nHV7rBepaLwg/N9oVupei9/7ecp6fe2EMfe7VV+b5856pQa9fHTDX79tkOPXQ32b4t5qspX12mDzDX9OsF/XxnjP870oWoPG5eDTNr7p+o73uaX7tpC5Bh/MhTc2HNDUf0vzqnQrTZxXNOdlP3V6M9llhRar/wfxb9PriBqm+P+ckHWfsgEn+uVbj6fWkspP8ntcnUF/7baL/3Mpnqd+cMMFf3GSCXU/G+/+MtetetMO5MV7NjfFqboz3y818kvbT7uTXfszUr+l4ip0f65ff/j2vY/PH+gc6D6B+Y9xXflrB07TNhce/9EsNHsrrbUyKP+vNRE86nA8paj6kqPmQ4r939mn63B8LjvCP7a1EfXnwcH/lvLx0Lr+8NNy/V3o77Wfhk8P97S7fm+9NGurP/ugCvX76+BA/duFSOoYt+Qf7z1SeRNvUjh3gz3j1BvW03/v749f0p+3bLe3rF/1nhr1GSf66qx2DDq9dkrp2SeraJfnz4/kZ0XZJt2Atxe7wZw7q81O7+qtz8dye0rirn2/1cOoXn+vip64eRO+t6XT2C47kuTftTEd/49kHtE2fSe39P2b/S702o53/0qWR1PeGtPXPVIiNl5Y1eVDjtv6Ahjnt+Cf6Wb7cj4lq/BPV+Ceq8U/0P8zLa4LboYVf5Xe+7yY1beqffoivtVeokT+21G+0TVZaQ79gZX5GTB/xf763cBq9/vqSBD/HknJ2bUzwMxrIeCao8UxQ45mgxjPBP/znM/Te7e3e8UfX4udU0RqOf/+9/9J5Va/3ij8udiDtJ3v9y/7iZlvsecX5Vwp+EnR4vnHqfOPU+cb5o01R2v+mXtX8Ut/ys7hOq+f9XPXfps/qW7Sqf3XPl7TNM3HP+edX3aXXXy9Rxa/xUnVeA9dU8t9MepveO3tWJb9zF/5eVBzK+ccuLKPtV+x5xp957yXqCRPL+Cvb8dr1brfSfq9GT/N9mlzK75fTPvejHY5VKTVWpdRYlfL/u6EE7Wf/2BL+gHiezzdbFPd75eG1aNjTj/rTslfT/s8/9YhfL34P9aO/F/SPnORn98/nC/gbbvr0+qG9Bfx2kUl2PY/1Fya286TVd0U1nrFqPGP9ZZ14XY0tnNtftrYDne9jZ3P67rXN1E9VuuvdXxih/Xx074Yn+/l18DWv7ak59N6ZzlVvx+/8HeaViVe8zWnH6L13P7/oramYSt3lmwvevnL56JjLNTvvXfxiN72esOGcd3o/r59H15/2/rjOa9Hl09le7bpH5XuI5y3tHrT6fuLdWpwm30+87TN61w23Cb6reMVSeH5+3uYXr0q/DOqCK372Up/PRfd7vyJHvME5a9LrDQdt9q71akrHmf7JJq/l8NHUtTtt9J7tOJ966oLo91m7bmD/9Ehe6tbT13lf5m5C22z8YbVXre0AeZ56/Y/2DVo9Z73Y7+T4072zMZ/VDbcJnrneyb9foGOr+NEKL/ew32n/Yw4v8wqk8DFP/XSp13RBBq+NNxd61xbc5PXqnYXe0F7c+9uDt6jwZOruFcH76MBg6kb9ZnkHXyxJY14036zg+u79aIZX/U4eWqOK1Z7hyXr1xYE070T/IvRe02m6F7tlPh1D7hxTg/fOema8d+fwWZ4/V8Z46y/OoF7caYxXfsX/+H7sOtI7X7ASfz8fkeKVaXBTnkFe7MiP5LmjxirF+22tjFWKN/qfz+uG2wTPI6/G0yXpePpeGOFt2fw9dYFDw72b9Vbwd+kWw7ydf/Aa+FzpoV7B2S/S66N+S/bKL6xLnWt1spf48BLqeq2/8CpvjNBxnmz+hbc35y/Ut2b29ZJmDqFt4lL7eGWG7uI5vLWndzCO7812eXp63X97iF4f+34Pb9apqdT7y/Tw5oxeRtt8e7CrN+z7E/T6tpgu3uODH9Axt/q1o1eyHY9/q10dvbv5b9M2Y57t6N3Kd42Ov9extt6/Xz9L+3lyTluv//VV1GduJ3of7spDc3LU2URv7LUB9N5CvVt7nWZtp36xZGuvcO2D/PxyWnkn0m/Qe8dVauUdzuLv0o0nNvN+nrefXr9T/APv5UOL6fWSdZp6Vy63p/2sOtPE67d4A3XZrxt7268l0/a7yzb2furB87PZB+95+a6O4e9v39f3XnjzQ9pmXbn63sfrjtB7N5R/11twYiz1D68leOPe4rkas6uOd/exVOp5U2t76yrzM7pzyVe8puOv0Ovv7IjzVpTJ5u1j4rzJA2UNjFPzJ84rOUnmT5yXNOfTuuE2wfPFe207r4dXalUN7nHsWafzUZe9WcWrf46/D8Sdr+Blle/E13FSBW/O1nnUg68+4X3ZLoau3V/nn/AmPxlPr28784h37Ey+eGm5p9o++XBw7xR/8s+I9Kvjfo38+2F+em+3Zj9H+uwrzePQ9OdIj5vP0FhlVTwcSSs2jo65+YxDkccu8lrafO+ByJyFrfmZuP5ApPlivh/v/L0/sir5GvWVP/dH7n61lManVot9kcKf8zG7f+2N7H7J8Pqwenfkxiy+LgU/2h2Z8sRb3C12R/KVX0Qd+/zuSLXBwN9zGu6K7FvM43Oy/q7I2P5naP/Fau+MJNzmdWDOlB2RuDw/0jaL922PVO37B/WobT9ERlVbT9s8esuPVN69g3r8r5FIzPHCPM8TtkVyT9hN59uzTUYk9gJ/bynTICOy7txh2r7T3k2RpYWv8tzYsynyQx2eM8vyb4qML/QxvX52w4bIhE58Xz+SuiESU3cv7efr6tFOvkT7ObNvfWTtwLw0Jo0eWx+Jufodz88S6yIxk9bwOf5nXSS1H6//G55aG4nZWom2eWvsmugxf8/ffy6vjlTr/BTtv+Xy1ZGeO/hZ1m/u6kiOrBF0rct2WR2J2c/73PP2ajxfXn+6pUdivuN7pGrGqsjFEvybQJETqyLlTsl3p/SIeqZEnE+myTMl0urXKxnhNsEzJfJHvSJ0PBveXxU95m30udU/WR6JadOZv3t8tyw6DrNpnzPrRXs5j+GWF5ZGX29Ex7DowWI8Zupnei+KHMrJ92nrVxbh9jRuhWotxP3QZxV5fD6OM+3z9wbzIjGFRvNadH9u9BzrUH87l5r2U+XArEjSlk/ovZNrzoruk78zVGo/A5u2WbBV7unoOjN9Oo4VvX7icnQMvuZ1tdCNbyMxlWLp+G/0oaZj85t/i+dL27zUONox/PtAt5e+xffyfGsyBa8pz5kKU3AMaftaaybj9nQuOWMn45yhba4dmITXnTp32Qk4tjwHHvkmEtPjDvXOv8ZEYsrwOjnQH4WvU895ZBSOCXXxeyNxvlGPNoNwzKk3f/+/iKxLS27kjcgaQn/yWygNUmzY9trRnz0G+rPznP7sWPF+joRtx5z+7LnQn73u9GfnOf3Ze4T/UsO215r+yiSHbcefj6eROoY66nMrqc8qrPZ/1wvSzivez2zV3cIO7hE6tkjM//dP7hHa3lX7UT07bLtu2OMJ265X9vjDtnOb99MobHvf0Z+dG/Rn54M9trDt3ODP+j5su4bw524L294vfAxHwrbznI/nUth2TvJ7c4RzzN47vB+ee7+vzuVID3n8K0eOv8YjExz53CXZEx25jkNem+TIHOj8VrTtXK1fJDr2dv6sajnZkeP5sW+07fxpX2GKI+f7Su9o2/n8TNa3jlyLkSWnOjIf/i8r2vYcm+We5si5DLg/3ZF5Hl1Dgmv+/PoZjszn/gnfOTL/d8XMcuS6P+RF216vn+vNduS+yzgy15HxefvCXEeu0acz5zlyr+28Nc+RsR13GxyZq3NrLXDkPt3+UbTt3NgyZIEj92b8yAVOY5ffm5G90JH763j57x2ZV8c2RdvOvWbOYkfmzPAdyxyZG/PuRVvWum7LHbmP2r6+wpH5tqDFCkfu8bYFVzo5IzyXhry50pF7890OK51if/AzqPrIlY7cy7XyrnJkzkefU070OUWf9eyFVU54j6c70WeQPLOc8H5Md6LPrDeko88yJ9yG78foc9OJPjfp9eiz1Yk+W2k/F75Z48ga9fw3G5xBj0WoEzZscCr9zv9NbbZvcN7r+Sntp9rSjU7/z/n53mD9Rkfui8llNznnq39M7z0X7c2HJ1I3nLPZ+bRgLtp+8qgtjqxFe37McApXLc+/b7TynCbHrtDrl+f5zqDO/G8ZuSDTmbn+T9r/+H8ynaMdvqQu5u9wiix5lJ9TmTucyzN60OsD2u509hr+zvnw0Cxn/AT+fnXs5yxnyrvzaPtej+9yCu/ZxN/JvV3OsM7p9Lr3zG4nLTKFth9za7ezGPi/We6U2ONsfp1/n2/52x7nhYQfqXt8tM8Zu+F9Gs/P5u9zXoirSa/PvrTP+bJUDmo/x35nSdOqtJ93m/zolGx1gL873fvR8RcNo2OocP9HJ/nKsjfpXD496Mxqz/+tV3H7ISerTgydyx+5jjj3Rq/j399Sf3X82Pq0nxcW3HRSbjxEnf3UQ27RwvnjpWVtea7DY26LIbxN+5pl3AqDYuxzLc41yUXle74bzqU4d2fOgdukSzdY44TbyNoe545e35U68kgd9+oB/reJNfmMm+/Befqsz0vWc4/OnESv/6fhO27xZXOoX2v2jtvqYgptc3TlO+7ot96jPvJGgju400b+rWn9/9z0xT3p9XbD33OTs3n92fJsI7dpWR7/1iMau7XOD6btP9nWxD35BN/jFRt94A5rzv8tmX9yM/eVEtm0fZ3pzdzdc3mbESnN3SkzeL7tKtbKHTBkOI3DCx1au59N4rW38/o2br466bwWLUp0S+xcRdtnrkt0S+75gbb/5k6iO/QSr8P/e6K9e/nVrrT9qWbt3VMpC6jfz+7g9vynFm0/IeEj98/XetB+vnY7u1cqVqUubzq7TXPw3Bv3U2d31eQ/afufP+7uvlabf5c+OLO7e37UPjrftV53N8F5il6f4fV2B6V51ANy9XFlXcXuPZ2/Y+fd1dedMYV/K+jUq787feuv1MOfGugemH+U5l5MxYHukvffoGu94fFBbtxnx+l4Rr00yH22Lf83y/UuyW6FT7tQN1+R7E7N9T71TyuT3YJXN/Cz71iyW6oUr+dbbiS70z8dR6879we775zmdaZB7iHul9Xm0OvdNw51B5WsQj30lWHuN0m8to9vNMzt+AHvBzv8zpbibvm3pPy+oeZtilvj3Z/rSh/pO9IJt5F5m+KebsnrasOtKe6ZM/yb5O4io9yfH/BvREmZo91d/V7meZI91u1UJZ7XjaGT3FlXDtPrB9Z+635Rhv/9qO1vM90TNw/RPjtUn+Mub8K/dX/z1XK37RT+dyt4Y6U797U19HrXJivd6Z/I98l0d1GXyrKeq3NJd1MSamyTfvLtxU64jZxLuvt467b8W0SHte7Ml3PTta5XeZNbKPll2mfmsK3uTwNi+d7v7burF/Jc6t4Q51Ex2mef8wfdBgfm8nq19Bd3+pS/aPur235xR3SLpdcfVDzuvlezh/1uk+1+cLKi/Jaojjk72KdtV7ryVx2pj1/Pdut9yff+xzlPuhO/yE3X9LWUU+6zlfl3od0dzrjp9/m37jVnz7k53p1L7/XOXHTX7xnIa06da+6vzXza/mLM9WBu/PXiLbdK8m/0eoGNt90V8R/QZ9WseMet0OpV2udjD99z79RcTr287D33zNGCtM3py/fc0yX/pi6RGGOWFN1E/cObuczQqVnUo9fkMr0unbTXLtZ8CeXlt2ITjgO2jAO1K316Qjvqwj3ymTV1d1C/+UUBU34ory3nvo4ei71/b4582LxteA05d/Yx83Zn/nfSpzs+bpqdnc3fD/MWN/eOrqXXN04obp4rX4OOZ2X9Uqb0Jv53jZjkUib/lTJBB8dJXcxR7Uq/vISPs3jP0qbnwdfo2Ea8Usbk2fQ6db2EMqZkn1m0zzyNKpqeD9em47kyoKJJHs1dZWiVYP9Dsp4z005spn0O3l3NmDnsN56/XsP0n37ezqs483/lSsnzSI1nnJm9/7Ot0sNBvnfGqbGNM/UP8X9rnBj9snkwhu/Bsztqmx83ptA+3zlS21z5Oz99btlKrxpn64v0ehGnjvnp5Wza/k7W2+aV+P52HBJMlaKPe9LhuCWY8vvs8US7rxwPbVMseG/DTbzOdOzZ0FRN20998F7DYEwm9G1ktu3mf4+bMbmJOfwT/7v/rEFNTa+F62n7qkObBtsfbdrc3Bn+PG1zPLuF2XfpKztWiWZz+yeDDsct0cTKcUa7TzBuiWrcEk21G82p/3Mw0Yw61piuxdLN7YJ5mFS+q5lw/Qva5sDYrmaVz//+BbN6mOQR9/heSE4ye1baaxftcKySzFa5dtEeEYxVkhqrJLN/44fUS5P7mzyzvqT9fLy2v6l14DnqMkcHmILFeB0rXH2gkfs9NmeyadOZv9t0GZgcjFXvM4PNseb/0HvblB1i8jxz3X63HxJs0+79ISb535X0ubsmDzHetVl0Lqf9IWbAp1N5/Wk5zPT/7ir1gkkjzINyIM8R422T7+EpasxTTJ1zvbZKOzNlzFPUmKeYkQ/zXO1WNcW09HmeFM0z0rz6gNfnPp+NMYPPnKVjLnBljKn3zil6vca9MebuiEn0+uz1Y03XSeOpn/5hrOm7kc3A1ALjzKfVavKx1R1vtq59KujgOKPdcW3vrdIbl6Q54Tb2OKN9cVlLXlsajjfflh1Fx3C/2SRzs80Kev2LJ1NNQmN2BfnXpRq/ET/rG9yK9ht9aZvnH6QGY/7HtilBX/xzionL5dE2S16aZob1+I2O8+/B082arSPs+U43lxNm2nmVFr1fSgcdzrE007qvzLE0s2uOzLE0NcfSjL+Pn5WTms00n52vwt+9V800qWsn8nc5Z55pVpzX2MvH55nHOvA53p8/37Sf8BSvUVsXmCZdH/Okg2OIdrsKdjyj/eLCNCfcxh5DtDOe7EC9auCSYBwSayw1tVbzulQ7xzKzdvso2n+tD5eZK4V4jXqxVbTfjtB7Wy1fbqaU439LWnxwlfGmy28s6ebY60/Idwk1J9PV8yhdzcN0c/k5/g7fZ1O6eboo3wtH864xT+Xn/36p23aNGbf0Ozt/MkzMmYphB3MpI9w/tyu9qmcbXvNvZ5gCh/m+/qTgVvNhCV7Hln271TQcxJ9b7hvPLHN57f28TqbZu78QnWOLETvMjVz77HXMMhNNcU86nANZ6lmWpa57lhnbpzN1hdF7TPp5Pq9bLfaaIi3Y6dU/vNcMGcff0/xK+03l2tWpv6i239RZNpn68o4D5vN96+x1PGQS6tpjiHY4Bw6Fx8AdbB+J5/Wt2dTDpsgLlem88l47YhabDfT6mp+PmkH/8n/Pnk743cxY+Dmvh33+MGtK8xr4eN/jptTgcfK9y3zwQpmg1Xcwda2z1bXONr1H8zrTbl622TjmJ75/758I1nns70qxi2tf+qRpsnksHeecQSdNwQ/43n9r6SnzfXNeG3/fecrsvML3Piw5bf598mbQsj4nrDptSlzh30Zq7j1jDi7l58jQ+X+auP/yfzNmnPzTrPjPH7TPOYUumOrH59v5c9Gk57XjHO1wvl1U8+2imm8XzSVoSj2u9TXjZnam9w7vc83keekV6t33r5v1Bfj7Wy7npinyA1vHmY/+ZWq1XGDnzG0zsJQ892+rOXZbzbHbao7dNgc/+Jg6Z6U7ptDx7jzOJ+6YDdV4fMZ887fZ2fsX2mehoffNN6/tom1Sc/wb7LP4EzHxOxP5+8ATm2Pi89WZRp3aMkd8rSfs98w2+N/BqWJH46vL8z3a/eTZStvYY4t2/2ye/9OcnPHlyvambjIzZ/z8S5uor57LE1+9/VT5Hht/bUCpoNV32vjU4DtYbHz4HSw2/Kxol/2cvzcuLxIb/9fmbdS3a+aNn9rrWdrnCsgb//dx/h2pZmv8Nz0+9+P78sWvHjyUthn4Wf74CrmW8D5xHoVWOV6uHXa/Gx2Cji1XPeiL+/i5jy37xM5dNGfQwXlR22vKHXzu8mv/C1ofT7XZ5Y10uQtNgt60o2vQMq/s8cfbJnf9/aIKrrhrafTM0uicpdFdS+Pv8KojYac60uiupdFOS6O7lkZ3LY3WN9hP9DixxV1ji7vGFneNLe4aW9w17ce6a+ngmK27Dtses3XX2OKu6XOtu8YWd40t7hpb3DW2uGvap3XX0sExWHcdtj0G666xxV1ji7vGFneNLe4aW9w17ce6a+ngc627Dtt+rnXX2OKuscVdY4u7xhZ3jS3uGlvcNba4a2xx19jirulzrbuWDo7Tuuuw7XFad40t7hpb3DW2uGtscdfY4q6l0V1ji7vGFnctLfNQ3DW2uGueM+yupcM5lh0ev3XX0uiuscVdY4u7xhZ3jS3uGlvcNZ27dde0T+uupYNjsO46bD4GcdfY4q5pHKy7xhZ3TWNo3TXPE3bX0uG8ylLzKkvNK3bXNAesu8YWd40t7hpb3DXNZ+uuscVd8xxgdy0dzpkMNWcy1Jxhd40t7hpb3DW2uGtscdfY4q6xxV3zdWR3LR1e93R13dPVdWd3jS3uGlvcNba4a2xx19jirvk6sruWDq/1AnWtF4Sfa901trhrbHHX1NZd03yz7hpb3DVdC+uuscVd8/Vldy0dzoc0NR/S1Hxgd40t7hpb3DW2uGtscdfY4q5pTlp3jS3umq81u2vpcG6MV3NjvJob7K6xxV1ji7umdcy6a2xx19jirvn6sruWDudDipoPKWo+sLumOWDdNba4a2xx19jirmmts+4aW9w1trhrbHHX2OKu6Rytu+Zrwe5aOrx2SeraJalrx+4aW9y1NLprur+su8YWd40t7hpb3DUdm3XX2OKuscVdY4u7lpY1Wdw1jye7a+lw/BPV+Ceq8Wd3jS3uGlvcNba4a2xx19jirrHFXfP4sLuWDsczQY1nghpPdtfY4q7pvrDuGlvcNba4az5+dtfS4fnGqfONU+fL7hpb3DWtgdZdY4u7xhZ3Tedo3TWtgdZdY4u7xhZ3jS3uGlvcNba4az53dtfS4ViVUmNVSo0Vu2tscdfY4q6xxV3TPLTuGlvcNba4a2xx1zw+7K6l1XdFNZ6xajzZXdMaZd01trhrbHHX2OKuscVdY4u7xhZ3jS3uGlvcNba4a2xx19jirmkNse6aj5PdtbT6fkLuWhrddbhN8F2F3DW2uGtscdfY4q6xxV1ji7umtci6a2xx19LorrHFXWOLu+ZjYHctrZ6z5K6l0V2H2wTPXHLX2OKuscVd0zFYd01ro3XXtF5Zd03zzbprbHHX2OKu6Z617hpb3DW2uGtscdfY4q6xxV1ji7um+WPdNba4a7ofrbvGFnfN58vuWlo9g8hdS6O7DrcJnkfkrmkdsO4aW9w1trhrbHHX2OKuscVdY4u7xhZ3jS3uGlvcNc1h667pWlh3jS3umq6FddfY4q6xxV1ji7umtu6arrt119jirrHFXWOLu6bzsu4aW9w1zQ3rrrHFXWOLu8YWd40t7hpb3DW2uGtscdfY4q6xxV1ji7vGFneNLe4aW9w1XVPrrrHFXWOLu8YWd81zgN21tHqmkLuWRncdbhM8X8hdY4u7lkZ3Tedo3TVdd+uu6Tpad40t7hpb3DVdX+uupeWeEneNLe4aW9w1trhrGgfrrrHFXWOLu6a27pruBeuu6Tpad03nZd01trhrbHHXtD5Yd01rpnXX1NZdY4u7xhZ3TfeLdde8hrC7xhZ3TWuCddc0V627xhZ3jS3umua5ddfY4q6xxV1ji7umuWHdNba4a2xx19jirrHFXdN9ZN01jYN11zQ/rbumc7TumuawddfY4q6xxV1ji7vGFndN88q6a2xx17T+WHeNLe4aW9w1z1t219LorqXRXYfbBM8Uctd0zNZd03Fad40t7hpb3DW2uGtscdfY4q6xxV3TmmPdNR2zdde0Dlh3TWuRddd0H1l3jS3uGlvcNba4a2xx17TOWHeNLe6ajsG6a2xx19jirrHFXdO9Zt01zTfrrmnOWHdN94t119jirrHFXWOLu6axte4aW9w1trhrui+su6Z1wLprbHHX2OKuscVd8zXl30ODtudLba8dtT0GajvPqe1Y8X6OhG3HnNqeC7W97tR2nlPbe4R/v04N215r6jLJYdvx5+NppI6hjvrcSuqzCqv93/WCtvOK9zNbdbewg3uEji0SNs8f+7qjtnfVflTPdtXnuup4XHWcRh2/Uedl1PkaNQ5GjY9R42bUsRk1zkaNf9h2DbHXK2x7v/AxHAnbznM+nkth2znJ780RzjF77/B+eO6Ju8YWd40t7prmqnXXtI1119jirrHFXWOLu8YWd40t7hpb3DWtP9ZdY4u7xhZ3TeuDddfY4q7tGuLIHBB3jS3uGlvcNba4a2xx19jirrHFXWOLu6Z737prbHHX2OKuscVd03pr3TW2uGv6LOuuscVdY4u7pvO17hpb3DW2uGta66y7xhZ3TWNi3TW9bt01XTvrrrHFXdP6Zt01rY3WXdOab901trhrnj/srqXD+5HdtTS663Abvh/FXdPnWneNLe6arqN119jirrHFXWOLu8YWd40t7hpb3DW2uGvaxrprbHHX2OKuscVdY4u7xhZ3jS3uGlvcNc1P666xxV3T9bXuGlvcNba4a2xx19jirrHFXWOLu8YWd40t7hpb3DW2uGu67tZdY4u7xhZ3Tedi3TW2uGtscdc0r6y7xhZ3TWNo3bW0rC3irmkdsO6a5wa7a+lwLrG7lkZ3HW4jazu7a2xx19jirrHFXWOLu8YWd40t7hpb3DW2uGtscdd0X1t3jS3uGlvcNY2bddfY4q6xxV1ji7umNcq6axpP665pjbXumtYi666xxV1ji7vGFneNLe4aW9w1trhrbHHX2OKuaX2z7prWSeuuscVdY4u7xhZ3TfPfumtpdNfY4q6xxV3T+mbdNV1T666xxV1ji7vGFneNLe4aW9w17ce6a7pe1l1ji7umdcO6a2xx19jirul+t+5aOvzOxu5aOpy37K6l0V2H28i8ZXdN65J119jirrHFXdM8se6a1g3rrrHFXdP9aN01trhrmhvWXWOLu8YWd23XZ3LX0mo9J3ctje463EbOhd01trhrbHHXNFetu6Z737prGnPrrrHFXdN6Zd01trhrbHHX/LnsrqXDY84O9inuWhrdNba4a2xx17QmWHdN18K6a1pPrLum9dm6a1pzrLvGFneNLe4aW9w1trhrbHHX2OKuscVdY4u7xhZ3jS3ums+L3bV0OA7srlXL78zkrrHFXWOLu8YWd40t7ppet+4aW9w1trhrbHHX2OKu6XOtu5YOjtO6a9WuNLprbHHXdC9Yd01zzLprbHHX2OKuscVdY4u7xhZ3jS3umseH3bW0eh6Ru5ZG8xNuEzyPyF1ji7vGFneNLe4aW9w1trhrbHHXPA7srqXDcWN3Ld1Xjse6a2l019jirmkNt+4aW9w1reHWXWOLu8YWd40t7pruI+uu+dzZXUuH48buWrpPMG6JatzYXWOLu8YWd03roXXXtO5Zd01rmnXXfL7srqXDsWJ3LT0iGKskNVbsrulzrbum9cG6a2xx13TvWHeNLe4aW9w1trhrbHHX2OKuscVdY4u7pnXAumtaf6y7xhZ3zePG7lpaPYPIXUujuw63CZ5B5K6xxV1ji7umNdm6a1rHrLvGFneNLe6a1gTrrrHFXdNnWXctHRynddfS6K7DbexxWndNa4t119jirrHFXWOLu8YWd033uHXX2OKuad227prWMeuuscVd8/myu+Z5wu5aOpxj7K6l0V2H28gcY3eNLe4aW9w1trhrbHHXdL7WXdN+rLuWDo7BumtpdNfhNvYYrLvGFneNLe4aW9w1trhrbHHX2OKuscVd87xidy2tvkuo51G6mofsrmm+WXdN64x119jirnk+sLsOOphLGeH+rbuWRndNa75119jirrHFXWOLu8YWd40t7pqvI7tr6XAOZKlnWZa67uyu6Vpbd40t7hpb3DW2uGua29Zd03yw7pqv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AQAAAACAAADMBgAAFgAAAA==eJzT0hoFo2AUjIJRMAqGNgAA9C4diA==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AQAAAACAAABgNgAAMAwAAA==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 + + diff --git a/geos-mesh/tests/data/fracture_res5_id_empty.vtu b/geos-mesh/tests/data/fracture_res5_id_empty.vtu new file mode 100644 index 00000000..a69adb95 --- /dev/null +++ b/geos-mesh/tests/data/fracture_res5_id_empty.vtu @@ -0,0 +1,41 @@ + + + + + + + AQAAAACAAACgBgAAbgEAAA==eJwtxdciEAAAAEBRUmlpK9q0aEpbey/tTUMb0d57T6VBO+2NSGjvoflDHrp7uYCA/6o40EGu6moOdnWHuIZrupZDXdt1XNf1XN9hbuCGbuTGbuKmbuZwN3cLRzjSLd3Krd3Gbd3O7R3laHdwR3dyZ3dxjGPd1d3c3T3c070c596Odx/3dT/39wAP9CAneLCHeKiHebhHeKRHebTHeKzHebwneKInebITPcVTPc3TPcMzPcuzPcdzPc/zvcBJTvZCL/JiL3GKl3qZl3uFV3qVVzvVaU73Gmc402u9zuu9wRu9yZu9xVu9zdu9wzu9y7u9x3u9z/t9wAd9yId9xEd9zMd9wid9ylk+7TPO9lmf83lfcI5zfdGXfNlXfNXXfN03nOebvuXbvuO7vuf7fuCHfuTHfuKnzneBC/3MRS72c5f4hUtd5nK/9Cu/9hu/9Tu/9wd/9Cd/9hd/9Td/9w9X+Kd/+bf/+K//uRLqf1df + + + + + AQAAAACAAADgBAAAEgEAAA==eJwtxddCCAAAAMAiozSkoaGh0NAeqGhrSNEg7SFkJKFhlIhoaCBUP9tDdy8XEHAo0Ed81EE+5uM+4ZMOdohPOdRhDneETzvSZxzlaMc41mcd53gnONHnnORkpzjV553mdF/wRV9yhjOd5Wxfdo5zned8F7jQRS52iUt9xVd9zWUud4Wv+4YrXeVq17jWda73TTe40U1u9i23+LZb3eY7vut2d7jTXb7n++72A/e4133u94AHPeRhj3jUDz3mR37sJx73Uz/zc7/whF960q885dd+47ee9oxnPed3fu8P/uh5L/iTF/3ZX7zkr/7mZX/3D6941Wte909veNNb3vYv//Yf7/iv//m/d73nfR8ARZMvOw== + + + + + AQAAAACAAADwCQAAXQIAAA==eJxtlaFOK1EQhheFQZCKq1E3+xQN6Zm+QR8ATdKER1iPQVVvVhxxa6rQtNy6DcFhGzwJkhzF9ITO/93kVn3Z7M78Z86XTtP4r7/YN6dfey7enInvvv4Gdx/ih3dx/ybejOKnrfj1UXxYiz97cbMSX96LrzrxzS3yLJBhir4tek1QvzwHXr+IuwG8FDe/wKtd89/faob3E+qAB/H1i7gp4sPEkF/cT8XdQnxzK77qxJf34mYl/uzFh7X49RF9t+LNiAxv4od35PkQ333h27N5cHsu7i/I+/Wf3+nnHbG7F+zuBbt7we5esLunb0exuxfs7gW7e8HuXrC7F+zuBbt7we6e8iyQYYq+LXpNUL88B7t7qjOAl2J3D9l24qbB8xneT6gDHhL6JuRJyGnIbziX4byGORjmY5ibIZthzob5i9093JfY3cP9it09+CB29+DPHF6J3T2wO5bhXoZ7Ge5luJfhXoZ7p29HcXUvw70M9zLcy3Avw70M9zLcO+VZIMMUfVv0mqD+0b0M9051BvBSXN2LbDvx0b14PsP7CXXAQ0LfhDwJOQ35DecynNcwB8N8DHMzZDPM2TB/cXUv7ktc3Yv7FVf3wgdxdS/8mcMrcXUveB//+3XnlnAvuO7cEu4F151bwj19O4rrzi3hXnDduSXc0+5ZievOLeFecN25pzwLZJiib4teE9Q/7twS7qnOAF6K686NbLt/dqSez/B+Qh3wkNA3IU9CTkN+w7kM5zXMwTAfw9wM2QxzFtedG/eCvltx3blxv+K6c8MHcd254c8cXonrzi3pG10/Tpw= + + + 0 + + + 2304.8861143 + + + + + + + AQAAAACAAACAEwAAWQMAAA==eJxd1Vf7CAQYhnEjpKiUWYmiREX29rf33nvvvffee29KoShbpE2plJXxhRy474Oe9+R3Xff1nL/1Cz2/BjgFJ2M9rB99EtaNnX0ifho7+wSsEzv7eKwdO/s4/CR29rH4cezsY/Cj2NlHY63Y2UdhzdjZR+KHsbOPwBqxsw/HD2JnH4bvx84+FKvHzj4Eq8XOPhjfi519EL4bO/tArBo7+wCsEjt7f3wndvZ+WDl29r74duzsffCt2Nl745uxs/fCSrGz98SKsbP3wAqxs3fH8rGzd8NysbN3xbKxs3fBN2Jn74yvx87eCcvEzt4RX4udvQO+Gjt7e3wldvZ2WDp29rZYKnb2Nvhy7Oyt8aXY2QuwZOzsrfDF2NlbYonY2Vtg8djZm2Ox2Nmb4QuxszfForGzN8EisbM3xsKxszcq9P8rHL0h+k/8O5/jZzgpdvbjODF29mM4IXb2ozg+dvYjOC529sM4Nnb2QzgmdvaDODp29gM4Knb2/TgydvZ9OCJ29r04PHb2PTgsdvbdODR29l04JHb2nTg4dvYdOCh29u04MHb2bTggdvat2D929i3YL3b2zdg3dvZN2Cd29o3YO3b2Ddgrdvb12DN29nXYI3b2tdg9dvY12C129tXYNXb2VdgldvaV2Dl29hXYKXb25dgxdvZl2CF29qXYPnb2JdgudvbF2DZ29kXYJnb2hdg6dvYFWBA7+3xsFTv7PGwZO/tcbBE7+xxsHjv7bGwWO/ssbBo7+0xsEjv7DGwcO/t0bBQ7+zRsGDv7VPSf+Hee4hM8Hjv7YzwWO/sjPBo7+394JHb2h3g4dvYHeCh29vt4MHb2e3ggdva7uD929n9xX+zs/+De2Nnv4J7Y2f/G3bGz/4W7Ymf/E3fGzn4bd8TO/gduj539d9wWO/st3Bo7+03cEjv7b7g5dvZfcVPs7L/gxtjZf8YNsbP/hOtjZ/8R18XO/gOujZ39Bq6Jnf17XB07+3VcFTv7NVwZO/t3uCJ29qu4PHb2K7gsdvbLuDR29ku4JHb2i7g4dvYLuCh29vO4MHb2c7ggdvZvcX7s7N/gvNjZz+Lc2NnP4JzY2b/G2bGzf4WzYmc/jTNjZz+FM2JnP4nTY2f/EqfFzv4FTo2d/QQ+A6EeATg= + + + AQAAAACAAADgBAAADgEAAA==eJwtxRFwAgAAAMC2C4IgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCAaDwSAIgiAIgiAIBkEQDPqXDwbeQg474qhjjjvhpFNOO+Osc8674KJLLrviqmuuu+GmW26746577nvgoUcee+KpZ5574aVXXnvjrb/87R//eue9Dz765LMvvvrmu//88NMvBz7eBR1y2BFHHXPcCSedctoZZ51z3gUXXXLZFVddc90NN91y2x133XPfAw898tgTTz3z3AsvvfLaG2/95W//+Nc7733w0SefffHVN9/954effjnw+S7okMOOOOqY40446ZTTzjjrnPMuuOiSy6646prrbrjpltvu+B9fwUXT + + + AQAAAACAAACcAAAADAAAAA==eJxjZx+8AABPhQRF + + + + + diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index 5f90bb13..cf0903aa 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -22,61 +22,59 @@ from geos.mesh.utils import arrayModifiers -@pytest.mark.parametrize( "attributeName, onpoints", [ ( "CellAttribute", False ), ( "PointAttribute", True ) ] ) +@pytest.mark.parametrize( "attributeName, nbComponents, onpoints, value_test", [ + ( "CellAttribute", 3, False, np.nan ), + ( "PointAttribute", 3, True, np.nan ), + ( "CELL_MARKERS", 1, False, np.nan ), + ( "PORO", 1, False, np.nan ), + ( "CellAttribute", 3, False, 2. ), + ( "PointAttribute", 3, True, 2. ), + ( "CELL_MARKERS", 1, False, 2. ), + ( "PORO", 1, False, 2. ), +] ) def test_fillPartialAttributes( dataSetTest: vtkMultiBlockDataSet, attributeName: str, + nbComponents: int, onpoints: bool, + value_test: float, ) -> None: - """Test filling a partial attribute from a multiblock with nan values.""" + """Test filling a partial attribute from a multiblock with values.""" + vtkMultiBlockDataSetTestRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) vtkMultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - arrayModifiers.fillPartialAttributes( vtkMultiBlockDataSetTest, attributeName, nbComponents=3, onPoints=onpoints ) - - iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() - iter.SetDataSet( vtkMultiBlockDataSetTest ) - iter.VisitOnlyLeavesOn() - iter.GoToFirstItem() - while iter.GetCurrentDataObject() is not None: - dataset: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) - data: Union[ vtkPointData, vtkCellData ] + arrayModifiers.fillPartialAttributes( vtkMultiBlockDataSetTest, + attributeName, + nbComponents, + onPoints=onpoints, + value=value_test ) + + nbBlock: int = vtkMultiBlockDataSetTestRef.GetNumberOfBlocks() + for block_id in range( nbBlock ): + datasetRef: vtkDataSet = cast( vtkDataSet, vtkMultiBlockDataSetTestRef.GetBlock( block_id ) ) + dataset: vtkDataSet = cast( vtkDataSet, vtkMultiBlockDataSetTest.GetBlock( block_id ) ) + expected_array: npt.NDArray[ np.float64 ] + array: npt.NDArray[ np.float64 ] if onpoints: - data = dataset.GetPointData() + array = vnp.vtk_to_numpy( dataset.GetPointData().GetArray( attributeName ) ) + if block_id == 0: + expected_array = vnp.vtk_to_numpy( datasetRef.GetPointData().GetArray( attributeName ) ) + else: + expected_array = np.array( [ [ value_test for i in range( nbComponents ) ] for _ in range( 212 ) ] ) else: - data = dataset.GetCellData() - assert data.HasArray( attributeName ) == 1 - - iter.GoToNextItem() - - -@pytest.mark.parametrize( "onpoints, expectedArrays", [ - ( True, ( "PointAttribute", "collocated_nodes" ) ), - ( False, ( "CELL_MARKERS", "CellAttribute", "FAULT", "PERM", "PORO" ) ), -] ) -def test_fillAllPartialAttributes( - dataSetTest: vtkMultiBlockDataSet, - onpoints: bool, - expectedArrays: tuple[ str, ...], -) -> None: - """Test filling all partial attributes from a multiblock with nan values.""" - vtkMultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - arrayModifiers.fillAllPartialAttributes( vtkMultiBlockDataSetTest, onpoints ) - - iter: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() - iter.SetDataSet( vtkMultiBlockDataSetTest ) - iter.VisitOnlyLeavesOn() - iter.GoToFirstItem() - while iter.GetCurrentDataObject() is not None: - dataset: vtkDataSet = vtkDataSet.SafeDownCast( iter.GetCurrentDataObject() ) - data: Union[ vtkPointData, vtkCellData ] - if onpoints: - data = dataset.GetPointData() + array = vnp.vtk_to_numpy( dataset.GetCellData().GetArray( attributeName ) ) + if block_id == 0: + expected_array = vnp.vtk_to_numpy( datasetRef.GetCellData().GetArray( attributeName ) ) + else: + expected_array = np.array( [ [ value_test for i in range( nbComponents ) ] for _ in range( 156 ) ] ) + + if block_id == 0: + assert ( array == expected_array ).all() else: - data = dataset.GetCellData() + if np.isnan( value_test ): + assert np.all( np.isnan( array ) == np.isnan( expected_array ) ) + else: + assert ( array == expected_array ).all() - for attribute in expectedArrays: - assert data.HasArray( attribute ) == 1 - - iter.GoToNextItem() @pytest.mark.parametrize( "attributeName, dataType, expectedDatatypeArray", [ @@ -200,40 +198,68 @@ def test_createAttribute( assert cnames == componentNames -def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet ) -> None: +@pytest.mark.parametrize( "attributeFrom, attributeTo, onPoint, idBlock", [ + ( "PORO", "POROTo", False, 0 ), + ( "CellAttribute", "CellAttributeTo", False, 0 ), + ( "FAULT", "FAULTTo", False, 0 ), + ( "PointAttribute", "PointAttributeTo", True, 0 ), + ( "collocated_nodes", "collocated_nodesTo", True, 1 ), +] ) +def test_copyAttribute( dataSetTest: vtkMultiBlockDataSet, attributeFrom:str, attributeTo: str, onPoint: bool, idBlock: int ) -> None: """Test copy of cell attribute from one multiblock to another.""" objectFrom: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - objectTo: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) + objectTo: vtkMultiBlockDataSet = dataSetTest( "emptymultiblock" ) - attributeFrom: str = "CellAttribute" - attributeTo: str = "CellAttributeTO" + arrayModifiers.copyAttribute( objectFrom, objectTo, attributeFrom, attributeTo, onPoint ) - arrayModifiers.copyAttribute( objectFrom, objectTo, attributeFrom, attributeTo ) - - blockIndex: int = 0 + blockIndex: int = idBlock blockFrom: vtkDataSet = cast( vtkDataSet, objectFrom.GetBlock( blockIndex ) ) blockTo: vtkDataSet = cast( vtkDataSet, objectTo.GetBlock( blockIndex ) ) - arrayFrom: npt.NDArray[ np.float64 ] = vnp.vtk_to_numpy( blockFrom.GetCellData().GetArray( attributeFrom ) ) - arrayTo: npt.NDArray[ np.float64 ] = vnp.vtk_to_numpy( blockTo.GetCellData().GetArray( attributeTo ) ) + if onPoint: + arrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( blockFrom.GetPointData().GetArray( attributeFrom ) ) + arrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( blockTo.GetPointData().GetArray( attributeTo ) ) + + typeArrayFrom: int = blockFrom.GetPointData().GetArray( attributeFrom ).GetDataType() + typeArrayTo: int = blockTo.GetPointData().GetArray( attributeTo ).GetDataType() + + else: + arrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( blockFrom.GetCellData().GetArray( attributeFrom ) ) + arrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( blockTo.GetCellData().GetArray( attributeTo ) ) + + typeArrayFrom: int = blockFrom.GetCellData().GetArray( attributeFrom ).GetDataType() + typeArrayTo: int = blockTo.GetCellData().GetArray( attributeTo ).GetDataType() assert ( arrayFrom == arrayTo ).all() + assert ( typeArrayFrom == typeArrayTo ) -def test_copyAttributeDataSet( dataSetTest: vtkDataSet, ) -> None: - """Test copy of cell attribute from one dataset to another.""" +@pytest.mark.parametrize( "attributeNameFrom, attributeNameTo, onPoint", [ + ( "CellAttribute", "CellAttributeTo", False ), + ( "PointAttribute", "PointAttributeTo", True ), +] ) +def test_copyAttributeDataSet( dataSetTest: vtkDataSet, attributeNameFrom:str, attributeNameTo: str, onPoint: bool ) -> None: + """Test copy of an attribute from one dataset to another.""" objectFrom: vtkDataSet = dataSetTest( "dataset" ) - objectTo: vtkDataSet = dataSetTest( "dataset" ) + objectTo: vtkDataSet = dataSetTest( "emptydataset" ) - attributNameFrom = "CellAttribute" - attributNameTo = "COPYATTRIBUTETO" + arrayModifiers.copyAttributeDataSet( objectFrom, objectTo, attributeNameFrom, attributeNameTo, onPoint ) - arrayModifiers.copyAttributeDataSet( objectFrom, objectTo, attributNameFrom, attributNameTo ) + if onPoint: + arrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( objectFrom.GetPointData().GetArray( attributeNameFrom ) ) + arrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( objectTo.GetPointData().GetArray( attributeNameTo ) ) + + typeArrayFrom: int = objectFrom.GetPointData().GetArray( attributeNameFrom ).GetDataType() + typeArrayTo: int = objectTo.GetPointData().GetArray( attributeNameTo ).GetDataType() + else: + arrayFrom: npt.NDArray[ any ] = vnp.vtk_to_numpy( objectFrom.GetCellData().GetArray( attributeNameFrom ) ) + arrayTo: npt.NDArray[ any ] = vnp.vtk_to_numpy( objectTo.GetCellData().GetArray( attributeNameTo ) ) - arrayFrom: npt.NDArray[ np.float64 ] = vnp.vtk_to_numpy( objectFrom.GetCellData().GetArray( attributNameFrom ) ) - arrayTo: npt.NDArray[ np.float64 ] = vnp.vtk_to_numpy( objectTo.GetCellData().GetArray( attributNameTo ) ) + typeArrayFrom: int = objectFrom.GetCellData().GetArray( attributeNameFrom ).GetDataType() + typeArrayTo: int = objectTo.GetCellData().GetArray( attributeNameTo ).GetDataType() assert ( arrayFrom == arrayTo ).all() + assert ( typeArrayFrom == typeArrayTo ) @pytest.mark.parametrize( "attributeName, onpoints", [ From ec0302f863938c7e9a49522e5505adadb4de222d Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 24 Jun 2025 17:12:12 +0200 Subject: [PATCH 17/58] 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 18/58] 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 19/58] 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 20/58] 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 21/58] 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 22/58] 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 23/58] 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 a0a2092703297b589100c31769ef5809a2da2b24 Mon Sep 17 00:00:00 2001 From: Romain Baville <126683264+RomainBaville@users.noreply.github.com> Date: Mon, 30 Jun 2025 13:42:55 +0200 Subject: [PATCH 24/58] Apply suggestions from code review Co-authored-by: paloma-martinez <104762252+paloma-martinez@users.noreply.github.com> --- .../src/geos/mesh/processing/FillPartialArrays.py | 14 +++++++------- geos-mesh/src/geos/mesh/utils/arrayModifiers.py | 8 ++++---- geos-pv/src/PVplugins/PVFillPartialArrays.py | 12 ++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py index e43ffc47..0b1f2e71 100644 --- a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py +++ b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py @@ -25,9 +25,9 @@ __doc__ = """ Fill partial arrays of input mesh with values (defaults to nan). -Several attributes can be fill in the same time but with the same value. +Several arrays can be filled in one application if the value is the same. -Input and output mesh are vtkMultiBlockDataSet. +Input and output meshes are vtkMultiBlockDataSet. To use it: @@ -107,9 +107,9 @@ def RequestData( """Inherited from VTKPythonAlgorithmBase::RequestData. Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects + request (vtkInformation): Request + inInfoVec (list[vtkInformationVector]): Input objects + outInfoVec (vtkInformationVector): Output objects Returns: int: 1 if calculation successfully ended, 0 otherwise. @@ -151,7 +151,7 @@ def _SetAttributesNameList( self: Self, attributesNameList: Union[ list[ str ], """Set the list of the partial attributes to fill. Args: - attributesNameList (Union[list[str], Tuple], optional): list of all the attributes name. + attributesNameList (Union[list[str], Tuple], optional): List of all the attributes name. Defaults to a empty list """ self._attributesNameList: Union[ list[ str ], Tuple ] = attributesNameList @@ -160,7 +160,7 @@ def _SetValueToFill( self: Self, valueToFill: float = np.nan ) -> None: """Set the value to fill in the partial attribute. Args: - valueToFill (float, optional): value to fill in the partial attribute. + valueToFill (float, optional): The filling value. Defaults to nan. """ self._valueToFill: float = valueToFill diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index 15c69e7a..5e5ef509 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -49,11 +49,11 @@ def fillPartialAttributes( multiBlockMesh: Union[ vtkMultiBlockDataSet, vtkCompo Args: multiBlockMesh (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): multiBlock mesh where to fill the attribute. - attributeName (str): attribute name. - nbComponents (int): number of components. + 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. + value (float, optional): The filling value. Defaults to nan. Returns: @@ -78,7 +78,7 @@ def fillAllPartialAttributes( multiBlockMesh: Union[ vtkMultiBlockDataSet, vtkCo 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. + value (float, optional): The filling value. Defaults to nan. Returns: diff --git a/geos-pv/src/PVplugins/PVFillPartialArrays.py b/geos-pv/src/PVplugins/PVFillPartialArrays.py index de4475a1..a3b0017e 100644 --- a/geos-pv/src/PVplugins/PVFillPartialArrays.py +++ b/geos-pv/src/PVplugins/PVFillPartialArrays.py @@ -39,7 +39,7 @@ * Load the module in Paraview: Tools>Manage Plugins...>Load new>PVFillPartialArrays. * Select the input mesh. * Select the partial arrays to fill. -* Set the value to fill (optinal defaults to nan). +* Set the filling value (defaults to nan). * Apply. """ @@ -88,7 +88,7 @@ def __init__( self: Self, ) -> None: Select all the attributes to fill. If several attributes - are selected, they will be fill with the same value. + are selected, they will be filled with the same value. @@ -131,7 +131,7 @@ def a01StringSingle( self: Self, value: str ) -> None: assert "," not in value, "Use '.' not ',' for decimal numbers" value_float: float - value_float = np.nan if value == "nan" else float( value ) + value_float = np.nan if value.lower() == "nan" else float( value ) if value_float != self._valueToFill: self._valueToFill = value_float @@ -170,9 +170,9 @@ def RequestData( """Inherited from VTKPythonAlgorithmBase::RequestData. Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects + request (vtkInformation): Request + inInfoVec (list[vtkInformationVector]): Input objects + outInfoVec (vtkInformationVector): Output objects Returns: int: 1 if calculation successfully ended, 0 otherwise. 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 25/58] 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 26/58] 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 27/58] 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 28/58] 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 29/58] 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 30/58] 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 31/58] 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 32/58] 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 33/58] 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 34/58] 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 35/58] 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 36/58] 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 37/58] 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 38/58] 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 39/58] 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 40/58] 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 41/58] 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 42/58] 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 43/58] 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 44/58] 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 45/58] 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 46/58] Clean for ci --- geos-mesh/src/geos/mesh/utils/arrayHelpers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index 0e8939d6..a4ae8018 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -134,7 +134,9 @@ def getNumpyGlobalIdsArray( data: Union[ vtkCellData, vtkPointData ] ) -> Option return vtk_to_numpy( global_ids ) -def getNumpyArrayByName( data: Union[ vtkCellData, vtkPointData ], name: str, sorted: bool = False ) -> Optional[ npt.NDArray ]: +def getNumpyArrayByName( data: Union[ vtkCellData, vtkPointData ], + name: str, + sorted: bool = False ) -> Optional[ npt.NDArray ]: """Get the numpy array of a given vtkDataArray found by its name. If sorted is selected, this allows the option to reorder the values wrt GlobalIds. If not GlobalIds was found, From aeba4ba9fe73118b4c95d2b2abdb030e896b656f Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 14:01:12 +0200 Subject: [PATCH 47/58] move the plugin file in the new folder --- .../{PVplugins => geos/pv/plugins}/PVFillPartialArrays.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename geos-pv/src/{PVplugins => geos/pv/plugins}/PVFillPartialArrays.py (97%) diff --git a/geos-pv/src/PVplugins/PVFillPartialArrays.py b/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py similarity index 97% rename from geos-pv/src/PVplugins/PVFillPartialArrays.py rename to geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py index a3b0017e..287cc03b 100644 --- a/geos-pv/src/PVplugins/PVFillPartialArrays.py +++ b/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py @@ -21,7 +21,7 @@ ) # update sys.path to load all GEOS Python Package dependencies -geos_pv_path: Path = Path( __file__ ).parent.parent.parent +geos_pv_path: Path = Path( __file__ ).parent.parent.parent.parent.parent sys.path.insert( 0, str( geos_pv_path / "src" ) ) from geos.pv.utils.config import update_paths @@ -61,11 +61,11 @@ def __init__( self: Self, ) -> None: inputType="vtkMultiBlockDataSet", outputType="vtkMultiBlockDataSet" ) - # Initialisation of an empty list of the attribute's name + # initialization of an empty list of the attribute's name self._clearSelectedAttributeMulti: bool = True self._attributesNameList: list[ str ] = [] - # Initialisation of the value (nan) to fill in the partial attributes + # initialization of the value (nan) to fill in the partial attributes self._valueToFill: float = np.nan @smproperty.stringvector( From 065d6ff7a22e07fdbc1326fe76349f9f048a090d Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 14:03:37 +0200 Subject: [PATCH 48/58] update the doc with the new filter --- docs/geos_mesh_docs/processing.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/geos_mesh_docs/processing.rst b/docs/geos_mesh_docs/processing.rst index d79db6db..3fe2ca75 100644 --- a/docs/geos_mesh_docs/processing.rst +++ b/docs/geos_mesh_docs/processing.rst @@ -4,6 +4,15 @@ Processing filters The `processing` module of `geos-mesh` package contains filters to process meshes. +geos.mesh.processing.FillPartialArrays filter +---------------------------------------------- + +.. automodule:: geos.mesh.processing.FillPartialArrays + :members: + :undoc-members: + :show-inheritance: + + geos.mesh.processing.SplitMesh filter -------------------------------------- From 20120be85ee15732bccf13b8036ae8f3a476b3fd Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 15:52:47 +0200 Subject: [PATCH 49/58] remove the use of VTKPythonAlgorythmBase --- .../geos/mesh/processing/FillPartialArrays.py | 237 +++++++++--------- 1 file changed, 113 insertions(+), 124 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py index 0b1f2e71..ecddb5db 100644 --- a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py +++ b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py @@ -3,31 +3,30 @@ # SPDX-FileContributor: Romain Baville, Martin Lemay from typing_extensions import Self -from typing import Union, Tuple -from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase +from typing import Union, Any -from geos.utils.Logger import Logger, getLogger +from geos.utils.Logger import logging, Logger, getLogger, CountWarningHandler from geos.mesh.utils.arrayModifiers import fillPartialAttributes from geos.mesh.utils.arrayHelpers import ( getNumberOfComponents, isAttributeInObject, ) -from vtkmodules.vtkCommonCore import ( - vtkInformation, - vtkInformationVector, -) - from vtkmodules.vtkCommonDataModel import ( vtkMultiBlockDataSet, ) import numpy as np __doc__ = """ -Fill partial arrays of input mesh with values (defaults to nan). -Several arrays can be filled in one application if the value is the same. +Fill partial attributes of input mesh with constant values per component. -Input and output meshes are vtkMultiBlockDataSet. +Input mesh is vtkMultiBlockDataSet and the attribute to fill must be partial. +By defaults, attributes are filled with the same constant value for each component: + 0 for uint data. + -1 for int data. + nan for float data. +The filling values per attribute is given by a dictionary. Its keys are the attribute names and its items are the list of filling values for each component. +To use a specific handler for the logger, set the variable 'speHandler' to True and use the member function addLoggerHandler. To use it: @@ -35,132 +34,122 @@ from geos.mesh.processing.FillPartialArrays import FillPartialArrays - # filter inputs - input_mesh: vtkMultiBlockDataSet - input_attributesNameList: list[str] - input_valueToFill: float, optional defaults to nan - - # Instanciate the filter - filter: FillPartialArrays = FillPartialArrays() - # Set the list of the partial atributes to fill - filter._SetAttributesNameList( input_attributesNameList ) - # Set the value to fill in the partial attributes if not nan - filter._SetValueToFill( input_valueToFill ) - # Set the mesh - filter.SetInputDataObject( input_mesh ) - # Do calculations - filter.Update() - - # get output object - output: vtkMultiBlockDataSet = filter.GetOutputDataObject( 0 ) ) -""" - + # Filter inputs. + multiBlockDataSet: vtkMultiBlockDataSet + dictAttributesValues: dict[ str, Any ] -class FillPartialArrays( VTKPythonAlgorithmBase ): + # Instantiate the filter. + filter: FillPartialArrays = FillPartialArrays( multiBlockDataSet, dictAttributesValues ) + + # Set the specific handler (only if speHandler is True). + specificHandler: logging.Handler + filter.addLoggerHandler( specificHandler ) - def __init__( self: Self ) -> None: - """Map the properties of a server mesh to a client mesh.""" - super().__init__( nInputPorts=1, - nOutputPorts=1, - inputType="vtkMultiBlockDataSet", - outputType="vtkMultiBlockDataSet" ) - - # Initialisation of an empty list of the attribute's name - self._SetAttributesNameList() + # Do calculations. + filter.applyFilter() +""" - # Initialisation of the value (nan) to fill in the partial attributes - self._SetValueToFill() - # Logger - self.m_logger: Logger = getLogger( "Fill Partial Attributes" ) +loggerTitle: str = "Fill Partial Attribute" - def RequestDataObject( - self: Self, - request: vtkInformation, - inInfoVec: list[ vtkInformationVector ], - outInfoVec: vtkInformationVector, - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestDataObject. - Args: - request (vtkInformation): Request - inInfoVec (list[vtkInformationVector]): Input objects - outInfoVec (vtkInformationVector): Output objects +class FillPartialArrays: - Returns: - int: 1 if calculation successfully ended, 0 otherwise. + def __init__( + self: Self, + multiBlockDataSet: vtkMultiBlockDataSet, + dictAttributesValues: dict[ str, Any ], + speHandler: bool = False, + ) -> None: """ - inData = self.GetInputData( inInfoVec, 0, 0 ) - outData = self.GetOutputData( outInfoVec, 0 ) - assert inData is not None - if outData is None or ( not outData.IsA( inData.GetClassName() ) ): - outData = inData.NewInstance() - outInfoVec.GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), outData ) - return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] - - def RequestData( - self: Self, - request: vtkInformation, # noqa: F841 - inInfoVec: list[ vtkInformationVector ], - outInfoVec: vtkInformationVector, - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestData. + Fill a partial attribute with constant value per component. If the list of filling values for an attribute is empty, it will filled with the default value: + 0 for uint data. + -1 for int data. + nan for float data. Args: - request (vtkInformation): Request - inInfoVec (list[vtkInformationVector]): Input objects - outInfoVec (vtkInformationVector): Output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. + multiBlockDataSet (vtkMultiBlockDataSet): The mesh where to fill the attribute. + dictAttributesValues (dict[str, Any]): The dictionary with the attribute to fill as keys and the list of filling values as items. + speHandler (bool, optional): True to use a specific handler, False to use the internal handler. + Defaults to False. """ - self.m_logger.info( f"Apply filter {__name__}" ) - try: - inputMesh: vtkMultiBlockDataSet = self.GetInputData( inInfoVec, 0, 0 ) - outData: vtkMultiBlockDataSet = self.GetOutputData( outInfoVec, 0 ) - - assert inputMesh is not None, "Input mesh is null." - assert outData is not None, "Output pipeline is null." - - outData.ShallowCopy( inputMesh ) - for attributeName in self._attributesNameList: - # cell and point arrays - for onPoints in ( False, True ): - if isAttributeInObject( outData, attributeName, onPoints ): - nbComponents = getNumberOfComponents( outData, attributeName, onPoints ) - fillPartialAttributes( outData, attributeName, nbComponents, onPoints, self._valueToFill ) - outData.Modified() - - mess: str = "Fill Partial arrays were successfully completed. " + str( - self._attributesNameList ) + " filled with value " + str( self._valueToFill ) - self.m_logger.info( mess ) - except AssertionError as e: - mess1: str = "Partial arrays filling failed due to:" - self.m_logger.error( mess1 ) - self.m_logger.error( e, exc_info=True ) - return 0 - except Exception as e: - mess0: str = "Partial arrays filling failed due to:" - self.m_logger.critical( mess0 ) - self.m_logger.critical( e, exc_info=True ) - return 0 - - return 1 - - def _SetAttributesNameList( self: Self, attributesNameList: Union[ list[ str ], Tuple ] = () ) -> None: - """Set the list of the partial attributes to fill. - + self.multiBlockDataSet: vtkMultiBlockDataSet = multiBlockDataSet + self.dictAttributesValues: dict[ str, Any ] = dictAttributesValues + + # Warnings counter. + self.counter: CountWarningHandler = CountWarningHandler() + self.counter.setLevel( logging.INFO ) + + # Logger. + if not speHandler: + self.logger: Logger = getLogger( loggerTitle, True ) + else: + self.logger: Logger = logging.getLogger( loggerTitle ) + self.logger.setLevel( logging.INFO ) + + + def setLoggerHandler( self: Self, handler: logging.Handler ) -> None: + """Set a specific handler for the filter logger. + In this filter 4 log levels are use, .info, .error, .warning and .critical, + be sure to have at least the same 4 levels. + Args: - attributesNameList (Union[list[str], Tuple], optional): List of all the attributes name. - Defaults to a empty list + handler (logging.Handler): The handler to add. """ - self._attributesNameList: Union[ list[ str ], Tuple ] = attributesNameList + if not self.logger.hasHandlers(): + self.logger.addHandler( handler ) + else: + # This warning does not count for the number of warning created during the application of the filter. + self.logger.warning( "The logger already has an handler, to use yours set the argument 'speHandler' to True during the filter initialization." ) - def _SetValueToFill( self: Self, valueToFill: float = np.nan ) -> None: - """Set the value to fill in the partial attribute. - Args: - valueToFill (float, optional): The filling value. - Defaults to nan. + def applyFilter( self: Self ) -> bool: + """Create a constant attribute per region in the mesh. + + Returns: + boolean (bool): True if calculation successfully ended, False otherwise. + """ + self.logger.info( f"Apply filter { self.logger.name }." ) + + # Add the handler to count warnings messages. + self.logger.addHandler( self.counter ) + + for attributeName in self.dictAttributesValues: + # cell and point arrays + self._setPieceRegionAttribute( attributeName ) + if self.onPoints is None: + self.logger.error( f"{ attributeName } is not in the mesh." ) + self.logger.error( f"The attribute { attributeName } has not been filled." ) + self.logger.error( f"The filter { self.logger.name } failed.") + return False + + if self.onBoth: + self.logger.error( f"Their is two attribute named { attributeName }, one on points and the other on cells. The attribute must be unique." ) + self.logger.error( f"The attribute { attributeName } has not been filled." ) + self.logger.error( f"The filter { self.logger.name } failed.") + return False + + nbComponents: int = getNumberOfComponents( self.multiBlockDataSet, attributeName, self.onPoints ) + if not fillPartialAttributes( self.multiBlockDataSet, attributeName, nbComponents, self.onPoints, self.dictAttributesValues[ attributeName ] ): + self.logger.error( f"The filter { self.logger.name } failed.") + return False + + + def _setPieceRegionAttribute( self: Self, attributeName: str ) -> None: + """Set the attribute self.onPoints and self.onBoth. + + self.onPoints is True if the region attribute is on points, False if it is on cells, None otherwise. + + self.onBoth is True if a region attribute is on points and on cells, False otherwise. + + Args: + attributeName (str): The name of the attribute to verify. """ - self._valueToFill: float = valueToFill + self.onPoints: Union[ bool, None ] = None + self.onBoth: bool = False + if isAttributeInObject( self.multiBlockDataSet, attributeName, False ): + self.onPoints = False + if isAttributeInObject( self.multiBlockDataSet, attributeName, True ): + if self.onPoints == False: + self.onBoth = True + self.onPoints = True From 9ca8b56271f9495c59d113a98e9480c9c6e4b327 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 16:17:45 +0200 Subject: [PATCH 50/58] Fix logger --- .../geos/pv/plugins/PVFillPartialArrays.py | 137 ++++++++---------- 1 file changed, 57 insertions(+), 80 deletions(-) diff --git a/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py b/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py index 287cc03b..b9227398 100644 --- a/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py +++ b/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py @@ -10,7 +10,10 @@ from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] VTKPythonAlgorithmBase, smdomain, smhint, smproperty, smproxy, -) +) # source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/util/vtkAlgorithm.py +from paraview.detail.loghandler import ( # type: ignore[import-not-found] + VTKHandler, +) # source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/detail/loghandler.py from vtkmodules.vtkCommonDataModel import ( vtkMultiBlockDataSet, ) @@ -55,86 +58,54 @@ class PVFillPartialArrays( VTKPythonAlgorithmBase ): def __init__( self: Self, ) -> None: - """Map the properties of a server mesh to a client mesh.""" + """Fill a partial attribute with constant value per component.""" super().__init__( nInputPorts=1, nOutputPorts=1, inputType="vtkMultiBlockDataSet", outputType="vtkMultiBlockDataSet" ) - # initialization of an empty list of the attribute's name - self._clearSelectedAttributeMulti: bool = True - self._attributesNameList: list[ str ] = [] - - # initialization of the value (nan) to fill in the partial attributes - self._valueToFill: float = np.nan - - @smproperty.stringvector( - name="SelectMultipleAttribute", - label="Select Attributes to fill", - repeat_command=1, - number_of_elements_per_command="1", - element_types="2", - default_values="N/A", - panel_visibility="default", - ) - @smdomain.xml( """ - - - - - - - Select all the attributes to fill. If several attributes - are selected, they will be filled with the same value. - - - - - """ ) - def a02SelectMultipleAttribute( self: Self, name: str ) -> None: - """Set the list of the names of the selected attributes to fill. - - Args: - name (str): Input value - """ - if self._clearSelectedAttributeMulti: - self._attributesNameList.clear() - self._clearSelectedAttributeMulti = False - - if name != "N/A": - self._attributesNameList.append( name ) - self.Modified() - - @smproperty.stringvector( - name="StringSingle", - label="Value to fill", - number_of_elements="1", - default_values="nan", - panel_visibility="default", - ) - @smdomain.xml( """ - - Enter the value to fill in the partial attributes. The - default value is nan - - """ ) - def a01StringSingle( self: Self, value: str ) -> None: - """Set the value to fill in the attributes. + self._clearDictAttributesValues: bool = True + self.dictAttributesValues: dict[ str, str ] = {} + + + @smproperty.xml(""" + + + Set the filling values for each partial attribute, use a coma between the value of each components:\n + attributeName | fillingValueComponent1 fillingValueComponent2 ...\n + To fill the attribute with the default value, live a blanc. The default value is:\n + 0 for uint type, -1 for int type and nan for float type. + + + + + + + + + + """ ) + def _setDictAttributesValues( self: Self, attributeName: str, values: str ) -> None: + """Set the the dictionary with the region indexes and its corresponding list of value for each components. Args: - value (str): Input + attributeName (str): Name of the attribute to consider. + values (str): List of the filing values. If multiple components use a coma between the value of each component. """ - assert value is not None, "Enter a number or nan" - assert "," not in value, "Use '.' not ',' for decimal numbers" - - value_float: float - value_float = np.nan if value.lower() == "nan" else float( value ) - - if value_float != self._valueToFill: - self._valueToFill = value_float + if self.clearDictAttributesValues: + self.dictAttributesValues = {} + self.clearDictAttributesValues = False + + if attributeName is not None and values is not None : + self.dictAttributesValues[ attributeName ] = list( values.split( "," ) ) + elif attributeName is not None and values is None: + self.dictAttributesValues[ attributeName ] = [] + self.Modified() def RequestDataObject( @@ -182,12 +153,18 @@ def RequestData( assert inputMesh is not None, "Input server mesh is null." assert outputMesh is not None, "Output pipeline is null." - filter: FillPartialArrays = FillPartialArrays() - filter._SetAttributesNameList( self._attributesNameList ) - filter._SetValueToFill( self._valueToFill ) - filter.SetInputDataObject( inputMesh ) - filter.Update() - outputMesh.ShallowCopy( filter.GetOutputDataObject( 0 ) ) + outputMesh.ShallowCopy( inputMesh ) + + filter: FillPartialArrays = FillPartialArrays( outputMesh, + self.dictAttributesValues, + True, + ) + + if not filter.logger.hasHandlers(): + filter.setLoggerHandler( VTKHandler() ) + + filter.applyFilter() + + self._clearDictAttributesValues = True - self._clearSelectedAttributeMulti = True return 1 From 68b29f5ce96d9b8813d333dfe75f5c21352bbd3a Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 18:09:58 +0200 Subject: [PATCH 51/58] update FillPartialArrays to deals with multiple component --- .../src/geos/mesh/utils/arrayModifiers.py | 51 +++++++++++-------- geos-mesh/tests/test_arrayModifiers.py | 51 +++++++++---------- 2 files changed, 53 insertions(+), 49 deletions(-) diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index 79a1b6b6..c7decca3 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -41,6 +41,7 @@ isAttributeGlobal, getVtkArrayTypeInObject, getVtkArrayTypeInMultiBlock, + getNumberOfComponentsMultiBlock, ) from geos.mesh.utils.multiblockHelpers import ( getBlockElementIndexesFlatten, @@ -61,7 +62,7 @@ def fillPartialAttributes( multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], attributeName: str, onPoints: bool = False, - value: Any = np.nan, + listValues: list[ Any ] = [], logger: Union[ Logger, None ] = None, ) -> bool: """Fill input partial attribute of multiBlockDataSet with the same value for all the components. @@ -71,8 +72,8 @@ 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 recommended to use numpy scalar type for the values. - Defaults to: + listValues (list[Any], optional): List of filling value for each component. + Defaults to [], the filling value is: -1 for int VTK arrays. 0 for uint VTK arrays. nan for float VTK arrays. @@ -98,40 +99,46 @@ def fillPartialAttributes( # Get information of the attribute to fill. vtkDataType: int = getVtkArrayTypeInMultiBlock( multiBlockDataSet, attributeName, onPoints ) - infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints ) - nbComponents: int = infoAttributes[ attributeName ] + nbComponents: int = getNumberOfComponentsMultiBlock( multiBlockDataSet, attributeName, onPoints ) componentNames: tuple[ str, ...] = () if nbComponents > 1: componentNames = getComponentNames( multiBlockDataSet, attributeName, onPoints ) + typeMapping: dict[ int, type ] = vnp.get_vtk_to_numpy_typemap() + valueType: type = typeMapping[ vtkDataType ] # Set the default value depending of the type of the attribute to fill - if np.isnan( value ): - typeMapping: dict[ int, type ] = vnp.get_vtk_to_numpy_typemap() - valueType: type = typeMapping[ vtkDataType ] + if len( listValues ) == 0: # 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 } corresponding to { value.dtype } numpy type, default value is automatically set to nan." + listValues.append( valueType( np.nan ) ) + logger.info( + f"{ attributeName } vtk data type is { vtkDataType } corresponding to { valueType().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 } corresponding to { value.dtype } numpy type, default value is automatically set to -1." + listValues.append( valueType( -1 ) ) + logger.info( + f"{ attributeName } vtk data type is { vtkDataType } corresponding to { valueType().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 } corresponding to { value.dtype } numpy type, default value is automatically set to 0." + listValues.append( valueType( 0 ) ) + logger.info( + f"{ attributeName } vtk data type is { vtkDataType } corresponding to { valueType().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." ) return False - - values: list[ Any ] = [ value for _ in range( nbComponents ) ] + + listValues = listValues * nbComponents + + else: + if len( listValues ) != nbComponents: + return False + + for idValue in range( nbComponents ): + listValues[ idValue ] = valueType( listValues[ idValue ] ) # Parse the multiBlockDataSet to create and fill the attribute on blocks where it is not. iterator: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() @@ -141,7 +148,7 @@ def fillPartialAttributes( 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 ): + not createConstantAttributeDataSet( dataSet, listValues, attributeName, componentNames, onPoints, vtkDataType, logger ): return False iterator.GoToNextItem() @@ -172,7 +179,7 @@ def fillAllPartialAttributes( infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints ) for attributeName in infoAttributes: if not isAttributeGlobal( multiBlockDataSet, attributeName, onPoints ) and \ - not fillPartialAttributes( multiBlockDataSet, attributeName, onPoints, logger=logger ): + not fillPartialAttributes( multiBlockDataSet, attributeName, onPoints=onPoints, listValues=[], logger=logger ): return False return True @@ -384,7 +391,7 @@ def createConstantAttributeDataSet( 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 }." + f"During the creation of the constant attribute { attributeName }, values have been converted from { valueType } to { npType }." ) logger.warning( "To avoid any issue with the conversion, please use directly numpy scalar type for the values" ) valueType = npType diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index cf9b6311..c05e5d8c 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -45,24 +45,21 @@ @pytest.mark.parametrize( - "idBlock, attributeName, nbComponentsTest, componentNamesTest, onPoints, value, valueTest, vtkDataTypeTest", + "idBlock, attributeName, nbComponentsTest, componentNamesTest, onPoints, listValues, listValuesTest, 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 ), + ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, [], [ np.float64( np.nan ), np.float64( np.nan ), np.float64( np.nan ) ], VTK_DOUBLE ), + ( 1, "PointAttribute", 3, ( "AX1", "AX2", "AX3" ), True, [], [ np.float64( np.nan ), np.float64( np.nan ), np.float64( np.nan ) ], VTK_DOUBLE ), # 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. - ( 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 ), + ( 1, "PORO", 1, (), False, [], [ np.float32( np.nan ) ], VTK_FLOAT ), + # Test fill an attribute with different type of value with default value. + ( 1, "FAULT", 1, (), False, [], [ np.int32( -1 ) ], VTK_INT ), + ( 0, "collocated_nodes", 2, ( None, None ), True, [], [ np.int64( -1 ), 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 ), + ( 1, "PORO", 1, (), False, [ 4 ], [ np.float32( 4 ) ], VTK_FLOAT ), + ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, [ 4, 4, 4 ], [ np.float64( 4 ), np.float64( 4 ), np.float64( 4 ) ], VTK_DOUBLE ), + ( 1, "FAULT", 1, (), False, [ 4 ], [ np.int32( 4 ) ], VTK_INT ), + ( 0, "collocated_nodes", 2, ( None, None ), True, [ 4, 4 ], [ np.int64( 4 ), np.int64( 4 ) ], VTK_ID_TYPE ), ] ) def test_fillPartialAttributes( dataSetTest: vtkMultiBlockDataSet, @@ -71,18 +68,18 @@ def test_fillPartialAttributes( nbComponentsTest: int, componentNamesTest: tuple[ str, ...], onPoints: bool, - value: Any, - valueTest: Any, + listValues: list[ Any ], + listValuesTest: list[ Any ], vtkDataTypeTest: int, ) -> None: """Test filling a partial attribute from a multiblock with values.""" multiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - + nbValues: int = len( listValues ) # Fill the attribute in the multiBlockDataSet. - assert arrayModifiers.fillPartialAttributes( multiBlockDataSetTest, attributeName, onPoints, value ) + assert arrayModifiers.fillPartialAttributes( multiBlockDataSetTest, attributeName, onPoints=onPoints, listValues=listValues ) # Get the dataSet where the attribute has been filled. - dataSet: vtkDataSet = cast( vtkDataSet, multiBlockDataSetTest.GetBlock( idBlock ) ) + dataSet: vtkDataSet = vtkDataSet.SafeDownCast( multiBlockDataSetTest.GetBlock( idBlock ) ) # Get the filled attribute. data: Union[ vtkPointData, vtkCellData ] @@ -107,13 +104,13 @@ def test_fillPartialAttributes( ## 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 ) ] ) + npArrayTest = np.array( [ listValuesTest for _ in range( nbElements ) ] ) else: - npArrayTest = np.array( [ valueTest for _ in range( nbElements ) ] ) + npArrayTest = np.array( [ listValuesTest[ 0 ] for _ in range( nbElements ) ] ) 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 ): + if nbValues == 0 and vtkDataTypeTest in ( VTK_FLOAT, VTK_DOUBLE ): assert np.isnan( npArrayFilled ).all() else: assert ( npArrayFilled == npArrayTest ).all() @@ -133,7 +130,7 @@ def test_FillAllPartialAttributes( nbBlock: int = multiBlockDataSetTest.GetNumberOfBlocks() for idBlock in range( nbBlock ): - dataSet: vtkDataSet = cast( vtkDataSet, multiBlockDataSetTest.GetBlock( idBlock ) ) + dataSet: vtkDataSet = vtkDataSet.SafeDownCast( multiBlockDataSetTest.GetBlock( idBlock ) ) attributeExist: int for attributeNameOnPoint in [ "PointAttribute", "collocated_nodes" ]: attributeExist = dataSet.GetPointData().HasArray( attributeNameOnPoint ) @@ -190,7 +187,7 @@ def test_createConstantAttributeMultiBlock( nbBlock = multiBlockDataSetTest.GetNumberOfBlocks() for idBlock in range( nbBlock ): - dataSet: vtkDataSet = cast( vtkDataSet, multiBlockDataSetTest.GetBlock( idBlock ) ) + dataSet: vtkDataSet = vtkDataSet.SafeDownCast( multiBlockDataSetTest.GetBlock( idBlock ) ) data: Union[ vtkPointData, vtkCellData ] data = dataSet.GetPointData() if onPoints else dataSet.GetCellData() @@ -409,8 +406,8 @@ def test_copyAttribute( # 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 ) ) + dataSetFrom: vtkDataSet = vtkDataSet.SafeDownCast( multiBlockDataSetFrom.GetBlock( idBlock ) ) + dataSetTo: vtkDataSet = vtkDataSet.SafeDownCast( multiBlockDataSetTo.GetBlock( idBlock ) ) dataFrom: Union[ vtkPointData, vtkCellData ] dataTo: Union[ vtkPointData, vtkCellData ] if onPoints: @@ -494,7 +491,7 @@ def test_renameAttributeMultiblock( newAttributeName, onPoints, ) - block: vtkDataSet = cast( vtkDataSet, vtkMultiBlockDataSetTest.GetBlock( 0 ) ) + block: vtkDataSet = vtkDataSet.SafeDownCast( vtkMultiBlockDataSetTest.GetBlock( 0 ) ) data: Union[ vtkPointData, vtkCellData ] if onPoints: data = block.GetPointData() From 729f58c15cae1eb03422db9a836d67ca23ff5276 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 6 Aug 2025 18:33:41 +0200 Subject: [PATCH 52/58] update do set a value per component --- .../src/geos/mesh/processing/FillPartialArrays.py | 14 +++++++------- geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py | 6 ++---- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py index ecddb5db..000ad69e 100644 --- a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py +++ b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py @@ -5,7 +5,8 @@ from typing_extensions import Self from typing import Union, Any -from geos.utils.Logger import logging, Logger, getLogger, CountWarningHandler +from geos.utils.Logger import logging, Logger, getLogger +#, CountWarningHandler from geos.mesh.utils.arrayModifiers import fillPartialAttributes from geos.mesh.utils.arrayHelpers import ( getNumberOfComponents, @@ -76,9 +77,9 @@ def __init__( self.multiBlockDataSet: vtkMultiBlockDataSet = multiBlockDataSet self.dictAttributesValues: dict[ str, Any ] = dictAttributesValues - # Warnings counter. - self.counter: CountWarningHandler = CountWarningHandler() - self.counter.setLevel( logging.INFO ) + # # Warnings counter. + # self.counter: CountWarningHandler = CountWarningHandler() + # self.counter.setLevel( logging.INFO ) # Logger. if not speHandler: @@ -112,7 +113,7 @@ def applyFilter( self: Self ) -> bool: self.logger.info( f"Apply filter { self.logger.name }." ) # Add the handler to count warnings messages. - self.logger.addHandler( self.counter ) + #self.logger.addHandler( self.counter ) for attributeName in self.dictAttributesValues: # cell and point arrays @@ -129,8 +130,7 @@ def applyFilter( self: Self ) -> bool: self.logger.error( f"The filter { self.logger.name } failed.") return False - nbComponents: int = getNumberOfComponents( self.multiBlockDataSet, attributeName, self.onPoints ) - if not fillPartialAttributes( self.multiBlockDataSet, attributeName, nbComponents, self.onPoints, self.dictAttributesValues[ attributeName ] ): + if not fillPartialAttributes( self.multiBlockDataSet, attributeName, onPoints=self.onPoints, listValues=self.dictAttributesValues[ attributeName ], logger=self.logger ): self.logger.error( f"The filter { self.logger.name } failed.") return False diff --git a/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py b/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py index b9227398..0341baee 100644 --- a/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py +++ b/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py @@ -6,8 +6,6 @@ from pathlib import Path from typing_extensions import Self -import numpy as np - from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] VTKPythonAlgorithmBase, smdomain, smhint, smproperty, smproxy, ) # source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/util/vtkAlgorithm.py @@ -64,7 +62,7 @@ def __init__( self: Self, ) -> None: inputType="vtkMultiBlockDataSet", outputType="vtkMultiBlockDataSet" ) - self._clearDictAttributesValues: bool = True + self.clearDictAttributesValues: bool = True self.dictAttributesValues: dict[ str, str ] = {} @@ -165,6 +163,6 @@ def RequestData( filter.applyFilter() - self._clearDictAttributesValues = True + self.clearDictAttributesValues = True return 1 From d23fc0ac336bc50a66d67b25a90644d0306520b4 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Thu, 7 Aug 2025 11:41:39 +0200 Subject: [PATCH 53/58] clean for ci --- .../geos/mesh/processing/FillPartialArrays.py | 117 ++++++++++-------- .../src/geos/mesh/utils/arrayModifiers.py | 45 ++++--- geos-mesh/tests/test_arrayModifiers.py | 36 +++--- 3 files changed, 110 insertions(+), 88 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py index 000ad69e..ec0bf251 100644 --- a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py +++ b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py @@ -8,26 +8,23 @@ from geos.utils.Logger import logging, Logger, getLogger #, CountWarningHandler from geos.mesh.utils.arrayModifiers import fillPartialAttributes -from geos.mesh.utils.arrayHelpers import ( - getNumberOfComponents, - isAttributeInObject, -) +from geos.mesh.utils.arrayHelpers import isAttributeInObject -from vtkmodules.vtkCommonDataModel import ( - vtkMultiBlockDataSet, ) - -import numpy as np +from vtkmodules.vtkCommonDataModel import vtkMultiBlockDataSet __doc__ = """ -Fill partial attributes of input mesh with constant values per component. +Fill partial attributes of the input mesh with constant values per component. + +Input mesh is vtkMultiBlockDataSet and attributes to fill must be partial. + +By defaults, attributes are filled with the same constant value for each component; +0 for uint data, -1 for int data and nan for float data. + +The list of filling values per attribute is given by a dictionary. +Its keys are the attribute names and its items are the list of filling values for each component. -Input mesh is vtkMultiBlockDataSet and the attribute to fill must be partial. -By defaults, attributes are filled with the same constant value for each component: - 0 for uint data. - -1 for int data. - nan for float data. -The filling values per attribute is given by a dictionary. Its keys are the attribute names and its items are the list of filling values for each component. -To use a specific handler for the logger, set the variable 'speHandler' to True and use the member function addLoggerHandler. +To use a handler of yours for the logger, set the variable 'speHandler' to True and add it to the filter +with the member function addLoggerHandler. To use it: @@ -38,35 +35,37 @@ # Filter inputs. multiBlockDataSet: vtkMultiBlockDataSet dictAttributesValues: dict[ str, Any ] + # Optional inputs. + speHandler: bool # Instantiate the filter. - filter: FillPartialArrays = FillPartialArrays( multiBlockDataSet, dictAttributesValues ) - - # Set the specific handler (only if speHandler is True). - specificHandler: logging.Handler - filter.addLoggerHandler( specificHandler ) + filter: FillPartialArrays = FillPartialArrays( multiBlockDataSet, dictAttributesValues, speHandler ) + + # Set the handler of yours (only if speHandler is True). + yourHandler: logging.Handler + filter.addLoggerHandler( yourHandler ) # Do calculations. filter.applyFilter() """ - loggerTitle: str = "Fill Partial Attribute" class FillPartialArrays: - def __init__( - self: Self, - multiBlockDataSet: vtkMultiBlockDataSet, - dictAttributesValues: dict[ str, Any ], - speHandler: bool = False, - ) -> None: - """ - Fill a partial attribute with constant value per component. If the list of filling values for an attribute is empty, it will filled with the default value: + def __init__( + self: Self, + multiBlockDataSet: vtkMultiBlockDataSet, + dictAttributesValues: dict[ str, Any ], + speHandler: bool = False, + ) -> None: + """Fill partial attributes with constant value per component. + + If the list of filling values for an attribute is empty, it will filled with the default value for each component: 0 for uint data. -1 for int data. - nan for float data. + nan for float data. Args: multiBlockDataSet (vtkMultiBlockDataSet): The mesh where to fill the attribute. @@ -82,27 +81,28 @@ def __init__( # self.counter.setLevel( logging.INFO ) # Logger. + self.logger: Logger if not speHandler: - self.logger: Logger = getLogger( loggerTitle, True ) + self.logger = getLogger( loggerTitle, True ) else: - self.logger: Logger = logging.getLogger( loggerTitle ) + self.logger = logging.getLogger( loggerTitle ) self.logger.setLevel( logging.INFO ) - - + def setLoggerHandler( self: Self, handler: logging.Handler ) -> None: """Set a specific handler for the filter logger. - In this filter 4 log levels are use, .info, .error, .warning and .critical, - be sure to have at least the same 4 levels. - + + In this filter 4 log levels are use, .info, .error, .warning and .critical, be sure to have at least the same 4 levels. + Args: - handler (logging.Handler): The handler to add. + handler (logging.Handler): The handler to add. """ if not self.logger.hasHandlers(): self.logger.addHandler( handler ) else: # This warning does not count for the number of warning created during the application of the filter. - self.logger.warning( "The logger already has an handler, to use yours set the argument 'speHandler' to True during the filter initialization." ) - + self.logger.warning( + "The logger already has an handler, to use yours set the argument 'speHandler' to True during the filter initialization." + ) def applyFilter( self: Self ) -> bool: """Create a constant attribute per region in the mesh. @@ -121,35 +121,44 @@ def applyFilter( self: Self ) -> bool: if self.onPoints is None: self.logger.error( f"{ attributeName } is not in the mesh." ) self.logger.error( f"The attribute { attributeName } has not been filled." ) - self.logger.error( f"The filter { self.logger.name } failed.") + self.logger.error( f"The filter { self.logger.name } failed." ) return False - + if self.onBoth: - self.logger.error( f"Their is two attribute named { attributeName }, one on points and the other on cells. The attribute must be unique." ) + self.logger.error( + f"Their is two attribute named { attributeName }, one on points and the other on cells. The attribute must be unique." + ) self.logger.error( f"The attribute { attributeName } has not been filled." ) - self.logger.error( f"The filter { self.logger.name } failed.") + self.logger.error( f"The filter { self.logger.name } failed." ) return False - - if not fillPartialAttributes( self.multiBlockDataSet, attributeName, onPoints=self.onPoints, listValues=self.dictAttributesValues[ attributeName ], logger=self.logger ): - self.logger.error( f"The filter { self.logger.name } failed.") + + listValues: Union[ list[ Any ], None ] = self.dictAttributesValues[ + attributeName ] if self.dictAttributesValues[ attributeName ] != [] else None + if not fillPartialAttributes( self.multiBlockDataSet, + attributeName, + onPoints=self.onPoints, + listValues=listValues, + logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed." ) return False - + + return True def _setPieceRegionAttribute( self: Self, attributeName: str ) -> None: """Set the attribute self.onPoints and self.onBoth. - self.onPoints is True if the region attribute is on points, False if it is on cells, None otherwise. + self.onPoints is True if the region attribute is on points, False if it is on cells, None otherwise. - self.onBoth is True if a region attribute is on points and on cells, False otherwise. + self.onBoth is True if a region attribute is on points and on cells, False otherwise. - Args: - attributeName (str): The name of the attribute to verify. + Args: + attributeName (str): The name of the attribute to verify. """ self.onPoints: Union[ bool, None ] = None self.onBoth: bool = False if isAttributeInObject( self.multiBlockDataSet, attributeName, False ): self.onPoints = False if isAttributeInObject( self.multiBlockDataSet, attributeName, True ): - if self.onPoints == False: + if self.onPoints is False: self.onBoth = True self.onPoints = True diff --git a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py index c7decca3..e5c8b0a6 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayModifiers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayModifiers.py @@ -62,10 +62,10 @@ def fillPartialAttributes( multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ], attributeName: str, onPoints: bool = False, - listValues: list[ Any ] = [], + listValues: Union[ list[ Any ], None ] = None, logger: Union[ Logger, None ] = None, ) -> bool: - """Fill input partial attribute of multiBlockDataSet with the same value for all the components. + """Fill input partial attribute of multiBlockDataSet with a constant value per component. Args: multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): MultiBlockDataSet where to fill the attribute. @@ -73,7 +73,7 @@ def fillPartialAttributes( onPoints (bool, optional): True if attributes are on points, False if they are on cells. Defaults to False. listValues (list[Any], optional): List of filling value for each component. - Defaults to [], the filling value is: + Defaults to None, the filling value is for all components: -1 for int VTK arrays. 0 for uint VTK arrays. nan for float VTK arrays. @@ -107,38 +107,45 @@ def fillPartialAttributes( typeMapping: dict[ int, type ] = vnp.get_vtk_to_numpy_typemap() valueType: type = typeMapping[ vtkDataType ] # Set the default value depending of the type of the attribute to fill - if len( listValues ) == 0: + if listValues is None: + defaultValue: Any + logger.warning( f"The attribute { attributeName } is filled with the default value for each component." ) # Default value for float types is nan. if vtkDataType in ( VTK_FLOAT, VTK_DOUBLE ): - listValues.append( valueType( np.nan ) ) - logger.info( - f"{ attributeName } vtk data type is { vtkDataType } corresponding to { valueType().dtype } numpy type, default value is automatically set to nan." + defaultValue = valueType( np.nan ) + logger.warning( + f"{ attributeName } vtk data type is { vtkDataType } corresponding to { defaultValue.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 ): - listValues.append( valueType( -1 ) ) - logger.info( - f"{ attributeName } vtk data type is { vtkDataType } corresponding to { valueType().dtype } numpy type, default value is automatically set to -1." + defaultValue = valueType( -1 ) + logger.warning( + f"{ attributeName } vtk data type is { vtkDataType } corresponding to { defaultValue.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 ): - listValues.append( valueType( 0 ) ) - logger.info( - f"{ attributeName } vtk data type is { vtkDataType } corresponding to { valueType().dtype } numpy type, default value is automatically set to 0." + defaultValue = valueType( 0 ) + logger.warning( + f"{ attributeName } vtk data type is { vtkDataType } corresponding to { defaultValue.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." ) return False - - listValues = listValues * nbComponents - + + listValues = [ defaultValue ] * nbComponents + else: if len( listValues ) != nbComponents: return False - + for idValue in range( nbComponents ): - listValues[ idValue ] = valueType( listValues[ idValue ] ) + value: Any = listValues[ idValue ] + if type( value ) is not valueType: + listValues[ idValue ] = valueType( listValues[ idValue ] ) + logger.warning( + f"The filling value { value } for the attribute { attributeName } has not the correct type, it is convert to the numpy scalar type { valueType().dtype }." + ) # Parse the multiBlockDataSet to create and fill the attribute on blocks where it is not. iterator: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator() @@ -179,7 +186,7 @@ def fillAllPartialAttributes( infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints ) for attributeName in infoAttributes: if not isAttributeGlobal( multiBlockDataSet, attributeName, onPoints ) and \ - not fillPartialAttributes( multiBlockDataSet, attributeName, onPoints=onPoints, listValues=[], logger=logger ): + not fillPartialAttributes( multiBlockDataSet, attributeName, onPoints=onPoints, logger=logger ): return False return True diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index c05e5d8c..3ae4e71e 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -5,7 +5,7 @@ # ruff: noqa: E402 # disable Module level import not at top of file # mypy: disable-error-code="operator" import pytest -from typing import Union, Any, cast +from typing import Union, Any import numpy as np import numpy.typing as npt @@ -48,18 +48,22 @@ "idBlock, attributeName, nbComponentsTest, componentNamesTest, onPoints, listValues, listValuesTest, vtkDataTypeTest", [ # Test fill an attribute on point and on cell. - ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, [], [ np.float64( np.nan ), np.float64( np.nan ), np.float64( np.nan ) ], VTK_DOUBLE ), - ( 1, "PointAttribute", 3, ( "AX1", "AX2", "AX3" ), True, [], [ np.float64( np.nan ), np.float64( np.nan ), np.float64( np.nan ) ], VTK_DOUBLE ), - # Test fill attributes with different number of component. - ( 1, "PORO", 1, (), False, [], [ np.float32( np.nan ) ], VTK_FLOAT ), - # Test fill an attribute with different type of value with default value. - ( 1, "FAULT", 1, (), False, [], [ np.int32( -1 ) ], VTK_INT ), - ( 0, "collocated_nodes", 2, ( None, None ), True, [], [ np.int64( -1 ), np.int64( -1 ) ], VTK_ID_TYPE ), - # Test fill an attribute with specified value. - ( 1, "PORO", 1, (), False, [ 4 ], [ np.float32( 4 ) ], VTK_FLOAT ), - ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, [ 4, 4, 4 ], [ np.float64( 4 ), np.float64( 4 ), np.float64( 4 ) ], VTK_DOUBLE ), + ( 1, "PointAttribute", 3, + ( "AX1", "AX2", "AX3" ), True, None, [ np.float64( + np.nan ), np.float64( np.nan ), np.float64( np.nan ) ], VTK_DOUBLE ), + ( 1, "CellAttribute", 3, + ( "AX1", "AX2", "AX3" ), False, None, [ np.float64( + np.nan ), np.float64( np.nan ), np.float64( np.nan ) ], VTK_DOUBLE ), + # Test fill attributes with different number of component with or without component names. + ( 1, "PORO", 1, (), False, None, [ np.float32( np.nan ) ], VTK_FLOAT ), + ( 0, "collocated_nodes", 2, ( None, None ), True, None, [ np.int64( -1 ), np.int64( -1 ) ], VTK_ID_TYPE ), + # Test fill an attribute with different type of value. + ( 1, "FAULT", 1, (), False, None, [ np.int32( -1 ) ], VTK_INT ), ( 1, "FAULT", 1, (), False, [ 4 ], [ np.int32( 4 ) ], VTK_INT ), + ( 1, "PORO", 1, (), False, [ 4 ], [ np.float32( 4 ) ], VTK_FLOAT ), ( 0, "collocated_nodes", 2, ( None, None ), True, [ 4, 4 ], [ np.int64( 4 ), np.int64( 4 ) ], VTK_ID_TYPE ), + ( 1, "CellAttribute", 3, ( "AX1", "AX2", "AX3" ), False, [ 4, 4, 4 ], + [ np.float64( 4 ), np.float64( 4 ), np.float64( 4 ) ], VTK_DOUBLE ), ] ) def test_fillPartialAttributes( dataSetTest: vtkMultiBlockDataSet, @@ -68,15 +72,17 @@ def test_fillPartialAttributes( nbComponentsTest: int, componentNamesTest: tuple[ str, ...], onPoints: bool, - listValues: list[ Any ], + listValues: Union[ list[ Any ], None ], listValuesTest: list[ Any ], vtkDataTypeTest: int, ) -> None: """Test filling a partial attribute from a multiblock with values.""" multiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - nbValues: int = len( listValues ) # Fill the attribute in the multiBlockDataSet. - assert arrayModifiers.fillPartialAttributes( multiBlockDataSetTest, attributeName, onPoints=onPoints, listValues=listValues ) + assert arrayModifiers.fillPartialAttributes( multiBlockDataSetTest, + attributeName, + onPoints=onPoints, + listValues=listValues ) # Get the dataSet where the attribute has been filled. dataSet: vtkDataSet = vtkDataSet.SafeDownCast( multiBlockDataSetTest.GetBlock( idBlock ) ) @@ -110,7 +116,7 @@ def test_fillPartialAttributes( npArrayFilled: npt.NDArray[ Any ] = vnp.vtk_to_numpy( attributeFilled ) assert npArrayFilled.dtype == npArrayTest.dtype - if nbValues == 0 and vtkDataTypeTest in ( VTK_FLOAT, VTK_DOUBLE ): + if listValues is None and vtkDataTypeTest in ( VTK_FLOAT, VTK_DOUBLE ): assert np.isnan( npArrayFilled ).all() else: assert ( npArrayFilled == npArrayTest ).all() From 0709b43e551eca9ecf73542276243a8b07a90679 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Thu, 7 Aug 2025 12:07:29 +0200 Subject: [PATCH 54/58] Update the test and the typing --- .../geos/mesh/processing/FillPartialArrays.py | 30 ++---- geos-mesh/tests/test_FillPartialArrays.py | 92 +++++++------------ .../geos/pv/plugins/PVFillPartialArrays.py | 5 +- 3 files changed, 48 insertions(+), 79 deletions(-) diff --git a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py index ec0bf251..b89df611 100644 --- a/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py +++ b/geos-mesh/src/geos/mesh/processing/FillPartialArrays.py @@ -6,7 +6,6 @@ from typing import Union, Any from geos.utils.Logger import logging, Logger, getLogger -#, CountWarningHandler from geos.mesh.utils.arrayModifiers import fillPartialAttributes from geos.mesh.utils.arrayHelpers import isAttributeInObject @@ -17,12 +16,12 @@ Input mesh is vtkMultiBlockDataSet and attributes to fill must be partial. -By defaults, attributes are filled with the same constant value for each component; -0 for uint data, -1 for int data and nan for float data. - The list of filling values per attribute is given by a dictionary. Its keys are the attribute names and its items are the list of filling values for each component. +If the list of filling value is None, attributes are filled with the same constant value for each component; +0 for uint data, -1 for int data and nan for float data. + To use a handler of yours for the logger, set the variable 'speHandler' to True and add it to the filter with the member function addLoggerHandler. @@ -34,7 +33,7 @@ # Filter inputs. multiBlockDataSet: vtkMultiBlockDataSet - dictAttributesValues: dict[ str, Any ] + dictAttributesValues: dict[ str, Union[ list[ Any ], None ] ] # Optional inputs. speHandler: bool @@ -57,12 +56,12 @@ class FillPartialArrays: def __init__( self: Self, multiBlockDataSet: vtkMultiBlockDataSet, - dictAttributesValues: dict[ str, Any ], + dictAttributesValues: dict[ str, Union[ list[ Any ], None ] ], speHandler: bool = False, ) -> None: """Fill partial attributes with constant value per component. - If the list of filling values for an attribute is empty, it will filled with the default value for each component: + If the list of filling values for an attribute is None, it will filled with the default value for each component: 0 for uint data. -1 for int data. nan for float data. @@ -74,11 +73,7 @@ def __init__( Defaults to False. """ self.multiBlockDataSet: vtkMultiBlockDataSet = multiBlockDataSet - self.dictAttributesValues: dict[ str, Any ] = dictAttributesValues - - # # Warnings counter. - # self.counter: CountWarningHandler = CountWarningHandler() - # self.counter.setLevel( logging.INFO ) + self.dictAttributesValues: dict[ str, Union[ list[ Any ], None ] ] = dictAttributesValues # Logger. self.logger: Logger @@ -99,7 +94,6 @@ def setLoggerHandler( self: Self, handler: logging.Handler ) -> None: if not self.logger.hasHandlers(): self.logger.addHandler( handler ) else: - # This warning does not count for the number of warning created during the application of the filter. self.logger.warning( "The logger already has an handler, to use yours set the argument 'speHandler' to True during the filter initialization." ) @@ -112,11 +106,7 @@ def applyFilter( self: Self ) -> bool: """ self.logger.info( f"Apply filter { self.logger.name }." ) - # Add the handler to count warnings messages. - #self.logger.addHandler( self.counter ) - for attributeName in self.dictAttributesValues: - # cell and point arrays self._setPieceRegionAttribute( attributeName ) if self.onPoints is None: self.logger.error( f"{ attributeName } is not in the mesh." ) @@ -132,16 +122,16 @@ def applyFilter( self: Self ) -> bool: self.logger.error( f"The filter { self.logger.name } failed." ) return False - listValues: Union[ list[ Any ], None ] = self.dictAttributesValues[ - attributeName ] if self.dictAttributesValues[ attributeName ] != [] else None if not fillPartialAttributes( self.multiBlockDataSet, attributeName, onPoints=self.onPoints, - listValues=listValues, + listValues=self.dictAttributesValues[ attributeName ], logger=self.logger ): self.logger.error( f"The filter { self.logger.name } failed." ) return False + self.logger.info( f"The filter { self.logger.name } succeed." ) + return True def _setPieceRegionAttribute( self: Self, attributeName: str ) -> None: diff --git a/geos-mesh/tests/test_FillPartialArrays.py b/geos-mesh/tests/test_FillPartialArrays.py index 31af5702..b1fd8f3a 100644 --- a/geos-mesh/tests/test_FillPartialArrays.py +++ b/geos-mesh/tests/test_FillPartialArrays.py @@ -5,71 +5,49 @@ # 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 -import numpy as np -import numpy.typing as npt - -import vtkmodules.util.numpy_support as vnp -from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet, vtkPointData, vtkCellData ) +from typing import Any +from vtkmodules.vtkCommonDataModel import vtkMultiBlockDataSet from geos.mesh.processing.FillPartialArrays import FillPartialArrays -@pytest.mark.parametrize( "onpoints, attributesList, value_test", [ - ( False, ( ( 0, "PORO", 1 ), ), np.nan ), - ( True, ( ( 0, "PointAttribute", 3 ), ( 1, "collocated_nodes", 2 ) ), 2. ), - ( False, ( ( 0, "CELL_MARKERS", 1 ), ( 0, "CellAttribute", 3 ), ( 0, "FAULT", 1 ), ( 0, "PERM", 3 ), - ( 0, "PORO", 1 ) ), 2. ), - ( False, ( ( 0, "PORO", 1 ), ), 2.0 ), - ( True, ( ( 0, "PointAttribute", 3 ), ( 1, "collocated_nodes", 2 ) ), np.nan ), - ( False, ( ( 0, "CELL_MARKERS", 1 ), ( 0, "CellAttribute", 3 ), ( 0, "FAULT", 1 ), ( 0, "PERM", 3 ), - ( 0, "PORO", 1 ) ), np.nan ), +@pytest.mark.parametrize( "dictAttributesValues", [ + ( { + "PORO": None + } ), + ( { + "PERM": None + } ), + ( { + "PORO": None, + "PERM": None + } ), + ( { + "PORO": [ 4 ] + } ), + ( { + "PERM": [ 4, 4, 4 ] + } ), + ( { + "PORO": [ 4 ], + "PERM": [ 4, 4, 4 ] + } ), + ( { + "PORO": None, + "PERM": [ 4, 4, 4 ] + } ), + ( { + "PORO": [ 4 ], + "PERM": None + } ), ] ) def test_FillPartialArrays( dataSetTest: vtkMultiBlockDataSet, - onpoints: bool, - attributesList: Tuple[ Tuple[ int, str, int ], ...], - value_test: float, + dictAttributesValues: dict[ str, Any ], ) -> None: """Test FillPartialArrays vtk filter.""" - vtkMultiBlockDataSetTestRef: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - vtkMultiBlockDataSetTest: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - attributesNameList: list[ str ] = [ attributesList[ i ][ 1 ] for i in range( len( attributesList ) ) ] - - filter: FillPartialArrays = FillPartialArrays() - filter._SetAttributesNameList( attributesNameList ) - filter._SetValueToFill( value_test ) - filter.SetInputDataObject( vtkMultiBlockDataSetTest ) - filter.Update() - - nbBlock: int = vtkMultiBlockDataSetTestRef.GetNumberOfBlocks() - for block_id in range( nbBlock ): - datasetRef: vtkDataSet = cast( vtkDataSet, vtkMultiBlockDataSetTestRef.GetBlock( block_id ) ) - dataset: vtkDataSet = cast( vtkDataSet, filter.GetOutputDataObject( 0 ).GetBlock( block_id ) ) - expected_array: npt.NDArray[ np.float64 ] - array: npt.NDArray[ np.float64 ] - dataRef: Union[ vtkPointData, vtkCellData ] - data: Union[ vtkPointData, vtkCellData ] - nbElements: list[ int ] - if onpoints: - dataRef = datasetRef.GetPointData() - data = dataset.GetPointData() - nbElements = [ 212, 4092 ] - else: - dataRef = datasetRef.GetCellData() - data = dataset.GetCellData() - nbElements = [ 156, 1740 ] + multiBlockDataSet: vtkMultiBlockDataSet = dataSetTest( "multiblock" ) - for inBlock, attribute, nbComponents in attributesList: - array = vnp.vtk_to_numpy( data.GetArray( attribute ) ) - if block_id == inBlock: - expected_array = vnp.vtk_to_numpy( dataRef.GetArray( attribute ) ) - assert ( array == expected_array ).all() - else: - expected_array = np.array( [ [ value_test for i in range( nbComponents ) ] - for _ in range( nbElements[ inBlock ] ) ] ) - if np.isnan( value_test ): - assert np.all( np.isnan( array ) == np.isnan( expected_array ) ) - else: - assert ( array == expected_array ).all() + filter: FillPartialArrays = FillPartialArrays( multiBlockDataSet, dictAttributesValues ) + assert filter.applyFilter() diff --git a/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py b/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py index 0341baee..0d2ca524 100644 --- a/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py +++ b/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py @@ -4,6 +4,7 @@ # ruff: noqa: E402 # disable Module level import not at top of file import sys from pathlib import Path +from typing import Union, Any from typing_extensions import Self from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] @@ -63,7 +64,7 @@ def __init__( self: Self, ) -> None: outputType="vtkMultiBlockDataSet" ) self.clearDictAttributesValues: bool = True - self.dictAttributesValues: dict[ str, str ] = {} + self.dictAttributesValues: dict[ str, Union[ list[ Any ], None ] ] = {} @smproperty.xml(""" @@ -102,7 +103,7 @@ def _setDictAttributesValues( self: Self, attributeName: str, values: str ) -> N if attributeName is not None and values is not None : self.dictAttributesValues[ attributeName ] = list( values.split( "," ) ) elif attributeName is not None and values is None: - self.dictAttributesValues[ attributeName ] = [] + self.dictAttributesValues[ attributeName ] = None self.Modified() From e58f95737b0ca8e20c1e792eccb551930f547a52 Mon Sep 17 00:00:00 2001 From: Romain Baville <126683264+RomainBaville@users.noreply.github.com> Date: Tue, 12 Aug 2025 09:21:08 +0200 Subject: [PATCH 55/58] Apply suggestions from Paloma's code review Co-authored-by: paloma-martinez <104762252+paloma-martinez@users.noreply.github.com> --- geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py b/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py index 0d2ca524..218d6bfc 100644 --- a/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py +++ b/geos-pv/src/geos/pv/plugins/PVFillPartialArrays.py @@ -90,20 +90,21 @@ def __init__( self: Self, ) -> None: """ ) def _setDictAttributesValues( self: Self, attributeName: str, values: str ) -> None: - """Set the the dictionary with the region indexes and its corresponding list of value for each components. + """Set the dictionary with the region indexes and its corresponding list of value for each components. Args: attributeName (str): Name of the attribute to consider. - values (str): List of the filing values. If multiple components use a coma between the value of each component. + values (str): List of the filing values. If multiple components use a comma between the value of each component. """ if self.clearDictAttributesValues: self.dictAttributesValues = {} self.clearDictAttributesValues = False - if attributeName is not None and values is not None : - self.dictAttributesValues[ attributeName ] = list( values.split( "," ) ) - elif attributeName is not None and values is None: - self.dictAttributesValues[ attributeName ] = None + if attributeName is not None: + if values is not None : + self.dictAttributesValues[ attributeName ] = list( values.split( "," ) ) + else: + self.dictAttributesValues[ attributeName ] = None self.Modified() From 71535ed0b5c187374d40df61634a3f3b444870b9 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 12 Aug 2025 09:54:12 +0200 Subject: [PATCH 56/58] Update the doc --- docs/geos_pv_docs/processing.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/geos_pv_docs/processing.rst b/docs/geos_pv_docs/processing.rst index 13a6d04d..2e304710 100644 --- a/docs/geos_pv_docs/processing.rst +++ b/docs/geos_pv_docs/processing.rst @@ -1,6 +1,11 @@ Post-/Pre-processing ========================= +PVFillPartialArrays +-------------------- +.. automodule:: geos.pv.plugins.FillPartialArrays + + PVSplitMesh ---------------------------------- From 94219b3e3483253b7e5192323ba7d90162cf30c8 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 12 Aug 2025 09:57:41 +0200 Subject: [PATCH 57/58] Apply suggestions from Paloma's review --- geos-mesh/tests/test_arrayModifiers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geos-mesh/tests/test_arrayModifiers.py b/geos-mesh/tests/test_arrayModifiers.py index 3ae4e71e..91ecc423 100644 --- a/geos-mesh/tests/test_arrayModifiers.py +++ b/geos-mesh/tests/test_arrayModifiers.py @@ -122,7 +122,7 @@ def test_fillPartialAttributes( assert ( npArrayFilled == npArrayTest ).all() vtkDataTypeFilled: int = attributeFilled.GetDataType() - assert vtkDataTypeTest == vtkDataTypeFilled + assert vtkDataTypeFilled == vtkDataTypeTest @pytest.mark.parametrize( "multiBlockDataSetName", [ "multiblock" ] ) From b58ded356ab26542b2176dbc1df2c63f893a0e89 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Tue, 12 Aug 2025 10:16:55 +0200 Subject: [PATCH 58/58] Fix doc --- docs/geos_pv_docs/processing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/geos_pv_docs/processing.rst b/docs/geos_pv_docs/processing.rst index 2e304710..43920078 100644 --- a/docs/geos_pv_docs/processing.rst +++ b/docs/geos_pv_docs/processing.rst @@ -3,7 +3,7 @@ Post-/Pre-processing PVFillPartialArrays -------------------- -.. automodule:: geos.pv.plugins.FillPartialArrays +.. automodule:: geos.pv.plugins.PVFillPartialArrays PVSplitMesh