from typing import List, Tuple, Dict, Any
import os
import functools
import pickle
import logging
from math import pi
import numpy as np
import matplotlib
from matplotlib import pyplot as plt
import argparse, yaml
[docs]
class YieldException(Exception):
"""Ensure the precedence of particular operations/stages over others."""
def __init__(self, message: str) -> None:
self.message = message
super().__init__(self.message)
[docs]
def config_matplotlib(font_size: int = 12,
font_family: str = 'sans-serif',
use_latex: bool = False,
interactive: str = False) -> None:
"""
Set Matplotlib RC parameters for font size, font family, and LaTeX usage.
Parameters
----------
font_size : int, optional
The font size of text. The default is 12.
font_family : str, optional
The font family of text. The default is 'sans-serif'.
use_latex : bool, optional
Whether to enable LaTeX text rendering in Matplotlib. The default is False.
interactive : bool, optional
Whether to enable plt.show() output. The default is False, i.e. display
and keep window open.
Returns
-------
None
"""
matplotlib.rcParams['font.size'] = font_size
matplotlib.rcParams['font.family'] = font_family
matplotlib.rcParams['text.usetex'] = use_latex
matplotlib.rcParams["interactive"] = interactive
[docs]
def logger_factory(name: str="root", level: str="DEBUG") -> logging.Logger:
"""
Instantiate a logger object.
Parameters
----------
name : str
name of the logger. The default is "root".
level: str
level of logging. The default is "DEBUG".
Return
------
logger : Logger from logging module.
"""
level = level.upper()
logger = logging.getLogger(name)
logger.setLevel(getattr(logging, level))
ch1 = logging.StreamHandler()
ch1.setLevel(getattr(logging, level))
logger.addHandler(ch1)
fmt1 = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s',
# "%Y-%m-%d %H:%M:%S"
"%H:%M:%S",
)
ch1.setFormatter(fmt1)
return logger
[docs]
def logger_manager(level: str, fmt: str = None) -> None:
"""
Manage loggers. Get the loggers to modify level and format.
Parameters
----------
level : str
level of logging. The default is "DEBUG".
fmt : str
format of logging
Return
------
None.
"""
for name, logg in logging.Logger.manager.loggerDict.items():
if isinstance(logg, logging.Logger):
if __package__ in name:
logg.setLevel(getattr(logging, level.upper()))
if fmt:
[h.setFormatter(
logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
) for h in logg.handlers]
_log = logger_factory(name=__name__, level="DEBUG")
[docs]
def state_modifier(string: str, target: str, repl_text: str, add_text: str) -> str:
"""
Modify a string by replacing or adding text based on a target substring.
Parameters
----------
string : str
The input string to be modified.
target : str
The target substring to search in the input string.
repl_text : str
The text that replaces the target substring when present.
add_text : str
The text added to the input string when if the target substring is absent.
Returns
-------
str
The modified string after applying the specified modifications.
"""
if target in string:
string = string.replace(target, repl_text)
else:
string += "_" + add_text
return string
[docs]
def printer(func: callable):
"""
A decorator for class methods that saves a figure if 'save' is True.
This decorator wraps a method that generates a figure and a title,
and it saves the figure to the specified location if 'save' is True.
Parameters
----------
func : callable
The function to be decorated, which generates a figure and a title.
Returns
-------
callable
The decorated function.
"""
@functools.wraps(func) # <- preserve function signature
def saver(self, *args, **kwargs):
fig, title = func(self, *args, **kwargs)
if self.save == True:
fig.savefig(self.folder + title + "." + self.fmt,
format = self.fmt,
dpi = self.dpi,
bbox_inches='tight')
_log.info(f"SAVE PIC: {self.folder + title}.{self.fmt}")
else:
_log.debug(f"SHOW PIC: {title}")
plt.show()
return saver
[docs]
def save(*args: Tuple, **kwargs: Dict[str, Any]) -> None:
"""
Save a collection of data objects to binary files.
Parameters
----------
args : Tuple
Any number of data objects to be saved as binary files.
kwargs : Dict[str, Any]
folder : str, optional
The directory where the binary files will be saved.
Raises
------
MissingInputException
If the 'folder' keyword argument is missing.
"""
try:
folder = kwargs.pop("folder")
except KeyError as KE:
raise MissingInputException(f"{KE} not provided")
for data in args:
with open(folder + data.name + "_" + data.__class__.__name__ + ".bfd", 'wb') as file:
pickle.dump(data, file)
[docs]
def load(**kwargs: Dict[str, Any]) -> List:
"""
Load data from binary files in a specified directory.
Parameters
----------
kwargs : dict
folder : str
The folder where the binary files are located.
extension : str, optional
If provided, filter by 'extension'.
filename : str, optional
If provided, load files matching 'filename'.
Returns
-------
List
A list with the loaded data objects loaded from the specified folder.
Raises
------
MissingInputException
If the 'folder' keyword argument is missing.
FileNotFoundError
If no matching files are found in the specified directory.
"""
readfiles = []
try:
folder = kwargs.pop("folder")
except KeyError as KE:
raise MissingInputException(f"{KE} not provided")
filelist = os.listdir(folder)
if kwargs.get("extension") is not None:
extension = kwargs.pop("extension")
filelist = [file for file in filelist if file.endswith(extension)]
if kwargs.get("filename") is not None:
filename = kwargs.pop("filename")
filelist = [file for file in filelist if filename == file]
if len(filelist) == 0:
raise FileNotFoundError
else:
for data in filelist:
with open(folder + data, 'rb') as file:
readfiles.append(pickle.load(file))
return readfiles
[docs]
def parse_arguments(config_path: str = "./config.yaml") -> argparse.Namespace:
"""
Parse command-line arguments to configure the execution using a YAML file.
Parameters
----------
config_path : str
Path to yaml config file. The default is "./config.yaml"
Command Line Arguments
----------------------
--config (str, optional): Path to the YAML config file. The default is "config.yaml".
Returns
-------
argparse.Namespace
An object containing the parsed command-line arguments.
"""
parser = argparse.ArgumentParser(description="Configure the execution via YAML file")
parser.add_argument("--config",
default=config_path,
help="Path to the YAML config file")
return parser.parse_args()
[docs]
def get_config_file(args: argparse.Namespace) -> Dict:
"""
Get the configuration data from a YAML file.
Parameters
----------
args : argparse.Namespace
A namespace containing parsed command-line arguments.
Returns
-------
dict
A dictionary containing the configuration data loaded from the YAML file.
"""
with open(args.config, "r") as config_file:
config = yaml.safe_load(config_file)
return config
[docs]
def identity(X: np.ndarray) -> np.ndarray:
"""
Return the input array unchanged.
Parameters
----------
X : numpy.ndarray
Input array.
Returns
-------
numpy.ndarray
Unchanged input array.
"""
return X
[docs]
class IdentityDictionary:
def __getitem__(self, key):
return key
dummy_translator = IdentityDictionary()
[docs]
def grid_factory(x1_bounds: List[float], x2_bounds: List[float], n1: int, n2: int, spacing: str = "lin") -> Tuple[np.ndarray]:
"""
Create a grid of points over a 2D space.
Parameters
----------
spacing : str
The type of spacing for the grid, either "lin" (linear) or "log" (logarithmic).
x1 : List[float]
A list containing the lower and upper bounds for the X-axis.
x2 : List[float]
A list containing the lower and upper bounds for the Y-axis.
n1 : int
The number of points along the X-axis.
n2 : int
The number of points along the Y-axis.
Returns
-------
Tuple
A tuple of two arrays:
1. X1: Flattened array of X-axis points for the entire grid.
2. X2: Flattened array of Y-axis points for the entire grid.
"""
if spacing == "lin":
x1_points = np.linspace(x1_bounds[0], x1_bounds[1], n1)
x2_points = np.linspace(x2_bounds[0], x2_bounds[1], n2)
elif spacing == "log":
x1_points = np.logspace(np.log10(x1_bounds[0]), np.log10(x1_bounds[1]), n1)
x2_points = np.logspace(np.log10(x2_bounds[0]), np.log10(x2_bounds[1]), n2)
else:
raise KeyError("distribution spacing kind not available")
X1, X2 = np.meshgrid(x1_points, x2_points)
return X1.flatten(), X2.flatten()
[docs]
def sif_range(delta_sigma: np.ndarray, y: np.ndarray, sqrt_area: np.ndarray) -> np.ndarray:
"""
Definition of Stress Intensity Factor (SIF) range, :math:`\Delta K`.
.. math::
\Delta K = \Delta\sigma\, Y \sqrt{\pi \sqrt{area}}
Parameters
----------
delta_sigma : array-like
applied stress range.
y : array-like
geometric factor of the defects.
sqrt_area : array-like
Murakami's characteristic length.
Returns
-------
array-like
stress intensity factor range.
"""
return delta_sigma * y * (pi * sqrt_area)**0.5
[docs]
def inv_sif_range(delta_k: np.ndarray, delta_sigma: np.ndarray, y: np.ndarray) -> np.ndarray:
"""
Compute the inverse of the SIF range, thus \
giving :math:`\sqrt{\\text{area}}`.
.. math::
\sqrt{\\text{area}} = {1 \over \pi} \\bigg({{\Delta K}
\over {Y \Delta \sigma}}\\bigg)^2
Parameters
----------
delta_k : array-like
stress intensity factor range.
delta_sigma : array-like
applied stress range.
y : array-like
geometric factor of the defects.
Returns
-------
array-like
sqrt_area
"""
return ((delta_k/(y*delta_sigma))**2)/pi
[docs]
def sif_equiv(sqrt_area_orig: np.ndarray, y_orig: np.ndarray, y_ref: float):
"""
Convert sqrt_area_orig into sqrt_area according to y_ref, given y_orig\
using the SIF-equivalence.
.. math::
\sqrt{\\text{area}}_{ref}=\sqrt{\\text{area}_{orig}}\,\\bigg({{Y_{orig}} \over {Y_{ref}}}\\bigg)^2
Parameters
----------
sqrt_area_orig : array-like
original (measured) sqrt_area_data.
y_orig : array-like
original (indirectly retrieved from measurements) y.
y_ref : float
user-defined value of y set as reference.
Returns
-------
array-like
equivalent sqrt_area computed by equalling delta_k.
"""
return ((y_orig/y_ref)**2) * sqrt_area_orig