From 9b8dad2d0301bf0a4453c7c229d7a4fac68a77ed Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 25 Jul 2025 12:19:28 +0200 Subject: [PATCH 1/9] Re-install PythonViewConfigurator --- .../src/PVplugins/PVPythonViewConfigurator.py | 861 +++++++++++ geos-pv/src/PVplugins/example.py | 4 + .../pv/pythonViewUtils/Figure2DGenerator.py | 137 ++ .../src/geos/pv/pythonViewUtils/__init__.py | 0 .../functionsFigure2DGenerator.py | 1375 +++++++++++++++++ .../geos/pv/pythonViewUtils/mainPythonView.py | 38 + 6 files changed, 2415 insertions(+) create mode 100755 geos-pv/src/PVplugins/PVPythonViewConfigurator.py create mode 100644 geos-pv/src/PVplugins/example.py create mode 100755 geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py create mode 100755 geos-pv/src/geos/pv/pythonViewUtils/__init__.py create mode 100755 geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py create mode 100755 geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py diff --git a/geos-pv/src/PVplugins/PVPythonViewConfigurator.py b/geos-pv/src/PVplugins/PVPythonViewConfigurator.py new file mode 100755 index 00000000..87f28b3c --- /dev/null +++ b/geos-pv/src/PVplugins/PVPythonViewConfigurator.py @@ -0,0 +1,861 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Alexandre Benedicto, Martin Lemay +# ruff: noqa: E402 # disable Module level import not at top of file +from pathlib import Path +import sys +from typing import Any, Union, cast + +import pandas as pd # type: ignore[import-untyped] +from typing_extensions import Self + +# 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() + +import geos_posp.visu.PVUtils.paraviewTreatments as pvt +from geos_posp.visu.PVUtils.checkboxFunction import ( # type: ignore[attr-defined] + createModifiedCallback, ) +from geos_posp.visu.PVUtils.DisplayOrganizationParaview import ( + DisplayOrganizationParaview, ) +from geos_posp.visu.PVUtils.matplotlibOptions import ( + FontStyleEnum, + FontWeightEnum, + LegendLocationEnum, + LineStyleEnum, + MarkerStyleEnum, + OptionSelectionEnum, + optionEnumToXml, +) +from paraview.simple import ( # type: ignore[import-not-found] + GetActiveSource, GetActiveView, Render, Show, servermanager, +) +from paraview.util.vtkAlgorithm import ( # type: ignore[import-not-found] + VTKPythonAlgorithmBase, smdomain, smhint, smproperty, smproxy, +) +from vtkmodules.vtkCommonCore import ( + vtkDataArraySelection, + vtkInformation, + vtkInformationVector, +) + +__doc__ = """ +PVPythonViewConfigurator is a Paraview plugin that allows to create cross-plots +from input data using the PythonView. + +Input type is vtkDataObject. + +This filter results in opening a new Python View window and displaying cross-plot. + +To use it: + +* Load the module in Paraview: Tools>Manage Plugins...>Load new>PVPythonViewConfigurator. +* Select the vtkDataObject containing the data to plot. +* Search and Apply PVPythonViewConfigurator Filter. + +""" + + +@smproxy.filter( name="PVPythonViewConfigurator", label="Python View Configurator" ) +@smhint.xml( '' ) +@smproperty.input( name="Input" ) +@smdomain.datatype( dataTypes=[ "vtkDataObject" ], composite_data_supported=True ) +class PVPythonViewConfigurator( VTKPythonAlgorithmBase ): + + def __init__( self: Self ) -> None: + """Paraview plugin to create cross-plots in a Python View. + + Input is a vtkDataObject. + """ + super().__init__( nInputPorts=1, nOutputPorts=1 ) + # python view layout and object + self.m_layoutName: str = "" + self.m_pythonView: Any + self.m_organizationDisplay = DisplayOrganizationParaview() + self.buildNewLayoutWithPythonView() + + # input source and curve names + inputSource = GetActiveSource() + dataset = servermanager.Fetch( inputSource ) + dataframe: pd.DataFrame = pvt.vtkToDataframe( dataset ) + self.m_pathPythonViewScript: Path = geos_pv_path / "src/geos/pv/pythonViewUtils/mainPythonView.py" + + # checkboxes + self.m_modifyInputs: int = 1 + self.m_modifyCurves: int = 1 + self.m_multiplyCurves: int = 0 + + # checkboxes curves available from the data of pipeline + self.m_validSources = vtkDataArraySelection() + self.m_curvesToPlot = vtkDataArraySelection() + self.m_curvesMinus1 = vtkDataArraySelection() + self.m_validSources.AddObserver( "ModifiedEvent", createModifiedCallback( self ) ) # type: ignore[arg-type] + self.m_curvesToPlot.AddObserver( "ModifiedEvent", createModifiedCallback( self ) ) # type: ignore[arg-type] + self.m_curvesMinus1.AddObserver( "ModifiedEvent", createModifiedCallback( self ) ) # type: ignore[arg-type] + validSourceNames: set[ str ] = pvt.getPossibleSourceNames() + for sourceName in validSourceNames: + self.m_validSources.AddArray( sourceName ) + validColumnsDataframe: list[ str ] = list( dataframe.columns ) + for name in list( dataframe.columns ): + for axis in [ "X", "Y", "Z" ]: + if "Points" + axis in name and "Points" + axis + "__" in name: + positionDoublon: int = validColumnsDataframe.index( "Points" + axis ) + validColumnsDataframe.pop( positionDoublon ) + break + self.m_validColumnsDataframe: list[ str ] = sorted( validColumnsDataframe, key=lambda x: x.lower() ) + for curveName in validColumnsDataframe: + self.m_curvesToPlot.AddArray( curveName ) + self.m_curvesMinus1.AddArray( curveName ) + self.m_validSources.DisableAllArrays() + self.m_curvesToPlot.DisableAllArrays() + self.m_curvesMinus1.DisableAllArrays() + self.m_curveToUse: str = "" + # to change the aspects of curves + self.m_curvesToModify: set[ str ] = pvt.integrateSourceNames( validSourceNames, set( validColumnsDataframe ) ) + self.m_color: tuple[ float, float, float ] = ( 0.0, 0.0, 0.0 ) + self.m_lineStyle: str = LineStyleEnum.SOLID.optionValue + self.m_lineWidth: float = 1.0 + self.m_markerStyle: str = MarkerStyleEnum.NONE.optionValue + self.m_markerSize: float = 1.0 + + # user choices + self.m_userChoices: dict[ str, Any ] = { + "variableName": "", + "curveNames": [], + "curveConvention": [], + "inputNames": [], + "plotRegions": False, + "reverseXY": False, + "logScaleX": False, + "logScaleY": False, + "minorticks": False, + "displayTitle": True, + "title": "title1", + "titleStyle": FontStyleEnum.NORMAL.optionValue, + "titleWeight": FontWeightEnum.BOLD.optionValue, + "titleSize": 12, + "legendDisplay": True, + "legendPosition": LegendLocationEnum.BEST.optionValue, + "legendSize": 10, + "removeJobName": True, + "removeRegions": False, + "curvesAspect": {}, + } + + def getUserChoices( self: Self ) -> dict[ str, Any ]: + """Access the m_userChoices attribute. + + Returns: + dict[str] : the user choices for the figure. + """ + return self.m_userChoices + + def getInputNames( self: Self ) -> set[ str ]: + """Get source names from user selection. + + Returns: + set[str] : source names from ParaView pipeline. + """ + inputAvailables = self.a01GetInputSources() + inputNames: set[ str ] = set( pvt.getArrayChoices( inputAvailables ) ) + return inputNames + + def defineInputNames( self: Self ) -> None: + """Adds the input names to the userChoices.""" + inputNames: set[ str ] = self.getInputNames() + self.m_userChoices[ "inputNames" ] = inputNames + + def defineUserChoicesCurves( self: Self ) -> None: + """Define user choices for curves to plot.""" + sourceNames: set[ str ] = self.getInputNames() + dasPlot = self.b02GetCurvesToPlot() + dasMinus1 = self.b07GetCurveConvention() + curveNames: set[ str ] = set( pvt.getArrayChoices( dasPlot ) ) + minus1Names: set[ str ] = set( pvt.getArrayChoices( dasMinus1 ) ) + toUse1: set[ str ] = pvt.integrateSourceNames( sourceNames, curveNames ) + toUse2: set[ str ] = pvt.integrateSourceNames( sourceNames, minus1Names ) + self.m_userChoices[ "curveNames" ] = tuple( toUse1 ) + self.m_userChoices[ "curveConvention" ] = tuple( toUse2 ) + + def defineCurvesAspect( self: Self ) -> None: + """Define user choices for curve aspect properties.""" + curveAspect: tuple[ tuple[ float, float, float ], str, float, str, float ] = ( self.getCurveAspect() ) + curveName: str = self.getCurveToUse() + self.m_userChoices[ "curvesAspect" ][ curveName ] = curveAspect + + def buildPythonViewScript( self: Self ) -> str: + """Builds the Python script used to launch the Python View. + + The script is returned as a string to be then injected in the Python + View. + + Returns: + str: Complete Python View script. + """ + sourceNames: set[ str ] = self.getInputNames() + userChoices: dict[ str, Any ] = self.getUserChoices() + script: str = f"timestep = '{str(GetActiveView().ViewTime)}'\n" + script += f"sourceNames = {sourceNames}\n" + script += f"variableName = '{userChoices['variableName']}'\n" + script += f"dir_path = '{geos_pv_path}'\n" + script += f"userChoices = {userChoices}\n\n\n" + with self.m_pathPythonViewScript.open() as file: + fileContents = file.read() + script += fileContents + return script + + def buildNewLayoutWithPythonView( self: Self ) -> None: + """Create a new Python View layout.""" + # we first built the new layout + layout_names: list[ str ] = self.m_organizationDisplay.getLayoutsNames() + nb_layouts: int = len( layout_names ) + # imagine two layouts already exists, the new one will be named "Layout #3" + layoutName: str = "Layout #" + str( nb_layouts + 1 ) + # check that we that the layoutName is new and does not belong to the list of layout_names, + # if not we modify the layoutName until it is a new one + if layoutName in layout_names: + cpt: int = 2 + while layoutName in layout_names: + layoutName = "Layout #" + str( nb_layouts + cpt ) + cpt += 1 + self.m_organizationDisplay.addLayout( layoutName ) + self.m_layoutName = layoutName + + # we then build the new python view + self.m_organizationDisplay.addViewToLayout( "PythonView", layoutName, 0 ) + self.m_pythonView = self.m_organizationDisplay.getLayoutViews()[ layoutName ][ 0 ] + Show( GetActiveSource(), self.m_pythonView, "PythonRepresentation" ) + + # widgets definition + """The names of the @smproperty methods command names below have a letter in lower case in + front because PARAVIEW displays properties in the alphabetical order. + See https://gitlab.kitware.com/paraview/paraview/-/issues/21493 for possible improvements on + this issue""" + + @smproperty.dataarrayselection( name="InputSources" ) + def a01GetInputSources( self: Self ) -> vtkDataArraySelection: + """Get all valid sources for the filter. + + Returns: + vtkDataArraySelection: valid data sources. + """ + return self.m_validSources + + @smproperty.xml( """ + + """ ) + def a02GroupFlow( self: Self ) -> None: + """Organize groups.""" + self.Modified() + + @smproperty.stringvector( name="CurvesAvailable", information_only="1" ) + def b00GetCurvesAvailable( self: Self ) -> list[ str ]: + """Get the available curves. + + Returns: + list[str]: list of curves. + """ + return self.m_validColumnsDataframe + + @smproperty.stringvector( name="Abscissa", number_of_elements="1" ) + @smdomain.xml( """ + + """ ) + def b01SetVariableName( self: Self, name: str ) -> None: + """Set the name of X axis variable. + + Args: + name: name of the variable. + """ + self.m_userChoices[ "variableName" ] = name + self.Modified() + + @smproperty.dataarrayselection( name="Ordinate" ) + def b02GetCurvesToPlot( self: Self ) -> vtkDataArraySelection: + """Get the curves to plot. + + Returns: + vtkDataArraySelection: data to plot. + """ + return self.m_curvesToPlot + + @smproperty.intvector( name="PlotsPerRegion", label="PlotsPerRegion", default_values=0 ) + @smdomain.xml( """""" ) + def b03SetPlotsPerRegion( self: Self, boolean: bool ) -> None: + """Set plot per region option. + + Args: + boolean: user choice. + """ + self.m_userChoices[ "plotRegions" ] = boolean + self.Modified() + + @smproperty.xml( """ + + + + """ ) + def b04GroupFlow( self: Self ) -> None: + """Organized groups.""" + self.Modified() + + @smproperty.intvector( + name="CurveConvention", + label="Select Curves To Change Convention", + default_values=0, + ) + @smdomain.xml( """""" ) + def b05SetCurveConvention( self: Self, boolean: bool ) -> None: + """Select Curves To Change Convention. + + Args: + boolean: user choice. + """ + self.m_multiplyCurves = boolean + + @smproperty.xml( """ + + """ ) + def b06GroupFlow( self: Self ) -> None: + """Organized groups.""" + self.Modified() + + @smproperty.dataarrayselection( name="CurveConventionSelection" ) + def b07GetCurveConvention( self: Self ) -> vtkDataArraySelection: + """Get the curves to change convention. + + Returns: + vtkDataArraySelection: selected curves to change convention. + """ + return self.m_curvesMinus1 + + @smproperty.xml( """ + + + """ ) + def b08GroupFlow( self: Self ) -> None: + """Organized groups.""" + self.Modified() + + @smproperty.intvector( name="EditAxisProperties", label="Edit Axis Properties", default_values=0 ) + @smdomain.xml( """""" ) + def c01SetEditAxisProperties( self: Self, boolean: bool ) -> None: + """Set option to edit axis properties. + + Args: + boolean (bool): user choice. + """ + self.Modified() + + @smproperty.xml( """ + + """ ) + def c02GroupFlow( self: Self ) -> None: + """Organized groups.""" + self.Modified() + + @smproperty.intvector( name="ReverseXY", label="Reverse XY Axes", default_values=0 ) + @smdomain.xml( """""" ) + def c02SetReverseXY( self: Self, boolean: bool ) -> None: + """Set option to reverse X and Y axes. + + Args: + boolean (bool): user choice. + """ + self.m_userChoices[ "reverseXY" ] = boolean + self.Modified() + + @smproperty.intvector( name="LogScaleX", label="X Axis Log Scale", default_values=0 ) + @smdomain.xml( """""" ) + def c03SetReverseXY( self: Self, boolean: bool ) -> None: + """Set option to log scale for X axis. + + Args: + boolean (bool): user choice. + """ + self.m_userChoices[ "logScaleX" ] = boolean + self.Modified() + + @smproperty.intvector( name="LogScaleY", label="Y Axis Log Scale", default_values=0 ) + @smdomain.xml( """""" ) + def c04SetReverseXY( self: Self, boolean: bool ) -> None: + """Set option to log scale for Y axis. + + Args: + boolean (bool): user choice. + """ + self.m_userChoices[ "logScaleY" ] = boolean + self.Modified() + + @smproperty.intvector( name="Minorticks", label="Display Minor ticks", default_values=0 ) + @smdomain.xml( """""" ) + def c05SetMinorticks( self: Self, boolean: bool ) -> None: + """Set option to display minor ticks. + + Args: + boolean (bool): user choice. + """ + self.m_userChoices[ "minorticks" ] = boolean + self.Modified() + + @smproperty.intvector( name="CustomAxisLim", label="Use Custom Axis Limits", default_values=0 ) + @smdomain.xml( """""" ) + def c06SetCustomAxisLim( self: Self, boolean: bool ) -> None: + """Set option to define axis limits. + + Args: + boolean (bool): user choice. + """ + self.m_userChoices[ "customAxisLim" ] = boolean + self.Modified() + + @smproperty.doublevector( name="LimMinX", label="X min", default_values=-1e36 ) + def c07LimMinX( self: Self, value: float ) -> None: + """Set X axis min. + + Args: + value (float): X axis min. + """ + value2: Union[ float, None ] = value + if value2 == -1e36: + value2 = None + self.m_userChoices[ "limMinX" ] = value2 + self.Modified() + + @smproperty.doublevector( name="LimMaxX", label="X max", default_values=1e36 ) + def c08LimMaxX( self: Self, value: float ) -> None: + """Set X axis max. + + Args: + value (float): X axis max. + """ + value2: Union[ float, None ] = value + if value2 == 1e36: + value2 = None + self.m_userChoices[ "limMaxX" ] = value2 + self.Modified() + + @smproperty.doublevector( name="LimMinY", label="Y min", default_values=-1e36 ) + def c09LimMinY( self: Self, value: float ) -> None: + """Set Y axis min. + + Args: + value (float): Y axis min. + """ + value2: Union[ float, None ] = value + if value2 == -1e36: + value2 = None + self.m_userChoices[ "limMinY" ] = value2 + self.Modified() + + @smproperty.doublevector( name="LimMaxY", label="Y max", default_values=1e36 ) + def c10LimMaxY( self: Self, value: float ) -> None: + """Set Y axis max. + + Args: + value (float): Y axis max. + """ + value2: Union[ float, None ] = value + if value2 == 1e36: + value2 = None + self.m_userChoices[ "limMaxY" ] = value2 + self.Modified() + + @smproperty.xml( """ + + + + + + """ ) + def c11GroupFlow( self: Self ) -> None: + """Organized groups.""" + self.Modified() + + @smproperty.xml( """ + + + + + + + """ ) + def c12GroupFlow( self: Self ) -> None: + """Organized groups.""" + self.Modified() + + @smproperty.intvector( name="DisplayTitle", label="Display Title", default_values=1 ) + @smdomain.xml( """""" ) + def d01SetDisplayTitle( self: Self, boolean: bool ) -> None: + """Set option to display title. + + Args: + boolean (bool): user choice. + """ + self.m_userChoices[ "displayTitle" ] = boolean + self.Modified() + + @smproperty.xml( """ + + """ ) + def d02GroupFlow( self: Self ) -> None: + """Organized groups.""" + self.Modified() + + @smproperty.stringvector( name="Title", default_values="title1" ) + def d03SetTitlePlot( self: Self, title: str ) -> None: + """Set title. + + Args: + title (str): title. + """ + self.m_userChoices[ "title" ] = title + self.Modified() + + @smproperty.intvector( name="TitleStyle", label="Title Style", default_values=0 ) + @smdomain.xml( optionEnumToXml( cast( OptionSelectionEnum, FontStyleEnum ) ) ) + def d04SetTitleStyle( self: Self, value: int ) -> None: + """Set title font style. + + Args: + value (int): title font style index in FontStyleEnum. + """ + choice = list( FontStyleEnum )[ value ] + self.m_userChoices[ "titleStyle" ] = choice.optionValue + self.Modified() + + @smproperty.intvector( name="TitleWeight", label="Title Weight", default_values=1 ) + @smdomain.xml( optionEnumToXml( cast( OptionSelectionEnum, FontWeightEnum ) ) ) + def d05SetTitleWeight( self: Self, value: int ) -> None: + """Set title font weight. + + Args: + value (int): title font weight index in FontWeightEnum. + """ + choice = list( FontWeightEnum )[ value ] + self.m_userChoices[ "titleWeight" ] = choice.optionValue + self.Modified() + + @smproperty.intvector( name="TitleSize", label="Title Size", default_values=12 ) + @smdomain.xml( """""" ) + def d06SetTitleSize( self: Self, size: float ) -> None: + """Set title font size. + + Args: + size (float): title font size between 1 and 50. + """ + self.m_userChoices[ "titleSize" ] = size + self.Modified() + + @smproperty.xml( """ + panel_visibility="advanced"> + + + + + + """ ) + def d07PropertyGroup( self: Self ) -> None: + """Organized groups.""" + self.Modified() + + @smproperty.intvector( name="DisplayLegend", label="Display Legend", default_values=1 ) + @smdomain.xml( """""" ) + def e00SetDisplayLegend( self: Self, boolean: bool ) -> None: + """Set option to display legend. + + Args: + boolean (bool): user choice. + """ + self.m_userChoices[ "displayLegend" ] = boolean + self.Modified() + + @smproperty.xml( """ + + """ ) + def e01PropertyGroup( self: Self ) -> None: + """Organized groups.""" + self.Modified() + + @smproperty.intvector( name="LegendPosition", label="Legend Position", default_values=0 ) + @smdomain.xml( optionEnumToXml( cast( OptionSelectionEnum, LegendLocationEnum ) ) ) + def e02SetLegendPosition( self: Self, value: int ) -> None: + """Set legend position. + + Args: + value (int): legend position index in LegendLocationEnum. + """ + choice = list( LegendLocationEnum )[ value ] + self.m_userChoices[ "legendPosition" ] = choice.optionValue + self.Modified() + + @smproperty.intvector( name="LegendSize", label="Legend Size", default_values=10 ) + @smdomain.xml( """""" ) + def e03SetLegendSize( self: Self, size: float ) -> None: + """Set legend font size. + + Args: + size (float): legend font size between 1 and 50. + """ + self.m_userChoices[ "legendSize" ] = size + self.Modified() + + @smproperty.intvector( name="RemoveJobName", label="Remove Job Name in legend", default_values=1 ) + @smdomain.xml( """""" ) + def e04SetRemoveJobName( self: Self, boolean: bool ) -> None: + """Set option to remove job names from legend. + + Args: + boolean (bool): user choice. + """ + self.m_userChoices[ "removeJobName" ] = boolean + self.Modified() + + @smproperty.intvector( + name="RemoveRegionsName", + label="Remove Regions Name in legend", + default_values=0, + ) + @smdomain.xml( """""" ) + def e05SetRemoveRegionsName( self: Self, boolean: bool ) -> None: + """Set option to remove region names from legend. + + Args: + boolean (bool): user choice. + """ + self.m_userChoices[ "removeRegions" ] = boolean + self.Modified() + + @smproperty.xml( """ + + + + + + """ ) + def e06PropertyGroup( self: Self ) -> None: + """Organized groups.""" + self.Modified() + + @smproperty.intvector( name="ModifyCurvesAspect", label="Edit Curve Graphics", default_values=1 ) + @smdomain.xml( """""" ) + def f01SetModifyCurvesAspect( self: Self, boolean: bool ) -> None: + """Set option to change curve aspects. + + Args: + boolean (bool): user choice. + """ + self.m_modifyCurvesAspect = boolean + + @smproperty.xml( """ + + """ ) + def f02PropertyGroup( self: Self ) -> None: + """Organized groups.""" + self.Modified() + + @smproperty.stringvector( name="CurvesInfo", information_only="1" ) + def f03GetCurveNames( self: Self ) -> list[ str ]: + """Get curves to modify aspects. + + Returns: + set[str]: curves to modify aspects. + """ + return list( self.m_curvesToModify ) + + # TODO: still usefull? + @smproperty.stringvector( name="CurveToModify", number_of_elements="1" ) + @smdomain.xml( """ + + """ ) + def f04SetCircleID( self: Self, value: str ) -> None: + """Set m_curveToUse. + + Args: + value (float): value of m_curveToUse + """ + self.m_curveToUse = value + self.Modified() + + def getCurveToUse( self: Self ) -> str: + """Get m_curveToUse.""" + return self.m_curveToUse + + @smproperty.intvector( name="LineStyle", label="Line Style", default_values=1 ) + @smdomain.xml( optionEnumToXml( cast( OptionSelectionEnum, LineStyleEnum ) ) ) + def f05SetLineStyle( self: Self, value: int ) -> None: + """Set line style. + + Args: + value (int): line style index in LineStyleEnum + """ + choice = list( LineStyleEnum )[ value ] + self.m_lineStyle = choice.optionValue + self.Modified() + + @smproperty.doublevector( name="LineWidth", default_values=1.0 ) + @smdomain.xml( """""" ) + def f06SetLineWidth( self: Self, value: float ) -> None: + """Set line width. + + Args: + value (float): line width between 1 and 10. + """ + self.m_lineWidth = value + self.Modified() + + @smproperty.intvector( name="MarkerStyle", label="Marker Style", default_values=0 ) + @smdomain.xml( optionEnumToXml( cast( LegendLocationEnum, MarkerStyleEnum ) ) ) + def f07SetMarkerStyle( self: Self, value: int ) -> None: + """Set marker style. + + Args: + value (int): Marker style index in MarkerStyleEnum + """ + choice = list( MarkerStyleEnum )[ value ] + self.m_markerStyle = choice.optionValue + self.Modified() + + @smproperty.doublevector( name="MarkerSize", default_values=1.0 ) + @smdomain.xml( """""" ) + def f08SetMarkerSize( self: Self, value: float ) -> None: + """Set marker size. + + Args: + value (float): size of markers between 1 and 30. + """ + self.m_markerSize = value + self.Modified() + + @smproperty.xml( """ + + + + + + + + """ ) + def f09PropertyGroup( self: Self ) -> None: + """Organized groups.""" + self.Modified() + + @smproperty.doublevector( name="ColorEnvelop", default_values=[ 0, 0, 0 ], number_of_elements=3 ) + @smdomain.xml( """""" ) + def f10SetColor( self: Self, value0: float, value1: float, value2: float ) -> None: + """Set envelope color. + + Args: + value0 (float): Red color between 0 and 1. + + value1 (float): Green color between 0 and 1. + + value2 (float): Blue color between 0 and 1. + """ + self.m_color = ( value0, value1, value2 ) + self.Modified() + + @smproperty.xml( """ + + + """ ) + def f11PropertyGroup( self: Self ) -> None: + """Organized groups.""" + self.Modified() + + def getCurveAspect( self: Self, ) -> tuple[ tuple[ float, float, float ], str, float, str, float ]: + """Get curve aspect properties according to user choices. + + Returns: + tuple: (color, linestyle, linewidth, marker, markersize) + """ + return ( + self.m_color, + self.m_lineStyle, + self.m_lineWidth, + self.m_markerStyle, + self.m_markerSize, + ) + + 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(), "vtkDataObject" ) + else: + info.Set( self.INPUT_REQUIRED_DATA_TYPE(), "vtkDataObject" ) + 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. + """ + 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. + """ + # pythonViewGeneration + assert self.m_pythonView is not None, "No Python View was found." + viewSize = GetActiveView().ViewSize + self.m_userChoices[ "ratio" ] = viewSize[ 0 ] / viewSize[ 1 ] + self.defineInputNames() + self.defineUserChoicesCurves() + self.defineCurvesAspect() + self.m_pythonView.Script = self.buildPythonViewScript() + Render() + return 1 diff --git a/geos-pv/src/PVplugins/example.py b/geos-pv/src/PVplugins/example.py new file mode 100644 index 00000000..92acb71c --- /dev/null +++ b/geos-pv/src/PVplugins/example.py @@ -0,0 +1,4 @@ +import sys +from pathlib import Path +geos_pv_path: Path = Path( __file__ ).parent.parent.parent +print(geos_pv_path) \ No newline at end of file diff --git a/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py b/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py new file mode 100755 index 00000000..bd6749f7 --- /dev/null +++ b/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py @@ -0,0 +1,137 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Alexandre Benedicto + +from typing import Any + +import pandas as pd # type: ignore[import-untyped] +from geos.utils.Logger import Logger, getLogger +from matplotlib import axes, figure, lines # type: ignore[import-untyped] +from matplotlib.font_manager import ( # type: ignore[import-untyped] + FontProperties, # type: ignore[import-untyped] +) +from typing_extensions import Self + +import geos.pv.pythonViewUtils.functionsFigure2DGenerator as fcts + + +class Figure2DGenerator: + + def __init__( self: Self, dataframe: pd.DataFrame, userChoices: dict[ str, list[ str ] ] ) -> None: + """Utility to create cross plots using Python View. + + We want to plot f(X) = Y where in this class, + "X" will be called "variable", "Y" will be called "curves". + + Args: + dataframe (pd.DataFrame): data to plot + userChoices (dict[str, list[str]]): user choices. + """ + self.m_dataframe: pd.DataFrame = dataframe + self.m_userChoices: dict[ str, Any ] = userChoices + self.m_fig: figure.Figure + self.m_axes: list[ axes._axes.Axes ] = [] + self.m_lines: list[ lines.Line2D ] = [] + self.m_labels: list[ str ] = [] + self.m_logger: Logger = getLogger( "Python View Configurator" ) + + try: + # apply minus 1 multiplication on certain columns + self.initMinus1Multiplication() + # defines m_fig, m_axes, m_lines and m_lables + self.plotInitialFigure() + # then to edit and customize the figure + self.enhanceFigure() + self.m_logger.info( "Data were successfully plotted." ) + + except Exception as e: + mess: str = "Plot creation failed due to:" + self.m_logger.critical( mess ) + self.m_logger.critical( e, exc_info=True ) + + def initMinus1Multiplication( self: Self ) -> None: + """Multiply by -1 certain columns of the input dataframe.""" + df: pd.DataFrame = self.m_dataframe.copy( deep=True ) + minus1CurveNames: list[ str ] = self.m_userChoices[ "curveConvention" ] + for name in minus1CurveNames: + df[ name ] = df[ name ] * ( -1 ) + self.m_dataframe = df + + def enhanceFigure( self: Self ) -> None: + """Apply all the enhancement features to the initial figure.""" + self.changeTitle() + self.changeMinorticks() + self.changeAxisScale() + self.changeAxisLimits() + + def plotInitialFigure( self: Self ) -> None: + """Generates a figure and axes objects from matplotlib. + + The figure plots all the curves along the X or Y axis, with legend and + label for X and Y. + """ + if self.m_userChoices[ "plotRegions" ]: + if not self.m_userChoices[ "reverseXY" ]: + ( fig, ax_all, lines, labels ) = fcts.multipleSubplots( self.m_dataframe, self.m_userChoices ) + else: + ( fig, ax_all, lines, labels ) = fcts.multipleSubplotsInverted( self.m_dataframe, self.m_userChoices ) + else: + if not self.m_userChoices[ "reverseXY" ]: + ( fig, ax_all, lines, labels ) = fcts.oneSubplot( self.m_dataframe, self.m_userChoices ) + else: + ( fig, ax_all, lines, labels ) = fcts.oneSubplotInverted( self.m_dataframe, self.m_userChoices ) + self.m_fig = fig + self.m_axes = ax_all + self.m_lines = lines + self.m_labels = labels + + def changeTitle( self: Self ) -> None: + """Update title of the first axis of the figure based on user choices.""" + if self.m_userChoices[ "displayTitle" ]: + title: str = self.m_userChoices[ "title" ] + fontTitle: FontProperties = fcts.buildFontTitle( self.m_userChoices ) + self.m_fig.suptitle( title, fontproperties=fontTitle ) + + def changeMinorticks( self: Self ) -> None: + """Set the minorticks on or off for every axes.""" + choice: bool = self.m_userChoices[ "minorticks" ] + if choice: + for ax in self.m_axes: + ax.minorticks_on() + else: + for ax in self.m_axes: + ax.minorticks_off() + + def changeAxisScale( self: Self ) -> None: + """Set the minorticks on or off for every axes.""" + for ax in self.m_axes: + if self.m_userChoices[ "logScaleX" ]: + ax.set_xscale( "log" ) + if self.m_userChoices[ "logScaleY" ]: + ax.set_yscale( "log" ) + + def changeAxisLimits( self: Self ) -> None: + """Update axis limits.""" + if self.m_userChoices[ "customAxisLim" ]: + for ax in self.m_axes: + xmin, xmax = ax.get_xlim() + if self.m_userChoices[ "limMinX" ] is not None: + xmin = self.m_userChoices[ "limMinX" ] + if self.m_userChoices[ "limMaxX" ] is not None: + xmax = self.m_userChoices[ "limMaxX" ] + ax.set_xlim( xmin, xmax ) + + ymin, ymax = ax.get_ylim() + if self.m_userChoices[ "limMinY" ] is not None: + ymin = self.m_userChoices[ "limMinY" ] + if self.m_userChoices[ "limMaxY" ] is not None: + ymax = self.m_userChoices[ "limMaxY" ] + ax.set_ylim( ymin, ymax ) + + def getFigure( self: Self ) -> figure.Figure: + """Acces the m_fig attribute. + + Returns: + figure.Figure: Figure containing all the plots. + """ + return self.m_fig diff --git a/geos-pv/src/geos/pv/pythonViewUtils/__init__.py b/geos-pv/src/geos/pv/pythonViewUtils/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py b/geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py new file mode 100755 index 00000000..4b629e9b --- /dev/null +++ b/geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py @@ -0,0 +1,1375 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Alexandre Benedicto +import math +from typing import Any + +import matplotlib.pyplot as plt # type: ignore[import-untyped] +import numpy as np +import numpy.typing as npt +import pandas as pd # type: ignore[import-untyped] +from matplotlib import axes, figure, lines # type: ignore[import-untyped] +from matplotlib.font_manager import ( # type: ignore[import-untyped] + FontProperties, # type: ignore[import-untyped] +) + +import geos.pv.geosLogReaderUtils.geosLogReaderFunctions as fcts +""" +Plotting tools for 2D figure and axes generation. +""" + + +def oneSubplot( + df: pd.DataFrame, + userChoices: dict[ str, Any ] ) -> tuple[ figure.Figure, list[ axes.Axes ], list[ lines.Line2D ], list[ str ] ]: + """Created a single subplot. + + From a dataframe, knowing which curves to plot along which variable, + generates a fig and its list of axes with the data plotted. + + Args: + df (pd.DataFrame): dataframe containing at least two columns, + one named "variableName" and the other "curveName" + userChoices (dict[str, Any]): Choices made by widget selection + in PythonViewConfigurator filter. + + Returns: + tuple[figure.Figure, list[axes.Axes], + list[lines.Line2D] , list[str]]: the fig and its list of axes. + """ + curveNames: list[ str ] = userChoices[ "curveNames" ] + variableName: str = userChoices[ "variableName" ] + curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, + float ] ] = userChoices[ "curvesAspect" ] + associatedProperties: dict[ str, list[ str ] ] = associatePropertyToAxeType( curveNames ) + fig, ax = plt.subplots( constrained_layout=True ) + all_ax: list[ axes.Axes ] = setupAllAxes( ax, variableName, associatedProperties, True ) + lineList: list[ lines.Line2D ] = [] + labels: list[ str ] = [] + cpt_cmap: int = 0 + x: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() + for cpt_ax, ( ax_name, propertyNames ) in enumerate( associatedProperties.items() ): + ax_to_use: axes.Axes = setupAxeToUse( all_ax, cpt_ax, ax_name, False ) + for propName in propertyNames: + y: npt.NDArray[ np.float64 ] = df[ propName ].to_numpy() + plotAxe( ax_to_use, x, y, propName, cpt_cmap, curvesAspect ) + cpt_cmap += 1 + new_lines, new_labels = ax_to_use.get_legend_handles_labels() + lineList += new_lines # type: ignore[arg-type] + labels += new_labels + labels, lineList = smartLabelsSorted( labels, lineList, userChoices ) + if userChoices[ "displayLegend" ]: + ax.legend( + lineList, + labels, + loc=userChoices[ "legendPosition" ], + fontsize=userChoices[ "legendSize" ], + ) + ax.grid() + return ( fig, all_ax, lineList, labels ) + + +def oneSubplotInverted( + df: pd.DataFrame, + userChoices: dict[ str, Any ] ) -> tuple[ figure.Figure, list[ axes.Axes ], list[ lines.Line2D ], list[ str ] ]: + """Created a single subplot with inverted X Y axes. + + From a dataframe, knowing which curves to plot along which variable, + generates a fig and its list of axes with the data plotted. + + Args: + df (pd.DataFrame): dataframe containing at least two columns, + one named "variableName" and the other "curveName" + userChoices (dict[str, Any]): Choices made by widget selection + in PythonViewConfigurator filter. + + Returns: + tuple[figure.Figure, list[axes.Axes], + list[lines.Line2D] , list[str]]: the fig and its list of axes. + """ + curveNames: list[ str ] = userChoices[ "curveNames" ] + variableName: str = userChoices[ "variableName" ] + curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, + float ] ] = userChoices[ "curvesAspect" ] + associatedProperties: dict[ str, list[ str ] ] = associatePropertyToAxeType( curveNames ) + fig, ax = plt.subplots( constrained_layout=True ) + all_ax: list[ axes.Axes ] = setupAllAxes( ax, variableName, associatedProperties, False ) + linesList: list[ lines.Line2D ] = [] + labels: list[ str ] = [] + cpt_cmap: int = 0 + y: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() + for cpt_ax, ( ax_name, propertyNames ) in enumerate( associatedProperties.items() ): + ax_to_use: axes.Axes = setupAxeToUse( all_ax, cpt_ax, ax_name, True ) + for propName in propertyNames: + x: npt.NDArray[ np.float64 ] = df[ propName ].to_numpy() + plotAxe( ax_to_use, x, y, propName, cpt_cmap, curvesAspect ) + cpt_cmap += 1 + new_lines, new_labels = ax_to_use.get_legend_handles_labels() + linesList += new_lines # type: ignore[arg-type] + labels += new_labels + labels, linesList = smartLabelsSorted( labels, linesList, userChoices ) + if userChoices[ "displayLegend" ]: + ax.legend( + linesList, + labels, + loc=userChoices[ "legendPosition" ], + fontsize=userChoices[ "legendSize" ], + ) + ax.grid() + return ( fig, all_ax, linesList, labels ) + + +def multipleSubplots( + df: pd.DataFrame, + userChoices: dict[ str, Any ] ) -> tuple[ figure.Figure, list[ axes.Axes ], list[ lines.Line2D ], list[ str ] ]: + """Created multiple subplots. + + From a dataframe, knowing which curves to plot along which variable, + generates a fig and its list of axes with the data plotted. + + Args: + df (pd.DataFrame): dataframe containing at least two columns, + one named "variableName" and the other "curveName". + userChoices (dict[str, Any]): Choices made by widget selection + in PythonViewConfigurator filter. + + Returns: + tuple[figure.Figure, list[axes.Axes], + list[lines.Line2D] , list[str]]: the fig and its list of axes. + """ + curveNames: list[ str ] = userChoices[ "curveNames" ] + variableName: str = userChoices[ "variableName" ] + curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, + float ] ] = userChoices[ "curvesAspect" ] + ratio: float = userChoices[ "ratio" ] + assosIdentifiers: dict[ str, dict[ str, list[ str ] ] ] = associationIdentifiers( curveNames ) + nbr_suplots: int = len( assosIdentifiers.keys() ) + # if only one subplots needs to be created + if nbr_suplots == 1: + return oneSubplot( df, userChoices ) + + layout: tuple[ int, int, int ] = smartLayout( nbr_suplots, ratio ) + fig, axs0 = plt.subplots( layout[ 0 ], layout[ 1 ], constrained_layout=True ) + axs: list[ axes.Axes ] = axs0.flatten().tolist() # type: ignore[union-attr] + for i in range( layout[ 2 ] ): + fig.delaxes( axs[ -( i + 1 ) ] ) + all_lines: list[ lines.Line2D ] = [] + all_labels: list[ str ] = [] + # first loop for subplots + propertiesExtremas: dict[ str, tuple[ float, float ] ] = ( findExtremasPropertiesForAssociatedIdentifiers( + df, assosIdentifiers, True ) ) + for j, identifier in enumerate( assosIdentifiers.keys() ): + first_ax: axes.Axes = axs[ j ] + associatedProperties: dict[ str, list[ str ] ] = assosIdentifiers[ identifier ] + all_ax: list[ axes.Axes ] = setupAllAxes( first_ax, variableName, associatedProperties, True ) + axs += all_ax[ 1: ] + linesList: list[ lines.Line2D ] = [] + labels: list[ str ] = [] + cpt_cmap: int = 0 + x: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() + # second loop for axes per subplot + for cpt_ax, ( ax_name, propertyNames ) in enumerate( associatedProperties.items() ): + ax_to_use: axes.Axes = setupAxeToUse( all_ax, cpt_ax, ax_name, False ) + for propName in propertyNames: + y: npt.NDArray[ np.float64 ] = df[ propName ].to_numpy() + plotAxe( ax_to_use, x, y, propName, cpt_cmap, curvesAspect ) + ax_to_use.set_ylim( *propertiesExtremas[ ax_name ] ) + cpt_cmap += 1 + new_lines, new_labels = ax_to_use.get_legend_handles_labels() + linesList += new_lines # type: ignore[arg-type] + all_lines += new_lines # type: ignore[arg-type] + labels += new_labels + all_labels += new_labels + labels, linesList = smartLabelsSorted( labels, linesList, userChoices ) + if userChoices[ "displayLegend" ]: + first_ax.legend( + linesList, + labels, + loc=userChoices[ "legendPosition" ], + fontsize=userChoices[ "legendSize" ], + ) + if userChoices[ "displayTitle" ]: + first_ax.set_title( identifier, fontsize=10 ) + first_ax.grid() + return ( fig, axs, all_lines, all_labels ) + + +def multipleSubplotsInverted( + df: pd.DataFrame, + userChoices: dict[ str, Any ] ) -> tuple[ figure.Figure, list[ axes.Axes ], list[ lines.Line2D ], list[ str ] ]: + """Created multiple subplots with inverted X Y axes. + + From a dataframe, knowing which curves to plot along which variable, + generates a fig and its list of axes with the data plotted. + + Args: + df (pd.DataFrame): dataframe containing at least two columns, + one named "variableName" and the other "curveName". + userChoices (dict[str, Any]): Choices made by widget selection + in PythonViewConfigurator filter. + + Returns: + tuple[figure.Figure, list[axes.Axes], + list[lines.Line2D] , list[str]]: the fig and its list of axes. + """ + curveNames: list[ str ] = userChoices[ "curveNames" ] + variableName: str = userChoices[ "variableName" ] + curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, + float ] ] = userChoices[ "curvesAspect" ] + ratio: float = userChoices[ "ratio" ] + assosIdentifiers: dict[ str, dict[ str, list[ str ] ] ] = associationIdentifiers( curveNames ) + nbr_suplots: int = len( assosIdentifiers.keys() ) + # if only one subplots needs to be created + if nbr_suplots == 1: + return oneSubplotInverted( df, userChoices ) + + layout: tuple[ int, int, int ] = smartLayout( nbr_suplots, ratio ) + fig, axs0 = plt.subplots( layout[ 0 ], layout[ 1 ], constrained_layout=True ) + axs: list[ axes.Axes ] = axs0.flatten().tolist() # type: ignore[union-attr] + for i in range( layout[ 2 ] ): + fig.delaxes( axs[ -( i + 1 ) ] ) + all_lines: list[ lines.Line2D ] = [] + all_labels: list[ str ] = [] + # first loop for subplots + propertiesExtremas: dict[ str, tuple[ float, float ] ] = ( findExtremasPropertiesForAssociatedIdentifiers( + df, assosIdentifiers, True ) ) + for j, identifier in enumerate( assosIdentifiers.keys() ): + first_ax: axes.Axes = axs[ j ] + associatedProperties: dict[ str, list[ str ] ] = assosIdentifiers[ identifier ] + all_ax: list[ axes.Axes ] = setupAllAxes( first_ax, variableName, associatedProperties, False ) + axs += all_ax[ 1: ] + linesList: list[ lines.Line2D ] = [] + labels: list[ str ] = [] + cpt_cmap: int = 0 + y: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() + # second loop for axes per subplot + for cpt_ax, ( ax_name, propertyNames ) in enumerate( associatedProperties.items() ): + ax_to_use: axes.Axes = setupAxeToUse( all_ax, cpt_ax, ax_name, True ) + for propName in propertyNames: + x: npt.NDArray[ np.float64 ] = df[ propName ].to_numpy() + plotAxe( ax_to_use, x, y, propName, cpt_cmap, curvesAspect ) + ax_to_use.set_xlim( propertiesExtremas[ ax_name ] ) + cpt_cmap += 1 + new_lines, new_labels = ax_to_use.get_legend_handles_labels() + linesList += new_lines # type: ignore[arg-type] + all_lines += new_lines # type: ignore[arg-type] + labels += new_labels + all_labels += new_labels + labels, linesList = smartLabelsSorted( labels, linesList, userChoices ) + if userChoices[ "displayLegend" ]: + first_ax.legend( + linesList, + labels, + loc=userChoices[ "legendPosition" ], + fontsize=userChoices[ "legendSize" ], + ) + if userChoices[ "displayTitle" ]: + first_ax.set_title( identifier, fontsize=10 ) + first_ax.grid() + return ( fig, axs, all_lines, all_labels ) + + +def setupAllAxes( + first_ax: axes.Axes, + variableName: str, + associatedProperties: dict[ str, list[ str ] ], + axisX: bool, +) -> list[ axes.Axes ]: + """Modify axis name and ticks avec X or Y axis of all subplots. + + Args: + first_ax (axes.Axes): subplot id. + variableName (str): name of the axis. + associatedProperties (dict[str, list[str]]): Name of the properties + axisX (bool): X (True) or Y (False) axis to modify. + + Returns: + list[axes.Axes]: modified subplots + """ + all_ax: list[ axes.Axes ] = [ first_ax ] + if axisX: + first_ax.set_xlabel( variableName ) + first_ax.ticklabel_format( style="sci", axis="x", scilimits=( 0, 0 ), useMathText=True ) + for i in range( 1, len( associatedProperties.keys() ) ): + second_ax = first_ax.twinx() + assert isinstance( second_ax, axes.Axes ) + all_ax.append( second_ax ) + all_ax[ i ].spines[ "right" ].set_position( ( "axes", 1 + 0.07 * ( i - 1 ) ) ) + all_ax[ i ].tick_params( axis="y", which="both", left=False, right=True ) + all_ax[ i ].yaxis.set_ticks_position( "right" ) + all_ax[ i ].yaxis.offsetText.set_position( ( 1.04 + 0.07 * ( i - 1 ), 0 ) ) + first_ax.yaxis.offsetText.set_position( ( -0.04, 0 ) ) + else: + first_ax.set_ylabel( variableName ) + first_ax.ticklabel_format( style="sci", axis="y", scilimits=( 0, 0 ), useMathText=True ) + for i in range( 1, len( associatedProperties.keys() ) ): + second_ax = first_ax.twiny() + assert isinstance( second_ax, axes.Axes ) + all_ax.append( second_ax ) + all_ax[ i ].spines[ "bottom" ].set_position( ( "axes", -0.08 * i ) ) + all_ax[ i ].xaxis.set_label_position( "bottom" ) + all_ax[ i ].tick_params( axis="x", which="both", bottom=True, top=False ) + all_ax[ i ].xaxis.set_ticks_position( "bottom" ) + return all_ax + + +def setupAxeToUse( all_ax: list[ axes.Axes ], axeId: int, ax_name: str, axisX: bool ) -> axes.Axes: + """Modify axis name and ticks avec X or Y axis of subplot axeId in all_ax. + + Args: + all_ax (list[axes.Axes]): list of all subplots + axeId (int): id of the subplot + ax_name (str): name of the X or Y axis + axisX (bool): X (True) or Y (False) axis to modify. + + Returns: + axes.Axes: modified subplot + """ + ax_to_use: axes.Axes = all_ax[ axeId ] + if axisX: + ax_to_use.set_xlabel( ax_name ) + ax_to_use.ticklabel_format( style="sci", axis="x", scilimits=( 0, 0 ), useMathText=True ) + else: + ax_to_use.set_ylabel( ax_name ) + ax_to_use.ticklabel_format( style="sci", axis="y", scilimits=( 0, 0 ), useMathText=True ) + return ax_to_use + + +def plotAxe( + ax_to_use: axes.Axes, + x: npt.NDArray[ np.float64 ], + y: npt.NDArray[ np.float64 ], + propertyName: str, + cpt_cmap: int, + curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, float ] ], +) -> None: + """Plot x, y data using input ax_to_use according to curvesAspect. + + Args: + ax_to_use (axes.Axes): subplot to use + x (npt.NDArray[np.float64]): abscissa data + y (npt.NDArray[np.float64]): ordinate data + propertyName (str): name of the property + cpt_cmap (int): colormap to use + curvesAspect (dict[str, tuple[tuple[float, float, float],str, float, str, float]]): + user choices on curve aspect + """ + cmap = plt.rcParams[ "axes.prop_cycle" ].by_key()[ "color" ][ cpt_cmap % 10 ] + mask = np.logical_and( np.isnan( x ), np.isnan( y ) ) + not_mask = ~mask + # Plot only when x and y values are not nan values + if propertyName in curvesAspect: + asp: tuple[ tuple[ float, float, float ], str, float, str, float ] = curvesAspect[ propertyName ] + ax_to_use.plot( + x[ not_mask ], + y[ not_mask ], + label=propertyName, + color=asp[ 0 ], + linestyle=asp[ 1 ], + linewidth=asp[ 2 ], + marker=asp[ 3 ], + markersize=asp[ 4 ], + ) + else: + ax_to_use.plot( x[ not_mask ], y[ not_mask ], label=propertyName, color=cmap ) + + +def getExtremaAllAxes( axes: list[ axes.Axes ], ) -> tuple[ tuple[ float, float ], tuple[ float, float ] ]: + """Gets the limits of both X and Y axis as a 2x2 element tuple. + + Args: + axes (list[axes.Axes]): list of subplots to get limits. + + Returns: + tuple[tuple[float, float], tuple[float, float]]:: ((xMin, xMax), (yMin, yMax)) + """ + assert len( axes ) > 0 + xMin, xMax, yMin, yMax = getAxeLimits( axes[ 0 ] ) + if len( axes ) > 1: + for i in range( 1, len( axes ) ): + x1, x2, y1, y2 = getAxeLimits( axes[ i ] ) + if x1 < xMin: + xMin = x1 + if x2 > xMax: + xMax = x2 + if y1 < yMin: + yMin = y1 + if y2 > yMax: + yMax = y2 + return ( ( xMin, xMax ), ( yMin, yMax ) ) + + +def getAxeLimits( ax: axes.Axes ) -> tuple[ float, float, float, float ]: + """Gets the limits of both X and Y axis as a 4 element tuple. + + Args: + ax (axes.Axes): subplot to get limits. + + Returns: + tuple[float, float, float, float]: (xMin, xMax, yMin, yMax) + """ + xMin, xMax = ax.get_xlim() + yMin, yMax = ax.get_ylim() + return ( xMin, xMax, yMin, yMax ) + + +def findExtremasPropertiesForAssociatedIdentifiers( + df: pd.DataFrame, + associatedIdentifiers: dict[ str, dict[ str, list[ str ] ] ], + offsetPlotting: bool = False, + offsetPercentage: int = 5, +) -> dict[ str, tuple[ float, float ] ]: + """Find min and max of all properties linked to a same identifier. + + Using an associatedIdentifiers dict containing associatedProperties dict, + we can find the extremas for each property of each identifier. Once we have them all, + we compare for each identifier what are the most extreme values and only the biggest and + lowest are kept in the end. + + + Args: + df (pd.DataFrame): Pandas dataframe + associatedIdentifiers (dict[str, dict[str, list[str]]]): property identifiers. + offsetPlotting (bool, optional): When using the values being returned, + we might want to add an offset to these values. If set to True, + the offsetPercentage is taken into account. Defaults to False. + offsetPercentage (int, optional): Value by which we will offset + the min and max values of each tuple of floats. Defaults to 5. + + Returns: + dict[str, tuple[float, float]]: { + "BHP (Pa)": (minAllWells, maxAllWells), + "TotalMassRate (kg)": (minAllWells, maxAllWells), + "TotalSurfaceVolumetricRate (m3/s)": (minAllWells, maxAllWells), + "SurfaceVolumetricRateCO2 (m3/s)": (minAllWells, maxAllWells), + "SurfaceVolumetricRateWater (m3/s)": (minAllWells, maxAllWells) + } + """ + extremasProperties: dict[ str, tuple[ float, float ] ] = {} + # first we need to find the extrema for each property type per region + propertyTypesExtremas: dict[ str, list[ tuple[ float, float ] ] ] = {} + for associatedProperties in associatedIdentifiers.values(): + extremasPerProperty: dict[ str, + tuple[ float, + float ] ] = ( findExtremasAssociatedProperties( df, associatedProperties ) ) + for propertyType, extremaFound in extremasPerProperty.items(): + if propertyType not in propertyTypesExtremas: + propertyTypesExtremas[ propertyType ] = [ extremaFound ] + else: + propertyTypesExtremas[ propertyType ].append( extremaFound ) + # then, once all extrema have been found for all regions, we need to figure out + # which extrema per property type is the most extreme one + for propertyType in propertyTypesExtremas: + values: list[ tuple[ float, float ] ] = propertyTypesExtremas[ propertyType ] + minValues: list[ float ] = [ values[ i ][ 0 ] for i in range( len( values ) ) ] + maxValues: list[ float ] = [ values[ i ][ 1 ] for i in range( len( values ) ) ] + lowest, highest = ( min( minValues ), max( maxValues ) ) + if offsetPlotting: + offset: float = ( highest - lowest ) / 100 * offsetPercentage + lowest, highest = ( lowest - offset, highest + offset ) + extremasProperties[ propertyType ] = ( lowest, highest ) + return extremasProperties + + +def findExtremasAssociatedProperties( + df: pd.DataFrame, associatedProperties: dict[ str, list[ str ] ] ) -> dict[ str, tuple[ float, float ] ]: + """Find the min and max of properties. + + Using an associatedProperties dict containing property types + as keys and a list of property names as values, + and a pandas dataframe whose column names are composed of those same + property names, you can find the min and max values of each property + type and return it as a tuple. + + Args: + df (pd.DataFrame): Pandas dataframe + associatedProperties (dict[str, list[str]]): { + "Pressure (Pa)": ["Reservoir__Pressure__Pa__Source1"], + "Mass (kg)": ["CO2__Mass__kg__Source1", + "Water__Mass__kg__Source1"] + } + + Returns: + dict[str, tuple[float, float]]: { + "Pressure (Pa)": (minPressure, maxPressure), + "Mass (kg)": (minMass, maxMass) + } + """ + extremasProperties: dict[ str, tuple[ float, float ] ] = {} + for propertyType, propertyNames in associatedProperties.items(): + minValues = np.empty( len( propertyNames ) ) + maxValues = np.empty( len( propertyNames ) ) + for i, propertyName in enumerate( propertyNames ): + values: npt.NDArray[ np.float64 ] = df[ propertyName ].to_numpy() + minValues[ i ] = np.nanmin( values ) + maxValues[ i ] = np.nanmax( values ) + extrema: tuple[ float, float ] = ( + float( np.min( minValues ) ), + float( np.max( maxValues ) ), + ) + extremasProperties[ propertyType ] = extrema + return extremasProperties + + +""" +Utils for treatment of the data +""" + + +def associatePropertyToAxeType( propertyNames: list[ str ] ) -> dict[ str, list[ str ] ]: + """Identify property types. + + From a list of property names, identify if each of this property + corresponds to a certain property type like "Pressure", "Mass", + "Temperature" etc ... and returns a dict where the keys are the property + type and the value the list of property names associated to it. + + Args: + propertyNames (list[str]): ["Reservoir__Pressure__Pa__Source1", + "CO2__Mass__kg__Source1", "Water__Mass__kg__Source1"] + + Returns: + dict[str, list[str]]: { "Pressure (Pa)": ["Reservoir__Pressure__Pa__Source1"], + "Mass (kg)": ["CO2__Mass__kg__Source1", + "Water__Mass__kg__Source1"] } + """ + propertyIds: list[ str ] = fcts.identifyProperties( propertyNames ) + associationTable: dict[ str, str ] = { + "0": "Pressure", + "1": "Pressure", + "2": "Temperature", + "3": "PoreVolume", + "4": "PoreVolume", + "5": "Mass", + "6": "Mass", + "7": "Mass", + "8": "Mass", + "9": "Mass", + "10": "Mass", + "11": "BHP", + "12": "MassRate", + "13": "VolumetricRate", + "14": "VolumetricRate", + "15": "BHP", + "16": "MassRate", + "17": "VolumetricRate", + "18": "VolumetricRate", + "19": "VolumetricRate", + "20": "Volume", + "21": "VolumetricRate", + "22": "Volume", + "23": "Iterations", + "24": "Iterations", + "25": "Stress", + "26": "Displacement", + "27": "Permeability", + "28": "Porosity", + "29": "Ratio", + "30": "Fraction", + "31": "BulkModulus", + "32": "ShearModulus", + "33": "OedometricModulus", + "34": "Points", + "35": "Density", + "36": "Mass", + "37": "Mass", + "38": "Time", + "39": "Time", + } + associatedPropertyToAxeType: dict[ str, list[ str ] ] = {} + noUnitProperties: list[ str ] = [ + "Iterations", + "Porosity", + "Ratio", + "Fraction", + "OedometricModulus", + ] + for i, propId in enumerate( propertyIds ): + idProp: str = propId.split( ":" )[ 0 ] + propNoId: str = propId.split( ":" )[ 1 ] + associatedType: str = associationTable[ idProp ] + if associatedType in noUnitProperties: + axeName: str = associatedType + else: + propIdElts: list[ str ] = propNoId.split( "__" ) + # no unit was found + if len( propIdElts ) <= 2: + axeName = associatedType + # there is a unit + else: + unit: str = propIdElts[ -2 ] + axeName = associatedType + " (" + unit + ")" + if axeName not in associatedPropertyToAxeType: + associatedPropertyToAxeType[ axeName ] = [] + associatedPropertyToAxeType[ axeName ].append( propertyNames[ i ] ) + return associatedPropertyToAxeType + + +def propertiesPerIdentifier( propertyNames: list[ str ] ) -> dict[ str, list[ str ] ]: + """Extract identifiers with associatied properties. + + From a list of property names, extracts the identifier (name of the + region for flow property or name of a well for well property) and creates + a dictionnary with identifiers as keys and the properties containing them + for value in a list. + + Args: + propertyNames (list[str]): property names + Example + + .. code-block:: python + + [ + "WellControls1__BHP__Pa__Source1", + "WellControls1__TotalMassRate__kg/s__Source1", + "WellControls2__BHP__Pa__Source1", + "WellControls2__TotalMassRate__kg/s__Source1" + ] + + Returns: + dict[str, list[str]]: property identifiers + Example + + .. code-block:: python + + { + "WellControls1": [ + "WellControls1__BHP__Pa__Source1", + "WellControls1__TotalMassRate__kg/s__Source1" + ], + "WellControls2": [ + "WellControls2__BHP__Pa__Source1", + "WellControls2__TotalMassRate__kg/s__Source1" + ] + } + """ + propsPerIdentfier: dict[ str, list[ str ] ] = {} + for propertyName in propertyNames: + elements: list[ str ] = propertyName.split( "__" ) + identifier: str = elements[ 0 ] + if identifier not in propsPerIdentfier: + propsPerIdentfier[ identifier ] = [] + propsPerIdentfier[ identifier ].append( propertyName ) + return propsPerIdentfier + + +def associationIdentifiers( propertyNames: list[ str ] ) -> dict[ str, dict[ str, list[ str ] ] ]: + """Extract identifiers with associatied curves. + + From a list of property names, extracts the identifier (name of the + region for flow property or name of a well for well property) and creates + a dictionnary with identifiers as keys and the properties containing them + for value in a list. + + Args: + propertyNames (list[str]): property names + Example + + .. code-block:: python + + [ + "WellControls1__BHP__Pa__Source1", + "WellControls1__TotalMassRate__kg/s__Source1", + "WellControls1__TotalSurfaceVolumetricRate__m3/s__Source1", + "WellControls1__SurfaceVolumetricRateCO2__m3/s__Source1", + "WellControls1__SurfaceVolumetricRateWater__m3/s__Source1", + "WellControls2__BHP__Pa__Source1", + "WellControls2__TotalMassRate__kg/s__Source1", + "WellControls2__TotalSurfaceVolumetricRate__m3/s__Source1", + "WellControls2__SurfaceVolumetricRateCO2__m3/s__Source1", + "WellControls2__SurfaceVolumetricRateWater__m3/s__Source1", + "WellControls3__BHP__Pa__Source1", + "WellControls3__TotalMassRate__tons/day__Source1", + "WellControls3__TotalSurfaceVolumetricRate__bbl/day__Source1", + "WellControls3__SurfaceVolumetricRateCO2__bbl/day__Source1", + "WellControls3__SurfaceVolumetricRateWater__bbl/day__Source1", + "Mean__BHP__Pa__Source1", + "Mean__TotalMassRate__tons/day__Source1", + "Mean__TotalSurfaceVolumetricRate__bbl/day__Source1", + "Mean__SurfaceVolumetricRateCO2__bbl/day__Source1", + "Mean__SurfaceVolumetricRateWater__bbl/day__Source1" + ] + + Returns: + dict[str, dict[str, list[str]]]: property identifiers + Example + + .. code-block:: python + + { + "WellControls1": { + 'BHP (Pa)': [ + 'WellControls1__BHP__Pa__Source1' + ], + 'MassRate (kg/s)': [ + 'WellControls1__TotalMassRate__kg/s__Source1' + ], + 'VolumetricRate (m3/s)': [ + 'WellControls1__TotalSurfaceVolumetricRate__m3/s__Source1', + 'WellControls1__SurfaceVolumetricRateCO2__m3/s__Source1', + 'WellControls1__SurfaceVolumetricRateWater__m3/s__Source1' + ] + }, + "WellControls2": { + 'BHP (Pa)': [ + 'WellControls2__BHP__Pa__Source1' + ], + 'MassRate (kg/s)': [ + 'WellControls2__TotalMassRate__kg/s__Source1' + ], + 'VolumetricRate (m3/s)': [ + 'WellControls2__TotalSurfaceVolumetricRate__m3/s__Source1', + 'WellControls2__SurfaceVolumetricRateCO2__m3/s__Source1', + 'WellControls2__SurfaceVolumetricRateWater__m3/s__Source1' + ] + }, + "WellControls3": { + 'BHP (Pa)': [ + 'WellControls3__BHP__Pa__Source1' + ], + 'MassRate (tons/day)': [ + 'WellControls3__TotalMassRate__tons/day__Source1' + ], + 'VolumetricRate (bbl/day)': [ + 'WellControls3__TotalSurfaceVolumetricRate__bbl/day__Source1', + 'WellControls3__SurfaceVolumetricRateCO2__bbl/day__Source1', + 'WellControls3__SurfaceVolumetricRateWater__bbl/day__Source1' + ] + }, + "Mean": { + 'BHP (Pa)': [ + 'Mean__BHP__Pa__Source1' + ], + 'MassRate (tons/day)': [ + 'Mean__TotalMassRate__tons/day__Source1' + ], + 'VolumetricRate (bbl/day)': [ + 'Mean__TotalSurfaceVolumetricRate__bbl/day__Source1', + 'Mean__SurfaceVolumetricRateCO2__bbl/day__Source1', + 'Mean__SurfaceVolumetricRateWater__bbl/day__Source1' + ] + } + } + """ + propsPerIdentfier: dict[ str, list[ str ] ] = propertiesPerIdentifier( propertyNames ) + assosIdentifier: dict[ str, dict[ str, list[ str ] ] ] = {} + for ident, propNames in propsPerIdentfier.items(): + assosPropsToAxeType: dict[ str, list[ str ] ] = associatePropertyToAxeType( propNames ) + assosIdentifier[ ident ] = assosPropsToAxeType + return assosIdentifier + + +def buildFontTitle( userChoices: dict[ str, Any ] ) -> FontProperties: + """Builds a Fontproperties object according to user choices on title. + + Args: + userChoices (dict[str, Any]): customization parameters. + + Returns: + FontProperties: FontProperties object for the title. + """ + fontTitle: FontProperties = FontProperties() + if "titleStyle" in userChoices: + fontTitle.set_style( userChoices[ "titleStyle" ] ) + if "titleWeight" in userChoices: + fontTitle.set_weight( userChoices[ "titleWeight" ] ) + if "titleSize" in userChoices: + fontTitle.set_size( userChoices[ "titleSize" ] ) + return fontTitle + + +def buildFontVariable( userChoices: dict[ str, Any ] ) -> FontProperties: + """Builds a Fontproperties object according to user choices on variables. + + Args: + userChoices (dict[str, Any]): customization parameters. + + Returns: + FontProperties: FontProperties object for the variable axes. + """ + fontVariable: FontProperties = FontProperties() + if "variableStyle" in userChoices: + fontVariable.set_style( userChoices[ "variableStyle" ] ) + if "variableWeight" in userChoices: + fontVariable.set_weight( userChoices[ "variableWeight" ] ) + if "variableSize" in userChoices: + fontVariable.set_size( userChoices[ "variableSize" ] ) + return fontVariable + + +def buildFontCurves( userChoices: dict[ str, Any ] ) -> FontProperties: + """Builds a Fontproperties object according to user choices on curves. + + Args: + userChoices (dict[str, str]): customization parameters. + + Returns: + FontProperties: FontProperties object for the curves axes. + """ + fontCurves: FontProperties = FontProperties() + if "curvesStyle" in userChoices: + fontCurves.set_style( userChoices[ "curvesStyle" ] ) + if "curvesWeight" in userChoices: + fontCurves.set_weight( userChoices[ "curvesWeight" ] ) + if "curvesSize" in userChoices: + fontCurves.set_size( userChoices[ "curvesSize" ] ) + return fontCurves + + +def customizeLines( userChoices: dict[ str, Any ], labels: list[ str ], + linesList: list[ lines.Line2D ] ) -> list[ lines.Line2D ]: + """Customize lines according to user choices. + + By applying the user choices, we modify or not the list of lines + and return it with the same number of lines in the same order. + + Args: + userChoices (dict[str, Any]): customization parameters. + labels (list[str]): labels of lines. + linesList (list[lines.Line2D]): list of lines object. + + Returns: + list[lines.Line2D]: list of lines object modified. + """ + if "linesModified" in userChoices: + linesModifs: dict[ str, dict[ str, Any ] ] = userChoices[ "linesModified" ] + linesChanged: list[ lines.Line2D ] = [] + for i, label in enumerate( labels ): + if label in linesModifs: + lineChanged: lines.Line2D = applyCustomizationOnLine( linesList[ i ], linesModifs[ label ] ) + linesChanged.append( lineChanged ) + else: + linesChanged.append( linesList[ i ] ) + return linesChanged + else: + return linesList + + +def applyCustomizationOnLine( line: lines.Line2D, parameters: dict[ str, Any ] ) -> lines.Line2D: + """Apply modification methods on a line from parameters. + + Args: + line (lines.Line2D): Matplotlib Line2D + parameters (dict[str, Any]): dictionary of { + "linestyle": one of ["-","--","-.",":"] + "linewidth": positive int + "color": color code + "marker": one of ["",".","o","^","s","*","D","+","x"] + "markersize":positive int + } + + Returns: + lines.Line2D: Line2D object modified. + """ + if "linestyle" in parameters: + line.set_linestyle( parameters[ "linestyle" ] ) + if "linewidth" in parameters: + line.set_linewidth( parameters[ "linewidth" ] ) + if "color" in parameters: + line.set_color( parameters[ "color" ] ) + if "marker" in parameters: + line.set_marker( parameters[ "marker" ] ) + if "markersize" in parameters: + line.set_markersize( parameters[ "markersize" ] ) + return line + + +""" +Layout tools for layering subplots in a figure +""" + + +def isprime( x: int ) -> bool: + """Checks if a number is primer or not. + + Args: + x (int): Positive number to test. + + Returns: + bool: True if prime, False if not. + """ + if x < 0: + print( "Invalid number entry, needs to be positive int" ) + return False + + return all( x % n != 0 for n in range( 2, int( x**0.5 ) + 1 ) ) + + +def findClosestPairIntegers( x: int ) -> tuple[ int, int ]: + """Get the pair of integers that multiply the closest to input value. + + Finds the closest pair of integers that when multiplied together, + gives a number the closest to the input number (always above or equal). + + Args: + x (int): Positive number. + + Returns: + tuple[int, int]: (highest int, lowest int) + """ + if x < 4: + return ( x, 1 ) + while isprime( x ): + x += 1 + N: int = round( math.sqrt( x ) ) + while x > N: + if x % N == 0: + M = x // N + highest = max( M, N ) + lowest = min( M, N ) + return ( highest, lowest ) + else: + N += 1 + return ( x, 1 ) + + +def smartLayout( x: int, ratio: float ) -> tuple[ int, int, int ]: + """Return the best layout according to the number of subplots. + + For multiple subplots, we need to have a layout that can adapt to + the number of subplots automatically. This function figures out the + best layout possible knowing the number of suplots and the figure ratio. + + Args: + x (int): Positive number. + ratio (float): width to height ratio of a figure. + + Returns: + tuple[int]: (nbr_rows, nbr_columns, number of axes to remove) + """ + pair: tuple[ int, int ] = findClosestPairIntegers( x ) + nbrAxesToRemove: int = pair[ 0 ] * pair[ 1 ] - x + if ratio < 1: + return ( pair[ 0 ], pair[ 1 ], nbrAxesToRemove ) + else: + return ( pair[ 1 ], pair[ 0 ], nbrAxesToRemove ) + + +""" +Legend tools +""" + +commonAssociations: dict[ str, str ] = { + "pressuremin": "Pmin", + "pressureMax": "Pmax", + "pressureaverage": "Pavg", + "deltapressuremin": "DPmin", + "deltapressuremax": "DPmax", + "temperaturemin": "Tmin", + "temperaturemax": "Tmax", + "temperatureaverage": "Tavg", + "effectivestressxx": "ESxx", + "effectivestresszz": "ESzz", + "effectivestressratio": "ESratio", + "totaldisplacementx": "TDx", + "totaldisplacementy": "TDy", + "totaldisplacementz": "TDz", + "totalstressXX": "TSxx", + "totalstressZZ": "TSzz", + "stressxx": "Sxx", + "stressyy": "Syy", + "stresszz": "Szz", + "stressxy": "Sxy", + "stressxz": "Sxz", + "stressyz": "Syz", + "poissonratio": "PR", + "porosity": "PORO", + "specificgravity": "SG", + "theoreticalverticalstress": "TVS", + "density": "DNST", + "pressure": "P", + "permeabilityx": "PERMX", + "permeabilityy": "PERMY", + "permeabilityz": "PERMZ", + "oedometric": "OEDO", + "young": "YOUNG", + "shear": "SHEAR", + "bulk": "BULK", + "totaldynamicporevolume": "TDPORV", + "time": "TIME", + "dt": "DT", + "meanbhp": "MBHP", + "meantotalmassrate": "MTMR", + "meantotalvolumetricrate": "MTSVR", + "bhp": "BHP", + "totalmassrate": "TMR", + "cumulatedlineariter": "CLI", + "cumulatednewtoniter": "CNI", + "lineariter": "LI", + "newtoniter": "NI", +} + +phasesAssociations: dict[ str, str ] = { + "dissolvedmass": " IN ", + "immobile": "IMOB ", + "mobile": "MOB ", + "nontrapped": "NTRP ", + "dynamicporevolume": "DPORV ", + "meansurfacevolumetricrate": "MSVR ", + "surfacevolumetricrate": "SVR ", +} + + +def smartLabelsSorted( labels: list[ str ], lines: list[ lines.Line2D ], + userChoices: dict[ str, Any ] ) -> tuple[ list[ str ], list[ lines.Line2D ] ]: + """Shorten all legend labels and sort them. + + To improve readability of the legend for an axe in ParaView, we can apply the + smartLegendLabel functionnality to reduce the size of each label. Plus we sort them + alphabetically and therefore, we also sort the lines the same way. + + Args: + labels (list[str]): Labels to use ax.legend() like + ["Region1__TemperatureAvg__K__job_123456", "Region1__PressureMin__Pa__job_123456"] + lines (list[lines.Line2D]): Lines plotted on axes of matplotlib figure like [line1, line2] + userChoices (dict[str, Any]): Choices made by widget selection + in PythonViewConfigurator filter. + + Returns: + tuple[list[str], list[lines.Line2D]]: Improved labels and sorted labels / lines like + (["Region1 Pmin", "Region1 Tavg"], [line2, line1]) + """ + smartLabels: list[ str ] = [ smartLabel( label, userChoices ) for label in labels ] + # I need the labels to be ordered alphabetically for better readability of the legend + # Therefore, if I sort smartLabels, I need to also sort lines with the same order. + # But this can only be done if there are no duplicates of labels in smartLabels. + # If a duplicate is found, "sorted" will try to sort with line which has no comparison built in + # which will throw an error. + if len( set( smartLabels ) ) == len( smartLabels ): + sortedBothLists = sorted( zip( smartLabels, lines, strict=False ) ) + sortedLabels, sortedLines = zip( *sortedBothLists, strict=False ) + return ( list( sortedLabels ), list( sortedLines ) ) + else: + return ( smartLabels, lines ) + + +def smartLabel( label: str, userChoices: dict[ str, Any ] ) -> str: + """Shorten label according to user choices. + + Labels name can tend to be too long. Therefore, we need to reduce the size of the label. + Depending on the choices made by the user, the identifier and the job name can disappear. + + Args: + label (str): A label to be plotted. + Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out + userChoices (dict[str, Any]): user choices. + + Returns: + str: "phaseName0 in phaseName1" or "Reservoir phaseName0 in phaseName1" + or "phaseName0 in phaseName1 job123456.out" or + "Reservoir phaseName0 in phaseName1 job123456.out" + """ + # first step is to abbreviate the label to reduce its size + smartLabel: str = abbreviateLabel( label ) + # When only one source is used as input, there is no need to precise which one is used + # in the label so the job name is useless. Same when removeJobName option is selected by user. + inputNames: list[ str ] = userChoices[ "inputNames" ] + removeJobName: bool = userChoices[ "removeJobName" ] + if len( inputNames ) > 1 and not removeJobName: + jobName: str = findJobName( label ) + smartLabel += " " + jobName + # When the user chooses to split the plot into subplots to plot by region or well, + # this identifier name will appear as a title of the subplot so no need to use it. + # Same applies when user decides to remove regions. + plotRegions: bool = userChoices[ "plotRegions" ] + removeRegions: bool = userChoices[ "removeRegions" ] + if not plotRegions and not removeRegions: + smartLabel = findIdentifier( label ) + " " + smartLabel + return smartLabel + + +def abbreviateLabel( label: str ) -> str: + """Get the abbreviation of the label according to reservoir nomenclature. + + When using labels to plot, the name can tend to be too long. Therefore, to respect + the logic of reservoir engineering vocabulary, abbreviations for common property names + can be used to shorten the name. The goal is therefore to generate the right abbreviation + for the label input. + + Args: + label (str): A label to be plotted. + Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out + + Returns: + str: "phaseName0 in phaseName1" + """ + for commonAsso in commonAssociations: + if commonAsso in label.lower(): + return commonAssociations[ commonAsso ] + for phaseAsso in phasesAssociations: + if phaseAsso in label.lower(): + phases: list[ str ] = findPhasesLabel( label ) + phase0: str = "" if len( phases ) < 1 else phases[ 0 ] + phase1: str = "" if len( phases ) < 2 else phases[ 1 ] + if phaseAsso == "dissolvedmass": + return phase0 + phasesAssociations[ phaseAsso ] + phase1 + else: + return phasesAssociations[ phaseAsso ] + phase0 + return label + + +def findIdentifier( label: str ) -> str: + """Find identifier inside the label. + + When looking at a label, it may contain or not an identifier at the beginning of it. + An identifier is either a regionName or a wellName. + The goal is to find it and extract it if present. + + Args: + label (str): A label to be plotted. + Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out + + Returns: + str: "Reservoir" + """ + identifier: str = "" + if "__" not in label: + print( "Invalid label, cannot search identifier when no '__' in label." ) + return identifier + subParts: list[ str ] = label.split( "__" ) + if len( subParts ) == 4: + identifier = subParts[ 0 ] + return identifier + + +def findJobName( label: str ) -> str: + """Find the Geos job name at the end of the label. + + When looking at a label, it may contain or not a job name at the end of it. + The goal is to find it and extract it if present. + + Args: + label (str): A label to be plotted. + Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out + + Returns: + str: "job123456.out" + """ + jobName: str = "" + if "__" not in label: + print( "Invalid label, cannot search jobName when no '__' in label." ) + return jobName + subParts: list[ str ] = label.split( "__" ) + if len( subParts ) == 4: + jobName = subParts[ 3 ] + return jobName + + +def findPhasesLabel( label: str ) -> list[ str ]: + """Find phase name inside label. + + When looking at a label, it may contain or not patterns that indicates + the presence of a phase name within it. Therefore, if one of these patterns + is present, one or multiple phase names can be found and be extracted. + + Args: + label (str): A label to be plotted. + Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out + + Returns: + list[str]: [phaseName0, phaseName1] + """ + phases: list[ str ] = [] + lowLabel: str = label.lower() + indexStart: int = 0 + indexEnd: int = 0 + if "__" not in label: + print( "Invalid label, cannot search phases when no '__' in label." ) + return phases + if "dissolvedmass" in lowLabel: + indexStart = lowLabel.index( "dissolvedmass" ) + len( "dissolvedmass" ) + indexEnd = lowLabel.rfind( "__" ) + phasesSubstring: str = lowLabel[ indexStart:indexEnd ] + phases = phasesSubstring.split( "in" ) + phases = [ phase.capitalize() for phase in phases ] + else: + if "dynamicporevolume" in lowLabel: + indexStart = lowLabel.index( "__" ) + 2 + indexEnd = lowLabel.index( "dynamicporevolume" ) + else: + for pattern in [ "nontrapped", "trapped", "immobile", "mobile", "rate" ]: + if pattern in lowLabel: + indexStart = lowLabel.index( pattern ) + len( pattern ) + indexEnd = lowLabel.rfind( "mass" ) + if indexEnd < 0: + indexEnd = indexStart + lowLabel[ indexStart: ].find( "__" ) + break + if indexStart < indexEnd: + phases = [ lowLabel[ indexStart:indexEnd ].capitalize() ] + return phases + + +""" +Under this is the first version of smartLabels without abbreviations. +""" + +# def smartLegendLabelsAndLines( +# labelNames: list[str], lines: list[Any], userChoices: dict[str, Any], regionName="" +# ) -> tuple[list[str], list[Any]]: +# """To improve readability of the legend for an axe in ParaView, we can apply the +# smartLegendLabel functionnality to reduce the size of each label. Plus we sort them +# alphabetically and therefore, we also sort the lines the same way. + +# Args: +# labelNames (list[str]): Labels to use ax.legend() like +# ["Region1__PressureMin__Pa__job_123456", "Region1__Temperature__K__job_123456"] +# lines (list[Any]): Lines plotted on axes of matplotlib figure like [line1, line2] +# userChoices (dict[str, Any]): Choices made by widget selection +# in PythonViewConfigurator filter. +# regionName (str, optional): name of the region. Defaults to "". + +# Returns: +# tuple[list[str], list[Any]]: Improved labels and sorted labels / lines like +# (["Temperature K", "PressureMin Pa"], [line2, line1]) +# """ +# smartLabels: list[str] = [ +# smartLegendLabel(labelName, userChoices, regionName) for labelName in labelNames +# ] +# # I need the labels to be ordered alphabetically for better readability of the legend +# # Therefore, if I sort smartLabels, I need to also sort lines with the same order +# sortedBothLists = sorted(zip(smartLabels, lines) +# sortedLabels, sortedLines = zip(*sortedBothLists) +# return (sortedLabels, sortedLines) + +# def smartLegendLabel(labelName: str, userChoices: dict[str, Any], regionName="") -> str: +# """When plotting legend label, the label format can be improved by removing some +# overwhelming / repetitive prefixe / suffixe and have a shorter label. + +# Args: +# labelName (str): Label to use ax.legend() like +# Region1__PressureMin__Pa__job_123456 +# userChoices (dict[str, Any]): Choices made by widget selection +# in PythonViewConfigurator filter. +# regionName (str, optional): name of the region. Defaults to "". + +# Returns: +# str: Improved label name like PressureMin Pa. +# """ +# smartLabel: str = "" +# # When only one source is used as input, there is no need to precise which one +# # is used in the label. Same when removeJobName option is selected by user. +# inputNames: list[str] = userChoices["inputNames"] +# removeJobName: bool = userChoices["removeJobName"] +# if len(inputNames) <= 1 or removeJobName: +# smartLabel = removeJobNameInLegendLabel(labelName, inputNames) +# # When the user chooses to split the plot into subplots to plot by region, +# # the region name will appear as a title of the subplot so no need to use it. +# # Same applies when user decides to remove regions. +# plotRegions: bool = userChoices["plotRegions"] +# removeRegions: bool = userChoices["removeRegions"] +# if plotRegions or removeRegions: +# smartLabel = removeIdentifierInLegendLabel(smartLabel, regionName) +# smartLabel = smartLabel.replace("__", " ") +# return smartLabel + +# def removeJobNameInLegendLabel(legendLabel: str, inputNames: list[str]) -> str: +# """When plotting legends, the name of the job is by default at the end of +# the label. Therefore, it can increase tremendously the size of the legend +# and we can avoid that by removing the job name from it. + +# Args: +# legendLabel (str): Label to use ax.legend() like +# Region1__PressureMin__Pa__job_123456 +# inputNames (list[str]): names of the sources use to plot. + +# Returns: +# str: Label without the job name like Region1__PressureMin__Pa. +# """ +# for inputName in inputNames: +# pattern: str = "__" + inputName +# if legendLabel.endswith(pattern): +# jobIndex: int = legendLabel.index(pattern) +# return legendLabel[:jobIndex] +# return legendLabel + +# def removeIdentifierInLegendLabel(legendLabel: str, regionName="") -> str: +# """When plotting legends, the name of the region is by default at the +# beginning of the label. Here we remove the region name from the legend label. + +# Args: +# legendLabel (str): Label to use ax.legend() like +# Region1__PressureMin__Pa__job_123456 +# regionName (str): name of the region. Defaults to "". + +# Returns: +# str: Label without the job name like PressureMin__Pa__job_123456 +# """ +# if "__" not in legendLabel: +# return legendLabel +# if regionName == "": +# firstRegionIndex: int = legendLabel.index("__") +# return legendLabel[firstRegionIndex + 2:] +# pattern: str = regionName + "__" +# if legendLabel.startswith(pattern): +# return legendLabel[len(pattern):] +# return legendLabel +""" +Other 2D tools for simplest figures +""" + + +def basicFigure( df: pd.DataFrame, variableName: str, curveName: str ) -> tuple[ figure.Figure, axes.Axes ]: + """Creates a plot. + + Generates a figure and axes objects from matplotlib that plots + one curve along the X axis, with legend and label for X and Y. + + Args: + df (pd.DataFrame): dataframe containing at least two columns, + one named "variableName" and the other "curveName" + variableName (str): Name of the variable column + curveName (str): Name of the column to display along that variable. + + Returns: + tuple[figure.Figure, axes.Axes]: the fig and the ax. + """ + fig, ax = plt.subplots() + x: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() + y: npt.NDArray[ np.float64 ] = df[ curveName ].to_numpy() + ax.plot( x, y, label=curveName ) + ax.set_xlabel( variableName ) + ax.set_ylabel( curveName ) + ax.legend( loc="best" ) + return ( fig, ax ) + + +def invertedBasicFigure( df: pd.DataFrame, variableName: str, curveName: str ) -> tuple[ figure.Figure, axes.Axes ]: + """Creates a plot with inverted XY axis. + + Generates a figure and axes objects from matplotlib that plots + one curve along the Y axis, with legend and label for X and Y. + + Args: + df (pd.DataFrame): dataframe containing at least two columns, + one named "variableName" and the other "curveName" + variableName (str): Name of the variable column + curveName (str): Name of the column to display along that variable. + + Returns: + tuple[figure.Figure, axes.Axes]: the fig and the ax. + """ + fig, ax = plt.subplots() + x: npt.NDArray[ np.float64 ] = df[ curveName ].to_numpy() + y: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() + ax.plot( x, y, label=variableName ) + ax.set_xlabel( curveName ) + ax.set_ylabel( variableName ) + ax.legend( loc="best" ) + return ( fig, ax ) + + +def adjust_subplots( fig: figure.Figure, invertXY: bool ) -> figure.Figure: + """Adjust the size of the subplot in the fig. + + Args: + fig (figure.Figure): Matplotlib figure + invertXY (bool): Choice to either intervert or not the X and Y axes + + Returns: + figure.Figure: Matplotlib figure with adjustements + """ + if invertXY: + fig.subplots_adjust( left=0.05, right=0.98, top=0.9, bottom=0.2 ) + else: + fig.subplots_adjust( left=0.06, right=0.94, top=0.95, bottom=0.08 ) + return fig diff --git a/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py b/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py new file mode 100755 index 00000000..e45e6df4 --- /dev/null +++ b/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Alexandre Benedicto +# type: ignore +# ruff: noqa +try: + import matplotlib.pyplot as plt + from paraview import python_view + + import geos_posp.visu.PVUtils.paraviewTreatments as pvt + from geos_posp.visu.pythonViewUtils.Figure2DGenerator import ( + Figure2DGenerator, ) + + plt.close() + if len( sourceNames ) == 0: # noqa: F821 + raise ValueError( "No source name was found. Please check at least" + " one source in <>" ) + + dataframes = pvt.getDataframesFromMultipleVTKSources( + sourceNames, + variableName # noqa: F821 + ) + dataframe = pvt.mergeDataframes( dataframes, variableName ) # noqa: F821 + obj_figure = Figure2DGenerator( dataframe, userChoices ) # noqa: F821 + fig = obj_figure.getFigure() + + def setup_data( view ) -> None: # noqa + pass + + def render( view, width: int, height: int ): # noqa + fig.set_size_inches( float( width ) / 100.0, float( height ) / 100.0 ) + imageToReturn = python_view.figure_to_image( fig ) + return imageToReturn + +except Exception as e: + from geos.utils.Logger import getLogger + + logger = getLogger( "Python View Configurator" ) + logger.critical( e, exc_info=True ) From 9ff03328fa8232b819a2c5eb820bbc225c494487 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 25 Jul 2025 12:52:04 +0200 Subject: [PATCH 2/9] Fix reference --- geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py b/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py index e45e6df4..891622a6 100755 --- a/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py +++ b/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py @@ -8,7 +8,7 @@ from paraview import python_view import geos_posp.visu.PVUtils.paraviewTreatments as pvt - from geos_posp.visu.pythonViewUtils.Figure2DGenerator import ( + from geos.pv.pythonViewUtils.Figure2DGenerator import ( Figure2DGenerator, ) plt.close() From 424375d56cece5d970a1848554d5623af23a74cf Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 25 Jul 2025 13:03:48 +0200 Subject: [PATCH 3/9] update file import --- .../src/PVplugins/PVPythonViewConfigurator.py | 8 +- .../pv/pythonViewUtils/Figure2DGenerator.py | 137 -- .../src/geos/pv/pythonViewUtils/__init__.py | 0 .../functionsFigure2DGenerator.py | 1375 ----------------- .../geos/pv/pythonViewUtils/mainPythonView.py | 38 - 5 files changed, 4 insertions(+), 1554 deletions(-) delete mode 100755 geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py delete mode 100755 geos-pv/src/geos/pv/pythonViewUtils/__init__.py delete mode 100755 geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py delete mode 100755 geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py diff --git a/geos-pv/src/PVplugins/PVPythonViewConfigurator.py b/geos-pv/src/PVplugins/PVPythonViewConfigurator.py index 87f28b3c..23ac6726 100755 --- a/geos-pv/src/PVplugins/PVPythonViewConfigurator.py +++ b/geos-pv/src/PVplugins/PVPythonViewConfigurator.py @@ -16,12 +16,12 @@ update_paths() -import geos_posp.visu.PVUtils.paraviewTreatments as pvt -from geos_posp.visu.PVUtils.checkboxFunction import ( # type: ignore[attr-defined] +import geos.pv.utils.paraviewTreatments as pvt +from geos.pv.utils.checkboxFunction import ( # type: ignore[attr-defined] createModifiedCallback, ) -from geos_posp.visu.PVUtils.DisplayOrganizationParaview import ( +from geos.pv.utils.DisplayOrganizationParaview import ( DisplayOrganizationParaview, ) -from geos_posp.visu.PVUtils.matplotlibOptions import ( +from geos.pv.pyplotUtils.matplotlibOptions import ( FontStyleEnum, FontWeightEnum, LegendLocationEnum, diff --git a/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py b/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py deleted file mode 100755 index bd6749f7..00000000 --- a/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py +++ /dev/null @@ -1,137 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. -# SPDX-FileContributor: Alexandre Benedicto - -from typing import Any - -import pandas as pd # type: ignore[import-untyped] -from geos.utils.Logger import Logger, getLogger -from matplotlib import axes, figure, lines # type: ignore[import-untyped] -from matplotlib.font_manager import ( # type: ignore[import-untyped] - FontProperties, # type: ignore[import-untyped] -) -from typing_extensions import Self - -import geos.pv.pythonViewUtils.functionsFigure2DGenerator as fcts - - -class Figure2DGenerator: - - def __init__( self: Self, dataframe: pd.DataFrame, userChoices: dict[ str, list[ str ] ] ) -> None: - """Utility to create cross plots using Python View. - - We want to plot f(X) = Y where in this class, - "X" will be called "variable", "Y" will be called "curves". - - Args: - dataframe (pd.DataFrame): data to plot - userChoices (dict[str, list[str]]): user choices. - """ - self.m_dataframe: pd.DataFrame = dataframe - self.m_userChoices: dict[ str, Any ] = userChoices - self.m_fig: figure.Figure - self.m_axes: list[ axes._axes.Axes ] = [] - self.m_lines: list[ lines.Line2D ] = [] - self.m_labels: list[ str ] = [] - self.m_logger: Logger = getLogger( "Python View Configurator" ) - - try: - # apply minus 1 multiplication on certain columns - self.initMinus1Multiplication() - # defines m_fig, m_axes, m_lines and m_lables - self.plotInitialFigure() - # then to edit and customize the figure - self.enhanceFigure() - self.m_logger.info( "Data were successfully plotted." ) - - except Exception as e: - mess: str = "Plot creation failed due to:" - self.m_logger.critical( mess ) - self.m_logger.critical( e, exc_info=True ) - - def initMinus1Multiplication( self: Self ) -> None: - """Multiply by -1 certain columns of the input dataframe.""" - df: pd.DataFrame = self.m_dataframe.copy( deep=True ) - minus1CurveNames: list[ str ] = self.m_userChoices[ "curveConvention" ] - for name in minus1CurveNames: - df[ name ] = df[ name ] * ( -1 ) - self.m_dataframe = df - - def enhanceFigure( self: Self ) -> None: - """Apply all the enhancement features to the initial figure.""" - self.changeTitle() - self.changeMinorticks() - self.changeAxisScale() - self.changeAxisLimits() - - def plotInitialFigure( self: Self ) -> None: - """Generates a figure and axes objects from matplotlib. - - The figure plots all the curves along the X or Y axis, with legend and - label for X and Y. - """ - if self.m_userChoices[ "plotRegions" ]: - if not self.m_userChoices[ "reverseXY" ]: - ( fig, ax_all, lines, labels ) = fcts.multipleSubplots( self.m_dataframe, self.m_userChoices ) - else: - ( fig, ax_all, lines, labels ) = fcts.multipleSubplotsInverted( self.m_dataframe, self.m_userChoices ) - else: - if not self.m_userChoices[ "reverseXY" ]: - ( fig, ax_all, lines, labels ) = fcts.oneSubplot( self.m_dataframe, self.m_userChoices ) - else: - ( fig, ax_all, lines, labels ) = fcts.oneSubplotInverted( self.m_dataframe, self.m_userChoices ) - self.m_fig = fig - self.m_axes = ax_all - self.m_lines = lines - self.m_labels = labels - - def changeTitle( self: Self ) -> None: - """Update title of the first axis of the figure based on user choices.""" - if self.m_userChoices[ "displayTitle" ]: - title: str = self.m_userChoices[ "title" ] - fontTitle: FontProperties = fcts.buildFontTitle( self.m_userChoices ) - self.m_fig.suptitle( title, fontproperties=fontTitle ) - - def changeMinorticks( self: Self ) -> None: - """Set the minorticks on or off for every axes.""" - choice: bool = self.m_userChoices[ "minorticks" ] - if choice: - for ax in self.m_axes: - ax.minorticks_on() - else: - for ax in self.m_axes: - ax.minorticks_off() - - def changeAxisScale( self: Self ) -> None: - """Set the minorticks on or off for every axes.""" - for ax in self.m_axes: - if self.m_userChoices[ "logScaleX" ]: - ax.set_xscale( "log" ) - if self.m_userChoices[ "logScaleY" ]: - ax.set_yscale( "log" ) - - def changeAxisLimits( self: Self ) -> None: - """Update axis limits.""" - if self.m_userChoices[ "customAxisLim" ]: - for ax in self.m_axes: - xmin, xmax = ax.get_xlim() - if self.m_userChoices[ "limMinX" ] is not None: - xmin = self.m_userChoices[ "limMinX" ] - if self.m_userChoices[ "limMaxX" ] is not None: - xmax = self.m_userChoices[ "limMaxX" ] - ax.set_xlim( xmin, xmax ) - - ymin, ymax = ax.get_ylim() - if self.m_userChoices[ "limMinY" ] is not None: - ymin = self.m_userChoices[ "limMinY" ] - if self.m_userChoices[ "limMaxY" ] is not None: - ymax = self.m_userChoices[ "limMaxY" ] - ax.set_ylim( ymin, ymax ) - - def getFigure( self: Self ) -> figure.Figure: - """Acces the m_fig attribute. - - Returns: - figure.Figure: Figure containing all the plots. - """ - return self.m_fig diff --git a/geos-pv/src/geos/pv/pythonViewUtils/__init__.py b/geos-pv/src/geos/pv/pythonViewUtils/__init__.py deleted file mode 100755 index e69de29b..00000000 diff --git a/geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py b/geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py deleted file mode 100755 index 4b629e9b..00000000 --- a/geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py +++ /dev/null @@ -1,1375 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. -# SPDX-FileContributor: Alexandre Benedicto -import math -from typing import Any - -import matplotlib.pyplot as plt # type: ignore[import-untyped] -import numpy as np -import numpy.typing as npt -import pandas as pd # type: ignore[import-untyped] -from matplotlib import axes, figure, lines # type: ignore[import-untyped] -from matplotlib.font_manager import ( # type: ignore[import-untyped] - FontProperties, # type: ignore[import-untyped] -) - -import geos.pv.geosLogReaderUtils.geosLogReaderFunctions as fcts -""" -Plotting tools for 2D figure and axes generation. -""" - - -def oneSubplot( - df: pd.DataFrame, - userChoices: dict[ str, Any ] ) -> tuple[ figure.Figure, list[ axes.Axes ], list[ lines.Line2D ], list[ str ] ]: - """Created a single subplot. - - From a dataframe, knowing which curves to plot along which variable, - generates a fig and its list of axes with the data plotted. - - Args: - df (pd.DataFrame): dataframe containing at least two columns, - one named "variableName" and the other "curveName" - userChoices (dict[str, Any]): Choices made by widget selection - in PythonViewConfigurator filter. - - Returns: - tuple[figure.Figure, list[axes.Axes], - list[lines.Line2D] , list[str]]: the fig and its list of axes. - """ - curveNames: list[ str ] = userChoices[ "curveNames" ] - variableName: str = userChoices[ "variableName" ] - curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, - float ] ] = userChoices[ "curvesAspect" ] - associatedProperties: dict[ str, list[ str ] ] = associatePropertyToAxeType( curveNames ) - fig, ax = plt.subplots( constrained_layout=True ) - all_ax: list[ axes.Axes ] = setupAllAxes( ax, variableName, associatedProperties, True ) - lineList: list[ lines.Line2D ] = [] - labels: list[ str ] = [] - cpt_cmap: int = 0 - x: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() - for cpt_ax, ( ax_name, propertyNames ) in enumerate( associatedProperties.items() ): - ax_to_use: axes.Axes = setupAxeToUse( all_ax, cpt_ax, ax_name, False ) - for propName in propertyNames: - y: npt.NDArray[ np.float64 ] = df[ propName ].to_numpy() - plotAxe( ax_to_use, x, y, propName, cpt_cmap, curvesAspect ) - cpt_cmap += 1 - new_lines, new_labels = ax_to_use.get_legend_handles_labels() - lineList += new_lines # type: ignore[arg-type] - labels += new_labels - labels, lineList = smartLabelsSorted( labels, lineList, userChoices ) - if userChoices[ "displayLegend" ]: - ax.legend( - lineList, - labels, - loc=userChoices[ "legendPosition" ], - fontsize=userChoices[ "legendSize" ], - ) - ax.grid() - return ( fig, all_ax, lineList, labels ) - - -def oneSubplotInverted( - df: pd.DataFrame, - userChoices: dict[ str, Any ] ) -> tuple[ figure.Figure, list[ axes.Axes ], list[ lines.Line2D ], list[ str ] ]: - """Created a single subplot with inverted X Y axes. - - From a dataframe, knowing which curves to plot along which variable, - generates a fig and its list of axes with the data plotted. - - Args: - df (pd.DataFrame): dataframe containing at least two columns, - one named "variableName" and the other "curveName" - userChoices (dict[str, Any]): Choices made by widget selection - in PythonViewConfigurator filter. - - Returns: - tuple[figure.Figure, list[axes.Axes], - list[lines.Line2D] , list[str]]: the fig and its list of axes. - """ - curveNames: list[ str ] = userChoices[ "curveNames" ] - variableName: str = userChoices[ "variableName" ] - curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, - float ] ] = userChoices[ "curvesAspect" ] - associatedProperties: dict[ str, list[ str ] ] = associatePropertyToAxeType( curveNames ) - fig, ax = plt.subplots( constrained_layout=True ) - all_ax: list[ axes.Axes ] = setupAllAxes( ax, variableName, associatedProperties, False ) - linesList: list[ lines.Line2D ] = [] - labels: list[ str ] = [] - cpt_cmap: int = 0 - y: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() - for cpt_ax, ( ax_name, propertyNames ) in enumerate( associatedProperties.items() ): - ax_to_use: axes.Axes = setupAxeToUse( all_ax, cpt_ax, ax_name, True ) - for propName in propertyNames: - x: npt.NDArray[ np.float64 ] = df[ propName ].to_numpy() - plotAxe( ax_to_use, x, y, propName, cpt_cmap, curvesAspect ) - cpt_cmap += 1 - new_lines, new_labels = ax_to_use.get_legend_handles_labels() - linesList += new_lines # type: ignore[arg-type] - labels += new_labels - labels, linesList = smartLabelsSorted( labels, linesList, userChoices ) - if userChoices[ "displayLegend" ]: - ax.legend( - linesList, - labels, - loc=userChoices[ "legendPosition" ], - fontsize=userChoices[ "legendSize" ], - ) - ax.grid() - return ( fig, all_ax, linesList, labels ) - - -def multipleSubplots( - df: pd.DataFrame, - userChoices: dict[ str, Any ] ) -> tuple[ figure.Figure, list[ axes.Axes ], list[ lines.Line2D ], list[ str ] ]: - """Created multiple subplots. - - From a dataframe, knowing which curves to plot along which variable, - generates a fig and its list of axes with the data plotted. - - Args: - df (pd.DataFrame): dataframe containing at least two columns, - one named "variableName" and the other "curveName". - userChoices (dict[str, Any]): Choices made by widget selection - in PythonViewConfigurator filter. - - Returns: - tuple[figure.Figure, list[axes.Axes], - list[lines.Line2D] , list[str]]: the fig and its list of axes. - """ - curveNames: list[ str ] = userChoices[ "curveNames" ] - variableName: str = userChoices[ "variableName" ] - curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, - float ] ] = userChoices[ "curvesAspect" ] - ratio: float = userChoices[ "ratio" ] - assosIdentifiers: dict[ str, dict[ str, list[ str ] ] ] = associationIdentifiers( curveNames ) - nbr_suplots: int = len( assosIdentifiers.keys() ) - # if only one subplots needs to be created - if nbr_suplots == 1: - return oneSubplot( df, userChoices ) - - layout: tuple[ int, int, int ] = smartLayout( nbr_suplots, ratio ) - fig, axs0 = plt.subplots( layout[ 0 ], layout[ 1 ], constrained_layout=True ) - axs: list[ axes.Axes ] = axs0.flatten().tolist() # type: ignore[union-attr] - for i in range( layout[ 2 ] ): - fig.delaxes( axs[ -( i + 1 ) ] ) - all_lines: list[ lines.Line2D ] = [] - all_labels: list[ str ] = [] - # first loop for subplots - propertiesExtremas: dict[ str, tuple[ float, float ] ] = ( findExtremasPropertiesForAssociatedIdentifiers( - df, assosIdentifiers, True ) ) - for j, identifier in enumerate( assosIdentifiers.keys() ): - first_ax: axes.Axes = axs[ j ] - associatedProperties: dict[ str, list[ str ] ] = assosIdentifiers[ identifier ] - all_ax: list[ axes.Axes ] = setupAllAxes( first_ax, variableName, associatedProperties, True ) - axs += all_ax[ 1: ] - linesList: list[ lines.Line2D ] = [] - labels: list[ str ] = [] - cpt_cmap: int = 0 - x: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() - # second loop for axes per subplot - for cpt_ax, ( ax_name, propertyNames ) in enumerate( associatedProperties.items() ): - ax_to_use: axes.Axes = setupAxeToUse( all_ax, cpt_ax, ax_name, False ) - for propName in propertyNames: - y: npt.NDArray[ np.float64 ] = df[ propName ].to_numpy() - plotAxe( ax_to_use, x, y, propName, cpt_cmap, curvesAspect ) - ax_to_use.set_ylim( *propertiesExtremas[ ax_name ] ) - cpt_cmap += 1 - new_lines, new_labels = ax_to_use.get_legend_handles_labels() - linesList += new_lines # type: ignore[arg-type] - all_lines += new_lines # type: ignore[arg-type] - labels += new_labels - all_labels += new_labels - labels, linesList = smartLabelsSorted( labels, linesList, userChoices ) - if userChoices[ "displayLegend" ]: - first_ax.legend( - linesList, - labels, - loc=userChoices[ "legendPosition" ], - fontsize=userChoices[ "legendSize" ], - ) - if userChoices[ "displayTitle" ]: - first_ax.set_title( identifier, fontsize=10 ) - first_ax.grid() - return ( fig, axs, all_lines, all_labels ) - - -def multipleSubplotsInverted( - df: pd.DataFrame, - userChoices: dict[ str, Any ] ) -> tuple[ figure.Figure, list[ axes.Axes ], list[ lines.Line2D ], list[ str ] ]: - """Created multiple subplots with inverted X Y axes. - - From a dataframe, knowing which curves to plot along which variable, - generates a fig and its list of axes with the data plotted. - - Args: - df (pd.DataFrame): dataframe containing at least two columns, - one named "variableName" and the other "curveName". - userChoices (dict[str, Any]): Choices made by widget selection - in PythonViewConfigurator filter. - - Returns: - tuple[figure.Figure, list[axes.Axes], - list[lines.Line2D] , list[str]]: the fig and its list of axes. - """ - curveNames: list[ str ] = userChoices[ "curveNames" ] - variableName: str = userChoices[ "variableName" ] - curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, - float ] ] = userChoices[ "curvesAspect" ] - ratio: float = userChoices[ "ratio" ] - assosIdentifiers: dict[ str, dict[ str, list[ str ] ] ] = associationIdentifiers( curveNames ) - nbr_suplots: int = len( assosIdentifiers.keys() ) - # if only one subplots needs to be created - if nbr_suplots == 1: - return oneSubplotInverted( df, userChoices ) - - layout: tuple[ int, int, int ] = smartLayout( nbr_suplots, ratio ) - fig, axs0 = plt.subplots( layout[ 0 ], layout[ 1 ], constrained_layout=True ) - axs: list[ axes.Axes ] = axs0.flatten().tolist() # type: ignore[union-attr] - for i in range( layout[ 2 ] ): - fig.delaxes( axs[ -( i + 1 ) ] ) - all_lines: list[ lines.Line2D ] = [] - all_labels: list[ str ] = [] - # first loop for subplots - propertiesExtremas: dict[ str, tuple[ float, float ] ] = ( findExtremasPropertiesForAssociatedIdentifiers( - df, assosIdentifiers, True ) ) - for j, identifier in enumerate( assosIdentifiers.keys() ): - first_ax: axes.Axes = axs[ j ] - associatedProperties: dict[ str, list[ str ] ] = assosIdentifiers[ identifier ] - all_ax: list[ axes.Axes ] = setupAllAxes( first_ax, variableName, associatedProperties, False ) - axs += all_ax[ 1: ] - linesList: list[ lines.Line2D ] = [] - labels: list[ str ] = [] - cpt_cmap: int = 0 - y: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() - # second loop for axes per subplot - for cpt_ax, ( ax_name, propertyNames ) in enumerate( associatedProperties.items() ): - ax_to_use: axes.Axes = setupAxeToUse( all_ax, cpt_ax, ax_name, True ) - for propName in propertyNames: - x: npt.NDArray[ np.float64 ] = df[ propName ].to_numpy() - plotAxe( ax_to_use, x, y, propName, cpt_cmap, curvesAspect ) - ax_to_use.set_xlim( propertiesExtremas[ ax_name ] ) - cpt_cmap += 1 - new_lines, new_labels = ax_to_use.get_legend_handles_labels() - linesList += new_lines # type: ignore[arg-type] - all_lines += new_lines # type: ignore[arg-type] - labels += new_labels - all_labels += new_labels - labels, linesList = smartLabelsSorted( labels, linesList, userChoices ) - if userChoices[ "displayLegend" ]: - first_ax.legend( - linesList, - labels, - loc=userChoices[ "legendPosition" ], - fontsize=userChoices[ "legendSize" ], - ) - if userChoices[ "displayTitle" ]: - first_ax.set_title( identifier, fontsize=10 ) - first_ax.grid() - return ( fig, axs, all_lines, all_labels ) - - -def setupAllAxes( - first_ax: axes.Axes, - variableName: str, - associatedProperties: dict[ str, list[ str ] ], - axisX: bool, -) -> list[ axes.Axes ]: - """Modify axis name and ticks avec X or Y axis of all subplots. - - Args: - first_ax (axes.Axes): subplot id. - variableName (str): name of the axis. - associatedProperties (dict[str, list[str]]): Name of the properties - axisX (bool): X (True) or Y (False) axis to modify. - - Returns: - list[axes.Axes]: modified subplots - """ - all_ax: list[ axes.Axes ] = [ first_ax ] - if axisX: - first_ax.set_xlabel( variableName ) - first_ax.ticklabel_format( style="sci", axis="x", scilimits=( 0, 0 ), useMathText=True ) - for i in range( 1, len( associatedProperties.keys() ) ): - second_ax = first_ax.twinx() - assert isinstance( second_ax, axes.Axes ) - all_ax.append( second_ax ) - all_ax[ i ].spines[ "right" ].set_position( ( "axes", 1 + 0.07 * ( i - 1 ) ) ) - all_ax[ i ].tick_params( axis="y", which="both", left=False, right=True ) - all_ax[ i ].yaxis.set_ticks_position( "right" ) - all_ax[ i ].yaxis.offsetText.set_position( ( 1.04 + 0.07 * ( i - 1 ), 0 ) ) - first_ax.yaxis.offsetText.set_position( ( -0.04, 0 ) ) - else: - first_ax.set_ylabel( variableName ) - first_ax.ticklabel_format( style="sci", axis="y", scilimits=( 0, 0 ), useMathText=True ) - for i in range( 1, len( associatedProperties.keys() ) ): - second_ax = first_ax.twiny() - assert isinstance( second_ax, axes.Axes ) - all_ax.append( second_ax ) - all_ax[ i ].spines[ "bottom" ].set_position( ( "axes", -0.08 * i ) ) - all_ax[ i ].xaxis.set_label_position( "bottom" ) - all_ax[ i ].tick_params( axis="x", which="both", bottom=True, top=False ) - all_ax[ i ].xaxis.set_ticks_position( "bottom" ) - return all_ax - - -def setupAxeToUse( all_ax: list[ axes.Axes ], axeId: int, ax_name: str, axisX: bool ) -> axes.Axes: - """Modify axis name and ticks avec X or Y axis of subplot axeId in all_ax. - - Args: - all_ax (list[axes.Axes]): list of all subplots - axeId (int): id of the subplot - ax_name (str): name of the X or Y axis - axisX (bool): X (True) or Y (False) axis to modify. - - Returns: - axes.Axes: modified subplot - """ - ax_to_use: axes.Axes = all_ax[ axeId ] - if axisX: - ax_to_use.set_xlabel( ax_name ) - ax_to_use.ticklabel_format( style="sci", axis="x", scilimits=( 0, 0 ), useMathText=True ) - else: - ax_to_use.set_ylabel( ax_name ) - ax_to_use.ticklabel_format( style="sci", axis="y", scilimits=( 0, 0 ), useMathText=True ) - return ax_to_use - - -def plotAxe( - ax_to_use: axes.Axes, - x: npt.NDArray[ np.float64 ], - y: npt.NDArray[ np.float64 ], - propertyName: str, - cpt_cmap: int, - curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, float ] ], -) -> None: - """Plot x, y data using input ax_to_use according to curvesAspect. - - Args: - ax_to_use (axes.Axes): subplot to use - x (npt.NDArray[np.float64]): abscissa data - y (npt.NDArray[np.float64]): ordinate data - propertyName (str): name of the property - cpt_cmap (int): colormap to use - curvesAspect (dict[str, tuple[tuple[float, float, float],str, float, str, float]]): - user choices on curve aspect - """ - cmap = plt.rcParams[ "axes.prop_cycle" ].by_key()[ "color" ][ cpt_cmap % 10 ] - mask = np.logical_and( np.isnan( x ), np.isnan( y ) ) - not_mask = ~mask - # Plot only when x and y values are not nan values - if propertyName in curvesAspect: - asp: tuple[ tuple[ float, float, float ], str, float, str, float ] = curvesAspect[ propertyName ] - ax_to_use.plot( - x[ not_mask ], - y[ not_mask ], - label=propertyName, - color=asp[ 0 ], - linestyle=asp[ 1 ], - linewidth=asp[ 2 ], - marker=asp[ 3 ], - markersize=asp[ 4 ], - ) - else: - ax_to_use.plot( x[ not_mask ], y[ not_mask ], label=propertyName, color=cmap ) - - -def getExtremaAllAxes( axes: list[ axes.Axes ], ) -> tuple[ tuple[ float, float ], tuple[ float, float ] ]: - """Gets the limits of both X and Y axis as a 2x2 element tuple. - - Args: - axes (list[axes.Axes]): list of subplots to get limits. - - Returns: - tuple[tuple[float, float], tuple[float, float]]:: ((xMin, xMax), (yMin, yMax)) - """ - assert len( axes ) > 0 - xMin, xMax, yMin, yMax = getAxeLimits( axes[ 0 ] ) - if len( axes ) > 1: - for i in range( 1, len( axes ) ): - x1, x2, y1, y2 = getAxeLimits( axes[ i ] ) - if x1 < xMin: - xMin = x1 - if x2 > xMax: - xMax = x2 - if y1 < yMin: - yMin = y1 - if y2 > yMax: - yMax = y2 - return ( ( xMin, xMax ), ( yMin, yMax ) ) - - -def getAxeLimits( ax: axes.Axes ) -> tuple[ float, float, float, float ]: - """Gets the limits of both X and Y axis as a 4 element tuple. - - Args: - ax (axes.Axes): subplot to get limits. - - Returns: - tuple[float, float, float, float]: (xMin, xMax, yMin, yMax) - """ - xMin, xMax = ax.get_xlim() - yMin, yMax = ax.get_ylim() - return ( xMin, xMax, yMin, yMax ) - - -def findExtremasPropertiesForAssociatedIdentifiers( - df: pd.DataFrame, - associatedIdentifiers: dict[ str, dict[ str, list[ str ] ] ], - offsetPlotting: bool = False, - offsetPercentage: int = 5, -) -> dict[ str, tuple[ float, float ] ]: - """Find min and max of all properties linked to a same identifier. - - Using an associatedIdentifiers dict containing associatedProperties dict, - we can find the extremas for each property of each identifier. Once we have them all, - we compare for each identifier what are the most extreme values and only the biggest and - lowest are kept in the end. - - - Args: - df (pd.DataFrame): Pandas dataframe - associatedIdentifiers (dict[str, dict[str, list[str]]]): property identifiers. - offsetPlotting (bool, optional): When using the values being returned, - we might want to add an offset to these values. If set to True, - the offsetPercentage is taken into account. Defaults to False. - offsetPercentage (int, optional): Value by which we will offset - the min and max values of each tuple of floats. Defaults to 5. - - Returns: - dict[str, tuple[float, float]]: { - "BHP (Pa)": (minAllWells, maxAllWells), - "TotalMassRate (kg)": (minAllWells, maxAllWells), - "TotalSurfaceVolumetricRate (m3/s)": (minAllWells, maxAllWells), - "SurfaceVolumetricRateCO2 (m3/s)": (minAllWells, maxAllWells), - "SurfaceVolumetricRateWater (m3/s)": (minAllWells, maxAllWells) - } - """ - extremasProperties: dict[ str, tuple[ float, float ] ] = {} - # first we need to find the extrema for each property type per region - propertyTypesExtremas: dict[ str, list[ tuple[ float, float ] ] ] = {} - for associatedProperties in associatedIdentifiers.values(): - extremasPerProperty: dict[ str, - tuple[ float, - float ] ] = ( findExtremasAssociatedProperties( df, associatedProperties ) ) - for propertyType, extremaFound in extremasPerProperty.items(): - if propertyType not in propertyTypesExtremas: - propertyTypesExtremas[ propertyType ] = [ extremaFound ] - else: - propertyTypesExtremas[ propertyType ].append( extremaFound ) - # then, once all extrema have been found for all regions, we need to figure out - # which extrema per property type is the most extreme one - for propertyType in propertyTypesExtremas: - values: list[ tuple[ float, float ] ] = propertyTypesExtremas[ propertyType ] - minValues: list[ float ] = [ values[ i ][ 0 ] for i in range( len( values ) ) ] - maxValues: list[ float ] = [ values[ i ][ 1 ] for i in range( len( values ) ) ] - lowest, highest = ( min( minValues ), max( maxValues ) ) - if offsetPlotting: - offset: float = ( highest - lowest ) / 100 * offsetPercentage - lowest, highest = ( lowest - offset, highest + offset ) - extremasProperties[ propertyType ] = ( lowest, highest ) - return extremasProperties - - -def findExtremasAssociatedProperties( - df: pd.DataFrame, associatedProperties: dict[ str, list[ str ] ] ) -> dict[ str, tuple[ float, float ] ]: - """Find the min and max of properties. - - Using an associatedProperties dict containing property types - as keys and a list of property names as values, - and a pandas dataframe whose column names are composed of those same - property names, you can find the min and max values of each property - type and return it as a tuple. - - Args: - df (pd.DataFrame): Pandas dataframe - associatedProperties (dict[str, list[str]]): { - "Pressure (Pa)": ["Reservoir__Pressure__Pa__Source1"], - "Mass (kg)": ["CO2__Mass__kg__Source1", - "Water__Mass__kg__Source1"] - } - - Returns: - dict[str, tuple[float, float]]: { - "Pressure (Pa)": (minPressure, maxPressure), - "Mass (kg)": (minMass, maxMass) - } - """ - extremasProperties: dict[ str, tuple[ float, float ] ] = {} - for propertyType, propertyNames in associatedProperties.items(): - minValues = np.empty( len( propertyNames ) ) - maxValues = np.empty( len( propertyNames ) ) - for i, propertyName in enumerate( propertyNames ): - values: npt.NDArray[ np.float64 ] = df[ propertyName ].to_numpy() - minValues[ i ] = np.nanmin( values ) - maxValues[ i ] = np.nanmax( values ) - extrema: tuple[ float, float ] = ( - float( np.min( minValues ) ), - float( np.max( maxValues ) ), - ) - extremasProperties[ propertyType ] = extrema - return extremasProperties - - -""" -Utils for treatment of the data -""" - - -def associatePropertyToAxeType( propertyNames: list[ str ] ) -> dict[ str, list[ str ] ]: - """Identify property types. - - From a list of property names, identify if each of this property - corresponds to a certain property type like "Pressure", "Mass", - "Temperature" etc ... and returns a dict where the keys are the property - type and the value the list of property names associated to it. - - Args: - propertyNames (list[str]): ["Reservoir__Pressure__Pa__Source1", - "CO2__Mass__kg__Source1", "Water__Mass__kg__Source1"] - - Returns: - dict[str, list[str]]: { "Pressure (Pa)": ["Reservoir__Pressure__Pa__Source1"], - "Mass (kg)": ["CO2__Mass__kg__Source1", - "Water__Mass__kg__Source1"] } - """ - propertyIds: list[ str ] = fcts.identifyProperties( propertyNames ) - associationTable: dict[ str, str ] = { - "0": "Pressure", - "1": "Pressure", - "2": "Temperature", - "3": "PoreVolume", - "4": "PoreVolume", - "5": "Mass", - "6": "Mass", - "7": "Mass", - "8": "Mass", - "9": "Mass", - "10": "Mass", - "11": "BHP", - "12": "MassRate", - "13": "VolumetricRate", - "14": "VolumetricRate", - "15": "BHP", - "16": "MassRate", - "17": "VolumetricRate", - "18": "VolumetricRate", - "19": "VolumetricRate", - "20": "Volume", - "21": "VolumetricRate", - "22": "Volume", - "23": "Iterations", - "24": "Iterations", - "25": "Stress", - "26": "Displacement", - "27": "Permeability", - "28": "Porosity", - "29": "Ratio", - "30": "Fraction", - "31": "BulkModulus", - "32": "ShearModulus", - "33": "OedometricModulus", - "34": "Points", - "35": "Density", - "36": "Mass", - "37": "Mass", - "38": "Time", - "39": "Time", - } - associatedPropertyToAxeType: dict[ str, list[ str ] ] = {} - noUnitProperties: list[ str ] = [ - "Iterations", - "Porosity", - "Ratio", - "Fraction", - "OedometricModulus", - ] - for i, propId in enumerate( propertyIds ): - idProp: str = propId.split( ":" )[ 0 ] - propNoId: str = propId.split( ":" )[ 1 ] - associatedType: str = associationTable[ idProp ] - if associatedType in noUnitProperties: - axeName: str = associatedType - else: - propIdElts: list[ str ] = propNoId.split( "__" ) - # no unit was found - if len( propIdElts ) <= 2: - axeName = associatedType - # there is a unit - else: - unit: str = propIdElts[ -2 ] - axeName = associatedType + " (" + unit + ")" - if axeName not in associatedPropertyToAxeType: - associatedPropertyToAxeType[ axeName ] = [] - associatedPropertyToAxeType[ axeName ].append( propertyNames[ i ] ) - return associatedPropertyToAxeType - - -def propertiesPerIdentifier( propertyNames: list[ str ] ) -> dict[ str, list[ str ] ]: - """Extract identifiers with associatied properties. - - From a list of property names, extracts the identifier (name of the - region for flow property or name of a well for well property) and creates - a dictionnary with identifiers as keys and the properties containing them - for value in a list. - - Args: - propertyNames (list[str]): property names - Example - - .. code-block:: python - - [ - "WellControls1__BHP__Pa__Source1", - "WellControls1__TotalMassRate__kg/s__Source1", - "WellControls2__BHP__Pa__Source1", - "WellControls2__TotalMassRate__kg/s__Source1" - ] - - Returns: - dict[str, list[str]]: property identifiers - Example - - .. code-block:: python - - { - "WellControls1": [ - "WellControls1__BHP__Pa__Source1", - "WellControls1__TotalMassRate__kg/s__Source1" - ], - "WellControls2": [ - "WellControls2__BHP__Pa__Source1", - "WellControls2__TotalMassRate__kg/s__Source1" - ] - } - """ - propsPerIdentfier: dict[ str, list[ str ] ] = {} - for propertyName in propertyNames: - elements: list[ str ] = propertyName.split( "__" ) - identifier: str = elements[ 0 ] - if identifier not in propsPerIdentfier: - propsPerIdentfier[ identifier ] = [] - propsPerIdentfier[ identifier ].append( propertyName ) - return propsPerIdentfier - - -def associationIdentifiers( propertyNames: list[ str ] ) -> dict[ str, dict[ str, list[ str ] ] ]: - """Extract identifiers with associatied curves. - - From a list of property names, extracts the identifier (name of the - region for flow property or name of a well for well property) and creates - a dictionnary with identifiers as keys and the properties containing them - for value in a list. - - Args: - propertyNames (list[str]): property names - Example - - .. code-block:: python - - [ - "WellControls1__BHP__Pa__Source1", - "WellControls1__TotalMassRate__kg/s__Source1", - "WellControls1__TotalSurfaceVolumetricRate__m3/s__Source1", - "WellControls1__SurfaceVolumetricRateCO2__m3/s__Source1", - "WellControls1__SurfaceVolumetricRateWater__m3/s__Source1", - "WellControls2__BHP__Pa__Source1", - "WellControls2__TotalMassRate__kg/s__Source1", - "WellControls2__TotalSurfaceVolumetricRate__m3/s__Source1", - "WellControls2__SurfaceVolumetricRateCO2__m3/s__Source1", - "WellControls2__SurfaceVolumetricRateWater__m3/s__Source1", - "WellControls3__BHP__Pa__Source1", - "WellControls3__TotalMassRate__tons/day__Source1", - "WellControls3__TotalSurfaceVolumetricRate__bbl/day__Source1", - "WellControls3__SurfaceVolumetricRateCO2__bbl/day__Source1", - "WellControls3__SurfaceVolumetricRateWater__bbl/day__Source1", - "Mean__BHP__Pa__Source1", - "Mean__TotalMassRate__tons/day__Source1", - "Mean__TotalSurfaceVolumetricRate__bbl/day__Source1", - "Mean__SurfaceVolumetricRateCO2__bbl/day__Source1", - "Mean__SurfaceVolumetricRateWater__bbl/day__Source1" - ] - - Returns: - dict[str, dict[str, list[str]]]: property identifiers - Example - - .. code-block:: python - - { - "WellControls1": { - 'BHP (Pa)': [ - 'WellControls1__BHP__Pa__Source1' - ], - 'MassRate (kg/s)': [ - 'WellControls1__TotalMassRate__kg/s__Source1' - ], - 'VolumetricRate (m3/s)': [ - 'WellControls1__TotalSurfaceVolumetricRate__m3/s__Source1', - 'WellControls1__SurfaceVolumetricRateCO2__m3/s__Source1', - 'WellControls1__SurfaceVolumetricRateWater__m3/s__Source1' - ] - }, - "WellControls2": { - 'BHP (Pa)': [ - 'WellControls2__BHP__Pa__Source1' - ], - 'MassRate (kg/s)': [ - 'WellControls2__TotalMassRate__kg/s__Source1' - ], - 'VolumetricRate (m3/s)': [ - 'WellControls2__TotalSurfaceVolumetricRate__m3/s__Source1', - 'WellControls2__SurfaceVolumetricRateCO2__m3/s__Source1', - 'WellControls2__SurfaceVolumetricRateWater__m3/s__Source1' - ] - }, - "WellControls3": { - 'BHP (Pa)': [ - 'WellControls3__BHP__Pa__Source1' - ], - 'MassRate (tons/day)': [ - 'WellControls3__TotalMassRate__tons/day__Source1' - ], - 'VolumetricRate (bbl/day)': [ - 'WellControls3__TotalSurfaceVolumetricRate__bbl/day__Source1', - 'WellControls3__SurfaceVolumetricRateCO2__bbl/day__Source1', - 'WellControls3__SurfaceVolumetricRateWater__bbl/day__Source1' - ] - }, - "Mean": { - 'BHP (Pa)': [ - 'Mean__BHP__Pa__Source1' - ], - 'MassRate (tons/day)': [ - 'Mean__TotalMassRate__tons/day__Source1' - ], - 'VolumetricRate (bbl/day)': [ - 'Mean__TotalSurfaceVolumetricRate__bbl/day__Source1', - 'Mean__SurfaceVolumetricRateCO2__bbl/day__Source1', - 'Mean__SurfaceVolumetricRateWater__bbl/day__Source1' - ] - } - } - """ - propsPerIdentfier: dict[ str, list[ str ] ] = propertiesPerIdentifier( propertyNames ) - assosIdentifier: dict[ str, dict[ str, list[ str ] ] ] = {} - for ident, propNames in propsPerIdentfier.items(): - assosPropsToAxeType: dict[ str, list[ str ] ] = associatePropertyToAxeType( propNames ) - assosIdentifier[ ident ] = assosPropsToAxeType - return assosIdentifier - - -def buildFontTitle( userChoices: dict[ str, Any ] ) -> FontProperties: - """Builds a Fontproperties object according to user choices on title. - - Args: - userChoices (dict[str, Any]): customization parameters. - - Returns: - FontProperties: FontProperties object for the title. - """ - fontTitle: FontProperties = FontProperties() - if "titleStyle" in userChoices: - fontTitle.set_style( userChoices[ "titleStyle" ] ) - if "titleWeight" in userChoices: - fontTitle.set_weight( userChoices[ "titleWeight" ] ) - if "titleSize" in userChoices: - fontTitle.set_size( userChoices[ "titleSize" ] ) - return fontTitle - - -def buildFontVariable( userChoices: dict[ str, Any ] ) -> FontProperties: - """Builds a Fontproperties object according to user choices on variables. - - Args: - userChoices (dict[str, Any]): customization parameters. - - Returns: - FontProperties: FontProperties object for the variable axes. - """ - fontVariable: FontProperties = FontProperties() - if "variableStyle" in userChoices: - fontVariable.set_style( userChoices[ "variableStyle" ] ) - if "variableWeight" in userChoices: - fontVariable.set_weight( userChoices[ "variableWeight" ] ) - if "variableSize" in userChoices: - fontVariable.set_size( userChoices[ "variableSize" ] ) - return fontVariable - - -def buildFontCurves( userChoices: dict[ str, Any ] ) -> FontProperties: - """Builds a Fontproperties object according to user choices on curves. - - Args: - userChoices (dict[str, str]): customization parameters. - - Returns: - FontProperties: FontProperties object for the curves axes. - """ - fontCurves: FontProperties = FontProperties() - if "curvesStyle" in userChoices: - fontCurves.set_style( userChoices[ "curvesStyle" ] ) - if "curvesWeight" in userChoices: - fontCurves.set_weight( userChoices[ "curvesWeight" ] ) - if "curvesSize" in userChoices: - fontCurves.set_size( userChoices[ "curvesSize" ] ) - return fontCurves - - -def customizeLines( userChoices: dict[ str, Any ], labels: list[ str ], - linesList: list[ lines.Line2D ] ) -> list[ lines.Line2D ]: - """Customize lines according to user choices. - - By applying the user choices, we modify or not the list of lines - and return it with the same number of lines in the same order. - - Args: - userChoices (dict[str, Any]): customization parameters. - labels (list[str]): labels of lines. - linesList (list[lines.Line2D]): list of lines object. - - Returns: - list[lines.Line2D]: list of lines object modified. - """ - if "linesModified" in userChoices: - linesModifs: dict[ str, dict[ str, Any ] ] = userChoices[ "linesModified" ] - linesChanged: list[ lines.Line2D ] = [] - for i, label in enumerate( labels ): - if label in linesModifs: - lineChanged: lines.Line2D = applyCustomizationOnLine( linesList[ i ], linesModifs[ label ] ) - linesChanged.append( lineChanged ) - else: - linesChanged.append( linesList[ i ] ) - return linesChanged - else: - return linesList - - -def applyCustomizationOnLine( line: lines.Line2D, parameters: dict[ str, Any ] ) -> lines.Line2D: - """Apply modification methods on a line from parameters. - - Args: - line (lines.Line2D): Matplotlib Line2D - parameters (dict[str, Any]): dictionary of { - "linestyle": one of ["-","--","-.",":"] - "linewidth": positive int - "color": color code - "marker": one of ["",".","o","^","s","*","D","+","x"] - "markersize":positive int - } - - Returns: - lines.Line2D: Line2D object modified. - """ - if "linestyle" in parameters: - line.set_linestyle( parameters[ "linestyle" ] ) - if "linewidth" in parameters: - line.set_linewidth( parameters[ "linewidth" ] ) - if "color" in parameters: - line.set_color( parameters[ "color" ] ) - if "marker" in parameters: - line.set_marker( parameters[ "marker" ] ) - if "markersize" in parameters: - line.set_markersize( parameters[ "markersize" ] ) - return line - - -""" -Layout tools for layering subplots in a figure -""" - - -def isprime( x: int ) -> bool: - """Checks if a number is primer or not. - - Args: - x (int): Positive number to test. - - Returns: - bool: True if prime, False if not. - """ - if x < 0: - print( "Invalid number entry, needs to be positive int" ) - return False - - return all( x % n != 0 for n in range( 2, int( x**0.5 ) + 1 ) ) - - -def findClosestPairIntegers( x: int ) -> tuple[ int, int ]: - """Get the pair of integers that multiply the closest to input value. - - Finds the closest pair of integers that when multiplied together, - gives a number the closest to the input number (always above or equal). - - Args: - x (int): Positive number. - - Returns: - tuple[int, int]: (highest int, lowest int) - """ - if x < 4: - return ( x, 1 ) - while isprime( x ): - x += 1 - N: int = round( math.sqrt( x ) ) - while x > N: - if x % N == 0: - M = x // N - highest = max( M, N ) - lowest = min( M, N ) - return ( highest, lowest ) - else: - N += 1 - return ( x, 1 ) - - -def smartLayout( x: int, ratio: float ) -> tuple[ int, int, int ]: - """Return the best layout according to the number of subplots. - - For multiple subplots, we need to have a layout that can adapt to - the number of subplots automatically. This function figures out the - best layout possible knowing the number of suplots and the figure ratio. - - Args: - x (int): Positive number. - ratio (float): width to height ratio of a figure. - - Returns: - tuple[int]: (nbr_rows, nbr_columns, number of axes to remove) - """ - pair: tuple[ int, int ] = findClosestPairIntegers( x ) - nbrAxesToRemove: int = pair[ 0 ] * pair[ 1 ] - x - if ratio < 1: - return ( pair[ 0 ], pair[ 1 ], nbrAxesToRemove ) - else: - return ( pair[ 1 ], pair[ 0 ], nbrAxesToRemove ) - - -""" -Legend tools -""" - -commonAssociations: dict[ str, str ] = { - "pressuremin": "Pmin", - "pressureMax": "Pmax", - "pressureaverage": "Pavg", - "deltapressuremin": "DPmin", - "deltapressuremax": "DPmax", - "temperaturemin": "Tmin", - "temperaturemax": "Tmax", - "temperatureaverage": "Tavg", - "effectivestressxx": "ESxx", - "effectivestresszz": "ESzz", - "effectivestressratio": "ESratio", - "totaldisplacementx": "TDx", - "totaldisplacementy": "TDy", - "totaldisplacementz": "TDz", - "totalstressXX": "TSxx", - "totalstressZZ": "TSzz", - "stressxx": "Sxx", - "stressyy": "Syy", - "stresszz": "Szz", - "stressxy": "Sxy", - "stressxz": "Sxz", - "stressyz": "Syz", - "poissonratio": "PR", - "porosity": "PORO", - "specificgravity": "SG", - "theoreticalverticalstress": "TVS", - "density": "DNST", - "pressure": "P", - "permeabilityx": "PERMX", - "permeabilityy": "PERMY", - "permeabilityz": "PERMZ", - "oedometric": "OEDO", - "young": "YOUNG", - "shear": "SHEAR", - "bulk": "BULK", - "totaldynamicporevolume": "TDPORV", - "time": "TIME", - "dt": "DT", - "meanbhp": "MBHP", - "meantotalmassrate": "MTMR", - "meantotalvolumetricrate": "MTSVR", - "bhp": "BHP", - "totalmassrate": "TMR", - "cumulatedlineariter": "CLI", - "cumulatednewtoniter": "CNI", - "lineariter": "LI", - "newtoniter": "NI", -} - -phasesAssociations: dict[ str, str ] = { - "dissolvedmass": " IN ", - "immobile": "IMOB ", - "mobile": "MOB ", - "nontrapped": "NTRP ", - "dynamicporevolume": "DPORV ", - "meansurfacevolumetricrate": "MSVR ", - "surfacevolumetricrate": "SVR ", -} - - -def smartLabelsSorted( labels: list[ str ], lines: list[ lines.Line2D ], - userChoices: dict[ str, Any ] ) -> tuple[ list[ str ], list[ lines.Line2D ] ]: - """Shorten all legend labels and sort them. - - To improve readability of the legend for an axe in ParaView, we can apply the - smartLegendLabel functionnality to reduce the size of each label. Plus we sort them - alphabetically and therefore, we also sort the lines the same way. - - Args: - labels (list[str]): Labels to use ax.legend() like - ["Region1__TemperatureAvg__K__job_123456", "Region1__PressureMin__Pa__job_123456"] - lines (list[lines.Line2D]): Lines plotted on axes of matplotlib figure like [line1, line2] - userChoices (dict[str, Any]): Choices made by widget selection - in PythonViewConfigurator filter. - - Returns: - tuple[list[str], list[lines.Line2D]]: Improved labels and sorted labels / lines like - (["Region1 Pmin", "Region1 Tavg"], [line2, line1]) - """ - smartLabels: list[ str ] = [ smartLabel( label, userChoices ) for label in labels ] - # I need the labels to be ordered alphabetically for better readability of the legend - # Therefore, if I sort smartLabels, I need to also sort lines with the same order. - # But this can only be done if there are no duplicates of labels in smartLabels. - # If a duplicate is found, "sorted" will try to sort with line which has no comparison built in - # which will throw an error. - if len( set( smartLabels ) ) == len( smartLabels ): - sortedBothLists = sorted( zip( smartLabels, lines, strict=False ) ) - sortedLabels, sortedLines = zip( *sortedBothLists, strict=False ) - return ( list( sortedLabels ), list( sortedLines ) ) - else: - return ( smartLabels, lines ) - - -def smartLabel( label: str, userChoices: dict[ str, Any ] ) -> str: - """Shorten label according to user choices. - - Labels name can tend to be too long. Therefore, we need to reduce the size of the label. - Depending on the choices made by the user, the identifier and the job name can disappear. - - Args: - label (str): A label to be plotted. - Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out - userChoices (dict[str, Any]): user choices. - - Returns: - str: "phaseName0 in phaseName1" or "Reservoir phaseName0 in phaseName1" - or "phaseName0 in phaseName1 job123456.out" or - "Reservoir phaseName0 in phaseName1 job123456.out" - """ - # first step is to abbreviate the label to reduce its size - smartLabel: str = abbreviateLabel( label ) - # When only one source is used as input, there is no need to precise which one is used - # in the label so the job name is useless. Same when removeJobName option is selected by user. - inputNames: list[ str ] = userChoices[ "inputNames" ] - removeJobName: bool = userChoices[ "removeJobName" ] - if len( inputNames ) > 1 and not removeJobName: - jobName: str = findJobName( label ) - smartLabel += " " + jobName - # When the user chooses to split the plot into subplots to plot by region or well, - # this identifier name will appear as a title of the subplot so no need to use it. - # Same applies when user decides to remove regions. - plotRegions: bool = userChoices[ "plotRegions" ] - removeRegions: bool = userChoices[ "removeRegions" ] - if not plotRegions and not removeRegions: - smartLabel = findIdentifier( label ) + " " + smartLabel - return smartLabel - - -def abbreviateLabel( label: str ) -> str: - """Get the abbreviation of the label according to reservoir nomenclature. - - When using labels to plot, the name can tend to be too long. Therefore, to respect - the logic of reservoir engineering vocabulary, abbreviations for common property names - can be used to shorten the name. The goal is therefore to generate the right abbreviation - for the label input. - - Args: - label (str): A label to be plotted. - Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out - - Returns: - str: "phaseName0 in phaseName1" - """ - for commonAsso in commonAssociations: - if commonAsso in label.lower(): - return commonAssociations[ commonAsso ] - for phaseAsso in phasesAssociations: - if phaseAsso in label.lower(): - phases: list[ str ] = findPhasesLabel( label ) - phase0: str = "" if len( phases ) < 1 else phases[ 0 ] - phase1: str = "" if len( phases ) < 2 else phases[ 1 ] - if phaseAsso == "dissolvedmass": - return phase0 + phasesAssociations[ phaseAsso ] + phase1 - else: - return phasesAssociations[ phaseAsso ] + phase0 - return label - - -def findIdentifier( label: str ) -> str: - """Find identifier inside the label. - - When looking at a label, it may contain or not an identifier at the beginning of it. - An identifier is either a regionName or a wellName. - The goal is to find it and extract it if present. - - Args: - label (str): A label to be plotted. - Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out - - Returns: - str: "Reservoir" - """ - identifier: str = "" - if "__" not in label: - print( "Invalid label, cannot search identifier when no '__' in label." ) - return identifier - subParts: list[ str ] = label.split( "__" ) - if len( subParts ) == 4: - identifier = subParts[ 0 ] - return identifier - - -def findJobName( label: str ) -> str: - """Find the Geos job name at the end of the label. - - When looking at a label, it may contain or not a job name at the end of it. - The goal is to find it and extract it if present. - - Args: - label (str): A label to be plotted. - Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out - - Returns: - str: "job123456.out" - """ - jobName: str = "" - if "__" not in label: - print( "Invalid label, cannot search jobName when no '__' in label." ) - return jobName - subParts: list[ str ] = label.split( "__" ) - if len( subParts ) == 4: - jobName = subParts[ 3 ] - return jobName - - -def findPhasesLabel( label: str ) -> list[ str ]: - """Find phase name inside label. - - When looking at a label, it may contain or not patterns that indicates - the presence of a phase name within it. Therefore, if one of these patterns - is present, one or multiple phase names can be found and be extracted. - - Args: - label (str): A label to be plotted. - Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out - - Returns: - list[str]: [phaseName0, phaseName1] - """ - phases: list[ str ] = [] - lowLabel: str = label.lower() - indexStart: int = 0 - indexEnd: int = 0 - if "__" not in label: - print( "Invalid label, cannot search phases when no '__' in label." ) - return phases - if "dissolvedmass" in lowLabel: - indexStart = lowLabel.index( "dissolvedmass" ) + len( "dissolvedmass" ) - indexEnd = lowLabel.rfind( "__" ) - phasesSubstring: str = lowLabel[ indexStart:indexEnd ] - phases = phasesSubstring.split( "in" ) - phases = [ phase.capitalize() for phase in phases ] - else: - if "dynamicporevolume" in lowLabel: - indexStart = lowLabel.index( "__" ) + 2 - indexEnd = lowLabel.index( "dynamicporevolume" ) - else: - for pattern in [ "nontrapped", "trapped", "immobile", "mobile", "rate" ]: - if pattern in lowLabel: - indexStart = lowLabel.index( pattern ) + len( pattern ) - indexEnd = lowLabel.rfind( "mass" ) - if indexEnd < 0: - indexEnd = indexStart + lowLabel[ indexStart: ].find( "__" ) - break - if indexStart < indexEnd: - phases = [ lowLabel[ indexStart:indexEnd ].capitalize() ] - return phases - - -""" -Under this is the first version of smartLabels without abbreviations. -""" - -# def smartLegendLabelsAndLines( -# labelNames: list[str], lines: list[Any], userChoices: dict[str, Any], regionName="" -# ) -> tuple[list[str], list[Any]]: -# """To improve readability of the legend for an axe in ParaView, we can apply the -# smartLegendLabel functionnality to reduce the size of each label. Plus we sort them -# alphabetically and therefore, we also sort the lines the same way. - -# Args: -# labelNames (list[str]): Labels to use ax.legend() like -# ["Region1__PressureMin__Pa__job_123456", "Region1__Temperature__K__job_123456"] -# lines (list[Any]): Lines plotted on axes of matplotlib figure like [line1, line2] -# userChoices (dict[str, Any]): Choices made by widget selection -# in PythonViewConfigurator filter. -# regionName (str, optional): name of the region. Defaults to "". - -# Returns: -# tuple[list[str], list[Any]]: Improved labels and sorted labels / lines like -# (["Temperature K", "PressureMin Pa"], [line2, line1]) -# """ -# smartLabels: list[str] = [ -# smartLegendLabel(labelName, userChoices, regionName) for labelName in labelNames -# ] -# # I need the labels to be ordered alphabetically for better readability of the legend -# # Therefore, if I sort smartLabels, I need to also sort lines with the same order -# sortedBothLists = sorted(zip(smartLabels, lines) -# sortedLabels, sortedLines = zip(*sortedBothLists) -# return (sortedLabels, sortedLines) - -# def smartLegendLabel(labelName: str, userChoices: dict[str, Any], regionName="") -> str: -# """When plotting legend label, the label format can be improved by removing some -# overwhelming / repetitive prefixe / suffixe and have a shorter label. - -# Args: -# labelName (str): Label to use ax.legend() like -# Region1__PressureMin__Pa__job_123456 -# userChoices (dict[str, Any]): Choices made by widget selection -# in PythonViewConfigurator filter. -# regionName (str, optional): name of the region. Defaults to "". - -# Returns: -# str: Improved label name like PressureMin Pa. -# """ -# smartLabel: str = "" -# # When only one source is used as input, there is no need to precise which one -# # is used in the label. Same when removeJobName option is selected by user. -# inputNames: list[str] = userChoices["inputNames"] -# removeJobName: bool = userChoices["removeJobName"] -# if len(inputNames) <= 1 or removeJobName: -# smartLabel = removeJobNameInLegendLabel(labelName, inputNames) -# # When the user chooses to split the plot into subplots to plot by region, -# # the region name will appear as a title of the subplot so no need to use it. -# # Same applies when user decides to remove regions. -# plotRegions: bool = userChoices["plotRegions"] -# removeRegions: bool = userChoices["removeRegions"] -# if plotRegions or removeRegions: -# smartLabel = removeIdentifierInLegendLabel(smartLabel, regionName) -# smartLabel = smartLabel.replace("__", " ") -# return smartLabel - -# def removeJobNameInLegendLabel(legendLabel: str, inputNames: list[str]) -> str: -# """When plotting legends, the name of the job is by default at the end of -# the label. Therefore, it can increase tremendously the size of the legend -# and we can avoid that by removing the job name from it. - -# Args: -# legendLabel (str): Label to use ax.legend() like -# Region1__PressureMin__Pa__job_123456 -# inputNames (list[str]): names of the sources use to plot. - -# Returns: -# str: Label without the job name like Region1__PressureMin__Pa. -# """ -# for inputName in inputNames: -# pattern: str = "__" + inputName -# if legendLabel.endswith(pattern): -# jobIndex: int = legendLabel.index(pattern) -# return legendLabel[:jobIndex] -# return legendLabel - -# def removeIdentifierInLegendLabel(legendLabel: str, regionName="") -> str: -# """When plotting legends, the name of the region is by default at the -# beginning of the label. Here we remove the region name from the legend label. - -# Args: -# legendLabel (str): Label to use ax.legend() like -# Region1__PressureMin__Pa__job_123456 -# regionName (str): name of the region. Defaults to "". - -# Returns: -# str: Label without the job name like PressureMin__Pa__job_123456 -# """ -# if "__" not in legendLabel: -# return legendLabel -# if regionName == "": -# firstRegionIndex: int = legendLabel.index("__") -# return legendLabel[firstRegionIndex + 2:] -# pattern: str = regionName + "__" -# if legendLabel.startswith(pattern): -# return legendLabel[len(pattern):] -# return legendLabel -""" -Other 2D tools for simplest figures -""" - - -def basicFigure( df: pd.DataFrame, variableName: str, curveName: str ) -> tuple[ figure.Figure, axes.Axes ]: - """Creates a plot. - - Generates a figure and axes objects from matplotlib that plots - one curve along the X axis, with legend and label for X and Y. - - Args: - df (pd.DataFrame): dataframe containing at least two columns, - one named "variableName" and the other "curveName" - variableName (str): Name of the variable column - curveName (str): Name of the column to display along that variable. - - Returns: - tuple[figure.Figure, axes.Axes]: the fig and the ax. - """ - fig, ax = plt.subplots() - x: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() - y: npt.NDArray[ np.float64 ] = df[ curveName ].to_numpy() - ax.plot( x, y, label=curveName ) - ax.set_xlabel( variableName ) - ax.set_ylabel( curveName ) - ax.legend( loc="best" ) - return ( fig, ax ) - - -def invertedBasicFigure( df: pd.DataFrame, variableName: str, curveName: str ) -> tuple[ figure.Figure, axes.Axes ]: - """Creates a plot with inverted XY axis. - - Generates a figure and axes objects from matplotlib that plots - one curve along the Y axis, with legend and label for X and Y. - - Args: - df (pd.DataFrame): dataframe containing at least two columns, - one named "variableName" and the other "curveName" - variableName (str): Name of the variable column - curveName (str): Name of the column to display along that variable. - - Returns: - tuple[figure.Figure, axes.Axes]: the fig and the ax. - """ - fig, ax = plt.subplots() - x: npt.NDArray[ np.float64 ] = df[ curveName ].to_numpy() - y: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() - ax.plot( x, y, label=variableName ) - ax.set_xlabel( curveName ) - ax.set_ylabel( variableName ) - ax.legend( loc="best" ) - return ( fig, ax ) - - -def adjust_subplots( fig: figure.Figure, invertXY: bool ) -> figure.Figure: - """Adjust the size of the subplot in the fig. - - Args: - fig (figure.Figure): Matplotlib figure - invertXY (bool): Choice to either intervert or not the X and Y axes - - Returns: - figure.Figure: Matplotlib figure with adjustements - """ - if invertXY: - fig.subplots_adjust( left=0.05, right=0.98, top=0.9, bottom=0.2 ) - else: - fig.subplots_adjust( left=0.06, right=0.94, top=0.95, bottom=0.08 ) - return fig diff --git a/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py b/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py deleted file mode 100755 index 891622a6..00000000 --- a/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py +++ /dev/null @@ -1,38 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. -# SPDX-FileContributor: Alexandre Benedicto -# type: ignore -# ruff: noqa -try: - import matplotlib.pyplot as plt - from paraview import python_view - - import geos_posp.visu.PVUtils.paraviewTreatments as pvt - from geos.pv.pythonViewUtils.Figure2DGenerator import ( - Figure2DGenerator, ) - - plt.close() - if len( sourceNames ) == 0: # noqa: F821 - raise ValueError( "No source name was found. Please check at least" + " one source in <>" ) - - dataframes = pvt.getDataframesFromMultipleVTKSources( - sourceNames, - variableName # noqa: F821 - ) - dataframe = pvt.mergeDataframes( dataframes, variableName ) # noqa: F821 - obj_figure = Figure2DGenerator( dataframe, userChoices ) # noqa: F821 - fig = obj_figure.getFigure() - - def setup_data( view ) -> None: # noqa - pass - - def render( view, width: int, height: int ): # noqa - fig.set_size_inches( float( width ) / 100.0, float( height ) / 100.0 ) - imageToReturn = python_view.figure_to_image( fig ) - return imageToReturn - -except Exception as e: - from geos.utils.Logger import getLogger - - logger = getLogger( "Python View Configurator" ) - logger.critical( e, exc_info=True ) From 4fda60fbd21b8ffb041725bafaa2b4aa40b5e560 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 25 Jul 2025 13:56:25 +0200 Subject: [PATCH 4/9] fix error deleting from last commit --- .../pv/pythonViewUtils/Figure2DGenerator.py | 137 ++ .../src/geos/pv/pythonViewUtils/__init__.py | 0 .../functionsFigure2DGenerator.py | 1375 +++++++++++++++++ .../geos/pv/pythonViewUtils/mainPythonView.py | 38 + 4 files changed, 1550 insertions(+) create mode 100755 geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py create mode 100755 geos-pv/src/geos/pv/pythonViewUtils/__init__.py create mode 100755 geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py create mode 100755 geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py diff --git a/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py b/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py new file mode 100755 index 00000000..bd6749f7 --- /dev/null +++ b/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py @@ -0,0 +1,137 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Alexandre Benedicto + +from typing import Any + +import pandas as pd # type: ignore[import-untyped] +from geos.utils.Logger import Logger, getLogger +from matplotlib import axes, figure, lines # type: ignore[import-untyped] +from matplotlib.font_manager import ( # type: ignore[import-untyped] + FontProperties, # type: ignore[import-untyped] +) +from typing_extensions import Self + +import geos.pv.pythonViewUtils.functionsFigure2DGenerator as fcts + + +class Figure2DGenerator: + + def __init__( self: Self, dataframe: pd.DataFrame, userChoices: dict[ str, list[ str ] ] ) -> None: + """Utility to create cross plots using Python View. + + We want to plot f(X) = Y where in this class, + "X" will be called "variable", "Y" will be called "curves". + + Args: + dataframe (pd.DataFrame): data to plot + userChoices (dict[str, list[str]]): user choices. + """ + self.m_dataframe: pd.DataFrame = dataframe + self.m_userChoices: dict[ str, Any ] = userChoices + self.m_fig: figure.Figure + self.m_axes: list[ axes._axes.Axes ] = [] + self.m_lines: list[ lines.Line2D ] = [] + self.m_labels: list[ str ] = [] + self.m_logger: Logger = getLogger( "Python View Configurator" ) + + try: + # apply minus 1 multiplication on certain columns + self.initMinus1Multiplication() + # defines m_fig, m_axes, m_lines and m_lables + self.plotInitialFigure() + # then to edit and customize the figure + self.enhanceFigure() + self.m_logger.info( "Data were successfully plotted." ) + + except Exception as e: + mess: str = "Plot creation failed due to:" + self.m_logger.critical( mess ) + self.m_logger.critical( e, exc_info=True ) + + def initMinus1Multiplication( self: Self ) -> None: + """Multiply by -1 certain columns of the input dataframe.""" + df: pd.DataFrame = self.m_dataframe.copy( deep=True ) + minus1CurveNames: list[ str ] = self.m_userChoices[ "curveConvention" ] + for name in minus1CurveNames: + df[ name ] = df[ name ] * ( -1 ) + self.m_dataframe = df + + def enhanceFigure( self: Self ) -> None: + """Apply all the enhancement features to the initial figure.""" + self.changeTitle() + self.changeMinorticks() + self.changeAxisScale() + self.changeAxisLimits() + + def plotInitialFigure( self: Self ) -> None: + """Generates a figure and axes objects from matplotlib. + + The figure plots all the curves along the X or Y axis, with legend and + label for X and Y. + """ + if self.m_userChoices[ "plotRegions" ]: + if not self.m_userChoices[ "reverseXY" ]: + ( fig, ax_all, lines, labels ) = fcts.multipleSubplots( self.m_dataframe, self.m_userChoices ) + else: + ( fig, ax_all, lines, labels ) = fcts.multipleSubplotsInverted( self.m_dataframe, self.m_userChoices ) + else: + if not self.m_userChoices[ "reverseXY" ]: + ( fig, ax_all, lines, labels ) = fcts.oneSubplot( self.m_dataframe, self.m_userChoices ) + else: + ( fig, ax_all, lines, labels ) = fcts.oneSubplotInverted( self.m_dataframe, self.m_userChoices ) + self.m_fig = fig + self.m_axes = ax_all + self.m_lines = lines + self.m_labels = labels + + def changeTitle( self: Self ) -> None: + """Update title of the first axis of the figure based on user choices.""" + if self.m_userChoices[ "displayTitle" ]: + title: str = self.m_userChoices[ "title" ] + fontTitle: FontProperties = fcts.buildFontTitle( self.m_userChoices ) + self.m_fig.suptitle( title, fontproperties=fontTitle ) + + def changeMinorticks( self: Self ) -> None: + """Set the minorticks on or off for every axes.""" + choice: bool = self.m_userChoices[ "minorticks" ] + if choice: + for ax in self.m_axes: + ax.minorticks_on() + else: + for ax in self.m_axes: + ax.minorticks_off() + + def changeAxisScale( self: Self ) -> None: + """Set the minorticks on or off for every axes.""" + for ax in self.m_axes: + if self.m_userChoices[ "logScaleX" ]: + ax.set_xscale( "log" ) + if self.m_userChoices[ "logScaleY" ]: + ax.set_yscale( "log" ) + + def changeAxisLimits( self: Self ) -> None: + """Update axis limits.""" + if self.m_userChoices[ "customAxisLim" ]: + for ax in self.m_axes: + xmin, xmax = ax.get_xlim() + if self.m_userChoices[ "limMinX" ] is not None: + xmin = self.m_userChoices[ "limMinX" ] + if self.m_userChoices[ "limMaxX" ] is not None: + xmax = self.m_userChoices[ "limMaxX" ] + ax.set_xlim( xmin, xmax ) + + ymin, ymax = ax.get_ylim() + if self.m_userChoices[ "limMinY" ] is not None: + ymin = self.m_userChoices[ "limMinY" ] + if self.m_userChoices[ "limMaxY" ] is not None: + ymax = self.m_userChoices[ "limMaxY" ] + ax.set_ylim( ymin, ymax ) + + def getFigure( self: Self ) -> figure.Figure: + """Acces the m_fig attribute. + + Returns: + figure.Figure: Figure containing all the plots. + """ + return self.m_fig diff --git a/geos-pv/src/geos/pv/pythonViewUtils/__init__.py b/geos-pv/src/geos/pv/pythonViewUtils/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py b/geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py new file mode 100755 index 00000000..4b629e9b --- /dev/null +++ b/geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py @@ -0,0 +1,1375 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Alexandre Benedicto +import math +from typing import Any + +import matplotlib.pyplot as plt # type: ignore[import-untyped] +import numpy as np +import numpy.typing as npt +import pandas as pd # type: ignore[import-untyped] +from matplotlib import axes, figure, lines # type: ignore[import-untyped] +from matplotlib.font_manager import ( # type: ignore[import-untyped] + FontProperties, # type: ignore[import-untyped] +) + +import geos.pv.geosLogReaderUtils.geosLogReaderFunctions as fcts +""" +Plotting tools for 2D figure and axes generation. +""" + + +def oneSubplot( + df: pd.DataFrame, + userChoices: dict[ str, Any ] ) -> tuple[ figure.Figure, list[ axes.Axes ], list[ lines.Line2D ], list[ str ] ]: + """Created a single subplot. + + From a dataframe, knowing which curves to plot along which variable, + generates a fig and its list of axes with the data plotted. + + Args: + df (pd.DataFrame): dataframe containing at least two columns, + one named "variableName" and the other "curveName" + userChoices (dict[str, Any]): Choices made by widget selection + in PythonViewConfigurator filter. + + Returns: + tuple[figure.Figure, list[axes.Axes], + list[lines.Line2D] , list[str]]: the fig and its list of axes. + """ + curveNames: list[ str ] = userChoices[ "curveNames" ] + variableName: str = userChoices[ "variableName" ] + curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, + float ] ] = userChoices[ "curvesAspect" ] + associatedProperties: dict[ str, list[ str ] ] = associatePropertyToAxeType( curveNames ) + fig, ax = plt.subplots( constrained_layout=True ) + all_ax: list[ axes.Axes ] = setupAllAxes( ax, variableName, associatedProperties, True ) + lineList: list[ lines.Line2D ] = [] + labels: list[ str ] = [] + cpt_cmap: int = 0 + x: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() + for cpt_ax, ( ax_name, propertyNames ) in enumerate( associatedProperties.items() ): + ax_to_use: axes.Axes = setupAxeToUse( all_ax, cpt_ax, ax_name, False ) + for propName in propertyNames: + y: npt.NDArray[ np.float64 ] = df[ propName ].to_numpy() + plotAxe( ax_to_use, x, y, propName, cpt_cmap, curvesAspect ) + cpt_cmap += 1 + new_lines, new_labels = ax_to_use.get_legend_handles_labels() + lineList += new_lines # type: ignore[arg-type] + labels += new_labels + labels, lineList = smartLabelsSorted( labels, lineList, userChoices ) + if userChoices[ "displayLegend" ]: + ax.legend( + lineList, + labels, + loc=userChoices[ "legendPosition" ], + fontsize=userChoices[ "legendSize" ], + ) + ax.grid() + return ( fig, all_ax, lineList, labels ) + + +def oneSubplotInverted( + df: pd.DataFrame, + userChoices: dict[ str, Any ] ) -> tuple[ figure.Figure, list[ axes.Axes ], list[ lines.Line2D ], list[ str ] ]: + """Created a single subplot with inverted X Y axes. + + From a dataframe, knowing which curves to plot along which variable, + generates a fig and its list of axes with the data plotted. + + Args: + df (pd.DataFrame): dataframe containing at least two columns, + one named "variableName" and the other "curveName" + userChoices (dict[str, Any]): Choices made by widget selection + in PythonViewConfigurator filter. + + Returns: + tuple[figure.Figure, list[axes.Axes], + list[lines.Line2D] , list[str]]: the fig and its list of axes. + """ + curveNames: list[ str ] = userChoices[ "curveNames" ] + variableName: str = userChoices[ "variableName" ] + curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, + float ] ] = userChoices[ "curvesAspect" ] + associatedProperties: dict[ str, list[ str ] ] = associatePropertyToAxeType( curveNames ) + fig, ax = plt.subplots( constrained_layout=True ) + all_ax: list[ axes.Axes ] = setupAllAxes( ax, variableName, associatedProperties, False ) + linesList: list[ lines.Line2D ] = [] + labels: list[ str ] = [] + cpt_cmap: int = 0 + y: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() + for cpt_ax, ( ax_name, propertyNames ) in enumerate( associatedProperties.items() ): + ax_to_use: axes.Axes = setupAxeToUse( all_ax, cpt_ax, ax_name, True ) + for propName in propertyNames: + x: npt.NDArray[ np.float64 ] = df[ propName ].to_numpy() + plotAxe( ax_to_use, x, y, propName, cpt_cmap, curvesAspect ) + cpt_cmap += 1 + new_lines, new_labels = ax_to_use.get_legend_handles_labels() + linesList += new_lines # type: ignore[arg-type] + labels += new_labels + labels, linesList = smartLabelsSorted( labels, linesList, userChoices ) + if userChoices[ "displayLegend" ]: + ax.legend( + linesList, + labels, + loc=userChoices[ "legendPosition" ], + fontsize=userChoices[ "legendSize" ], + ) + ax.grid() + return ( fig, all_ax, linesList, labels ) + + +def multipleSubplots( + df: pd.DataFrame, + userChoices: dict[ str, Any ] ) -> tuple[ figure.Figure, list[ axes.Axes ], list[ lines.Line2D ], list[ str ] ]: + """Created multiple subplots. + + From a dataframe, knowing which curves to plot along which variable, + generates a fig and its list of axes with the data plotted. + + Args: + df (pd.DataFrame): dataframe containing at least two columns, + one named "variableName" and the other "curveName". + userChoices (dict[str, Any]): Choices made by widget selection + in PythonViewConfigurator filter. + + Returns: + tuple[figure.Figure, list[axes.Axes], + list[lines.Line2D] , list[str]]: the fig and its list of axes. + """ + curveNames: list[ str ] = userChoices[ "curveNames" ] + variableName: str = userChoices[ "variableName" ] + curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, + float ] ] = userChoices[ "curvesAspect" ] + ratio: float = userChoices[ "ratio" ] + assosIdentifiers: dict[ str, dict[ str, list[ str ] ] ] = associationIdentifiers( curveNames ) + nbr_suplots: int = len( assosIdentifiers.keys() ) + # if only one subplots needs to be created + if nbr_suplots == 1: + return oneSubplot( df, userChoices ) + + layout: tuple[ int, int, int ] = smartLayout( nbr_suplots, ratio ) + fig, axs0 = plt.subplots( layout[ 0 ], layout[ 1 ], constrained_layout=True ) + axs: list[ axes.Axes ] = axs0.flatten().tolist() # type: ignore[union-attr] + for i in range( layout[ 2 ] ): + fig.delaxes( axs[ -( i + 1 ) ] ) + all_lines: list[ lines.Line2D ] = [] + all_labels: list[ str ] = [] + # first loop for subplots + propertiesExtremas: dict[ str, tuple[ float, float ] ] = ( findExtremasPropertiesForAssociatedIdentifiers( + df, assosIdentifiers, True ) ) + for j, identifier in enumerate( assosIdentifiers.keys() ): + first_ax: axes.Axes = axs[ j ] + associatedProperties: dict[ str, list[ str ] ] = assosIdentifiers[ identifier ] + all_ax: list[ axes.Axes ] = setupAllAxes( first_ax, variableName, associatedProperties, True ) + axs += all_ax[ 1: ] + linesList: list[ lines.Line2D ] = [] + labels: list[ str ] = [] + cpt_cmap: int = 0 + x: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() + # second loop for axes per subplot + for cpt_ax, ( ax_name, propertyNames ) in enumerate( associatedProperties.items() ): + ax_to_use: axes.Axes = setupAxeToUse( all_ax, cpt_ax, ax_name, False ) + for propName in propertyNames: + y: npt.NDArray[ np.float64 ] = df[ propName ].to_numpy() + plotAxe( ax_to_use, x, y, propName, cpt_cmap, curvesAspect ) + ax_to_use.set_ylim( *propertiesExtremas[ ax_name ] ) + cpt_cmap += 1 + new_lines, new_labels = ax_to_use.get_legend_handles_labels() + linesList += new_lines # type: ignore[arg-type] + all_lines += new_lines # type: ignore[arg-type] + labels += new_labels + all_labels += new_labels + labels, linesList = smartLabelsSorted( labels, linesList, userChoices ) + if userChoices[ "displayLegend" ]: + first_ax.legend( + linesList, + labels, + loc=userChoices[ "legendPosition" ], + fontsize=userChoices[ "legendSize" ], + ) + if userChoices[ "displayTitle" ]: + first_ax.set_title( identifier, fontsize=10 ) + first_ax.grid() + return ( fig, axs, all_lines, all_labels ) + + +def multipleSubplotsInverted( + df: pd.DataFrame, + userChoices: dict[ str, Any ] ) -> tuple[ figure.Figure, list[ axes.Axes ], list[ lines.Line2D ], list[ str ] ]: + """Created multiple subplots with inverted X Y axes. + + From a dataframe, knowing which curves to plot along which variable, + generates a fig and its list of axes with the data plotted. + + Args: + df (pd.DataFrame): dataframe containing at least two columns, + one named "variableName" and the other "curveName". + userChoices (dict[str, Any]): Choices made by widget selection + in PythonViewConfigurator filter. + + Returns: + tuple[figure.Figure, list[axes.Axes], + list[lines.Line2D] , list[str]]: the fig and its list of axes. + """ + curveNames: list[ str ] = userChoices[ "curveNames" ] + variableName: str = userChoices[ "variableName" ] + curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, + float ] ] = userChoices[ "curvesAspect" ] + ratio: float = userChoices[ "ratio" ] + assosIdentifiers: dict[ str, dict[ str, list[ str ] ] ] = associationIdentifiers( curveNames ) + nbr_suplots: int = len( assosIdentifiers.keys() ) + # if only one subplots needs to be created + if nbr_suplots == 1: + return oneSubplotInverted( df, userChoices ) + + layout: tuple[ int, int, int ] = smartLayout( nbr_suplots, ratio ) + fig, axs0 = plt.subplots( layout[ 0 ], layout[ 1 ], constrained_layout=True ) + axs: list[ axes.Axes ] = axs0.flatten().tolist() # type: ignore[union-attr] + for i in range( layout[ 2 ] ): + fig.delaxes( axs[ -( i + 1 ) ] ) + all_lines: list[ lines.Line2D ] = [] + all_labels: list[ str ] = [] + # first loop for subplots + propertiesExtremas: dict[ str, tuple[ float, float ] ] = ( findExtremasPropertiesForAssociatedIdentifiers( + df, assosIdentifiers, True ) ) + for j, identifier in enumerate( assosIdentifiers.keys() ): + first_ax: axes.Axes = axs[ j ] + associatedProperties: dict[ str, list[ str ] ] = assosIdentifiers[ identifier ] + all_ax: list[ axes.Axes ] = setupAllAxes( first_ax, variableName, associatedProperties, False ) + axs += all_ax[ 1: ] + linesList: list[ lines.Line2D ] = [] + labels: list[ str ] = [] + cpt_cmap: int = 0 + y: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() + # second loop for axes per subplot + for cpt_ax, ( ax_name, propertyNames ) in enumerate( associatedProperties.items() ): + ax_to_use: axes.Axes = setupAxeToUse( all_ax, cpt_ax, ax_name, True ) + for propName in propertyNames: + x: npt.NDArray[ np.float64 ] = df[ propName ].to_numpy() + plotAxe( ax_to_use, x, y, propName, cpt_cmap, curvesAspect ) + ax_to_use.set_xlim( propertiesExtremas[ ax_name ] ) + cpt_cmap += 1 + new_lines, new_labels = ax_to_use.get_legend_handles_labels() + linesList += new_lines # type: ignore[arg-type] + all_lines += new_lines # type: ignore[arg-type] + labels += new_labels + all_labels += new_labels + labels, linesList = smartLabelsSorted( labels, linesList, userChoices ) + if userChoices[ "displayLegend" ]: + first_ax.legend( + linesList, + labels, + loc=userChoices[ "legendPosition" ], + fontsize=userChoices[ "legendSize" ], + ) + if userChoices[ "displayTitle" ]: + first_ax.set_title( identifier, fontsize=10 ) + first_ax.grid() + return ( fig, axs, all_lines, all_labels ) + + +def setupAllAxes( + first_ax: axes.Axes, + variableName: str, + associatedProperties: dict[ str, list[ str ] ], + axisX: bool, +) -> list[ axes.Axes ]: + """Modify axis name and ticks avec X or Y axis of all subplots. + + Args: + first_ax (axes.Axes): subplot id. + variableName (str): name of the axis. + associatedProperties (dict[str, list[str]]): Name of the properties + axisX (bool): X (True) or Y (False) axis to modify. + + Returns: + list[axes.Axes]: modified subplots + """ + all_ax: list[ axes.Axes ] = [ first_ax ] + if axisX: + first_ax.set_xlabel( variableName ) + first_ax.ticklabel_format( style="sci", axis="x", scilimits=( 0, 0 ), useMathText=True ) + for i in range( 1, len( associatedProperties.keys() ) ): + second_ax = first_ax.twinx() + assert isinstance( second_ax, axes.Axes ) + all_ax.append( second_ax ) + all_ax[ i ].spines[ "right" ].set_position( ( "axes", 1 + 0.07 * ( i - 1 ) ) ) + all_ax[ i ].tick_params( axis="y", which="both", left=False, right=True ) + all_ax[ i ].yaxis.set_ticks_position( "right" ) + all_ax[ i ].yaxis.offsetText.set_position( ( 1.04 + 0.07 * ( i - 1 ), 0 ) ) + first_ax.yaxis.offsetText.set_position( ( -0.04, 0 ) ) + else: + first_ax.set_ylabel( variableName ) + first_ax.ticklabel_format( style="sci", axis="y", scilimits=( 0, 0 ), useMathText=True ) + for i in range( 1, len( associatedProperties.keys() ) ): + second_ax = first_ax.twiny() + assert isinstance( second_ax, axes.Axes ) + all_ax.append( second_ax ) + all_ax[ i ].spines[ "bottom" ].set_position( ( "axes", -0.08 * i ) ) + all_ax[ i ].xaxis.set_label_position( "bottom" ) + all_ax[ i ].tick_params( axis="x", which="both", bottom=True, top=False ) + all_ax[ i ].xaxis.set_ticks_position( "bottom" ) + return all_ax + + +def setupAxeToUse( all_ax: list[ axes.Axes ], axeId: int, ax_name: str, axisX: bool ) -> axes.Axes: + """Modify axis name and ticks avec X or Y axis of subplot axeId in all_ax. + + Args: + all_ax (list[axes.Axes]): list of all subplots + axeId (int): id of the subplot + ax_name (str): name of the X or Y axis + axisX (bool): X (True) or Y (False) axis to modify. + + Returns: + axes.Axes: modified subplot + """ + ax_to_use: axes.Axes = all_ax[ axeId ] + if axisX: + ax_to_use.set_xlabel( ax_name ) + ax_to_use.ticklabel_format( style="sci", axis="x", scilimits=( 0, 0 ), useMathText=True ) + else: + ax_to_use.set_ylabel( ax_name ) + ax_to_use.ticklabel_format( style="sci", axis="y", scilimits=( 0, 0 ), useMathText=True ) + return ax_to_use + + +def plotAxe( + ax_to_use: axes.Axes, + x: npt.NDArray[ np.float64 ], + y: npt.NDArray[ np.float64 ], + propertyName: str, + cpt_cmap: int, + curvesAspect: dict[ str, tuple[ tuple[ float, float, float ], str, float, str, float ] ], +) -> None: + """Plot x, y data using input ax_to_use according to curvesAspect. + + Args: + ax_to_use (axes.Axes): subplot to use + x (npt.NDArray[np.float64]): abscissa data + y (npt.NDArray[np.float64]): ordinate data + propertyName (str): name of the property + cpt_cmap (int): colormap to use + curvesAspect (dict[str, tuple[tuple[float, float, float],str, float, str, float]]): + user choices on curve aspect + """ + cmap = plt.rcParams[ "axes.prop_cycle" ].by_key()[ "color" ][ cpt_cmap % 10 ] + mask = np.logical_and( np.isnan( x ), np.isnan( y ) ) + not_mask = ~mask + # Plot only when x and y values are not nan values + if propertyName in curvesAspect: + asp: tuple[ tuple[ float, float, float ], str, float, str, float ] = curvesAspect[ propertyName ] + ax_to_use.plot( + x[ not_mask ], + y[ not_mask ], + label=propertyName, + color=asp[ 0 ], + linestyle=asp[ 1 ], + linewidth=asp[ 2 ], + marker=asp[ 3 ], + markersize=asp[ 4 ], + ) + else: + ax_to_use.plot( x[ not_mask ], y[ not_mask ], label=propertyName, color=cmap ) + + +def getExtremaAllAxes( axes: list[ axes.Axes ], ) -> tuple[ tuple[ float, float ], tuple[ float, float ] ]: + """Gets the limits of both X and Y axis as a 2x2 element tuple. + + Args: + axes (list[axes.Axes]): list of subplots to get limits. + + Returns: + tuple[tuple[float, float], tuple[float, float]]:: ((xMin, xMax), (yMin, yMax)) + """ + assert len( axes ) > 0 + xMin, xMax, yMin, yMax = getAxeLimits( axes[ 0 ] ) + if len( axes ) > 1: + for i in range( 1, len( axes ) ): + x1, x2, y1, y2 = getAxeLimits( axes[ i ] ) + if x1 < xMin: + xMin = x1 + if x2 > xMax: + xMax = x2 + if y1 < yMin: + yMin = y1 + if y2 > yMax: + yMax = y2 + return ( ( xMin, xMax ), ( yMin, yMax ) ) + + +def getAxeLimits( ax: axes.Axes ) -> tuple[ float, float, float, float ]: + """Gets the limits of both X and Y axis as a 4 element tuple. + + Args: + ax (axes.Axes): subplot to get limits. + + Returns: + tuple[float, float, float, float]: (xMin, xMax, yMin, yMax) + """ + xMin, xMax = ax.get_xlim() + yMin, yMax = ax.get_ylim() + return ( xMin, xMax, yMin, yMax ) + + +def findExtremasPropertiesForAssociatedIdentifiers( + df: pd.DataFrame, + associatedIdentifiers: dict[ str, dict[ str, list[ str ] ] ], + offsetPlotting: bool = False, + offsetPercentage: int = 5, +) -> dict[ str, tuple[ float, float ] ]: + """Find min and max of all properties linked to a same identifier. + + Using an associatedIdentifiers dict containing associatedProperties dict, + we can find the extremas for each property of each identifier. Once we have them all, + we compare for each identifier what are the most extreme values and only the biggest and + lowest are kept in the end. + + + Args: + df (pd.DataFrame): Pandas dataframe + associatedIdentifiers (dict[str, dict[str, list[str]]]): property identifiers. + offsetPlotting (bool, optional): When using the values being returned, + we might want to add an offset to these values. If set to True, + the offsetPercentage is taken into account. Defaults to False. + offsetPercentage (int, optional): Value by which we will offset + the min and max values of each tuple of floats. Defaults to 5. + + Returns: + dict[str, tuple[float, float]]: { + "BHP (Pa)": (minAllWells, maxAllWells), + "TotalMassRate (kg)": (minAllWells, maxAllWells), + "TotalSurfaceVolumetricRate (m3/s)": (minAllWells, maxAllWells), + "SurfaceVolumetricRateCO2 (m3/s)": (minAllWells, maxAllWells), + "SurfaceVolumetricRateWater (m3/s)": (minAllWells, maxAllWells) + } + """ + extremasProperties: dict[ str, tuple[ float, float ] ] = {} + # first we need to find the extrema for each property type per region + propertyTypesExtremas: dict[ str, list[ tuple[ float, float ] ] ] = {} + for associatedProperties in associatedIdentifiers.values(): + extremasPerProperty: dict[ str, + tuple[ float, + float ] ] = ( findExtremasAssociatedProperties( df, associatedProperties ) ) + for propertyType, extremaFound in extremasPerProperty.items(): + if propertyType not in propertyTypesExtremas: + propertyTypesExtremas[ propertyType ] = [ extremaFound ] + else: + propertyTypesExtremas[ propertyType ].append( extremaFound ) + # then, once all extrema have been found for all regions, we need to figure out + # which extrema per property type is the most extreme one + for propertyType in propertyTypesExtremas: + values: list[ tuple[ float, float ] ] = propertyTypesExtremas[ propertyType ] + minValues: list[ float ] = [ values[ i ][ 0 ] for i in range( len( values ) ) ] + maxValues: list[ float ] = [ values[ i ][ 1 ] for i in range( len( values ) ) ] + lowest, highest = ( min( minValues ), max( maxValues ) ) + if offsetPlotting: + offset: float = ( highest - lowest ) / 100 * offsetPercentage + lowest, highest = ( lowest - offset, highest + offset ) + extremasProperties[ propertyType ] = ( lowest, highest ) + return extremasProperties + + +def findExtremasAssociatedProperties( + df: pd.DataFrame, associatedProperties: dict[ str, list[ str ] ] ) -> dict[ str, tuple[ float, float ] ]: + """Find the min and max of properties. + + Using an associatedProperties dict containing property types + as keys and a list of property names as values, + and a pandas dataframe whose column names are composed of those same + property names, you can find the min and max values of each property + type and return it as a tuple. + + Args: + df (pd.DataFrame): Pandas dataframe + associatedProperties (dict[str, list[str]]): { + "Pressure (Pa)": ["Reservoir__Pressure__Pa__Source1"], + "Mass (kg)": ["CO2__Mass__kg__Source1", + "Water__Mass__kg__Source1"] + } + + Returns: + dict[str, tuple[float, float]]: { + "Pressure (Pa)": (minPressure, maxPressure), + "Mass (kg)": (minMass, maxMass) + } + """ + extremasProperties: dict[ str, tuple[ float, float ] ] = {} + for propertyType, propertyNames in associatedProperties.items(): + minValues = np.empty( len( propertyNames ) ) + maxValues = np.empty( len( propertyNames ) ) + for i, propertyName in enumerate( propertyNames ): + values: npt.NDArray[ np.float64 ] = df[ propertyName ].to_numpy() + minValues[ i ] = np.nanmin( values ) + maxValues[ i ] = np.nanmax( values ) + extrema: tuple[ float, float ] = ( + float( np.min( minValues ) ), + float( np.max( maxValues ) ), + ) + extremasProperties[ propertyType ] = extrema + return extremasProperties + + +""" +Utils for treatment of the data +""" + + +def associatePropertyToAxeType( propertyNames: list[ str ] ) -> dict[ str, list[ str ] ]: + """Identify property types. + + From a list of property names, identify if each of this property + corresponds to a certain property type like "Pressure", "Mass", + "Temperature" etc ... and returns a dict where the keys are the property + type and the value the list of property names associated to it. + + Args: + propertyNames (list[str]): ["Reservoir__Pressure__Pa__Source1", + "CO2__Mass__kg__Source1", "Water__Mass__kg__Source1"] + + Returns: + dict[str, list[str]]: { "Pressure (Pa)": ["Reservoir__Pressure__Pa__Source1"], + "Mass (kg)": ["CO2__Mass__kg__Source1", + "Water__Mass__kg__Source1"] } + """ + propertyIds: list[ str ] = fcts.identifyProperties( propertyNames ) + associationTable: dict[ str, str ] = { + "0": "Pressure", + "1": "Pressure", + "2": "Temperature", + "3": "PoreVolume", + "4": "PoreVolume", + "5": "Mass", + "6": "Mass", + "7": "Mass", + "8": "Mass", + "9": "Mass", + "10": "Mass", + "11": "BHP", + "12": "MassRate", + "13": "VolumetricRate", + "14": "VolumetricRate", + "15": "BHP", + "16": "MassRate", + "17": "VolumetricRate", + "18": "VolumetricRate", + "19": "VolumetricRate", + "20": "Volume", + "21": "VolumetricRate", + "22": "Volume", + "23": "Iterations", + "24": "Iterations", + "25": "Stress", + "26": "Displacement", + "27": "Permeability", + "28": "Porosity", + "29": "Ratio", + "30": "Fraction", + "31": "BulkModulus", + "32": "ShearModulus", + "33": "OedometricModulus", + "34": "Points", + "35": "Density", + "36": "Mass", + "37": "Mass", + "38": "Time", + "39": "Time", + } + associatedPropertyToAxeType: dict[ str, list[ str ] ] = {} + noUnitProperties: list[ str ] = [ + "Iterations", + "Porosity", + "Ratio", + "Fraction", + "OedometricModulus", + ] + for i, propId in enumerate( propertyIds ): + idProp: str = propId.split( ":" )[ 0 ] + propNoId: str = propId.split( ":" )[ 1 ] + associatedType: str = associationTable[ idProp ] + if associatedType in noUnitProperties: + axeName: str = associatedType + else: + propIdElts: list[ str ] = propNoId.split( "__" ) + # no unit was found + if len( propIdElts ) <= 2: + axeName = associatedType + # there is a unit + else: + unit: str = propIdElts[ -2 ] + axeName = associatedType + " (" + unit + ")" + if axeName not in associatedPropertyToAxeType: + associatedPropertyToAxeType[ axeName ] = [] + associatedPropertyToAxeType[ axeName ].append( propertyNames[ i ] ) + return associatedPropertyToAxeType + + +def propertiesPerIdentifier( propertyNames: list[ str ] ) -> dict[ str, list[ str ] ]: + """Extract identifiers with associatied properties. + + From a list of property names, extracts the identifier (name of the + region for flow property or name of a well for well property) and creates + a dictionnary with identifiers as keys and the properties containing them + for value in a list. + + Args: + propertyNames (list[str]): property names + Example + + .. code-block:: python + + [ + "WellControls1__BHP__Pa__Source1", + "WellControls1__TotalMassRate__kg/s__Source1", + "WellControls2__BHP__Pa__Source1", + "WellControls2__TotalMassRate__kg/s__Source1" + ] + + Returns: + dict[str, list[str]]: property identifiers + Example + + .. code-block:: python + + { + "WellControls1": [ + "WellControls1__BHP__Pa__Source1", + "WellControls1__TotalMassRate__kg/s__Source1" + ], + "WellControls2": [ + "WellControls2__BHP__Pa__Source1", + "WellControls2__TotalMassRate__kg/s__Source1" + ] + } + """ + propsPerIdentfier: dict[ str, list[ str ] ] = {} + for propertyName in propertyNames: + elements: list[ str ] = propertyName.split( "__" ) + identifier: str = elements[ 0 ] + if identifier not in propsPerIdentfier: + propsPerIdentfier[ identifier ] = [] + propsPerIdentfier[ identifier ].append( propertyName ) + return propsPerIdentfier + + +def associationIdentifiers( propertyNames: list[ str ] ) -> dict[ str, dict[ str, list[ str ] ] ]: + """Extract identifiers with associatied curves. + + From a list of property names, extracts the identifier (name of the + region for flow property or name of a well for well property) and creates + a dictionnary with identifiers as keys and the properties containing them + for value in a list. + + Args: + propertyNames (list[str]): property names + Example + + .. code-block:: python + + [ + "WellControls1__BHP__Pa__Source1", + "WellControls1__TotalMassRate__kg/s__Source1", + "WellControls1__TotalSurfaceVolumetricRate__m3/s__Source1", + "WellControls1__SurfaceVolumetricRateCO2__m3/s__Source1", + "WellControls1__SurfaceVolumetricRateWater__m3/s__Source1", + "WellControls2__BHP__Pa__Source1", + "WellControls2__TotalMassRate__kg/s__Source1", + "WellControls2__TotalSurfaceVolumetricRate__m3/s__Source1", + "WellControls2__SurfaceVolumetricRateCO2__m3/s__Source1", + "WellControls2__SurfaceVolumetricRateWater__m3/s__Source1", + "WellControls3__BHP__Pa__Source1", + "WellControls3__TotalMassRate__tons/day__Source1", + "WellControls3__TotalSurfaceVolumetricRate__bbl/day__Source1", + "WellControls3__SurfaceVolumetricRateCO2__bbl/day__Source1", + "WellControls3__SurfaceVolumetricRateWater__bbl/day__Source1", + "Mean__BHP__Pa__Source1", + "Mean__TotalMassRate__tons/day__Source1", + "Mean__TotalSurfaceVolumetricRate__bbl/day__Source1", + "Mean__SurfaceVolumetricRateCO2__bbl/day__Source1", + "Mean__SurfaceVolumetricRateWater__bbl/day__Source1" + ] + + Returns: + dict[str, dict[str, list[str]]]: property identifiers + Example + + .. code-block:: python + + { + "WellControls1": { + 'BHP (Pa)': [ + 'WellControls1__BHP__Pa__Source1' + ], + 'MassRate (kg/s)': [ + 'WellControls1__TotalMassRate__kg/s__Source1' + ], + 'VolumetricRate (m3/s)': [ + 'WellControls1__TotalSurfaceVolumetricRate__m3/s__Source1', + 'WellControls1__SurfaceVolumetricRateCO2__m3/s__Source1', + 'WellControls1__SurfaceVolumetricRateWater__m3/s__Source1' + ] + }, + "WellControls2": { + 'BHP (Pa)': [ + 'WellControls2__BHP__Pa__Source1' + ], + 'MassRate (kg/s)': [ + 'WellControls2__TotalMassRate__kg/s__Source1' + ], + 'VolumetricRate (m3/s)': [ + 'WellControls2__TotalSurfaceVolumetricRate__m3/s__Source1', + 'WellControls2__SurfaceVolumetricRateCO2__m3/s__Source1', + 'WellControls2__SurfaceVolumetricRateWater__m3/s__Source1' + ] + }, + "WellControls3": { + 'BHP (Pa)': [ + 'WellControls3__BHP__Pa__Source1' + ], + 'MassRate (tons/day)': [ + 'WellControls3__TotalMassRate__tons/day__Source1' + ], + 'VolumetricRate (bbl/day)': [ + 'WellControls3__TotalSurfaceVolumetricRate__bbl/day__Source1', + 'WellControls3__SurfaceVolumetricRateCO2__bbl/day__Source1', + 'WellControls3__SurfaceVolumetricRateWater__bbl/day__Source1' + ] + }, + "Mean": { + 'BHP (Pa)': [ + 'Mean__BHP__Pa__Source1' + ], + 'MassRate (tons/day)': [ + 'Mean__TotalMassRate__tons/day__Source1' + ], + 'VolumetricRate (bbl/day)': [ + 'Mean__TotalSurfaceVolumetricRate__bbl/day__Source1', + 'Mean__SurfaceVolumetricRateCO2__bbl/day__Source1', + 'Mean__SurfaceVolumetricRateWater__bbl/day__Source1' + ] + } + } + """ + propsPerIdentfier: dict[ str, list[ str ] ] = propertiesPerIdentifier( propertyNames ) + assosIdentifier: dict[ str, dict[ str, list[ str ] ] ] = {} + for ident, propNames in propsPerIdentfier.items(): + assosPropsToAxeType: dict[ str, list[ str ] ] = associatePropertyToAxeType( propNames ) + assosIdentifier[ ident ] = assosPropsToAxeType + return assosIdentifier + + +def buildFontTitle( userChoices: dict[ str, Any ] ) -> FontProperties: + """Builds a Fontproperties object according to user choices on title. + + Args: + userChoices (dict[str, Any]): customization parameters. + + Returns: + FontProperties: FontProperties object for the title. + """ + fontTitle: FontProperties = FontProperties() + if "titleStyle" in userChoices: + fontTitle.set_style( userChoices[ "titleStyle" ] ) + if "titleWeight" in userChoices: + fontTitle.set_weight( userChoices[ "titleWeight" ] ) + if "titleSize" in userChoices: + fontTitle.set_size( userChoices[ "titleSize" ] ) + return fontTitle + + +def buildFontVariable( userChoices: dict[ str, Any ] ) -> FontProperties: + """Builds a Fontproperties object according to user choices on variables. + + Args: + userChoices (dict[str, Any]): customization parameters. + + Returns: + FontProperties: FontProperties object for the variable axes. + """ + fontVariable: FontProperties = FontProperties() + if "variableStyle" in userChoices: + fontVariable.set_style( userChoices[ "variableStyle" ] ) + if "variableWeight" in userChoices: + fontVariable.set_weight( userChoices[ "variableWeight" ] ) + if "variableSize" in userChoices: + fontVariable.set_size( userChoices[ "variableSize" ] ) + return fontVariable + + +def buildFontCurves( userChoices: dict[ str, Any ] ) -> FontProperties: + """Builds a Fontproperties object according to user choices on curves. + + Args: + userChoices (dict[str, str]): customization parameters. + + Returns: + FontProperties: FontProperties object for the curves axes. + """ + fontCurves: FontProperties = FontProperties() + if "curvesStyle" in userChoices: + fontCurves.set_style( userChoices[ "curvesStyle" ] ) + if "curvesWeight" in userChoices: + fontCurves.set_weight( userChoices[ "curvesWeight" ] ) + if "curvesSize" in userChoices: + fontCurves.set_size( userChoices[ "curvesSize" ] ) + return fontCurves + + +def customizeLines( userChoices: dict[ str, Any ], labels: list[ str ], + linesList: list[ lines.Line2D ] ) -> list[ lines.Line2D ]: + """Customize lines according to user choices. + + By applying the user choices, we modify or not the list of lines + and return it with the same number of lines in the same order. + + Args: + userChoices (dict[str, Any]): customization parameters. + labels (list[str]): labels of lines. + linesList (list[lines.Line2D]): list of lines object. + + Returns: + list[lines.Line2D]: list of lines object modified. + """ + if "linesModified" in userChoices: + linesModifs: dict[ str, dict[ str, Any ] ] = userChoices[ "linesModified" ] + linesChanged: list[ lines.Line2D ] = [] + for i, label in enumerate( labels ): + if label in linesModifs: + lineChanged: lines.Line2D = applyCustomizationOnLine( linesList[ i ], linesModifs[ label ] ) + linesChanged.append( lineChanged ) + else: + linesChanged.append( linesList[ i ] ) + return linesChanged + else: + return linesList + + +def applyCustomizationOnLine( line: lines.Line2D, parameters: dict[ str, Any ] ) -> lines.Line2D: + """Apply modification methods on a line from parameters. + + Args: + line (lines.Line2D): Matplotlib Line2D + parameters (dict[str, Any]): dictionary of { + "linestyle": one of ["-","--","-.",":"] + "linewidth": positive int + "color": color code + "marker": one of ["",".","o","^","s","*","D","+","x"] + "markersize":positive int + } + + Returns: + lines.Line2D: Line2D object modified. + """ + if "linestyle" in parameters: + line.set_linestyle( parameters[ "linestyle" ] ) + if "linewidth" in parameters: + line.set_linewidth( parameters[ "linewidth" ] ) + if "color" in parameters: + line.set_color( parameters[ "color" ] ) + if "marker" in parameters: + line.set_marker( parameters[ "marker" ] ) + if "markersize" in parameters: + line.set_markersize( parameters[ "markersize" ] ) + return line + + +""" +Layout tools for layering subplots in a figure +""" + + +def isprime( x: int ) -> bool: + """Checks if a number is primer or not. + + Args: + x (int): Positive number to test. + + Returns: + bool: True if prime, False if not. + """ + if x < 0: + print( "Invalid number entry, needs to be positive int" ) + return False + + return all( x % n != 0 for n in range( 2, int( x**0.5 ) + 1 ) ) + + +def findClosestPairIntegers( x: int ) -> tuple[ int, int ]: + """Get the pair of integers that multiply the closest to input value. + + Finds the closest pair of integers that when multiplied together, + gives a number the closest to the input number (always above or equal). + + Args: + x (int): Positive number. + + Returns: + tuple[int, int]: (highest int, lowest int) + """ + if x < 4: + return ( x, 1 ) + while isprime( x ): + x += 1 + N: int = round( math.sqrt( x ) ) + while x > N: + if x % N == 0: + M = x // N + highest = max( M, N ) + lowest = min( M, N ) + return ( highest, lowest ) + else: + N += 1 + return ( x, 1 ) + + +def smartLayout( x: int, ratio: float ) -> tuple[ int, int, int ]: + """Return the best layout according to the number of subplots. + + For multiple subplots, we need to have a layout that can adapt to + the number of subplots automatically. This function figures out the + best layout possible knowing the number of suplots and the figure ratio. + + Args: + x (int): Positive number. + ratio (float): width to height ratio of a figure. + + Returns: + tuple[int]: (nbr_rows, nbr_columns, number of axes to remove) + """ + pair: tuple[ int, int ] = findClosestPairIntegers( x ) + nbrAxesToRemove: int = pair[ 0 ] * pair[ 1 ] - x + if ratio < 1: + return ( pair[ 0 ], pair[ 1 ], nbrAxesToRemove ) + else: + return ( pair[ 1 ], pair[ 0 ], nbrAxesToRemove ) + + +""" +Legend tools +""" + +commonAssociations: dict[ str, str ] = { + "pressuremin": "Pmin", + "pressureMax": "Pmax", + "pressureaverage": "Pavg", + "deltapressuremin": "DPmin", + "deltapressuremax": "DPmax", + "temperaturemin": "Tmin", + "temperaturemax": "Tmax", + "temperatureaverage": "Tavg", + "effectivestressxx": "ESxx", + "effectivestresszz": "ESzz", + "effectivestressratio": "ESratio", + "totaldisplacementx": "TDx", + "totaldisplacementy": "TDy", + "totaldisplacementz": "TDz", + "totalstressXX": "TSxx", + "totalstressZZ": "TSzz", + "stressxx": "Sxx", + "stressyy": "Syy", + "stresszz": "Szz", + "stressxy": "Sxy", + "stressxz": "Sxz", + "stressyz": "Syz", + "poissonratio": "PR", + "porosity": "PORO", + "specificgravity": "SG", + "theoreticalverticalstress": "TVS", + "density": "DNST", + "pressure": "P", + "permeabilityx": "PERMX", + "permeabilityy": "PERMY", + "permeabilityz": "PERMZ", + "oedometric": "OEDO", + "young": "YOUNG", + "shear": "SHEAR", + "bulk": "BULK", + "totaldynamicporevolume": "TDPORV", + "time": "TIME", + "dt": "DT", + "meanbhp": "MBHP", + "meantotalmassrate": "MTMR", + "meantotalvolumetricrate": "MTSVR", + "bhp": "BHP", + "totalmassrate": "TMR", + "cumulatedlineariter": "CLI", + "cumulatednewtoniter": "CNI", + "lineariter": "LI", + "newtoniter": "NI", +} + +phasesAssociations: dict[ str, str ] = { + "dissolvedmass": " IN ", + "immobile": "IMOB ", + "mobile": "MOB ", + "nontrapped": "NTRP ", + "dynamicporevolume": "DPORV ", + "meansurfacevolumetricrate": "MSVR ", + "surfacevolumetricrate": "SVR ", +} + + +def smartLabelsSorted( labels: list[ str ], lines: list[ lines.Line2D ], + userChoices: dict[ str, Any ] ) -> tuple[ list[ str ], list[ lines.Line2D ] ]: + """Shorten all legend labels and sort them. + + To improve readability of the legend for an axe in ParaView, we can apply the + smartLegendLabel functionnality to reduce the size of each label. Plus we sort them + alphabetically and therefore, we also sort the lines the same way. + + Args: + labels (list[str]): Labels to use ax.legend() like + ["Region1__TemperatureAvg__K__job_123456", "Region1__PressureMin__Pa__job_123456"] + lines (list[lines.Line2D]): Lines plotted on axes of matplotlib figure like [line1, line2] + userChoices (dict[str, Any]): Choices made by widget selection + in PythonViewConfigurator filter. + + Returns: + tuple[list[str], list[lines.Line2D]]: Improved labels and sorted labels / lines like + (["Region1 Pmin", "Region1 Tavg"], [line2, line1]) + """ + smartLabels: list[ str ] = [ smartLabel( label, userChoices ) for label in labels ] + # I need the labels to be ordered alphabetically for better readability of the legend + # Therefore, if I sort smartLabels, I need to also sort lines with the same order. + # But this can only be done if there are no duplicates of labels in smartLabels. + # If a duplicate is found, "sorted" will try to sort with line which has no comparison built in + # which will throw an error. + if len( set( smartLabels ) ) == len( smartLabels ): + sortedBothLists = sorted( zip( smartLabels, lines, strict=False ) ) + sortedLabels, sortedLines = zip( *sortedBothLists, strict=False ) + return ( list( sortedLabels ), list( sortedLines ) ) + else: + return ( smartLabels, lines ) + + +def smartLabel( label: str, userChoices: dict[ str, Any ] ) -> str: + """Shorten label according to user choices. + + Labels name can tend to be too long. Therefore, we need to reduce the size of the label. + Depending on the choices made by the user, the identifier and the job name can disappear. + + Args: + label (str): A label to be plotted. + Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out + userChoices (dict[str, Any]): user choices. + + Returns: + str: "phaseName0 in phaseName1" or "Reservoir phaseName0 in phaseName1" + or "phaseName0 in phaseName1 job123456.out" or + "Reservoir phaseName0 in phaseName1 job123456.out" + """ + # first step is to abbreviate the label to reduce its size + smartLabel: str = abbreviateLabel( label ) + # When only one source is used as input, there is no need to precise which one is used + # in the label so the job name is useless. Same when removeJobName option is selected by user. + inputNames: list[ str ] = userChoices[ "inputNames" ] + removeJobName: bool = userChoices[ "removeJobName" ] + if len( inputNames ) > 1 and not removeJobName: + jobName: str = findJobName( label ) + smartLabel += " " + jobName + # When the user chooses to split the plot into subplots to plot by region or well, + # this identifier name will appear as a title of the subplot so no need to use it. + # Same applies when user decides to remove regions. + plotRegions: bool = userChoices[ "plotRegions" ] + removeRegions: bool = userChoices[ "removeRegions" ] + if not plotRegions and not removeRegions: + smartLabel = findIdentifier( label ) + " " + smartLabel + return smartLabel + + +def abbreviateLabel( label: str ) -> str: + """Get the abbreviation of the label according to reservoir nomenclature. + + When using labels to plot, the name can tend to be too long. Therefore, to respect + the logic of reservoir engineering vocabulary, abbreviations for common property names + can be used to shorten the name. The goal is therefore to generate the right abbreviation + for the label input. + + Args: + label (str): A label to be plotted. + Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out + + Returns: + str: "phaseName0 in phaseName1" + """ + for commonAsso in commonAssociations: + if commonAsso in label.lower(): + return commonAssociations[ commonAsso ] + for phaseAsso in phasesAssociations: + if phaseAsso in label.lower(): + phases: list[ str ] = findPhasesLabel( label ) + phase0: str = "" if len( phases ) < 1 else phases[ 0 ] + phase1: str = "" if len( phases ) < 2 else phases[ 1 ] + if phaseAsso == "dissolvedmass": + return phase0 + phasesAssociations[ phaseAsso ] + phase1 + else: + return phasesAssociations[ phaseAsso ] + phase0 + return label + + +def findIdentifier( label: str ) -> str: + """Find identifier inside the label. + + When looking at a label, it may contain or not an identifier at the beginning of it. + An identifier is either a regionName or a wellName. + The goal is to find it and extract it if present. + + Args: + label (str): A label to be plotted. + Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out + + Returns: + str: "Reservoir" + """ + identifier: str = "" + if "__" not in label: + print( "Invalid label, cannot search identifier when no '__' in label." ) + return identifier + subParts: list[ str ] = label.split( "__" ) + if len( subParts ) == 4: + identifier = subParts[ 0 ] + return identifier + + +def findJobName( label: str ) -> str: + """Find the Geos job name at the end of the label. + + When looking at a label, it may contain or not a job name at the end of it. + The goal is to find it and extract it if present. + + Args: + label (str): A label to be plotted. + Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out + + Returns: + str: "job123456.out" + """ + jobName: str = "" + if "__" not in label: + print( "Invalid label, cannot search jobName when no '__' in label." ) + return jobName + subParts: list[ str ] = label.split( "__" ) + if len( subParts ) == 4: + jobName = subParts[ 3 ] + return jobName + + +def findPhasesLabel( label: str ) -> list[ str ]: + """Find phase name inside label. + + When looking at a label, it may contain or not patterns that indicates + the presence of a phase name within it. Therefore, if one of these patterns + is present, one or multiple phase names can be found and be extracted. + + Args: + label (str): A label to be plotted. + Example- Reservoir__DissolvedMassphaseName0InphaseName1__kg__job123456.out + + Returns: + list[str]: [phaseName0, phaseName1] + """ + phases: list[ str ] = [] + lowLabel: str = label.lower() + indexStart: int = 0 + indexEnd: int = 0 + if "__" not in label: + print( "Invalid label, cannot search phases when no '__' in label." ) + return phases + if "dissolvedmass" in lowLabel: + indexStart = lowLabel.index( "dissolvedmass" ) + len( "dissolvedmass" ) + indexEnd = lowLabel.rfind( "__" ) + phasesSubstring: str = lowLabel[ indexStart:indexEnd ] + phases = phasesSubstring.split( "in" ) + phases = [ phase.capitalize() for phase in phases ] + else: + if "dynamicporevolume" in lowLabel: + indexStart = lowLabel.index( "__" ) + 2 + indexEnd = lowLabel.index( "dynamicporevolume" ) + else: + for pattern in [ "nontrapped", "trapped", "immobile", "mobile", "rate" ]: + if pattern in lowLabel: + indexStart = lowLabel.index( pattern ) + len( pattern ) + indexEnd = lowLabel.rfind( "mass" ) + if indexEnd < 0: + indexEnd = indexStart + lowLabel[ indexStart: ].find( "__" ) + break + if indexStart < indexEnd: + phases = [ lowLabel[ indexStart:indexEnd ].capitalize() ] + return phases + + +""" +Under this is the first version of smartLabels without abbreviations. +""" + +# def smartLegendLabelsAndLines( +# labelNames: list[str], lines: list[Any], userChoices: dict[str, Any], regionName="" +# ) -> tuple[list[str], list[Any]]: +# """To improve readability of the legend for an axe in ParaView, we can apply the +# smartLegendLabel functionnality to reduce the size of each label. Plus we sort them +# alphabetically and therefore, we also sort the lines the same way. + +# Args: +# labelNames (list[str]): Labels to use ax.legend() like +# ["Region1__PressureMin__Pa__job_123456", "Region1__Temperature__K__job_123456"] +# lines (list[Any]): Lines plotted on axes of matplotlib figure like [line1, line2] +# userChoices (dict[str, Any]): Choices made by widget selection +# in PythonViewConfigurator filter. +# regionName (str, optional): name of the region. Defaults to "". + +# Returns: +# tuple[list[str], list[Any]]: Improved labels and sorted labels / lines like +# (["Temperature K", "PressureMin Pa"], [line2, line1]) +# """ +# smartLabels: list[str] = [ +# smartLegendLabel(labelName, userChoices, regionName) for labelName in labelNames +# ] +# # I need the labels to be ordered alphabetically for better readability of the legend +# # Therefore, if I sort smartLabels, I need to also sort lines with the same order +# sortedBothLists = sorted(zip(smartLabels, lines) +# sortedLabels, sortedLines = zip(*sortedBothLists) +# return (sortedLabels, sortedLines) + +# def smartLegendLabel(labelName: str, userChoices: dict[str, Any], regionName="") -> str: +# """When plotting legend label, the label format can be improved by removing some +# overwhelming / repetitive prefixe / suffixe and have a shorter label. + +# Args: +# labelName (str): Label to use ax.legend() like +# Region1__PressureMin__Pa__job_123456 +# userChoices (dict[str, Any]): Choices made by widget selection +# in PythonViewConfigurator filter. +# regionName (str, optional): name of the region. Defaults to "". + +# Returns: +# str: Improved label name like PressureMin Pa. +# """ +# smartLabel: str = "" +# # When only one source is used as input, there is no need to precise which one +# # is used in the label. Same when removeJobName option is selected by user. +# inputNames: list[str] = userChoices["inputNames"] +# removeJobName: bool = userChoices["removeJobName"] +# if len(inputNames) <= 1 or removeJobName: +# smartLabel = removeJobNameInLegendLabel(labelName, inputNames) +# # When the user chooses to split the plot into subplots to plot by region, +# # the region name will appear as a title of the subplot so no need to use it. +# # Same applies when user decides to remove regions. +# plotRegions: bool = userChoices["plotRegions"] +# removeRegions: bool = userChoices["removeRegions"] +# if plotRegions or removeRegions: +# smartLabel = removeIdentifierInLegendLabel(smartLabel, regionName) +# smartLabel = smartLabel.replace("__", " ") +# return smartLabel + +# def removeJobNameInLegendLabel(legendLabel: str, inputNames: list[str]) -> str: +# """When plotting legends, the name of the job is by default at the end of +# the label. Therefore, it can increase tremendously the size of the legend +# and we can avoid that by removing the job name from it. + +# Args: +# legendLabel (str): Label to use ax.legend() like +# Region1__PressureMin__Pa__job_123456 +# inputNames (list[str]): names of the sources use to plot. + +# Returns: +# str: Label without the job name like Region1__PressureMin__Pa. +# """ +# for inputName in inputNames: +# pattern: str = "__" + inputName +# if legendLabel.endswith(pattern): +# jobIndex: int = legendLabel.index(pattern) +# return legendLabel[:jobIndex] +# return legendLabel + +# def removeIdentifierInLegendLabel(legendLabel: str, regionName="") -> str: +# """When plotting legends, the name of the region is by default at the +# beginning of the label. Here we remove the region name from the legend label. + +# Args: +# legendLabel (str): Label to use ax.legend() like +# Region1__PressureMin__Pa__job_123456 +# regionName (str): name of the region. Defaults to "". + +# Returns: +# str: Label without the job name like PressureMin__Pa__job_123456 +# """ +# if "__" not in legendLabel: +# return legendLabel +# if regionName == "": +# firstRegionIndex: int = legendLabel.index("__") +# return legendLabel[firstRegionIndex + 2:] +# pattern: str = regionName + "__" +# if legendLabel.startswith(pattern): +# return legendLabel[len(pattern):] +# return legendLabel +""" +Other 2D tools for simplest figures +""" + + +def basicFigure( df: pd.DataFrame, variableName: str, curveName: str ) -> tuple[ figure.Figure, axes.Axes ]: + """Creates a plot. + + Generates a figure and axes objects from matplotlib that plots + one curve along the X axis, with legend and label for X and Y. + + Args: + df (pd.DataFrame): dataframe containing at least two columns, + one named "variableName" and the other "curveName" + variableName (str): Name of the variable column + curveName (str): Name of the column to display along that variable. + + Returns: + tuple[figure.Figure, axes.Axes]: the fig and the ax. + """ + fig, ax = plt.subplots() + x: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() + y: npt.NDArray[ np.float64 ] = df[ curveName ].to_numpy() + ax.plot( x, y, label=curveName ) + ax.set_xlabel( variableName ) + ax.set_ylabel( curveName ) + ax.legend( loc="best" ) + return ( fig, ax ) + + +def invertedBasicFigure( df: pd.DataFrame, variableName: str, curveName: str ) -> tuple[ figure.Figure, axes.Axes ]: + """Creates a plot with inverted XY axis. + + Generates a figure and axes objects from matplotlib that plots + one curve along the Y axis, with legend and label for X and Y. + + Args: + df (pd.DataFrame): dataframe containing at least two columns, + one named "variableName" and the other "curveName" + variableName (str): Name of the variable column + curveName (str): Name of the column to display along that variable. + + Returns: + tuple[figure.Figure, axes.Axes]: the fig and the ax. + """ + fig, ax = plt.subplots() + x: npt.NDArray[ np.float64 ] = df[ curveName ].to_numpy() + y: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() + ax.plot( x, y, label=variableName ) + ax.set_xlabel( curveName ) + ax.set_ylabel( variableName ) + ax.legend( loc="best" ) + return ( fig, ax ) + + +def adjust_subplots( fig: figure.Figure, invertXY: bool ) -> figure.Figure: + """Adjust the size of the subplot in the fig. + + Args: + fig (figure.Figure): Matplotlib figure + invertXY (bool): Choice to either intervert or not the X and Y axes + + Returns: + figure.Figure: Matplotlib figure with adjustements + """ + if invertXY: + fig.subplots_adjust( left=0.05, right=0.98, top=0.9, bottom=0.2 ) + else: + fig.subplots_adjust( left=0.06, right=0.94, top=0.95, bottom=0.08 ) + return fig diff --git a/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py b/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py new file mode 100755 index 00000000..7a12f8c6 --- /dev/null +++ b/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Alexandre Benedicto +# type: ignore +# ruff: noqa +try: + import matplotlib.pyplot as plt + from paraview import python_view + + import geos.pv.utils.paraviewTreatments as pvt + from geos.pv.pythonViewUtils.Figure2DGenerator import ( + Figure2DGenerator, ) + + plt.close() + if len( sourceNames ) == 0: # noqa: F821 + raise ValueError( "No source name was found. Please check at least" + " one source in <>" ) + + dataframes = pvt.getDataframesFromMultipleVTKSources( + sourceNames, + variableName # noqa: F821 + ) + dataframe = pvt.mergeDataframes( dataframes, variableName ) # noqa: F821 + obj_figure = Figure2DGenerator( dataframe, userChoices ) # noqa: F821 + fig = obj_figure.getFigure() + + def setup_data( view ) -> None: # noqa + pass + + def render( view, width: int, height: int ): # noqa + fig.set_size_inches( float( width ) / 100.0, float( height ) / 100.0 ) + imageToReturn = python_view.figure_to_image( fig ) + return imageToReturn + +except Exception as e: + from geos.utils.Logger import getLogger + + logger = getLogger( "Python View Configurator" ) + logger.critical( e, exc_info=True ) From bd5bbd5955b20c025cee24ca8881b2685fb120cd Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 25 Jul 2025 14:22:45 +0200 Subject: [PATCH 5/9] Update to the new architectur --- .../{PVplugins => geos/pv/plugins}/PVPythonViewConfigurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename geos-pv/src/{PVplugins => geos/pv/plugins}/PVPythonViewConfigurator.py (97%) diff --git a/geos-pv/src/PVplugins/PVPythonViewConfigurator.py b/geos-pv/src/geos/pv/plugins/PVPythonViewConfigurator.py similarity index 97% rename from geos-pv/src/PVplugins/PVPythonViewConfigurator.py rename to geos-pv/src/geos/pv/plugins/PVPythonViewConfigurator.py index 23ac6726..cf6b36ba 100755 --- a/geos-pv/src/PVplugins/PVPythonViewConfigurator.py +++ b/geos-pv/src/geos/pv/plugins/PVPythonViewConfigurator.py @@ -10,7 +10,7 @@ from typing_extensions import Self # 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 From 51f4b4b7e58c5002894a6426f791a7db29ef6257 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 25 Jul 2025 17:07:40 +0200 Subject: [PATCH 6/9] Fix logger issue on paraview --- .../geos/pv/plugins/PVPythonViewConfigurator.py | 2 +- .../pv/pythonViewUtils/Figure2DGenerator.py | 8 ++++---- .../geos/pv/pythonViewUtils/mainPythonView.py | 17 ++++++++++++----- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/geos-pv/src/geos/pv/plugins/PVPythonViewConfigurator.py b/geos-pv/src/geos/pv/plugins/PVPythonViewConfigurator.py index cf6b36ba..ca7940d3 100755 --- a/geos-pv/src/geos/pv/plugins/PVPythonViewConfigurator.py +++ b/geos-pv/src/geos/pv/plugins/PVPythonViewConfigurator.py @@ -2,8 +2,8 @@ # SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. # SPDX-FileContributor: Alexandre Benedicto, Martin Lemay # ruff: noqa: E402 # disable Module level import not at top of file -from pathlib import Path import sys +from pathlib import Path from typing import Any, Union, cast import pandas as pd # type: ignore[import-untyped] diff --git a/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py b/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py index bd6749f7..836cefb9 100755 --- a/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py +++ b/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py @@ -1,11 +1,10 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. # SPDX-FileContributor: Alexandre Benedicto - +from logging import Logger from typing import Any import pandas as pd # type: ignore[import-untyped] -from geos.utils.Logger import Logger, getLogger from matplotlib import axes, figure, lines # type: ignore[import-untyped] from matplotlib.font_manager import ( # type: ignore[import-untyped] FontProperties, # type: ignore[import-untyped] @@ -17,7 +16,7 @@ class Figure2DGenerator: - def __init__( self: Self, dataframe: pd.DataFrame, userChoices: dict[ str, list[ str ] ] ) -> None: + def __init__( self: Self, dataframe: pd.DataFrame, userChoices: dict[ str, list[ str ] ], logger: Logger ) -> None: """Utility to create cross plots using Python View. We want to plot f(X) = Y where in this class, @@ -26,6 +25,7 @@ def __init__( self: Self, dataframe: pd.DataFrame, userChoices: dict[ str, list[ Args: dataframe (pd.DataFrame): data to plot userChoices (dict[str, list[str]]): user choices. + logger (Logger): Logger to use. """ self.m_dataframe: pd.DataFrame = dataframe self.m_userChoices: dict[ str, Any ] = userChoices @@ -33,7 +33,7 @@ def __init__( self: Self, dataframe: pd.DataFrame, userChoices: dict[ str, list[ self.m_axes: list[ axes._axes.Axes ] = [] self.m_lines: list[ lines.Line2D ] = [] self.m_labels: list[ str ] = [] - self.m_logger: Logger = getLogger( "Python View Configurator" ) + self.m_logger: Logger = logger try: # apply minus 1 multiplication on certain columns diff --git a/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py b/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py index 7a12f8c6..853451da 100755 --- a/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py +++ b/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py @@ -3,6 +3,16 @@ # SPDX-FileContributor: Alexandre Benedicto # type: ignore # ruff: noqa +from logging import Logger, getLogger, INFO +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 + +logger: Logger = getLogger( "Python View Configurator" ) +logger.setLevel( INFO ) +vtkHandler: VTKHandler = VTKHandler() +logger.addHandler( vtkHandler ) + try: import matplotlib.pyplot as plt from paraview import python_view @@ -20,7 +30,7 @@ variableName # noqa: F821 ) dataframe = pvt.mergeDataframes( dataframes, variableName ) # noqa: F821 - obj_figure = Figure2DGenerator( dataframe, userChoices ) # noqa: F821 + obj_figure = Figure2DGenerator( dataframe, userChoices, logger ) # noqa: F821 fig = obj_figure.getFigure() def setup_data( view ) -> None: # noqa @@ -32,7 +42,4 @@ def render( view, width: int, height: int ): # noqa return imageToReturn except Exception as e: - from geos.utils.Logger import getLogger - - logger = getLogger( "Python View Configurator" ) - logger.critical( e, exc_info=True ) + logger.critical( e, exc_info=True ) From a0f10ac4bab71a5eee290509ff04f87028741f6d Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 25 Jul 2025 18:19:32 +0200 Subject: [PATCH 7/9] fix ci issue --- geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py b/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py index 853451da..55d01f2f 100755 --- a/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py +++ b/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py @@ -6,7 +6,7 @@ from logging import Logger, getLogger, INFO from paraview.detail.loghandler import ( # type: ignore[import-not-found] VTKHandler, -) # source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/detail/loghandler.py +) # source: https://github.com/Kitware/ParaView/blob/master/Wrapping/Python/paraview/detail/loghandler.py logger: Logger = getLogger( "Python View Configurator" ) logger.setLevel( INFO ) @@ -42,4 +42,4 @@ def render( view, width: int, height: int ): # noqa return imageToReturn except Exception as e: - logger.critical( e, exc_info=True ) + logger.critical( e, exc_info=True ) From 9c7f48d6ee0f94a246cb82e9992fa9bb2ecaab73 Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Fri, 25 Jul 2025 18:24:33 +0200 Subject: [PATCH 8/9] Remove a test file --- geos-pv/src/PVplugins/example.py | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 geos-pv/src/PVplugins/example.py diff --git a/geos-pv/src/PVplugins/example.py b/geos-pv/src/PVplugins/example.py deleted file mode 100644 index 92acb71c..00000000 --- a/geos-pv/src/PVplugins/example.py +++ /dev/null @@ -1,4 +0,0 @@ -import sys -from pathlib import Path -geos_pv_path: Path = Path( __file__ ).parent.parent.parent -print(geos_pv_path) \ No newline at end of file From 8b6e51ab8fbc9e337bee83fb505f0432bd9869ab Mon Sep 17 00:00:00 2001 From: Romain Baville Date: Wed, 30 Jul 2025 11:23:51 +0200 Subject: [PATCH 9/9] Clean the doc --- .../pv/plugins/PVPythonViewConfigurator.py | 115 ++++++----- .../pv/pythonViewUtils/Figure2DGenerator.py | 12 +- .../functionsFigure2DGenerator.py | 183 +++++++++--------- .../geos/pv/pythonViewUtils/mainPythonView.py | 2 +- 4 files changed, 156 insertions(+), 156 deletions(-) diff --git a/geos-pv/src/geos/pv/plugins/PVPythonViewConfigurator.py b/geos-pv/src/geos/pv/plugins/PVPythonViewConfigurator.py index ca7940d3..ad008a63 100755 --- a/geos-pv/src/geos/pv/plugins/PVPythonViewConfigurator.py +++ b/geos-pv/src/geos/pv/plugins/PVPythonViewConfigurator.py @@ -71,24 +71,24 @@ def __init__( self: Self ) -> None: Input is a vtkDataObject. """ super().__init__( nInputPorts=1, nOutputPorts=1 ) - # python view layout and object + # Python view layout and object. self.m_layoutName: str = "" self.m_pythonView: Any self.m_organizationDisplay = DisplayOrganizationParaview() self.buildNewLayoutWithPythonView() - # input source and curve names + # Input source and curve names. inputSource = GetActiveSource() dataset = servermanager.Fetch( inputSource ) dataframe: pd.DataFrame = pvt.vtkToDataframe( dataset ) self.m_pathPythonViewScript: Path = geos_pv_path / "src/geos/pv/pythonViewUtils/mainPythonView.py" - # checkboxes + # Checkboxes. self.m_modifyInputs: int = 1 self.m_modifyCurves: int = 1 self.m_multiplyCurves: int = 0 - # checkboxes curves available from the data of pipeline + # Checkboxes curves available from the data of pipeline. self.m_validSources = vtkDataArraySelection() self.m_curvesToPlot = vtkDataArraySelection() self.m_curvesMinus1 = vtkDataArraySelection() @@ -102,8 +102,8 @@ def __init__( self: Self ) -> None: for name in list( dataframe.columns ): for axis in [ "X", "Y", "Z" ]: if "Points" + axis in name and "Points" + axis + "__" in name: - positionDoublon: int = validColumnsDataframe.index( "Points" + axis ) - validColumnsDataframe.pop( positionDoublon ) + doublePosition: int = validColumnsDataframe.index( "Points" + axis ) + validColumnsDataframe.pop( doublePosition ) break self.m_validColumnsDataframe: list[ str ] = sorted( validColumnsDataframe, key=lambda x: x.lower() ) for curveName in validColumnsDataframe: @@ -113,7 +113,7 @@ def __init__( self: Self ) -> None: self.m_curvesToPlot.DisableAllArrays() self.m_curvesMinus1.DisableAllArrays() self.m_curveToUse: str = "" - # to change the aspects of curves + # To change the aspects of curves. self.m_curvesToModify: set[ str ] = pvt.integrateSourceNames( validSourceNames, set( validColumnsDataframe ) ) self.m_color: tuple[ float, float, float ] = ( 0.0, 0.0, 0.0 ) self.m_lineStyle: str = LineStyleEnum.SOLID.optionValue @@ -121,7 +121,7 @@ def __init__( self: Self ) -> None: self.m_markerStyle: str = MarkerStyleEnum.NONE.optionValue self.m_markerSize: float = 1.0 - # user choices + # User choices. self.m_userChoices: dict[ str, Any ] = { "variableName": "", "curveNames": [], @@ -149,7 +149,7 @@ def getUserChoices( self: Self ) -> dict[ str, Any ]: """Access the m_userChoices attribute. Returns: - dict[str] : the user choices for the figure. + dict[str] : The user choices for the figure. """ return self.m_userChoices @@ -157,10 +157,10 @@ def getInputNames( self: Self ) -> set[ str ]: """Get source names from user selection. Returns: - set[str] : source names from ParaView pipeline. + set[str] : Source names from ParaView pipeline. """ - inputAvailables = self.a01GetInputSources() - inputNames: set[ str ] = set( pvt.getArrayChoices( inputAvailables ) ) + inputAvailable = self.a01GetInputSources() + inputNames: set[ str ] = set( pvt.getArrayChoices( inputAvailable ) ) return inputNames def defineInputNames( self: Self ) -> None: @@ -209,13 +209,13 @@ def buildPythonViewScript( self: Self ) -> str: def buildNewLayoutWithPythonView( self: Self ) -> None: """Create a new Python View layout.""" - # we first built the new layout + # We first built the new layout. layout_names: list[ str ] = self.m_organizationDisplay.getLayoutsNames() nb_layouts: int = len( layout_names ) - # imagine two layouts already exists, the new one will be named "Layout #3" + # Imagine two layouts already exists, the new one will be named "Layout #3". layoutName: str = "Layout #" + str( nb_layouts + 1 ) - # check that we that the layoutName is new and does not belong to the list of layout_names, - # if not we modify the layoutName until it is a new one + # Check that we that the layoutName is new and does not belong to the list of layout_names, + # if not we modify the layoutName until it is a new one. if layoutName in layout_names: cpt: int = 2 while layoutName in layout_names: @@ -224,23 +224,23 @@ def buildNewLayoutWithPythonView( self: Self ) -> None: self.m_organizationDisplay.addLayout( layoutName ) self.m_layoutName = layoutName - # we then build the new python view + # We then build the new python view. self.m_organizationDisplay.addViewToLayout( "PythonView", layoutName, 0 ) self.m_pythonView = self.m_organizationDisplay.getLayoutViews()[ layoutName ][ 0 ] Show( GetActiveSource(), self.m_pythonView, "PythonRepresentation" ) - # widgets definition + # Widgets definition """The names of the @smproperty methods command names below have a letter in lower case in front because PARAVIEW displays properties in the alphabetical order. See https://gitlab.kitware.com/paraview/paraview/-/issues/21493 for possible improvements on - this issue""" + this issue.""" @smproperty.dataarrayselection( name="InputSources" ) def a01GetInputSources( self: Self ) -> vtkDataArraySelection: """Get all valid sources for the filter. Returns: - vtkDataArraySelection: valid data sources. + vtkDataArraySelection: Valid data sources. """ return self.m_validSources @@ -256,7 +256,7 @@ def b00GetCurvesAvailable( self: Self ) -> list[ str ]: """Get the available curves. Returns: - list[str]: list of curves. + list[str]: List of curves. """ return self.m_validColumnsDataframe @@ -269,7 +269,7 @@ def b01SetVariableName( self: Self, name: str ) -> None: """Set the name of X axis variable. Args: - name: name of the variable. + name (str): Name of the variable. """ self.m_userChoices[ "variableName" ] = name self.Modified() @@ -279,7 +279,7 @@ def b02GetCurvesToPlot( self: Self ) -> vtkDataArraySelection: """Get the curves to plot. Returns: - vtkDataArraySelection: data to plot. + vtkDataArraySelection: Data to plot. """ return self.m_curvesToPlot @@ -289,7 +289,7 @@ def b03SetPlotsPerRegion( self: Self, boolean: bool ) -> None: """Set plot per region option. Args: - boolean: user choice. + boolean (bool): User choice. """ self.m_userChoices[ "plotRegions" ] = boolean self.Modified() @@ -313,7 +313,7 @@ def b05SetCurveConvention( self: Self, boolean: bool ) -> None: """Select Curves To Change Convention. Args: - boolean: user choice. + boolean (bool): User choice. """ self.m_multiplyCurves = boolean @@ -329,7 +329,7 @@ def b07GetCurveConvention( self: Self ) -> vtkDataArraySelection: """Get the curves to change convention. Returns: - vtkDataArraySelection: selected curves to change convention. + vtkDataArraySelection: Selected curves to change convention. """ return self.m_curvesMinus1 @@ -349,7 +349,7 @@ def c01SetEditAxisProperties( self: Self, boolean: bool ) -> None: """Set option to edit axis properties. Args: - boolean (bool): user choice. + boolean (bool): User choice. """ self.Modified() @@ -366,7 +366,7 @@ def c02SetReverseXY( self: Self, boolean: bool ) -> None: """Set option to reverse X and Y axes. Args: - boolean (bool): user choice. + boolean (bool): User choice. """ self.m_userChoices[ "reverseXY" ] = boolean self.Modified() @@ -377,7 +377,7 @@ def c03SetReverseXY( self: Self, boolean: bool ) -> None: """Set option to log scale for X axis. Args: - boolean (bool): user choice. + boolean (bool): User choice. """ self.m_userChoices[ "logScaleX" ] = boolean self.Modified() @@ -399,7 +399,7 @@ def c05SetMinorticks( self: Self, boolean: bool ) -> None: """Set option to display minor ticks. Args: - boolean (bool): user choice. + boolean (bool): User choice. """ self.m_userChoices[ "minorticks" ] = boolean self.Modified() @@ -410,7 +410,7 @@ def c06SetCustomAxisLim( self: Self, boolean: bool ) -> None: """Set option to define axis limits. Args: - boolean (bool): user choice. + boolean (bool): User choice. """ self.m_userChoices[ "customAxisLim" ] = boolean self.Modified() @@ -500,7 +500,7 @@ def d01SetDisplayTitle( self: Self, boolean: bool ) -> None: """Set option to display title. Args: - boolean (bool): user choice. + boolean (bool): User choice. """ self.m_userChoices[ "displayTitle" ] = boolean self.Modified() @@ -517,7 +517,7 @@ def d03SetTitlePlot( self: Self, title: str ) -> None: """Set title. Args: - title (str): title. + title (str): Title. """ self.m_userChoices[ "title" ] = title self.Modified() @@ -528,7 +528,7 @@ def d04SetTitleStyle( self: Self, value: int ) -> None: """Set title font style. Args: - value (int): title font style index in FontStyleEnum. + value (int): Title font style index in FontStyleEnum. """ choice = list( FontStyleEnum )[ value ] self.m_userChoices[ "titleStyle" ] = choice.optionValue @@ -540,7 +540,7 @@ def d05SetTitleWeight( self: Self, value: int ) -> None: """Set title font weight. Args: - value (int): title font weight index in FontWeightEnum. + value (int): Title font weight index in FontWeightEnum. """ choice = list( FontWeightEnum )[ value ] self.m_userChoices[ "titleWeight" ] = choice.optionValue @@ -552,7 +552,7 @@ def d06SetTitleSize( self: Self, size: float ) -> None: """Set title font size. Args: - size (float): title font size between 1 and 50. + size (float): Title font size between 1 and 50. """ self.m_userChoices[ "titleSize" ] = size self.Modified() @@ -576,7 +576,7 @@ def e00SetDisplayLegend( self: Self, boolean: bool ) -> None: """Set option to display legend. Args: - boolean (bool): user choice. + boolean (bool): User choice. """ self.m_userChoices[ "displayLegend" ] = boolean self.Modified() @@ -594,7 +594,7 @@ def e02SetLegendPosition( self: Self, value: int ) -> None: """Set legend position. Args: - value (int): legend position index in LegendLocationEnum. + value (int): Legend position index in LegendLocationEnum. """ choice = list( LegendLocationEnum )[ value ] self.m_userChoices[ "legendPosition" ] = choice.optionValue @@ -606,7 +606,7 @@ def e03SetLegendSize( self: Self, size: float ) -> None: """Set legend font size. Args: - size (float): legend font size between 1 and 50. + size (float): Legend font size between 1 and 50. """ self.m_userChoices[ "legendSize" ] = size self.Modified() @@ -617,7 +617,7 @@ def e04SetRemoveJobName( self: Self, boolean: bool ) -> None: """Set option to remove job names from legend. Args: - boolean (bool): user choice. + boolean (bool): User choice. """ self.m_userChoices[ "removeJobName" ] = boolean self.Modified() @@ -632,7 +632,7 @@ def e05SetRemoveRegionsName( self: Self, boolean: bool ) -> None: """Set option to remove region names from legend. Args: - boolean (bool): user choice. + boolean (bool): User choice. """ self.m_userChoices[ "removeRegions" ] = boolean self.Modified() @@ -655,7 +655,7 @@ def f01SetModifyCurvesAspect( self: Self, boolean: bool ) -> None: """Set option to change curve aspects. Args: - boolean (bool): user choice. + boolean (bool): User choice. """ self.m_modifyCurvesAspect = boolean @@ -671,11 +671,10 @@ def f03GetCurveNames( self: Self ) -> list[ str ]: """Get curves to modify aspects. Returns: - set[str]: curves to modify aspects. + set[str]: Curves to modify aspects. """ return list( self.m_curvesToModify ) - # TODO: still usefull? @smproperty.stringvector( name="CurveToModify", number_of_elements="1" ) @smdomain.xml( """ None: """Set m_curveToUse. Args: - value (float): value of m_curveToUse + value (float): Value of m_curveToUse. """ self.m_curveToUse = value self.Modified() @@ -700,7 +699,7 @@ def f05SetLineStyle( self: Self, value: int ) -> None: """Set line style. Args: - value (int): line style index in LineStyleEnum + value (int): Line style index in LineStyleEnum. """ choice = list( LineStyleEnum )[ value ] self.m_lineStyle = choice.optionValue @@ -712,7 +711,7 @@ def f06SetLineWidth( self: Self, value: float ) -> None: """Set line width. Args: - value (float): line width between 1 and 10. + value (float): Line width between 1 and 10. """ self.m_lineWidth = value self.Modified() @@ -723,7 +722,7 @@ def f07SetMarkerStyle( self: Self, value: int ) -> None: """Set marker style. Args: - value (int): Marker style index in MarkerStyleEnum + value (int): Marker style index in MarkerStyleEnum. """ choice = list( MarkerStyleEnum )[ value ] self.m_markerStyle = choice.optionValue @@ -735,7 +734,7 @@ def f08SetMarkerSize( self: Self, value: float ) -> None: """Set marker size. Args: - value (float): size of markers between 1 and 30. + value (float): Size of markers between 1 and 30. """ self.m_markerSize = value self.Modified() @@ -783,7 +782,7 @@ def getCurveAspect( self: Self, ) -> tuple[ tuple[ float, float, float ], str, f """Get curve aspect properties according to user choices. Returns: - tuple: (color, linestyle, linewidth, marker, markersize) + tuple: (color, lineStyle, linewidth, marker, markerSize) """ return ( self.m_color, @@ -797,8 +796,8 @@ def FillInputPortInformation( self: Self, port: int, info: vtkInformation ) -> i """Inherited from VTKPythonAlgorithmBase::RequestInformation. Args: - port (int): input port - info (vtkInformationVector): info + port (int): Input port. + info (vtkInformationVector): Info. Returns: int: 1 if calculation successfully ended, 0 otherwise. @@ -818,9 +817,9 @@ def RequestDataObject( """Inherited from VTKPythonAlgorithmBase::RequestDataObject. Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects + request (vtkInformation): Request. + inInfoVec (list[vtkInformationVector]): Input objects. + outInfoVec (vtkInformationVector): Output objects. Returns: int: 1 if calculation successfully ended, 0 otherwise. @@ -842,9 +841,9 @@ def RequestData( """Inherited from VTKPythonAlgorithmBase::RequestData. Args: - request (vtkInformation): request - inInfoVec (list[vtkInformationVector]): input objects - outInfoVec (vtkInformationVector): output objects + request (vtkInformation): Request. + inInfoVec (list[vtkInformationVector]): Input objects. + outInfoVec (vtkInformationVector): Output objects. Returns: int: 1 if calculation successfully ended, 0 otherwise. diff --git a/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py b/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py index 836cefb9..00eceded 100755 --- a/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py +++ b/geos-pv/src/geos/pv/pythonViewUtils/Figure2DGenerator.py @@ -23,8 +23,8 @@ def __init__( self: Self, dataframe: pd.DataFrame, userChoices: dict[ str, list[ "X" will be called "variable", "Y" will be called "curves". Args: - dataframe (pd.DataFrame): data to plot - userChoices (dict[str, list[str]]): user choices. + dataframe (pd.DataFrame): Data to plot. + userChoices (dict[str, list[str]]): User choices. logger (Logger): Logger to use. """ self.m_dataframe: pd.DataFrame = dataframe @@ -36,11 +36,11 @@ def __init__( self: Self, dataframe: pd.DataFrame, userChoices: dict[ str, list[ self.m_logger: Logger = logger try: - # apply minus 1 multiplication on certain columns + # Apply minus 1 multiplication on certain columns. self.initMinus1Multiplication() - # defines m_fig, m_axes, m_lines and m_lables + # Defines m_fig, m_axes, m_lines and m_labels. self.plotInitialFigure() - # then to edit and customize the figure + # Then to edit and customize the figure. self.enhanceFigure() self.m_logger.info( "Data were successfully plotted." ) @@ -129,7 +129,7 @@ def changeAxisLimits( self: Self ) -> None: ax.set_ylim( ymin, ymax ) def getFigure( self: Self ) -> figure.Figure: - """Acces the m_fig attribute. + """access the m_fig attribute. Returns: figure.Figure: Figure containing all the plots. diff --git a/geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py b/geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py index 4b629e9b..11395341 100755 --- a/geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py +++ b/geos-pv/src/geos/pv/pythonViewUtils/functionsFigure2DGenerator.py @@ -28,14 +28,14 @@ def oneSubplot( generates a fig and its list of axes with the data plotted. Args: - df (pd.DataFrame): dataframe containing at least two columns, - one named "variableName" and the other "curveName" + df (pd.DataFrame): Dataframe containing at least two columns, + one named "variableName" and the other "curveName". userChoices (dict[str, Any]): Choices made by widget selection in PythonViewConfigurator filter. Returns: - tuple[figure.Figure, list[axes.Axes], - list[lines.Line2D] , list[str]]: the fig and its list of axes. + tuple(figure.Figure, list[axes.Axes], + list[lines.Line2D] , list[str]): The fig and its list of axes. """ curveNames: list[ str ] = userChoices[ "curveNames" ] variableName: str = userChoices[ "variableName" ] @@ -78,14 +78,14 @@ def oneSubplotInverted( generates a fig and its list of axes with the data plotted. Args: - df (pd.DataFrame): dataframe containing at least two columns, - one named "variableName" and the other "curveName" + df (pd.DataFrame): Dataframe containing at least two columns, + one named "variableName" and the other "curveName". userChoices (dict[str, Any]): Choices made by widget selection in PythonViewConfigurator filter. Returns: - tuple[figure.Figure, list[axes.Axes], - list[lines.Line2D] , list[str]]: the fig and its list of axes. + tuple(figure.Figure, list[axes.Axes], + list[lines.Line2D] , list[str]): The fig and its list of axes. """ curveNames: list[ str ] = userChoices[ "curveNames" ] variableName: str = userChoices[ "variableName" ] @@ -128,14 +128,14 @@ def multipleSubplots( generates a fig and its list of axes with the data plotted. Args: - df (pd.DataFrame): dataframe containing at least two columns, + df (pd.DataFrame): Dataframe containing at least two columns, one named "variableName" and the other "curveName". userChoices (dict[str, Any]): Choices made by widget selection in PythonViewConfigurator filter. Returns: - tuple[figure.Figure, list[axes.Axes], - list[lines.Line2D] , list[str]]: the fig and its list of axes. + tuple(figure.Figure, list[axes.Axes], + list[lines.Line2D] , list[str]): The fig and its list of axes. """ curveNames: list[ str ] = userChoices[ "curveNames" ] variableName: str = userChoices[ "variableName" ] @@ -203,14 +203,14 @@ def multipleSubplotsInverted( generates a fig and its list of axes with the data plotted. Args: - df (pd.DataFrame): dataframe containing at least two columns, + df (pd.DataFrame): Dataframe containing at least two columns, one named "variableName" and the other "curveName". userChoices (dict[str, Any]): Choices made by widget selection in PythonViewConfigurator filter. Returns: - tuple[figure.Figure, list[axes.Axes], - list[lines.Line2D] , list[str]]: the fig and its list of axes. + tuple(figure.Figure, list[axes.Axes], + list[lines.Line2D] , list[str]): The fig and its list of axes. """ curveNames: list[ str ] = userChoices[ "curveNames" ] variableName: str = userChoices[ "variableName" ] @@ -219,7 +219,7 @@ def multipleSubplotsInverted( ratio: float = userChoices[ "ratio" ] assosIdentifiers: dict[ str, dict[ str, list[ str ] ] ] = associationIdentifiers( curveNames ) nbr_suplots: int = len( assosIdentifiers.keys() ) - # if only one subplots needs to be created + # If only one subplots needs to be created. if nbr_suplots == 1: return oneSubplotInverted( df, userChoices ) @@ -230,7 +230,7 @@ def multipleSubplotsInverted( fig.delaxes( axs[ -( i + 1 ) ] ) all_lines: list[ lines.Line2D ] = [] all_labels: list[ str ] = [] - # first loop for subplots + # First loop for subplots. propertiesExtremas: dict[ str, tuple[ float, float ] ] = ( findExtremasPropertiesForAssociatedIdentifiers( df, assosIdentifiers, True ) ) for j, identifier in enumerate( assosIdentifiers.keys() ): @@ -242,7 +242,7 @@ def multipleSubplotsInverted( labels: list[ str ] = [] cpt_cmap: int = 0 y: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() - # second loop for axes per subplot + # Second loop for axes per subplot. for cpt_ax, ( ax_name, propertyNames ) in enumerate( associatedProperties.items() ): ax_to_use: axes.Axes = setupAxeToUse( all_ax, cpt_ax, ax_name, True ) for propName in propertyNames: @@ -275,16 +275,16 @@ def setupAllAxes( associatedProperties: dict[ str, list[ str ] ], axisX: bool, ) -> list[ axes.Axes ]: - """Modify axis name and ticks avec X or Y axis of all subplots. + """Modify axis name and ticks with X or Y axis of all subplots. Args: - first_ax (axes.Axes): subplot id. - variableName (str): name of the axis. - associatedProperties (dict[str, list[str]]): Name of the properties + first_ax (axes.Axes): Subplot id. + variableName (str): Name of the axis. + associatedProperties (dict[str, list[str]]): Name of the properties. axisX (bool): X (True) or Y (False) axis to modify. Returns: - list[axes.Axes]: modified subplots + list[axes.Axes]: Modified subplots. """ all_ax: list[ axes.Axes ] = [ first_ax ] if axisX: @@ -314,16 +314,16 @@ def setupAllAxes( def setupAxeToUse( all_ax: list[ axes.Axes ], axeId: int, ax_name: str, axisX: bool ) -> axes.Axes: - """Modify axis name and ticks avec X or Y axis of subplot axeId in all_ax. + """Modify axis name and ticks with X or Y axis of subplot axeId in all_ax. Args: - all_ax (list[axes.Axes]): list of all subplots - axeId (int): id of the subplot - ax_name (str): name of the X or Y axis + all_ax (list[axes.Axes]): List of all subplots. + axeId (int): Id of the subplot. + ax_name (str): Name of the X or Y axis. axisX (bool): X (True) or Y (False) axis to modify. Returns: - axes.Axes: modified subplot + axes.Axes: Modified subplot. """ ax_to_use: axes.Axes = all_ax[ axeId ] if axisX: @@ -346,18 +346,18 @@ def plotAxe( """Plot x, y data using input ax_to_use according to curvesAspect. Args: - ax_to_use (axes.Axes): subplot to use - x (npt.NDArray[np.float64]): abscissa data - y (npt.NDArray[np.float64]): ordinate data - propertyName (str): name of the property - cpt_cmap (int): colormap to use + ax_to_use (axes.Axes): Subplot to use. + x (npt.NDArray[np.float64]): Abscissa data. + y (npt.NDArray[np.float64]): Ordinate data. + propertyName (str): Name of the property. + cpt_cmap (int): Colormap to use. curvesAspect (dict[str, tuple[tuple[float, float, float],str, float, str, float]]): - user choices on curve aspect + User choices on curve aspect. """ cmap = plt.rcParams[ "axes.prop_cycle" ].by_key()[ "color" ][ cpt_cmap % 10 ] mask = np.logical_and( np.isnan( x ), np.isnan( y ) ) not_mask = ~mask - # Plot only when x and y values are not nan values + # Plot only when x and y values are not nan values. if propertyName in curvesAspect: asp: tuple[ tuple[ float, float, float ], str, float, str, float ] = curvesAspect[ propertyName ] ax_to_use.plot( @@ -378,10 +378,10 @@ def getExtremaAllAxes( axes: list[ axes.Axes ], ) -> tuple[ tuple[ float, float """Gets the limits of both X and Y axis as a 2x2 element tuple. Args: - axes (list[axes.Axes]): list of subplots to get limits. + axes (list[axes.Axes]): List of subplots to get limits. Returns: - tuple[tuple[float, float], tuple[float, float]]:: ((xMin, xMax), (yMin, yMax)) + tuple[tuple[float, float], tuple[float, float]]: ((xMin, xMax), (yMin, yMax)) """ assert len( axes ) > 0 xMin, xMax, yMin, yMax = getAxeLimits( axes[ 0 ] ) @@ -403,7 +403,7 @@ def getAxeLimits( ax: axes.Axes ) -> tuple[ float, float, float, float ]: """Gets the limits of both X and Y axis as a 4 element tuple. Args: - ax (axes.Axes): subplot to get limits. + ax (axes.Axes): Subplot to get limits. Returns: tuple[float, float, float, float]: (xMin, xMax, yMin, yMax) @@ -428,8 +428,8 @@ def findExtremasPropertiesForAssociatedIdentifiers( Args: - df (pd.DataFrame): Pandas dataframe - associatedIdentifiers (dict[str, dict[str, list[str]]]): property identifiers. + df (pd.DataFrame): Pandas dataframe. + associatedIdentifiers (dict[str, dict[str, list[str]]]): Property identifiers. offsetPlotting (bool, optional): When using the values being returned, we might want to add an offset to these values. If set to True, the offsetPercentage is taken into account. Defaults to False. @@ -446,7 +446,7 @@ def findExtremasPropertiesForAssociatedIdentifiers( } """ extremasProperties: dict[ str, tuple[ float, float ] ] = {} - # first we need to find the extrema for each property type per region + # First we need to find the extrema for each property type per region. propertyTypesExtremas: dict[ str, list[ tuple[ float, float ] ] ] = {} for associatedProperties in associatedIdentifiers.values(): extremasPerProperty: dict[ str, @@ -457,8 +457,8 @@ def findExtremasPropertiesForAssociatedIdentifiers( propertyTypesExtremas[ propertyType ] = [ extremaFound ] else: propertyTypesExtremas[ propertyType ].append( extremaFound ) - # then, once all extrema have been found for all regions, we need to figure out - # which extrema per property type is the most extreme one + # Then, once all extrema have been found for all regions, we need to figure out + # which extrema per property type is the most extreme one. for propertyType in propertyTypesExtremas: values: list[ tuple[ float, float ] ] = propertyTypesExtremas[ propertyType ] minValues: list[ float ] = [ values[ i ][ 0 ] for i in range( len( values ) ) ] @@ -482,7 +482,7 @@ def findExtremasAssociatedProperties( type and return it as a tuple. Args: - df (pd.DataFrame): Pandas dataframe + df (pd.DataFrame): Pandas dataframe. associatedProperties (dict[str, list[str]]): { "Pressure (Pa)": ["Reservoir__Pressure__Pa__Source1"], "Mass (kg)": ["CO2__Mass__kg__Source1", @@ -512,7 +512,7 @@ def findExtremasAssociatedProperties( """ -Utils for treatment of the data +Utils for treatment of the data. """ @@ -592,10 +592,10 @@ def associatePropertyToAxeType( propertyNames: list[ str ] ) -> dict[ str, list[ axeName: str = associatedType else: propIdElts: list[ str ] = propNoId.split( "__" ) - # no unit was found + # No unit was found. if len( propIdElts ) <= 2: axeName = associatedType - # there is a unit + # There is a unit. else: unit: str = propIdElts[ -2 ] axeName = associatedType + " (" + unit + ")" @@ -606,15 +606,15 @@ def associatePropertyToAxeType( propertyNames: list[ str ] ) -> dict[ str, list[ def propertiesPerIdentifier( propertyNames: list[ str ] ) -> dict[ str, list[ str ] ]: - """Extract identifiers with associatied properties. + """Extract identifiers with associated properties. From a list of property names, extracts the identifier (name of the region for flow property or name of a well for well property) and creates - a dictionnary with identifiers as keys and the properties containing them + a dictionary with identifiers as keys and the properties containing them for value in a list. Args: - propertyNames (list[str]): property names + propertyNames (list[str]): Property names. Example .. code-block:: python @@ -627,7 +627,7 @@ def propertiesPerIdentifier( propertyNames: list[ str ] ) -> dict[ str, list[ st ] Returns: - dict[str, list[str]]: property identifiers + dict[str, list[str]]: Property identifiers. Example .. code-block:: python @@ -643,26 +643,26 @@ def propertiesPerIdentifier( propertyNames: list[ str ] ) -> dict[ str, list[ st ] } """ - propsPerIdentfier: dict[ str, list[ str ] ] = {} + propsPerIdentifier: dict[ str, list[ str ] ] = {} for propertyName in propertyNames: elements: list[ str ] = propertyName.split( "__" ) identifier: str = elements[ 0 ] - if identifier not in propsPerIdentfier: - propsPerIdentfier[ identifier ] = [] - propsPerIdentfier[ identifier ].append( propertyName ) - return propsPerIdentfier + if identifier not in propsPerIdentifier: + propsPerIdentifier[ identifier ] = [] + propsPerIdentifier[ identifier ].append( propertyName ) + return propsPerIdentifier def associationIdentifiers( propertyNames: list[ str ] ) -> dict[ str, dict[ str, list[ str ] ] ]: - """Extract identifiers with associatied curves. + """Extract identifiers with associated curves. From a list of property names, extracts the identifier (name of the region for flow property or name of a well for well property) and creates - a dictionnary with identifiers as keys and the properties containing them + a dictionary with identifiers as keys and the properties containing them for value in a list. Args: - propertyNames (list[str]): property names + propertyNames (list[str]): Property names. Example .. code-block:: python @@ -691,7 +691,7 @@ def associationIdentifiers( propertyNames: list[ str ] ) -> dict[ str, dict[ str ] Returns: - dict[str, dict[str, list[str]]]: property identifiers + dict[str, dict[str, list[str]]]: Property identifiers. Example .. code-block:: python @@ -751,9 +751,9 @@ def associationIdentifiers( propertyNames: list[ str ] ) -> dict[ str, dict[ str } } """ - propsPerIdentfier: dict[ str, list[ str ] ] = propertiesPerIdentifier( propertyNames ) + propsPerIdentifier: dict[ str, list[ str ] ] = propertiesPerIdentifier( propertyNames ) assosIdentifier: dict[ str, dict[ str, list[ str ] ] ] = {} - for ident, propNames in propsPerIdentfier.items(): + for ident, propNames in propsPerIdentifier.items(): assosPropsToAxeType: dict[ str, list[ str ] ] = associatePropertyToAxeType( propNames ) assosIdentifier[ ident ] = assosPropsToAxeType return assosIdentifier @@ -763,7 +763,7 @@ def buildFontTitle( userChoices: dict[ str, Any ] ) -> FontProperties: """Builds a Fontproperties object according to user choices on title. Args: - userChoices (dict[str, Any]): customization parameters. + userChoices (dict[str, Any]): Customization parameters. Returns: FontProperties: FontProperties object for the title. @@ -782,7 +782,7 @@ def buildFontVariable( userChoices: dict[ str, Any ] ) -> FontProperties: """Builds a Fontproperties object according to user choices on variables. Args: - userChoices (dict[str, Any]): customization parameters. + userChoices (dict[str, Any]): Customization parameters. Returns: FontProperties: FontProperties object for the variable axes. @@ -801,7 +801,7 @@ def buildFontCurves( userChoices: dict[ str, Any ] ) -> FontProperties: """Builds a Fontproperties object according to user choices on curves. Args: - userChoices (dict[str, str]): customization parameters. + userChoices (dict[str, str]): Customization parameters. Returns: FontProperties: FontProperties object for the curves axes. @@ -824,19 +824,19 @@ def customizeLines( userChoices: dict[ str, Any ], labels: list[ str ], and return it with the same number of lines in the same order. Args: - userChoices (dict[str, Any]): customization parameters. - labels (list[str]): labels of lines. - linesList (list[lines.Line2D]): list of lines object. + userChoices (dict[str, Any]): Customization parameters. + labels (list[str]): Labels of lines. + linesList (list[lines.Line2D]): List of lines object. Returns: - list[lines.Line2D]: list of lines object modified. + list[lines.Line2D]: List of lines object modified. """ if "linesModified" in userChoices: - linesModifs: dict[ str, dict[ str, Any ] ] = userChoices[ "linesModified" ] + linesModified: dict[ str, dict[ str, Any ] ] = userChoices[ "linesModified" ] linesChanged: list[ lines.Line2D ] = [] for i, label in enumerate( labels ): - if label in linesModifs: - lineChanged: lines.Line2D = applyCustomizationOnLine( linesList[ i ], linesModifs[ label ] ) + if label in linesModified: + lineChanged: lines.Line2D = applyCustomizationOnLine( linesList[ i ], linesModified[ label ] ) linesChanged.append( lineChanged ) else: linesChanged.append( linesList[ i ] ) @@ -849,8 +849,8 @@ def applyCustomizationOnLine( line: lines.Line2D, parameters: dict[ str, Any ] ) """Apply modification methods on a line from parameters. Args: - line (lines.Line2D): Matplotlib Line2D - parameters (dict[str, Any]): dictionary of { + line (lines.Line2D): Matplotlib Line2D. + parameters (dict[str, Any]): Dictionary of { "linestyle": one of ["-","--","-.",":"] "linewidth": positive int "color": color code @@ -875,7 +875,7 @@ def applyCustomizationOnLine( line: lines.Line2D, parameters: dict[ str, Any ] ) """ -Layout tools for layering subplots in a figure +Layout tools for layering subplots in a figure. """ @@ -889,7 +889,7 @@ def isprime( x: int ) -> bool: bool: True if prime, False if not. """ if x < 0: - print( "Invalid number entry, needs to be positive int" ) + print( "Invalid number entry, needs to be positive int." ) return False return all( x % n != 0 for n in range( 2, int( x**0.5 ) + 1 ) ) @@ -932,7 +932,7 @@ def smartLayout( x: int, ratio: float ) -> tuple[ int, int, int ]: Args: x (int): Positive number. - ratio (float): width to height ratio of a figure. + ratio (float): Width to height ratio of a figure. Returns: tuple[int]: (nbr_rows, nbr_columns, number of axes to remove) @@ -1015,7 +1015,7 @@ def smartLabelsSorted( labels: list[ str ], lines: list[ lines.Line2D ], """Shorten all legend labels and sort them. To improve readability of the legend for an axe in ParaView, we can apply the - smartLegendLabel functionnality to reduce the size of each label. Plus we sort them + smartLegendLabel functionality to reduce the size of each label. Plus we sort them alphabetically and therefore, we also sort the lines the same way. Args: @@ -1059,7 +1059,7 @@ def smartLabel( label: str, userChoices: dict[ str, Any ] ) -> str: or "phaseName0 in phaseName1 job123456.out" or "Reservoir phaseName0 in phaseName1 job123456.out" """ - # first step is to abbreviate the label to reduce its size + # First step is to abbreviate the label to reduce its size. smartLabel: str = abbreviateLabel( label ) # When only one source is used as input, there is no need to precise which one is used # in the label so the job name is useless. Same when removeJobName option is selected by user. @@ -1207,7 +1207,7 @@ def findPhasesLabel( label: str ) -> list[ str ]: # labelNames: list[str], lines: list[Any], userChoices: dict[str, Any], regionName="" # ) -> tuple[list[str], list[Any]]: # """To improve readability of the legend for an axe in ParaView, we can apply the -# smartLegendLabel functionnality to reduce the size of each label. Plus we sort them +# smartLegendLabel functionality to reduce the size of each label. Plus we sort them # alphabetically and therefore, we also sort the lines the same way. # Args: @@ -1233,7 +1233,7 @@ def findPhasesLabel( label: str ) -> list[ str ]: # def smartLegendLabel(labelName: str, userChoices: dict[str, Any], regionName="") -> str: # """When plotting legend label, the label format can be improved by removing some -# overwhelming / repetitive prefixe / suffixe and have a shorter label. +# overwhelming / repetitive prefix / suffix and have a shorter label. # Args: # labelName (str): Label to use ax.legend() like @@ -1303,6 +1303,7 @@ def findPhasesLabel( label: str ) -> list[ str ]: # if legendLabel.startswith(pattern): # return legendLabel[len(pattern):] # return legendLabel + """ Other 2D tools for simplest figures """ @@ -1315,13 +1316,13 @@ def basicFigure( df: pd.DataFrame, variableName: str, curveName: str ) -> tuple[ one curve along the X axis, with legend and label for X and Y. Args: - df (pd.DataFrame): dataframe containing at least two columns, - one named "variableName" and the other "curveName" - variableName (str): Name of the variable column + df (pd.DataFrame): Dataframe containing at least two columns, + one named "variableName" and the other "curveName". + variableName (str): Name of the variable column. curveName (str): Name of the column to display along that variable. Returns: - tuple[figure.Figure, axes.Axes]: the fig and the ax. + tuple[figure.Figure, axes.Axes]: The fig and the ax. """ fig, ax = plt.subplots() x: npt.NDArray[ np.float64 ] = df[ variableName ].to_numpy() @@ -1340,13 +1341,13 @@ def invertedBasicFigure( df: pd.DataFrame, variableName: str, curveName: str ) - one curve along the Y axis, with legend and label for X and Y. Args: - df (pd.DataFrame): dataframe containing at least two columns, - one named "variableName" and the other "curveName" - variableName (str): Name of the variable column + df (pd.DataFrame): Dataframe containing at least two columns, + one named "variableName" and the other "curveName". + variableName (str): Name of the variable column. curveName (str): Name of the column to display along that variable. Returns: - tuple[figure.Figure, axes.Axes]: the fig and the ax. + tuple[figure.Figure, axes.Axes]: The fig and the ax. """ fig, ax = plt.subplots() x: npt.NDArray[ np.float64 ] = df[ curveName ].to_numpy() @@ -1362,11 +1363,11 @@ def adjust_subplots( fig: figure.Figure, invertXY: bool ) -> figure.Figure: """Adjust the size of the subplot in the fig. Args: - fig (figure.Figure): Matplotlib figure - invertXY (bool): Choice to either intervert or not the X and Y axes + fig (figure.Figure): Matplotlib figure. + invertXY (bool): Choice to either swap or not the X and Y axes. Returns: - figure.Figure: Matplotlib figure with adjustements + figure.Figure: Matplotlib figure with adjustments. """ if invertXY: fig.subplots_adjust( left=0.05, right=0.98, top=0.9, bottom=0.2 ) diff --git a/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py b/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py index 55d01f2f..bc7e7818 100755 --- a/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py +++ b/geos-pv/src/geos/pv/pythonViewUtils/mainPythonView.py @@ -23,7 +23,7 @@ plt.close() if len( sourceNames ) == 0: # noqa: F821 - raise ValueError( "No source name was found. Please check at least" + " one source in <>" ) + raise ValueError( "No source name was found. Please check at least one source in <>." ) dataframes = pvt.getDataframesFromMultipleVTKSources( sourceNames,