# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
# SPDX-FileContributor: Alexandre Benedicto, Martin Lemay
from typing import Any
import matplotlib.pyplot as plt # type: ignore[import-untyped]
import numpy as np
import numpy.typing as npt
from geos.geomechanics.model.MohrCircle import MohrCircle
from geos.geomechanics.model.MohrCoulomb import MohrCoulomb
from geos.utils.enumUnits import Pressure, Unit, convert
from geos.utils.GeosOutputsConstants import FAILURE_ENVELOPE
from matplotlib import ticker
from matplotlib.axes import Axes # type: ignore[import-untyped]
from matplotlib.figure import Figure # type: ignore[import-untyped]
from matplotlib.lines import Line2D # type: ignore[import-untyped]
import geos_posp.visu.mohrCircles.functionsMohrCircle as mcf
from geos_posp.visu.PVUtils.matplotlibOptions import (
FontStyleEnum,
FontWeightEnum,
LegendLocationEnum,
LineStyleEnum,
MarkerStyleEnum,
)
__doc__ = """
plotMohrCircles module provides a set of functions to plot multiple Mohr's
circles and a failure envelope from a list of MohrCircle and MohrCoulomb
objects respectively.
"""
def _plotMohrCircles(
ax: Axes,
mohrCircles: list[ MohrCircle ],
circlesAspect: dict[ str, dict[ str, Any ] ],
annotate: bool,
) -> None:
"""Plot multiple Mohr's circles on input Axes.
Args:
ax (Axes): Axes where to plot Mohr's circles
mohrCircles (list[MohrCircle]): list of MohrCircle objects to plot.
circlesAspect (dict[str, dict[str, Any]]): dictionnary defining Mohr's
circle line properties.
annotate (bool): if True, display min and max normal stress.
"""
nbPts: int = 361
ang: npt.NDArray[ np.float64 ] = np.linspace( 0.0, np.pi, nbPts ).astype( np.float64 )
for mohrCircle in mohrCircles:
radius: float = mohrCircle.getCircleRadius()
xCoords = mohrCircle.getCircleCenter() + radius * np.cos( ang )
yCoords = radius * ( np.sin( ang ) )
label: str = mohrCircle.getCircleId()
p: list[ Line2D ] # plotted lines to get the color later
if label in circlesAspect:
circleAspect: dict[ str, Any ] = circlesAspect[ label ]
color: tuple[ float, float, float ] = circleAspect.get( "color", "k" )
linestyle: str = circleAspect.get( "linestyle", LineStyleEnum.SOLID.optionValue )
linewidth: float = circleAspect.get( "linewidth", 1 )
marker: str = circleAspect.get( "marker", MarkerStyleEnum.NONE.optionValue )
markersize: float = circleAspect.get( "markersize", 1 )
p = ax.plot(
xCoords,
yCoords,
label=label,
color=color,
linestyle=linestyle,
linewidth=linewidth,
marker=marker,
markersize=markersize,
)
else:
p = ax.plot( xCoords, yCoords, label=label, linestyle="-", marker="" )
if annotate:
fontColor = p[ 0 ].get_color()
annot: tuple[ str, str, tuple[ float, float ], tuple[ float,
float ] ] = ( mcf.findAnnotateTuples( mohrCircle ) )
ax.annotate( annot[ 0 ], xy=annot[ 2 ], ha="left", rotation=30, color=fontColor )
ax.annotate( annot[ 1 ], xy=annot[ 3 ], ha="right", rotation=30, color=fontColor )
def _plotMohrCoulomb( ax: Axes, mohrCoulomb: MohrCoulomb, curvesAspect: dict[ str, Any ] ) -> None:
"""Plot Mohr-Coulomb failure envelope in input Axes object.
Args:
ax (Axes): Axes where to plot the failure envelope.
mohrCoulomb (MohrCoulomb): MohrCoulomb object to define failure envelope
parameters.
curvesAspect (dict[str, Any]): dictionnary defining line properties of
the failure envelope.
"""
xmin, xmax = ax.get_xlim()
principalStresses, shearStress = mohrCoulomb.computeFailureEnvelop( xmax )
color: tuple[ float, float, float ] = curvesAspect.get( "color", "k" )
linestyle: str = curvesAspect.get( "linestyle", LineStyleEnum.SOLID.optionValue )
linewidth: float = curvesAspect.get( "linewidth", 1 )
marker: str = curvesAspect.get( "marker", MarkerStyleEnum.NONE.optionValue )
markersize: float = curvesAspect.get( "markersize", 1 )
ax.plot(
principalStresses,
shearStress,
label="Failure Envelope",
color=color,
linestyle=linestyle,
linewidth=linewidth,
marker=marker,
markersize=markersize,
)
def _setUserChoices( ax: Axes, userChoices: dict[ str, Any ] ) -> None:
"""Set user preferences on input Axes.
Args:
ax (Axes): Axes object to modify.
userChoices (dict[str, Any]): dictionnary of user-defined properties.
"""
_updateAxis( ax, userChoices )
# set title properties
if userChoices.get( "displayTitle", False ):
updateTitle( ax, userChoices )
# set legend
if userChoices.get( "displayLegend", False ):
_updateLegend( ax, userChoices )
if userChoices.get( "customAxisLim", False ):
_updateAxisLimits( ax, userChoices )
def _updateAxis( ax: Axes, userChoices: dict[ str, Any ] ) -> None:
"""Update axis ticks and labels.
Args:
ax (Axes): axes object.
userChoices (dict[str, Any]): user parameters.
"""
# update axis labels
xlabel: str = userChoices.get( "xAxis", "Normal stress" )
ylabel: str = userChoices.get( "xAyAxisxis", "Shear stress" )
# get unit
unitChoice: int = userChoices.get( "stressUnit", 0 )
unitObj: Unit = list( Pressure )[ unitChoice ].value
unitLabel: str = unitObj.unitLabel
# change displayed units
xlabel += f" ({unitLabel})"
ylabel += f" ({unitLabel})"
ax.set_xlabel( xlabel )
ax.set_ylabel( ylabel )
# function to do conversion and set format
def _tickFormatterFunc( x: float, pos: str ) -> str:
return f"{convert(x, unitObj):.2E}"
# apply formatting to xticks and yticks
ax.xaxis.set_major_formatter( ticker.FuncFormatter( _tickFormatterFunc ) )
ax.yaxis.set_major_formatter( ticker.FuncFormatter( _tickFormatterFunc ) )
# set axis properties
ax.set_aspect( "equal", anchor="C" )
xmin, xmax = ax.get_xlim()
ax.set_xlim( 0.0 )
ax.set_ylim( 0.0, xmax )
ax.grid()
ax.minorticks_off()
if "minorticks" in userChoices and userChoices[ "minorticks" ]:
ax.minorticks_on()
[docs]
def updateTitle( ax: Axes, userChoices: dict[ str, Any ] ) -> None:
"""Update title.
Args:
ax (Axes): axes object.
userChoices (dict[str, Any]): user parameters.
"""
title = userChoices.get( "title", "Mohr's Circles" )
style = userChoices.get( "titleStyle", FontStyleEnum.NORMAL.optionValue )
weight = userChoices.get( "titleWeight", FontWeightEnum.BOLD.optionValue )
size = userChoices.get( "titleSize", 12 )
ax.set_title( title, fontstyle=style, weight=weight, fontsize=size )
def _updateLegend( ax: Axes, userChoices: dict[ str, Any ] ) -> None:
"""Update legend.
Args:
ax (Axes): axes object.
userChoices (dict[str, Any]): user parameters.
"""
loc = userChoices.get( "legendPosition", LegendLocationEnum.BEST.optionValue )
size = userChoices.get( "legendSize", 10 )
ax.legend( loc=loc, fontsize=size )
def _updateAxisLimits( ax: Axes, userChoices: dict[ str, Any ] ) -> None:
"""Update axis limits.
Args:
ax (Axes): axes object.
userChoices (dict[str, Any]): user parameters.
"""
xmin, xmax = ax.get_xlim()
if userChoices.get( "limMinX" ) is not None:
xmin = userChoices[ "limMinX" ]
if userChoices.get( "limMaxX" ) is not None:
xmax = userChoices[ "limMaxX" ]
ax.set_xlim( xmin, xmax )
ymin, ymax = ax.get_xlim()
if userChoices.get( "limMinY" ) is not None:
ymin = userChoices[ "limMinY" ]
if userChoices.get( "limMaxY" ) is not None:
ymax = userChoices[ "limMaxY" ]
ax.set_ylim( ymin, ymax )