diff --git a/docs/geos_mesh_docs/processing.rst b/docs/geos_mesh_docs/processing.rst index fec7f4b7..844c071a 100644 --- a/docs/geos_mesh_docs/processing.rst +++ b/docs/geos_mesh_docs/processing.rst @@ -3,6 +3,13 @@ Processing filters The `processing` module of `geos-mesh` package contains filters to process meshes. +geos.mesh.processing.CreateConstantAttributePerRegion filter +------------------------------------------------------------- + +.. automodule:: geos.mesh.processing.CreateConstantAttributePerRegion + :members: + :undoc-members: + :show-inheritance: geos.mesh.processing.FillPartialArrays filter ---------------------------------------------- diff --git a/docs/geos_posp_docs/PVplugins.rst b/docs/geos_posp_docs/PVplugins.rst index 91c859b6..293e0a65 100644 --- a/docs/geos_posp_docs/PVplugins.rst +++ b/docs/geos_posp_docs/PVplugins.rst @@ -16,12 +16,6 @@ PVAttributeMapping plugin .. automodule:: PVplugins.PVAttributeMapping -PVCreateConstantAttributePerRegion plugin ---------------------------------------------------- - -.. automodule:: PVplugins.PVCreateConstantAttributePerRegion - - PVExtractMergeBlocksVolume plugin ------------------------------------------- diff --git a/docs/geos_pv_docs/processing.rst b/docs/geos_pv_docs/processing.rst index 43920078..f5fcaf24 100644 --- a/docs/geos_pv_docs/processing.rst +++ b/docs/geos_pv_docs/processing.rst @@ -1,8 +1,16 @@ Post-/Pre-processing ========================= + +PVCreateConstantAttributePerRegion +----------------------------------- + +.. automodule:: geos.pv.plugins.PVCreateConstantAttributePerRegion + + PVFillPartialArrays -------------------- + .. automodule:: geos.pv.plugins.PVFillPartialArrays diff --git a/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py new file mode 100644 index 00000000..8c97c950 --- /dev/null +++ b/geos-mesh/src/geos/mesh/processing/CreateConstantAttributePerRegion.py @@ -0,0 +1,478 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Romain Baville +import numpy as np +import numpy.typing as npt + +from typing import Union, Any +from typing_extensions import Self + +import vtkmodules.util.numpy_support as vnp +from vtkmodules.vtkCommonDataModel import ( + vtkMultiBlockDataSet, + vtkDataSet, +) + +from geos.utils.Logger import ( getLogger, Logger, logging, CountWarningHandler ) +from geos.mesh.utils.arrayHelpers import ( getArrayInObject, getComponentNames, getNumberOfComponents, + getVtkDataTypeInObject, isAttributeGlobal, isAttributeInObject ) +from geos.mesh.utils.arrayModifiers import ( createAttribute, createConstantAttributeDataSet, + createConstantAttributeMultiBlock ) + +__doc__ = """ +CreateConstantAttributePerRegion is a vtk filter that allows to create an attribute +with constant values per components for each chosen indexes of a reference/region attribute. +If other region indexes exist values are set to nan for float type, -1 for int type or 0 for uint type. + +Input mesh is either vtkMultiBlockDataSet or vtkDataSet and the region attribute must have one component. +The relation index/values is given by a dictionary. Its keys are the indexes and its items are the list of values for each component. +To use a handler of yours, set the variable 'speHandler' to True and add it using the member function addLoggerHandler. + +By default, the value type is set to float32, their is one component and no name and the logger use an intern handler. + +To use it: + +.. code-block:: python + + from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion + + # Filter inputs. + mesh: Union[vtkMultiBlockDataSet, vtkDataSet] + regionName: str + dictRegionValues: dict[ Any, Any ] + newAttributeName: str + + # Optional inputs. + valueNpType: type + nbComponents: int + componentNames: tuple[ str, ... ] + speHandler: bool + + # Instantiate the filter + filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( mesh, + regionName, + dictRegionValues, + newAttributeName, + valueNpType, + nbComponents, + componentNames, + speHandler, + ) + + # Set your handler (only if speHandler is True). + yourHandler: logging.Handler + filter.addLoggerHandler( yourHandler ) + + # Do calculations. + filter.applyFilter() +""" + +loggerTitle: str = "Create Constant Attribute Per Region" + + +class CreateConstantAttributePerRegion: + + def __init__( + self: Self, + mesh: Union[ vtkDataSet, vtkMultiBlockDataSet ], + regionName: str, + dictRegionValues: dict[ Any, Any ], + newAttributeName: str, + valueNpType: type = np.float32, + nbComponents: int = 1, + componentNames: tuple[ str, ...] = (), # noqa: C408 + speHandler: bool = False, + ) -> None: + """Create an attribute with constant value per region. + + Args: + mesh (Union[ vtkDataSet, vtkMultiBlockDataSet ]): The mesh where to create the constant attribute per region. + regionName (str): The name of the attribute with the region indexes. + dictRegionValues (dict[ Any, Any ]): The dictionary with the region indexes as keys and the list of values as items. + newAttributeName (str): The name of the new attribute to create. + nbComponents (int, optional): Number of components for the new attribute. + Defaults to 1. + componentNames (tuple[str,...], optional): Name of the components for vectorial attributes. If one component, gives an empty tuple. + Defaults to an empty tuple. + valueNpType (type, optional): The numpy scalar type for values. + Defaults to numpy.float32. + speHandler (bool, optional): True to use a specific handler, False to use the internal handler. + Defaults to False. + """ + self.mesh: Union[ vtkDataSet, vtkMultiBlockDataSet ] = mesh + + # New attribute settings. + self.newAttributeName: str = newAttributeName + self.valueNpType: type = valueNpType + self.nbComponents: int = nbComponents + self.componentNames: tuple[ str, ...] = componentNames + + # Region attribute settings. + self.regionName: str = regionName + self.dictRegionValues: dict[ Any, Any ] = dictRegionValues + + self.useDefaultValue: bool = False # Check if the new component have default values (information for the output message). + + # Warnings counter. + self.counter: CountWarningHandler = CountWarningHandler() + self.counter.setLevel( logging.INFO ) + + # Logger. + self.logger: Logger + if not speHandler: + self.logger = getLogger( loggerTitle, True ) + else: + self.logger = logging.getLogger( loggerTitle ) + self.logger.setLevel( logging.INFO ) + + def setLoggerHandler( self: Self, handler: logging.Handler ) -> None: + """Set a specific handler for the filter logger. + + In this filter 4 log levels are use, .info, .error, .warning and .critical, + be sure to have at least the same 4 levels. + + Args: + handler (logging.Handler): The handler to add. + """ + if not self.logger.hasHandlers(): + self.logger.addHandler( handler ) + else: + # This warning does not count for the number of warning created during the application of the filter. + self.logger.warning( + "The logger already has an handler, to use yours set the argument 'speHandler' to True during the filter initialization." + ) + + def applyFilter( self: Self ) -> bool: + """Create a constant attribute per region in the mesh. + + 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 ) + + # Check the validity of the attribute region. + self._setPieceRegionAttribute() + if self.onPoints is None: + self.logger.error( f"{ self.regionName } is not in the mesh." ) + self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) + self.logger.error( f"The filter { self.logger.name } failed." ) + return False + + if self.onBoth: + self.logger.error( + f"Their is two attribute named { self.regionName }, one on points and the other on cells. The region attribute must be unique." + ) + self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) + self.logger.error( f"The filter { self.logger.name } failed." ) + return False + + nbComponentsRegion: int = getNumberOfComponents( self.mesh, self.regionName, self.onPoints ) + if nbComponentsRegion != 1: + self.logger.error( f"The region attribute { self.regionName } has to many components, one is requires." ) + self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) + self.logger.error( f"The filter { self.logger.name } failed." ) + return False + + self._setInfoRegion() + # Check if the number of components and number of values for the region indexes are coherent. + for index in self.dictRegionValues: + if len( self.dictRegionValues[ index ] ) != self.nbComponents: + self.logger.error( + f"The number of value given for the region index { index } is not correct. You must set a value for each component, in this case { self.nbComponents }." + ) + return False + + trueIndexes: list[ Any ] = [] + falseIndexes: list[ Any ] = [] + regionNpArray: npt.NDArray[ Any ] + npArray: npt.NDArray[ Any ] + if isinstance( self.mesh, vtkMultiBlockDataSet ): + # Check if the attribute region is global. + if not isAttributeGlobal( self.mesh, self.regionName, self.onPoints ): + self.logger.error( f"The region attribute { self.regionName } has to be global." ) + self.logger.error( f"The new attribute { self.newAttributeName } has not been add." ) + self.logger.error( f"The filter { self.logger.name } failed." ) + return False + + trueIndexes, falseIndexes = self._getTrueIndexesInMultiBlock( self.mesh ) + if len( trueIndexes ) == 0: + if len( self.dictRegionValues ) == 0: + self.logger.warning( "No region indexes entered." ) + else: + self.logger.warning( + f"The region indexes entered are not in the region attribute { self.regionName }." ) + + if not createConstantAttributeMultiBlock( self.mesh, + self.defaultValue, + self.newAttributeName, + componentNames=self.componentNames, + onPoints=self.onPoints, + logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed." ) + return False + + else: + if len( falseIndexes ) > 0: + self.logger.warning( + f"The region indexes { falseIndexes } are not in the region attribute { self.regionName }." ) + + # Parse the mesh to add the attribute on each block. + nbBlock: int = self.mesh.GetNumberOfBlocks() + for idBlock in range( nbBlock ): + dataSet: vtkDataSet = vtkDataSet.SafeDownCast( self.mesh.GetBlock( idBlock ) ) + + regionNpArray = getArrayInObject( dataSet, self.regionName, self.onPoints ) + npArray = self._createNpArray( regionNpArray ) + if not createAttribute( dataSet, + npArray, + self.newAttributeName, + componentNames=self.componentNames, + onPoints=self.onPoints, + logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed." ) + return False + + else: + trueIndexes, falseIndexes = self._getTrueIndexesInDataSet( self.mesh ) + if len( trueIndexes ) == 0: + if len( self.dictRegionValues ) == 0: + self.logger.warning( "No region indexes entered." ) + else: + self.logger.warning( + f"The region indexes entered are not in the region attribute { self.regionName }." ) + + if not createConstantAttributeDataSet( self.mesh, + self.defaultValue, + self.newAttributeName, + componentNames=self.componentNames, + onPoints=self.onPoints, + logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed." ) + return False + + else: + if len( falseIndexes ) > 0: + self.logger.warning( + f"The region indexes { falseIndexes } are not in the region attribute { self.regionName }." ) + + regionNpArray = getArrayInObject( self.mesh, self.regionName, self.onPoints ) + npArray = self._createNpArray( regionNpArray ) + if not createAttribute( self.mesh, + npArray, + self.newAttributeName, + componentNames=self.componentNames, + onPoints=self.onPoints, + logger=self.logger ): + self.logger.error( f"The filter { self.logger.name } failed." ) + return False + + # Log the output message. + self._logOutputMessage( trueIndexes ) + + return True + + def _setPieceRegionAttribute( self: Self ) -> None: + """Set the attribute self.onPoints and self.onBoth. + + self.onPoints is True if the region attribute is on points, False if it is on cells, None otherwise. + + self.onBoth is True if a region attribute is on points and on cells, False otherwise. + """ + self.onPoints: Union[ bool, None ] = None + self.onBoth: bool = False + if isAttributeInObject( self.mesh, self.regionName, False ): + self.onPoints = False + if isAttributeInObject( self.mesh, self.regionName, True ): + if self.onPoints is False: + self.onBoth = True + self.onPoints = True + + def _setInfoRegion( self: Self ) -> None: + """Update self.dictRegionValues and set self.defaultValue. + + Values and default value type are set with the numpy type given by self.valueNpType. + Default value is set to nan for float data, -1 for int data and 0 for uint data. + """ + # Get the numpy type from the vtk typecode. + dictType: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() + regionVtkType: int = getVtkDataTypeInObject( self.mesh, self.regionName, self.onPoints ) + regionNpType: type = dictType[ regionVtkType ] + + # Set the correct type of values and region index. + dictRegionValuesUpdateType: dict[ Any, Any ] = {} + for idRegion in self.dictRegionValues: + dictRegionValuesUpdateType[ regionNpType( idRegion ) ] = [ + self.valueNpType( value ) for value in self.dictRegionValues[ idRegion ] + ] + self.dictRegionValues = dictRegionValuesUpdateType + + # Set the list of default value for each component depending of the type. + self.defaultValue: list[ Any ] + ## Default value for float types is nan. + if self.valueNpType in ( np.float32, np.float64 ): + self.defaultValue = [ self.valueNpType( np.nan ) for _ in range( self.nbComponents ) ] + ## Default value for int types is -1. + elif self.valueNpType in ( np.int8, np.int16, np.int32, np.int64 ): + self.defaultValue = [ self.valueNpType( -1 ) for _ in range( self.nbComponents ) ] + ## Default value for uint types is 0. + elif self.valueNpType in ( np.uint8, np.uint16, np.uint32, np.uint64 ): + self.defaultValue = [ self.valueNpType( 0 ) for _ in range( self.nbComponents ) ] + + def _getTrueIndexesInMultiBlock( self: Self, + multiBlockDataSet: vtkMultiBlockDataSet ) -> tuple[ list[ Any ], list[ Any ] ]: + """Check for each region index if it is a true index (the index is value of the attribute of at least one block), or a false index. + + Args: + multiBlockDataSet (vtkMultiBlockDataSet): The mesh with the attribute to check. + + Returns: + tuple(list[Any], list[Any]): Tuple with the list of the true indexes and the list of the false indexes. + """ + trueIndexes: list[ Any ] = [] + falseIndexes: list[ Any ] = [] + nbBlock: int = multiBlockDataSet.GetNumberOfBlocks() + # Parse all blocks to get the true indexes of each block. + for idBlock in range( nbBlock ): + block: vtkDataSet = vtkDataSet.SafeDownCast( multiBlockDataSet.GetBlock( idBlock ) ) + # Get the true and false indexes of the block. + trueIndexesBlock: list[ Any ] = self._getTrueIndexesInDataSet( block )[ 0 ] + + # Keep the new true indexes. + for index in trueIndexesBlock: + if index not in trueIndexes: + trueIndexes.append( index ) + + # Get the false indexes. + for index in self.dictRegionValues: + if index not in trueIndexes: + falseIndexes.append( index ) + + return ( trueIndexes, falseIndexes ) + + def _getTrueIndexesInDataSet( self: Self, dataSet: vtkDataSet ) -> tuple[ list[ Any ], list[ Any ] ]: + """Check for each region index if it is a true index (the index is value of the attribute), or a false index. + + Args: + dataSet (vtkDataSet): The mesh with the attribute to check. + + Returns: + tuple(list[Any], list[Any]): The tuple with the list of the true indexes and the list of the false indexes. + """ + regionNpArray = getArrayInObject( dataSet, self.regionName, self.onPoints ) + trueIndexes: list[ Any ] = [] + falseIndexes: list[ Any ] = [] + for index in self.dictRegionValues: + if index in regionNpArray: + trueIndexes.append( index ) + else: + falseIndexes.append( index ) + + return ( trueIndexes, falseIndexes ) + + def _createNpArray( self: Self, regionNpArray: npt.NDArray[ Any ] ) -> npt.NDArray[ Any ]: + """Create an array from the input one. + + If the value of the input array is a key of self.dictRegionValues, the corresponding list of value for each component of the created array is its item. + If their is other indexes than those given, their list of values are self.defaultValue and self.useDefaultValue is set to True. + + Args: + regionNpArray (npt.NDArray[Any]): The array with the region indexes. + + Returns: + npt.NDArray[Any]: The array with values instead of indexes. + """ + nbElements: int = len( regionNpArray ) + npArray: npt.NDArray[ Any ] + if self.nbComponents == 1: + npArray = np.ones( nbElements, self.valueNpType ) + else: + npArray = np.ones( ( nbElements, self.nbComponents ), self.valueNpType ) + + for elem in range( nbElements ): + value: Any = regionNpArray[ elem ] + if value in self.dictRegionValues: + if self.nbComponents == 1: + npArray[ elem ] = self.dictRegionValues[ value ][ 0 ] + else: + npArray[ elem ] = self.dictRegionValues[ value ] + else: + if self.nbComponents == 1: + npArray[ elem ] = self.defaultValue[ 0 ] + self.useDefaultValue = True + else: + npArray[ elem ] = self.defaultValue + self.useDefaultValue = True + + return npArray + + def _logOutputMessage( self: Self, trueIndexes: list[ Any ] ) -> None: + """Create and log result messages of the filter. + + Args: + trueIndexes (list[Any]): The list of the true region indexes use to create the attribute. + """ + # The Filter succeed. + self.logger.info( f"The filter { self.logger.name } succeed." ) + + # Info about the created attribute. + ## The piece where the attribute is created. + piece: str = "points" if self.onPoints else "cells" + self.logger.info( f"The new attribute { self.newAttributeName } is created on { piece }." ) + + ## The number of component and they names if multiple. + componentNamesCreated: tuple[ str, ...] = getComponentNames( self.mesh, self.newAttributeName, self.onPoints ) + if self.nbComponents > 1: + messComponent: str = f"The new attribute { self.newAttributeName } has { self.nbComponents } components named { componentNamesCreated }." + if componentNamesCreated != self.componentNames: + ### Warn the user because other component names than those given have been used. + self.logger.warning( messComponent ) + else: + self.logger.info( messComponent ) + + ## The values of the attribute. + messValue: str = f"The new attribute { self.newAttributeName } is constant" + if len( trueIndexes ) == 0: + ### Create the message to have the value of each component. + messValue = f"{ messValue } with" + if self.nbComponents > 1: + for idComponent in range( self.nbComponents ): + messValue = f"{ messValue } the value { self.defaultValue[ idComponent ] } for the component { componentNamesCreated[ idComponent ] }," + messValue = f"{ messValue[:-1] }." + else: + messValue = f"{ messValue } the value { self.defaultValue[ 0 ] }." + ### Warn the user because no region index has been used. + self.logger.warning( messValue ) + + else: + ### Create the message to have for each component the value of the region index. + messValue = f"{ messValue } per region indexes with:\n" + for index in trueIndexes: + messValue = f"{ messValue }\tThe value { self.dictRegionValues[ index ][ 0 ] } for the" + if self.nbComponents > 1: + messValue = f"{ messValue } component { componentNamesCreated[ 0 ] }," + for idComponent in range( 1, self.nbComponents - 1 ): + messValue = f"{ messValue } the value { self.dictRegionValues[ index ][ idComponent ] } for the component { componentNamesCreated[ idComponent ] }," + messValue = f"{ messValue[ : -1 ] } and the value { self.dictRegionValues[ index ][ -1 ] } for the component { componentNamesCreated[ -1 ] } for the index { index }.\n" + else: + messValue = f"{ messValue } index { index }.\n" + + if self.useDefaultValue: + messValue = f"{ messValue }\tThe value { self.defaultValue[ 0 ] } for the" + if self.nbComponents > 1: + messValue = f"{ messValue } component { componentNamesCreated[ 0 ] }," + for idComponent in range( 1, self.nbComponents - 1 ): + messValue = f"{ messValue } the value { self.defaultValue[ idComponent ] } for the component { componentNamesCreated[ idComponent ] }," + messValue = f"{ messValue[ : -1 ] } and the value { self.defaultValue[ -1 ] } for the component { componentNamesCreated[ -1 ] } for the other indexes." + else: + messValue = f"{ messValue } other indexes." + ### Warn the user because a default value has been used. + self.logger.warning( messValue ) + else: + if self.counter.warningCount > 0: + ### Warn the user because other component names than those given have been used. + self.logger.warning( messValue ) + else: + self.logger.info( messValue ) diff --git a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py index 98388fd5..bef1e3d6 100644 --- a/geos-mesh/src/geos/mesh/utils/arrayHelpers.py +++ b/geos-mesh/src/geos/mesh/utils/arrayHelpers.py @@ -368,6 +368,24 @@ def getArrayInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) - return npArray +def getVtkDataTypeInObject( object: Union[ vtkDataSet, vtkMultiBlockDataSet ], attributeName: str, + onPoints: bool ) -> int: + """Return VTK type of requested array from input mesh. + + Args: + object (Union[vtkDataSet, vtkMultiBlockDataSet]): Input object. + attributeName (str): Name of the attribute. + onPoints (bool): True if attributes are on points, False if they are on cells. + + Returns: + int: The type of the vtk array corresponding to input attribute name. + """ + if isinstance( object, vtkDataSet ): + return getVtkArrayTypeInObject( object, attributeName, onPoints ) + else: + return getVtkArrayTypeInMultiBlock( object, attributeName, onPoints ) + + def getVtkArrayTypeInObject( object: vtkDataSet, attributeName: str, onPoints: bool ) -> int: """Return VTK type of requested array from dataset input. diff --git a/geos-mesh/tests/test_CreateConstantAttributePerRegion.py b/geos-mesh/tests/test_CreateConstantAttributePerRegion.py new file mode 100644 index 00000000..857fbfe6 --- /dev/null +++ b/geos-mesh/tests/test_CreateConstantAttributePerRegion.py @@ -0,0 +1,122 @@ +# 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, Any +from vtkmodules.vtkCommonDataModel import ( vtkDataSet, vtkMultiBlockDataSet ) + +from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion, np + + +@pytest.mark.parametrize( + "meshType, newAttributeName, regionName, dictRegionValues, componentNames, componentNamesTest, valueNpType, succeed", + [ + # Test the name of the new attribute (new on the mesh, one present on the other piece). + ## For vtkDataSet. + ( "dataset", "newAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + ( "dataset", "CellAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + ## For vtkMultiBlockDataSet. + ( "multiblock", "newAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + ( "multiblock", "CellAttribute", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + ( "multiblock", "GLOBAL_IDS_CELLS", "GLOBAL_IDS_POINTS", {}, (), (), np.float32, True ), + # Test if the region attribute is on cells or on points. + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.float32, True ), + # Test the component name. + ( "dataset", "newAttribute", "FAULT", {}, ( "X" ), (), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), ( "Component0", "Component1" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", {}, ( "X" ), ( "Component0", "Component1" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", {}, ( "X", "Y" ), ( "X", "Y" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", {}, ( "X", "Y", "Z" ), ( "X", "Y" ), np.float32, True ), + # Test the type of value. + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int8, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int16, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int32, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.int64, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint8, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint16, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint32, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.uint64, True ), + ( "dataset", "newAttribute", "FAULT", {}, (), (), np.float64, True ), + # Test index/value. + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0 ], + 100: [ 1 ] + }, (), (), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0 ], + 100: [ 1 ], + 101: [ 2 ] + }, (), (), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0 ], + 100: [ 1 ], + 101: [ 2 ], + 2: [ 3 ] + }, (), (), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0, 0 ], + 100: [ 1, 1 ] + }, (), ( "Component0", "Component1" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0, 0 ], + 100: [ 1, 1 ], + 101: [ 2, 2 ] + }, (), ( "Component0", "Component1" ), np.float32, True ), + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0, 0 ], + 100: [ 1, 1 ], + 101: [ 2, 2 ], + 2: [ 3, 3 ] + }, (), ( "Component0", "Component1" ), np.float32, True ), + # Test common error. + ## Number of components. + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0 ], + 100: [ 1, 1 ] + }, (), (), np.float32, False ), # Number of value inconsistent. + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0, 0 ], + 100: [ 1, 1 ] + }, (), (), np.float32, False ), # More values than components. + ( "dataset", "newAttribute", "FAULT", { + 0: [ 0 ], + 100: [ 1 ] + }, ( "X", "Y" ), ( "X", "Y" ), np.float32, False ), # More components than value. + ## Attribute name. + ( "dataset", "PERM", "FAULT", {}, (), (), np.float32, False ), # The attribute name already exist. + ## Region attribute. + ( "dataset", "newAttribute", "PERM", {}, (), + (), np.float32, False ), # Region attribute has too many components. + ( "multiblock", "newAttribute", "FAULT", {}, (), (), np.float32, False ), # Region attribute is partial. + ] ) +def test_CreateConstantAttributePerRegion( + dataSetTest: Union[ vtkMultiBlockDataSet, vtkDataSet ], + meshType: str, + newAttributeName: str, + regionName: str, + dictRegionValues: dict[ Any, Any ], + componentNames: tuple[ str, ...], + componentNamesTest: tuple[ str, ...], + valueNpType: int, + succeed: bool, +) -> None: + """Test CreateConstantAttributePerRegion.""" + mesh: Union[ vtkMultiBlockDataSet, vtkDataSet ] = dataSetTest( meshType ) + nbComponents: int = len( componentNamesTest ) + if nbComponents == 0: # If one component their is no name. + nbComponents += 1 + + filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( + mesh, + regionName, + dictRegionValues, + newAttributeName, + valueNpType=valueNpType, + nbComponents=nbComponents, + componentNames=componentNames, + ) + + assert filter.applyFilter() == succeed diff --git a/geos-posp/src/PVplugins/PVCreateConstantAttributePerRegion.py b/geos-posp/src/PVplugins/PVCreateConstantAttributePerRegion.py deleted file mode 100644 index 8f25df08..00000000 --- a/geos-posp/src/PVplugins/PVCreateConstantAttributePerRegion.py +++ /dev/null @@ -1,303 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. -# SPDX-FileContributor: Martin Lemay -# ruff: noqa: E402 # disable Module level import not at top of file -import os -import sys -from typing import Union - -import numpy as np -import numpy.typing as npt -from typing_extensions import Self - -dir_path = os.path.dirname( os.path.realpath( __file__ ) ) -parent_dir_path = os.path.dirname( dir_path ) -if parent_dir_path not in sys.path: - sys.path.append( parent_dir_path ) - -import PVplugins # noqa: F401 - -import vtkmodules.util.numpy_support as vnp -from geos.utils.Logger import Logger, getLogger -from geos.mesh.utils.multiblockHelpers import ( - getBlockElementIndexesFlatten, - getBlockFromFlatIndex, -) -from geos.mesh.utils.arrayHelpers import isAttributeInObject -from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] - VTKPythonAlgorithmBase, smdomain, smhint, smproperty, smproxy, -) -from vtk import VTK_DOUBLE # type: ignore[import-untyped] -from vtkmodules.vtkCommonCore import ( - vtkDataArray, - vtkInformation, - vtkInformationVector, -) -from vtkmodules.vtkCommonDataModel import ( - vtkDataObject, - vtkMultiBlockDataSet, - vtkUnstructuredGrid, -) - -__doc__ = """ -PVCreateConstantAttributePerRegion is a Paraview plugin that allows to -create 2 attributes whom values are constant for each region index. - -Input and output are either vtkMultiBlockDataSet or vtkUnstructuredGrid. - -To use it: - -* Load the module in Paraview: Tools>Manage Plugins...>Load new>PVCreateConstantAttributePerRegion. -* Select the mesh you want to create the attributes and containing a region attribute. -* Search and Apply Create Constant Attribute Per Region Filter. - -""" - -SOURCE_NAME: str = "" -DEFAULT_REGION_ATTRIBUTE_NAME = "region" - - -@smproxy.filter( - name="PVCreateConstantAttributePerRegion", - label="Create Constant Attribute Per Region", -) -@smhint.xml( """""" ) -@smproperty.input( name="Input", port_index=0 ) -@smdomain.datatype( - dataTypes=[ "vtkMultiBlockDataSet", "vtkUnstructuredGrid" ], - composite_data_supported=True, -) -class PVCreateConstantAttributePerRegion( VTKPythonAlgorithmBase ): - - def __init__( self: Self ) -> None: - """Create an attribute with constant value per region.""" - super().__init__( nInputPorts=1, nOutputPorts=1, outputType="vtkDataSet" ) - - self.m_table: list[ tuple[ int, float ] ] = [] - self.m_regionAttributeName: str = DEFAULT_REGION_ATTRIBUTE_NAME - self.m_attributeName: str = "attribute" - - # logger - self.m_logger: Logger = getLogger( "Create Constant Attribute Per Region Filter" ) - - def SetLogger( self: Self, logger: Logger ) -> None: - """Set filter logger. - - Args: - logger (Logger): logger - """ - self.m_logger = logger - - @smproperty.xml( """ - - - - - - - - Select an attribute containing the indexes of the regions - - - """ ) - def a01SetRegionAttributeName( self: Self, name: str ) -> None: - """Set region attribute name.""" - self.m_regionAttributeName = name - self.Modified() - - @smproperty.xml( """ - - - Name of the new attribute - - - """ ) - def a02SetAttributeName( self: Self, value: str ) -> None: - """Set attribute name. - - Args: - value (str): attribute name. - """ - self.m_attributeName = value - self.Modified() - - @smproperty.xml( """ - - - - - - - - - Set new attributes values for each region index. - - - """ ) - def b01SetAttributeValues( self: Self, regionIndex: int, value: float ) -> None: - """Set attribute values per region. - - Args: - regionIndex (int): region index. - - value (float): attribute value. - """ - self.m_table.append( ( regionIndex, value ) ) - self.Modified() - - @smproperty.xml( """ - - """ ) - def b02GroupFlow( self: Self ) -> None: - """Organize groups.""" - self.Modified() - - def RequestDataObject( - self: Self, - request: vtkInformation, - inInfoVec: list[ vtkInformationVector ], - outInfoVec: vtkInformationVector, - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestDataObject. - - Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - inData = self.GetInputData( inInfoVec, 0, 0 ) - outData = self.GetOutputData( outInfoVec, 0 ) - assert inData is not None - if outData is None or ( not outData.IsA( inData.GetClassName() ) ): - outData = inData.NewInstance() - outInfoVec.GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), outData ) - return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] - - def RequestData( - self: Self, - request: vtkInformation, # noqa: F841 - inInfoVec: list[ vtkInformationVector ], # noqa: F841 - outInfoVec: vtkInformationVector, # noqa: F841 - ) -> int: - """Inherited from VTKPythonAlgorithmBase::RequestData. - - Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects - - Returns: - int: 1 if calculation successfully ended, 0 otherwise. - """ - self.m_logger.info( f"Apply filter {__name__}" ) - try: - input0: Union[ vtkUnstructuredGrid, vtkMultiBlockDataSet ] = ( self.GetInputData( inInfoVec, 0, 0 ) ) - output: Union[ vtkUnstructuredGrid, vtkMultiBlockDataSet ] = ( self.GetOutputData( outInfoVec, 0 ) ) - - assert input0 is not None, "Input Surface is null." - assert output is not None, "Output pipeline is null." - - output.ShallowCopy( input0 ) - - assert ( len( self.m_regionAttributeName ) - > 0 ), "Region attribute is undefined, please select an attribute." - if isinstance( output, vtkMultiBlockDataSet ): - self.createAttributesMultiBlock( output ) - else: - self.createAttributes( output ) - - mess: str = ( f"The new attribute {self.m_attributeName} was successfully added." ) - self.Modified() - self.m_logger.info( mess ) - except AssertionError as e: - mess1: str = "The new attribute was not added due to:" - self.m_logger.error( mess1 ) - self.m_logger.error( e, exc_info=True ) - return 0 - except Exception as e: - mess0: str = "The new attribute was not added due to:" - self.m_logger.critical( mess0 ) - self.m_logger.critical( e, exc_info=True ) - return 0 - self.m_compute = True - return 1 - - def createAttributesMultiBlock( self: Self, output: vtkMultiBlockDataSet ) -> None: - """Create attributes on vtkMultiBlockDataSet from input data. - - Args: - output (vtkMultiBlockDataSet): mesh where to create the attributes. - """ - # for each block - blockIndexes: list[ int ] = getBlockElementIndexesFlatten( output ) - for blockIndex in blockIndexes: - block0: vtkDataObject = getBlockFromFlatIndex( output, blockIndex ) - assert block0 is not None, "Block is undefined." - block: vtkUnstructuredGrid = vtkUnstructuredGrid.SafeDownCast( block0 ) - try: - self.createAttributes( block ) - except AssertionError as e: - self.m_logger.warning( f"Block {blockIndex}: {e}" ) - output.Modified() - - def createAttributes( self: Self, mesh: vtkUnstructuredGrid ) -> None: - """Create attributes on vtkUnstructuredGrid from input data. - - Args: - mesh (vtkUnstructuredGrid): mesh where to create the attributes. - """ - assert isAttributeInObject( mesh, self.m_regionAttributeName, - False ), f"{self.m_regionAttributeName} is not in the mesh." - regionAttr: vtkDataArray = mesh.GetCellData().GetArray( self.m_regionAttributeName ) - assert regionAttr is not None, "Region attribute is undefined" - npArray: npt.NDArray[ np.float64 ] = self.createNpArray( regionAttr ) - newAttr: vtkDataArray = vnp.numpy_to_vtk( npArray, True, VTK_DOUBLE ) - newAttr.SetName( self.m_attributeName ) - mesh.GetCellData().AddArray( newAttr ) - mesh.GetCellData().Modified() - mesh.Modified() - - def createNpArray( self: Self, regionAttr: vtkDataArray ) -> npt.NDArray[ np.float64 ]: - """Create numpy arrays from input data. - - Args: - regionAttr (vtkDataArray): Region attribute - - Returns: - npt.NDArray[np.float64]: numpy array of the new attribute. - """ - regionNpArray: npt.NDArray[ np.float64 ] = vnp.vtk_to_numpy( regionAttr ) - npArray: npt.NDArray[ np.float64 ] = np.full_like( regionNpArray, np.nan ) - # for each region - for regionIndex, value in self.m_table: - if regionIndex in np.unique( regionNpArray ): - mask: npt.NDArray[ np.bool_ ] = regionNpArray == regionIndex - npArray[ mask ] = value - else: - self.m_logger.warning( f"Index {regionIndex} is not in the values of the region" + - f" attribute '{regionAttr.GetName()}'" ) - return npArray diff --git a/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py b/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py new file mode 100644 index 00000000..7965634d --- /dev/null +++ b/geos-pv/src/geos/pv/plugins/PVCreateConstantAttributePerRegion.py @@ -0,0 +1,352 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Martin Lemay, Romain Baville +# ruff: noqa: E402 # disable Module level import not at top of file +import sys +from pathlib import Path + +from typing import Union, Any +from typing_extensions import Self + +from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] + smdomain, smhint, smproperty, smproxy, +) # source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/util/vtkAlgorithm.py +from paraview.detail.loghandler import ( # type: ignore[import-not-found] + VTKHandler, +) # source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/detail/loghandler.py + +from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase +from vtkmodules.vtkCommonCore import ( + vtkInformation, + vtkInformationVector, +) +from vtkmodules.vtkCommonDataModel import ( + vtkMultiBlockDataSet, + vtkDataSet, +) + +# update sys.path to load all GEOS Python Package dependencies +geos_pv_path: Path = Path( __file__ ).parent.parent.parent.parent.parent +sys.path.insert( 0, str( geos_pv_path / "src" ) ) +from geos.pv.utils.config import update_paths + +update_paths() + +from geos.mesh.processing.CreateConstantAttributePerRegion import CreateConstantAttributePerRegion, vnp, np + +__doc__ = """ +PVCreateConstantAttributePerRegion is a Paraview plugin that allows to create an attribute +with constant values per components for each chosen indexes of a reference/region attribute. +If other region indexes exist values are set to nan for float type, -1 for int type or 0 for uint type. + +Input mesh is either vtkMultiBlockDataSet or vtkDataSet and the region attribute must have one component. +The relation index/values is given by a dictionary. Its keys are the indexes and its items are the list of values for each component. + +To use it: + +* Load the module in Paraview: Tools>Manage Plugins...>Load new>PVCreateConstantAttributePerRegion. +* Select the mesh you want to create the attributes and containing a region attribute. +* Select the filter Create Constant Attribute Per Region in filter|0- Geos Pre-processing. +* Choose the region attribute, the relation index/values, the new attribute name, the type of the value, the number of components and their names. +* Apply. + +""" + + +@smproxy.filter( + name="PVCreateConstantAttributePerRegion", + label="Create Constant Attribute Per Region", +) +@smhint.xml( """""" ) +@smproperty.input( name="Input", port_index=0 ) +@smdomain.datatype( + dataTypes=[ "vtkMultiBlockDataSet", "vtkDataSet" ], + composite_data_supported=True, +) +class PVCreateConstantAttributePerRegion( VTKPythonAlgorithmBase ): + + def __init__( self: Self ) -> None: + """Create an attribute with constant value per region.""" + super().__init__( nInputPorts=1, nOutputPorts=1, inputType="vtkDataObject", outputType="vtkDataObject" ) + + self.clearDictRegionValues: bool = True + + # Region attribute settings. + self.regionName: str = "" + self.dictRegionValues: dict[ Any, Any ] = {} + + # New attribute settings. + self.newAttributeName: str = "newAttribute" + self.valueNpType: type = np.float32 + self.nbComponents: int = 1 + self.componentNames: tuple[ str, ...] = () + + # Use the handler of paraview for the log. + self.speHandler: bool = True + + # Settings of the attribute with the region indexes: + @smproperty.stringvector( + name="ChooseRegionAttribute", + label="Attribute with region indexes", + default_values="Choose an attribute", + number_of_elements="1", + element_types="2", + ) + @smdomain.xml( """ + + + + + + + Select the attribute to consider as the region attribute containing the indexes of the regions. + + + + + """ ) + def _setRegionAttributeName( self: Self, regionName: str ) -> None: + """Set region attribute name. + + Args: + regionName (str): The name of the attribute to consider as the region attribute. + """ + self.regionName = regionName + self.Modified() + + @smproperty.xml( """ + + + Set the value of the new attribute for each region indexes, use a coma between the value of each components:\n + valueRegionIndex : valueComponent1, valueComponent2 ...\n + If the region attribute has other indexes than those given, a default value is use:\n + 0 for uint type, -1 for int type and nan for float type. + + + + + + + + + + """ ) + def _setDictRegionValues( self: Self, regionIndex: str, value: str ) -> None: + """Set the the dictionary with the region indexes and its corresponding list of value for each components. + + Args: + regionIndex (str): Region index of the region attribute to consider. + value (str): List of value to use for the regionIndex. If multiple components use a coma between the value of each component. + """ + if self.clearDictRegionValues: + self.dictRegionValues = {} + self.clearDictRegionValues = False + + if regionIndex is not None and value is not None: + self.dictRegionValues[ regionIndex ] = list( value.split( "," ) ) + + self.Modified() + + @smproperty.xml( """ + + + + + """ ) + def _groupeRegionAttributeSettingsWidgets( self: Self ) -> None: + """Group the widgets to set the settings of the region attribute.""" + self.Modified() + + # Settings of the new attribute: + @smproperty.xml( """ + + + Name of the new attribute to create. + + + """ ) + def _setAttributeName( self: Self, newAttributeName: str ) -> None: + """Set attribute name. + + Args: + newAttributeName (str): Name of the new attribute to create. + """ + self.newAttributeName = newAttributeName + self.Modified() + + @smproperty.intvector( + name="ValueType", + label="The type of the values:", + number_of_elements=1, + default_values=10, + panel_visibility="default", + ) + @smdomain.xml( """ + + + + + + + + + + + + + + The wanted numpy scalar type for values of the new attribute. + + """ ) + def _setValueType( self: Self, valueType: int ) -> None: + """Set the type for the value used to create the new attribute. + + Args: + valueType (int): The type for the value encoding with the vtk typecode. + """ + dictType: dict[ int, Any ] = vnp.get_vtk_to_numpy_typemap() + self.valueNpType = dictType[ valueType ] + self.Modified() + + @smproperty.intvector( + name="NumberOfComponents", + label="Number of components:", + number_of_elements=1, + default_values=1, + panel_visibility="default", + ) + @smdomain.xml( """ + + The number of components for the new attribute to create. + + """ ) + def _setNbComponent( self: Self, nbComponents: int ) -> None: + """Set the number of components of the attribute to create. + + Args: + nbComponents (int): Number of components for the new attribute. + """ + self.nbComponents = nbComponents + self.Modified() + + @smproperty.stringvector( + name="ComponentNames", + label="Names of components:", + number_of_elements=1, + default_values="Change if multiple components", + panel_visibility="default", + ) + @smdomain.xml( """ + + Names of components if multiple for the new attribute to create. + Use the coma and a coma between each component name:\n + Names of components: X, Y, Z + + """ ) + def _setComponentNames( self: Self, componentNames: str ) -> None: + """Set the names of the components of the attribute to create. + + Args: + componentNames (str): Names of component for the new attribute. Use a coma between each component names. + """ + if componentNames == "" or componentNames == "Change if multiple components" or self.nbComponents == 1: + self.componentNames = () + else: + self.componentNames = tuple( componentNames.split( "," ) ) + + self.Modified() + + @smproperty.xml( """ + + + + + + """ ) + def _groupNewAttributeSettingsWidgets( self: Self ) -> None: + """Group the widgets to set the settings of the new attribute.""" + self.Modified() + + def RequestDataObject( + self: Self, + request: vtkInformation, + inInfoVec: list[ vtkInformationVector ], + outInfoVec: vtkInformationVector, + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestDataObject. + + Args: + request (vtkInformation): request + inInfoVec (list[vtkInformationVector]): input objects + outInfoVec (vtkInformationVector): output objects + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + inData = self.GetInputData( inInfoVec, 0, 0 ) + outData = self.GetOutputData( outInfoVec, 0 ) + assert inData is not None + if outData is None or ( not outData.IsA( inData.GetClassName() ) ): + outData = inData.NewInstance() + outInfoVec.GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), outData ) + return super().RequestDataObject( request, inInfoVec, outInfoVec ) # type: ignore[no-any-return] + + def RequestData( + self: Self, + request: vtkInformation, # noqa: F841 + inInfoVec: list[ vtkInformationVector ], # noqa: F841 + outInfoVec: vtkInformationVector, # noqa: F841 + ) -> int: + """Inherited from VTKPythonAlgorithmBase::RequestData. + + Args: + request (vtkInformation): Request. + inInfoVec (list[vtkInformationVector]): Input objects. + outInfoVec (vtkInformationVector): Output objects. + + Returns: + int: 1 if calculation successfully ended, 0 otherwise. + """ + inputMesh: Union[ vtkDataSet, vtkMultiBlockDataSet ] = ( self.GetInputData( inInfoVec, 0, 0 ) ) + outputMesh: Union[ vtkDataSet, vtkMultiBlockDataSet ] = ( self.GetOutputData( outInfoVec, 0 ) ) + + assert inputMesh is not None, "Input Surface is null." + assert outputMesh is not None, "Output pipeline is null." + + outputMesh.ShallowCopy( inputMesh ) + filter: CreateConstantAttributePerRegion = CreateConstantAttributePerRegion( + outputMesh, + self.regionName, + self.dictRegionValues, + self.newAttributeName, + self.valueNpType, + self.nbComponents, + self.componentNames, + self.speHandler, + ) + + if not filter.logger.hasHandlers(): + filter.setLoggerHandler( VTKHandler() ) + + filter.applyFilter() + + self.clearDictRegion = True + + return 1 diff --git a/geos-utils/src/geos/utils/Logger.py b/geos-utils/src/geos/utils/Logger.py index 69ec6ec5..9d3fe5c4 100644 --- a/geos-utils/src/geos/utils/Logger.py +++ b/geos-utils/src/geos/utils/Logger.py @@ -12,6 +12,24 @@ """ +class CountWarningHandler( logging.Handler ): + """Create an handler to count the warnings logged.""" + + def __init__( self: Self ) -> None: + """Init the handler.""" + super().__init__() + self.warningCount = 0 + + def emit( self: Self, record: logging.LogRecord ) -> None: + """Count all the warnings logged. + + Args: + record (logging.LogRecord): Record. + """ + if record.levelno == logging.WARNING: + self.warningCount += 1 + + # Add the convenience method for the logger def results( self: logging.Logger, message: str, *args: Any, **kws: Any ) -> None: """Logs a message with the custom 'RESULTS' severity level. @@ -49,7 +67,7 @@ def results( self: logging.Logger, message: str, *args: Any, **kws: Any ) -> Non class CustomLoggerFormatter( logging.Formatter ): """Custom formatter for the logger. - .. WARNING:: Colors do not work in the ouput message window of Paraview. + .. WARNING:: Colors do not work in the output message window of Paraview. To use it: @@ -78,7 +96,7 @@ class CustomLoggerFormatter( logging.Formatter ): #: format for each logger output type with colors FORMATS_COLOR: dict[ int, str ] = { DEBUG: grey + format2 + reset, - INFO: grey + format1 + reset, + INFO: green + format1 + reset, WARNING: yellow + format1 + reset, ERROR: red + format1 + reset, CRITICAL: bold_red + format2 + reset, @@ -151,7 +169,7 @@ def getLogger( title: str, use_color: bool = False ) -> Logger: # module import import Logger - # logger instanciation + # logger instantiation logger :Logger.Logger = Logger.getLogger("My application") # logger use