Skip to content

Commit 8def014

Browse files
feat: add FillPartialArrays vtk filter and paraview plugin (#105)
New feature to identify and fill partial arrays in a VTK mesh with specified or default values. A core FillPartialArrays filter provides the backend logic, and a ParaView plugin is included for user interaction. Key Enhancements: Customization: Users can now programmatically choose which attributes to fill and specify a custom value. The feature supports setting different fill values for each component in multi-component arrays. Type Preservation: The filter was improved to preserve the original vtkDataType of the array being modified. Robustness: Added better error handling, including checks for missing attributes and a new function to test if an array is partial. Refactoring & Fixes: The codebase has been significantly cleaned, with clearer variable names, improved documentation, and better log messages for user output. Fixed bugs related to the default fill value for uint types and issues in attribute transfer functions. Updated and added new tests for the filter and its utility functions. Applied multiple formatting changes to align with CI standards. Co-authored-by: paloma-martinez <104762252+paloma-martinez@users.noreply.github.com>
1 parent 0fa4d8f commit 8def014

File tree

7 files changed

+459
-50
lines changed

7 files changed

+459
-50
lines changed

docs/geos_mesh_docs/processing.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ Processing filters
44
The `processing` module of `geos-mesh` package contains filters to process meshes.
55

66

7+
geos.mesh.processing.FillPartialArrays filter
8+
----------------------------------------------
9+
10+
.. automodule:: geos.mesh.processing.FillPartialArrays
11+
:members:
12+
:undoc-members:
13+
:show-inheritance:
14+
15+
716
geos.mesh.processing.meshQualityMetricHelpers module
817
-----------------------------------------------------
918

@@ -12,6 +21,7 @@ geos.mesh.processing.meshQualityMetricHelpers module
1221
:undoc-members:
1322
:show-inheritance:
1423

24+
1525
geos.mesh.processing.SplitMesh filter
1626
--------------------------------------
1727

docs/geos_pv_docs/processing.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
Post-/Pre-processing
22
=========================
33

4+
PVFillPartialArrays
5+
--------------------
6+
.. automodule:: geos.pv.plugins.PVFillPartialArrays
7+
8+
49
PVSplitMesh
510
----------------------------------
611

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
3+
# SPDX-FileContributor: Romain Baville, Martin Lemay
4+
5+
from typing_extensions import Self
6+
from typing import Union, Any
7+
8+
from geos.utils.Logger import logging, Logger, getLogger
9+
from geos.mesh.utils.arrayModifiers import fillPartialAttributes
10+
from geos.mesh.utils.arrayHelpers import isAttributeInObject
11+
12+
from vtkmodules.vtkCommonDataModel import vtkMultiBlockDataSet
13+
14+
__doc__ = """
15+
Fill partial attributes of the input mesh with constant values per component.
16+
17+
Input mesh is vtkMultiBlockDataSet and attributes to fill must be partial.
18+
19+
The list of filling values per attribute is given by a dictionary.
20+
Its keys are the attribute names and its items are the list of filling values for each component.
21+
22+
If the list of filling value is None, attributes are filled with the same constant value for each component;
23+
0 for uint data, -1 for int data and nan for float data.
24+
25+
To use a handler of yours for the logger, set the variable 'speHandler' to True and add it to the filter
26+
with the member function addLoggerHandler.
27+
28+
To use it:
29+
30+
.. code-block:: python
31+
32+
from geos.mesh.processing.FillPartialArrays import FillPartialArrays
33+
34+
# Filter inputs.
35+
multiBlockDataSet: vtkMultiBlockDataSet
36+
dictAttributesValues: dict[ str, Union[ list[ Any ], None ] ]
37+
# Optional inputs.
38+
speHandler: bool
39+
40+
# Instantiate the filter.
41+
filter: FillPartialArrays = FillPartialArrays( multiBlockDataSet, dictAttributesValues, speHandler )
42+
43+
# Set the handler of yours (only if speHandler is True).
44+
yourHandler: logging.Handler
45+
filter.addLoggerHandler( yourHandler )
46+
47+
# Do calculations.
48+
filter.applyFilter()
49+
"""
50+
51+
loggerTitle: str = "Fill Partial Attribute"
52+
53+
54+
class FillPartialArrays:
55+
56+
def __init__(
57+
self: Self,
58+
multiBlockDataSet: vtkMultiBlockDataSet,
59+
dictAttributesValues: dict[ str, Union[ list[ Any ], None ] ],
60+
speHandler: bool = False,
61+
) -> None:
62+
"""Fill partial attributes with constant value per component.
63+
64+
If the list of filling values for an attribute is None, it will filled with the default value for each component:
65+
0 for uint data.
66+
-1 for int data.
67+
nan for float data.
68+
69+
Args:
70+
multiBlockDataSet (vtkMultiBlockDataSet): The mesh where to fill the attribute.
71+
dictAttributesValues (dict[str, Any]): The dictionary with the attribute to fill as keys and the list of filling values as items.
72+
speHandler (bool, optional): True to use a specific handler, False to use the internal handler.
73+
Defaults to False.
74+
"""
75+
self.multiBlockDataSet: vtkMultiBlockDataSet = multiBlockDataSet
76+
self.dictAttributesValues: dict[ str, Union[ list[ Any ], None ] ] = dictAttributesValues
77+
78+
# Logger.
79+
self.logger: Logger
80+
if not speHandler:
81+
self.logger = getLogger( loggerTitle, True )
82+
else:
83+
self.logger = logging.getLogger( loggerTitle )
84+
self.logger.setLevel( logging.INFO )
85+
86+
def setLoggerHandler( self: Self, handler: logging.Handler ) -> None:
87+
"""Set a specific handler for the filter logger.
88+
89+
In this filter 4 log levels are use, .info, .error, .warning and .critical, be sure to have at least the same 4 levels.
90+
91+
Args:
92+
handler (logging.Handler): The handler to add.
93+
"""
94+
if not self.logger.hasHandlers():
95+
self.logger.addHandler( handler )
96+
else:
97+
self.logger.warning(
98+
"The logger already has an handler, to use yours set the argument 'speHandler' to True during the filter initialization."
99+
)
100+
101+
def applyFilter( self: Self ) -> bool:
102+
"""Create a constant attribute per region in the mesh.
103+
104+
Returns:
105+
boolean (bool): True if calculation successfully ended, False otherwise.
106+
"""
107+
self.logger.info( f"Apply filter { self.logger.name }." )
108+
109+
for attributeName in self.dictAttributesValues:
110+
self._setPieceRegionAttribute( attributeName )
111+
if self.onPoints is None:
112+
self.logger.error( f"{ attributeName } is not in the mesh." )
113+
self.logger.error( f"The attribute { attributeName } has not been filled." )
114+
self.logger.error( f"The filter { self.logger.name } failed." )
115+
return False
116+
117+
if self.onBoth:
118+
self.logger.error(
119+
f"Their is two attribute named { attributeName }, one on points and the other on cells. The attribute must be unique."
120+
)
121+
self.logger.error( f"The attribute { attributeName } has not been filled." )
122+
self.logger.error( f"The filter { self.logger.name } failed." )
123+
return False
124+
125+
if not fillPartialAttributes( self.multiBlockDataSet,
126+
attributeName,
127+
onPoints=self.onPoints,
128+
listValues=self.dictAttributesValues[ attributeName ],
129+
logger=self.logger ):
130+
self.logger.error( f"The filter { self.logger.name } failed." )
131+
return False
132+
133+
self.logger.info( f"The filter { self.logger.name } succeed." )
134+
135+
return True
136+
137+
def _setPieceRegionAttribute( self: Self, attributeName: str ) -> None:
138+
"""Set the attribute self.onPoints and self.onBoth.
139+
140+
self.onPoints is True if the region attribute is on points, False if it is on cells, None otherwise.
141+
142+
self.onBoth is True if a region attribute is on points and on cells, False otherwise.
143+
144+
Args:
145+
attributeName (str): The name of the attribute to verify.
146+
"""
147+
self.onPoints: Union[ bool, None ] = None
148+
self.onBoth: bool = False
149+
if isAttributeInObject( self.multiBlockDataSet, attributeName, False ):
150+
self.onPoints = False
151+
if isAttributeInObject( self.multiBlockDataSet, attributeName, True ):
152+
if self.onPoints is False:
153+
self.onBoth = True
154+
self.onPoints = True

geos-mesh/src/geos/mesh/utils/arrayModifiers.py

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
isAttributeGlobal,
4242
getVtkArrayTypeInObject,
4343
getVtkArrayTypeInMultiBlock,
44+
getNumberOfComponentsMultiBlock,
4445
)
4546
from geos.mesh.utils.multiblockHelpers import (
4647
getBlockElementIndexesFlatten,
@@ -61,18 +62,18 @@ def fillPartialAttributes(
6162
multiBlockDataSet: Union[ vtkMultiBlockDataSet, vtkCompositeDataSet, vtkDataObject ],
6263
attributeName: str,
6364
onPoints: bool = False,
64-
value: Any = np.nan,
65+
listValues: Union[ list[ Any ], None ] = None,
6566
logger: Union[ Logger, None ] = None,
6667
) -> bool:
67-
"""Fill input partial attribute of multiBlockDataSet with the same value for all the components.
68+
"""Fill input partial attribute of multiBlockDataSet with a constant value per component.
6869
6970
Args:
7071
multiBlockDataSet (vtkMultiBlockDataSet | vtkCompositeDataSet | vtkDataObject): MultiBlockDataSet where to fill the attribute.
7172
attributeName (str): Attribute name.
7273
onPoints (bool, optional): True if attributes are on points, False if they are on cells.
7374
Defaults to False.
74-
value (Any, optional): Filling value. It is recommended to use numpy scalar type for the values.
75-
Defaults to:
75+
listValues (list[Any], optional): List of filling value for each component.
76+
Defaults to None, the filling value is for all components:
7677
-1 for int VTK arrays.
7778
0 for uint VTK arrays.
7879
nan for float VTK arrays.
@@ -98,40 +99,53 @@ def fillPartialAttributes(
9899

99100
# Get information of the attribute to fill.
100101
vtkDataType: int = getVtkArrayTypeInMultiBlock( multiBlockDataSet, attributeName, onPoints )
101-
infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints )
102-
nbComponents: int = infoAttributes[ attributeName ]
102+
nbComponents: int = getNumberOfComponentsMultiBlock( multiBlockDataSet, attributeName, onPoints )
103103
componentNames: tuple[ str, ...] = ()
104104
if nbComponents > 1:
105105
componentNames = getComponentNames( multiBlockDataSet, attributeName, onPoints )
106106

107+
typeMapping: dict[ int, type ] = vnp.get_vtk_to_numpy_typemap()
108+
valueType: type = typeMapping[ vtkDataType ]
107109
# Set the default value depending of the type of the attribute to fill
108-
if np.isnan( value ):
109-
typeMapping: dict[ int, type ] = vnp.get_vtk_to_numpy_typemap()
110-
valueType: type = typeMapping[ vtkDataType ]
110+
if listValues is None:
111+
defaultValue: Any
112+
logger.warning( f"The attribute { attributeName } is filled with the default value for each component." )
111113
# Default value for float types is nan.
112114
if vtkDataType in ( VTK_FLOAT, VTK_DOUBLE ):
113-
value = valueType( value )
115+
defaultValue = valueType( np.nan )
114116
logger.warning(
115-
f"{ attributeName } vtk data type is { vtkDataType } corresponding to { value.dtype } numpy type, default value is automatically set to nan."
117+
f"{ attributeName } vtk data type is { vtkDataType } corresponding to { defaultValue.dtype } numpy type, default value is automatically set to nan."
116118
)
117119
# Default value for int types is -1.
118120
elif vtkDataType in ( VTK_CHAR, VTK_SIGNED_CHAR, VTK_SHORT, VTK_LONG, VTK_INT, VTK_LONG_LONG, VTK_ID_TYPE ):
119-
value = valueType( -1 )
121+
defaultValue = valueType( -1 )
120122
logger.warning(
121-
f"{ attributeName } vtk data type is { vtkDataType } corresponding to { value.dtype } numpy type, default value is automatically set to -1."
123+
f"{ attributeName } vtk data type is { vtkDataType } corresponding to { defaultValue.dtype } numpy type, default value is automatically set to -1."
122124
)
123125
# Default value for uint types is 0.
124126
elif vtkDataType in ( VTK_BIT, VTK_UNSIGNED_CHAR, VTK_UNSIGNED_SHORT, VTK_UNSIGNED_LONG, VTK_UNSIGNED_INT,
125127
VTK_UNSIGNED_LONG_LONG ):
126-
value = valueType( 0 )
128+
defaultValue = valueType( 0 )
127129
logger.warning(
128-
f"{ attributeName } vtk data type is { vtkDataType } corresponding to { value.dtype } numpy type, default value is automatically set to 0."
130+
f"{ attributeName } vtk data type is { vtkDataType } corresponding to { defaultValue.dtype } numpy type, default value is automatically set to 0."
129131
)
130132
else:
131133
logger.error( f"The type of the attribute { attributeName } is not compatible with the function." )
132134
return False
133135

134-
values: list[ Any ] = [ value for _ in range( nbComponents ) ]
136+
listValues = [ defaultValue ] * nbComponents
137+
138+
else:
139+
if len( listValues ) != nbComponents:
140+
return False
141+
142+
for idValue in range( nbComponents ):
143+
value: Any = listValues[ idValue ]
144+
if type( value ) is not valueType:
145+
listValues[ idValue ] = valueType( listValues[ idValue ] )
146+
logger.warning(
147+
f"The filling value { value } for the attribute { attributeName } has not the correct type, it is convert to the numpy scalar type { valueType().dtype }."
148+
)
135149

136150
# Parse the multiBlockDataSet to create and fill the attribute on blocks where it is not.
137151
iterator: vtkDataObjectTreeIterator = vtkDataObjectTreeIterator()
@@ -141,7 +155,7 @@ def fillPartialAttributes(
141155
while iterator.GetCurrentDataObject() is not None:
142156
dataSet: vtkDataSet = vtkDataSet.SafeDownCast( iterator.GetCurrentDataObject() )
143157
if not isAttributeInObjectDataSet( dataSet, attributeName, onPoints ) and \
144-
not createConstantAttributeDataSet( dataSet, values, attributeName, componentNames, onPoints, vtkDataType, logger ):
158+
not createConstantAttributeDataSet( dataSet, listValues, attributeName, componentNames, onPoints, vtkDataType, logger ):
145159
return False
146160

147161
iterator.GoToNextItem()
@@ -172,7 +186,7 @@ def fillAllPartialAttributes(
172186
infoAttributes: dict[ str, int ] = getAttributesWithNumberOfComponents( multiBlockDataSet, onPoints )
173187
for attributeName in infoAttributes:
174188
if not isAttributeGlobal( multiBlockDataSet, attributeName, onPoints ) and \
175-
not fillPartialAttributes( multiBlockDataSet, attributeName, onPoints, logger=logger ):
189+
not fillPartialAttributes( multiBlockDataSet, attributeName, onPoints=onPoints, logger=logger ):
176190
return False
177191

178192
return True
@@ -384,7 +398,7 @@ def createConstantAttributeDataSet(
384398
if valueType in ( int, float ):
385399
npType: type = type( np.array( listValues )[ 0 ] )
386400
logger.warning(
387-
f"During the creation of the constant attribute { attributeName }, values will be converted from { valueType } to { npType }."
401+
f"During the creation of the constant attribute { attributeName }, values have been converted from { valueType } to { npType }."
388402
)
389403
logger.warning( "To avoid any issue with the conversion, please use directly numpy scalar type for the values" )
390404
valueType = npType
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
3+
# SPDX-FileContributor: Romain Baville
4+
# SPDX-License-Identifier: Apache 2.0
5+
# ruff: noqa: E402 # disable Module level import not at top of file
6+
# mypy: disable-error-code="operator"
7+
import pytest
8+
9+
from typing import Any
10+
from vtkmodules.vtkCommonDataModel import vtkMultiBlockDataSet
11+
12+
from geos.mesh.processing.FillPartialArrays import FillPartialArrays
13+
14+
15+
@pytest.mark.parametrize( "dictAttributesValues", [
16+
( {
17+
"PORO": None
18+
} ),
19+
( {
20+
"PERM": None
21+
} ),
22+
( {
23+
"PORO": None,
24+
"PERM": None
25+
} ),
26+
( {
27+
"PORO": [ 4 ]
28+
} ),
29+
( {
30+
"PERM": [ 4, 4, 4 ]
31+
} ),
32+
( {
33+
"PORO": [ 4 ],
34+
"PERM": [ 4, 4, 4 ]
35+
} ),
36+
( {
37+
"PORO": None,
38+
"PERM": [ 4, 4, 4 ]
39+
} ),
40+
( {
41+
"PORO": [ 4 ],
42+
"PERM": None
43+
} ),
44+
] )
45+
def test_FillPartialArrays(
46+
dataSetTest: vtkMultiBlockDataSet,
47+
dictAttributesValues: dict[ str, Any ],
48+
) -> None:
49+
"""Test FillPartialArrays vtk filter."""
50+
multiBlockDataSet: vtkMultiBlockDataSet = dataSetTest( "multiblock" )
51+
52+
filter: FillPartialArrays = FillPartialArrays( multiBlockDataSet, dictAttributesValues )
53+
assert filter.applyFilter()

0 commit comments

Comments
 (0)