"""Post-processor methods."""
from __future__ import annotations
import contextlib
from dataclasses import dataclass
from typing import TYPE_CHECKING
import matplotlib.pyplot as plt
import numpy as np
from quantiphy import Quantity
if TYPE_CHECKING:
import matplotlib.axes
[docs]
@contextlib.contextmanager
def plotting_context(
ax: matplotlib.axes.Axes | None = None,
pause: bool = True,
title: str = "",
aspect: bool = False,
filename: str = "",
render: bool = True,
axis_index: int | tuple[int, int] | None = None,
**kwargs,
):
"""Executes code required to set up a matplotlib figure.
Args:
ax: Axes object on which to plot
pause: If set to true, the figure pauses the script until the window is closed.
If set to false, the script continues immediately after the window is
rendered.
title: Plot title
aspect: If set to True, the axes of the figure are set to an equal aspect ratio
filename: Pass a non-empty string or path to save the image as. If this option
is used, the figure is closed after the file is saved.
render: If set to False, the image is not displayed. This may be useful if the
figure or axes will be embedded or further edited before being displayed.
axis_index: If more than 1 axes is created by subplot, then this is the axis to
plot on. This may be a tuple if a 2D array of plots is returned. The default
value of None will select the top left plot.
kwargs: Passed to :func:`matplotlib.pyplot.subplots`
Raises:
ValueError: ``axis_index`` is invalid
Yields:
Matplotlib figure and axes
"""
if filename:
render = False
if ax is None:
if not render or pause:
plt.ioff()
else:
plt.ion()
ax_supplied = False
fig, ax = plt.subplots(**kwargs)
try:
if axis_index is None:
axis_index = (0,) * ax.ndim # pyright: ignore
ax = ax[axis_index] # pyright: ignore
except (AttributeError, TypeError):
pass # only 1 axis, not an array
except IndexError as exc:
msg = f"axis_index={axis_index} is not compatible "
msg += f"with arguments to subplots: {kwargs}"
raise ValueError(msg) from exc
else:
fig = ax.get_figure()
ax_supplied = True
if not render:
plt.ioff()
yield fig, ax
if ax:
ax.set_title(title)
if ax_supplied:
# if an axis was supplied, don't continue displaying or configuring the plot
return
# if no axes was supplied, finish the plot and return the figure and axes
plt.tight_layout()
if aspect and ax:
ax.set_aspect("equal", anchor="C")
if filename and fig:
fig.savefig(filename, dpi=fig.dpi) # pyright: ignore
plt.close(fig) # pyright: ignore # close the figure to free the memory
return # if the figure was to be saved, then don't show it also
if render:
if pause:
plt.show()
else:
plt.draw()
plt.pause(0.001)
[docs]
@dataclass
class UnitDisplay:
"""Class for displaying units in ``concreteproperties``.
Attributes:
length: Length unit string
force: Force unit string
mass: Mass unit string
radians: If set to ``True``, displays angles in radians, otherwise displays
angles in degrees. Defaults to ``True``.
length_factor: Factor by which the ``length`` unit differs from the base units.
Defaults to ``1.0``.
force_factor: Factor by which the ``force`` unit differs from the base units.
Defaults to ``1.0``.
mass_factor: Factor by which the ``mass`` unit differs from the base units.
Defaults to ``1.0``.
"""
length: str
force: str
mass: str
radians: bool = True
length_factor: float = 1.0
force_factor: float = 1.0
mass_factor: float = 1.0
@property
def length_unit(self) -> str:
"""Returns the length unit string."""
return self.length if self.length == "" else f" {self.length}"
@property
def length_scale(self) -> float:
"""Returns the length scale."""
return 1 / self.length_factor
@property
def force_unit(self) -> str:
"""Returns the force unit string."""
return self.force if self.force == "" else f" {self.force}"
@property
def force_scale(self) -> float:
"""Returns the force scale."""
return 1 / self.force_factor
@property
def mass_unit(self) -> str:
"""Returns the mass unit string."""
return self.mass if self.mass == "" else f" {self.mass}"
@property
def mass_scale(self) -> float:
"""Returns the mass scale."""
return 1 / self.mass_factor
@property
def angle_unit(self) -> str:
"""Returns the angle unit string."""
return " rads" if self.radians else " degs"
@property
def angle_scale(self) -> float:
"""Returns the angle scale."""
return 1 if self.radians else 180.0 / np.pi
@property
def area_unit(self) -> str:
"""Returns the area unit string."""
return self.length if self.length == "" else f" {self.length}^2"
@property
def area_scale(self) -> float:
"""Returns the area scale."""
return 1 / self.length_factor / self.length_factor
@property
def mass_per_length_unit(self) -> str:
"""Returns the mass/length unit string."""
return self.mass if self.mass == "" else f" {self.mass}/{self.length}"
@property
def mass_per_length_scale(self) -> float:
"""Returns the mass/length scale."""
return 1 / self.mass_factor * self.length_factor
@property
def moment_unit(self) -> str:
"""Returns the moment unit string."""
return self.length if self.length == "" else f" {self.force}.{self.length}"
@property
def moment_scale(self) -> float:
"""Returns the moment scale."""
return 1 / self.force_factor / self.length_factor
@property
def flex_rig_unit(self) -> str:
"""Returns the flexural rigidity unit string."""
return self.length if self.length == "" else f" {self.force}.{self.length}^2"
@property
def flex_rig_scale(self) -> float:
"""Returns the flexural rigidity scale."""
return 1 / self.force_factor / self.length_factor / self.length_factor
@property
def stress_unit(self) -> str:
"""Returns the stress unit string."""
if self.length == "mm" and self.force == "N":
return " MPa"
elif self.length == "m" and self.force == "kN":
return " kPa"
elif self.length == "":
return ""
else:
return f" {self.force}/{self.length}^2"
@property
def stress_scale(self) -> float:
"""Returns the stress scale."""
return 1 / self.force_factor * self.length_factor * self.length_factor
@property
def length_3_unit(self) -> str:
"""Returns the length^3 unit string."""
return self.length if self.length == "" else f" {self.length}^3"
@property
def length_3_scale(self) -> float:
"""Returns the length^3 scale."""
return 1 / self.length_factor**3
@property
def length_4_unit(self) -> str:
"""Returns the length^4 unit string."""
return self.length if self.length == "" else f" {self.length}^4"
@property
def length_4_scale(self) -> float:
"""Returns the length^4 scale."""
return 1 / self.length_factor**4
DEFAULT_UNITS = UnitDisplay(length="", force="", mass="")
si_n_mm = UnitDisplay(length="mm", force="N", mass="kg")
si_kn_m = UnitDisplay(
length="m", force="kN", mass="kg", length_factor=1e3, force_factor=1e3
)