diff --git a/src/fatpy/core/stress_life/damage_params/uniaxial_stress_eq_amp.py b/src/fatpy/core/stress_life/damage_params/uniaxial_stress_eq_amp.py new file mode 100644 index 0000000..856ae70 --- /dev/null +++ b/src/fatpy/core/stress_life/damage_params/uniaxial_stress_eq_amp.py @@ -0,0 +1,654 @@ +"""Uniaxial fatigue criteria methods for the stress-life approach. + +Contains criteria that address uniaxial high-cycle fatigue by incorporating the mean +stress effect through an equivalent stress amplitude approach. By adjusting the stress +amplitude to account for mean stress influences—using models such as Goodman, Gerber, +or Soderberg—they enable more accurate fatigue life predictions where mean stresses +significantly affect material endurance. + +For more information you can refer to the following resource: +https://doi.org/10.1051/matecconf/201816510018 +""" + +import warnings + +import numpy as np +from numpy.typing import ArrayLike, NDArray + + +def calc_stress_eq_amp_ASME( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + yield_strength: ArrayLike | np.float64, +) -> NDArray[np.float64]: + r"""Calculate equivalent stress amplitude using ASME criterion. + + ??? info "ASME Use-case" + The ASME criterion accounts for mean stress effects in high-cycle fatigue + by modifying the stress amplitude based on the yield strength using a linear + relationship. + + ??? abstract "Math Equations" + The ASME equivalent stress amplitude is calculated as: + + $$ + \sigma_{aeq}=\frac{\sigma_a}{\left[1-\left(\frac{\sigma_m}{R_e}\right)^2\right]^{1/2}} + $$ + + Args: + stress_amp: Array-like of stress amplitudes. Leading dimensions are preserved. + mean_stress: Array-like of mean stresses. Must be broadcastable with + stress_amp. Leading dimensions are preserved. + yield_strength: Array-like of yield strengths. Must be broadcastable with + stress_amp and mean_stress. Leading dimensions are preserved. + + Raises: + ValueError: If yield strength is not positive. + ValueError: If mean stress magnitude is equal or greater to yield strength, + resulting in infinite equivalent stress amplitude. + """ + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + yield_strength_arr = np.asarray(yield_strength, dtype=np.float64) + + if np.any(yield_strength_arr <= 0): + raise ValueError("Yield strength must be positive") + + # Check if mean stress approaches or exceeds material parameter + ratio = np.abs(mean_stress_arr) / yield_strength_arr + if np.any(ratio >= 1.0): + raise ValueError("Mean stress magnitude equal or greater than yield strength.") + + return stress_amp_arr / (1 - (mean_stress_arr / yield_strength_arr) ** 2) ** 0.5 + + +def calc_stress_eq_amp_bagci( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + yield_strength: ArrayLike | np.float64, +) -> NDArray[np.float64]: + r"""Calculate equivalent stress amplitude using Bagci criterion. + + ??? info "Bagci Use-case" + The Bagci criterion accounts for mean stress effects in high-cycle fatigue + by modifying the stress amplitude based on the yield strength using a linear + relationship. + + ??? abstract "Math Equations" + The Bagci equivalent stress amplitude is calculated as: + + $$ + \sigma_{aeq}=\frac{\sigma_a}{1-\left(\frac{\sigma_m}{R_e}\right)^4} + $$ + + Args: + stress_amp: Array-like of stress amplitudes. Leading dimensions are preserved. + mean_stress: Array-like of mean stresses. Must be broadcastable with + stress_amp. Leading dimensions are preserved. + yield_strength: Array-like of yield strengths. Must be broadcastable with + stress_amp and mean_stress. Leading dimensions are preserved. + + Returns: + Array of equivalent stress amplitudes. Shape follows NumPy broadcasting + rules for the input arrays. + + Raises: + Warning: If mean stress magnitude exceeds yield strength. + ValueError: If yield strength is not positive. + ValueError: If mean stress magnitude is equal to yield strength, + resulting in infinite equivalent stress amplitude. + """ + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + yield_strength_arr = np.asarray(yield_strength, dtype=np.float64) + + if np.any(yield_strength_arr <= 0): + raise ValueError("Yield strength must be positive") + + # Check if mean stress approaches or exceeds material parameter + ratio = np.abs(mean_stress_arr) / yield_strength_arr + if np.any(ratio == 1.0): + raise ValueError( + "Mean stress magnitude equals yield strength this would result in " + "infinite equivalent stress amplitude." + ) + elif np.any(ratio > 1.0): + warnings.warn( + "Mean stress magnitude exceeds yield strength.", + UserWarning, + stacklevel=2, + ) + + return stress_amp_arr / (1 - (mean_stress_arr / yield_strength_arr) ** 4) + + +def calc_stress_eq_amp_gerber( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + ult_stress: ArrayLike | np.float64, +) -> NDArray[np.float64]: + r"""Calculate equivalent stress amplitude using Gerber criterion. + + ??? info "Gerber Use-case" + The Gerber criterion accounts for mean stress effects in high-cycle fatigue + by modifying the stress amplitude based on the ultimate tensile strength. + + ??? abstract "Math Equations" + The Gerber equivalent stress amplitude is calculated as: + + $$ + \displaystyle\sigma_{aeq}=\frac{\sigma_a}{1-\left(\frac{\sigma_m}{\sigma_{UTS}} + \right)^2 } + $$ + + Args: + stress_amp: Array-like of stress amplitudes. Leading dimensions are preserved. + mean_stress: Array-like of mean stresses. Must be broadcastable with + stress_amp. Leading dimensions are preserved. + ult_stress: Array-like of ultimate tensile strengths. Must be broadcastable + with stress_amp and mean_stress. Leading dimensions are preserved. + + Returns: + Array of equivalent stress amplitudes. Shape follows NumPy broadcasting + rules for the input arrays. + + Raises: + Warning: If mean stress magnitude exceeds ultimate tensile strength. + ValueError: If ultimate tensile strength is not positive. + ValueError: If mean stress magnitude is equal to ultimate tensile strength, + resulting in infinite equivalent stress amplitude. + + """ + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + ult_stress_arr = np.asarray(ult_stress, dtype=np.float64) + + if np.any(ult_stress_arr <= 0): + raise ValueError("Ultimate tensile strength must be positive") + + # Check if mean stress approaches or exceeds material parameter + ratio = np.abs(mean_stress_arr) / ult_stress_arr + + if np.any(ratio == 1.0): + raise ValueError( + "Mean stress magnitude equals ultimate tensile strength this would " + "result in infinite equivalent stress amplitude." + ) + elif np.any(ratio > 1.0): + warnings.warn( + "Mean stress magnitude exceeds ultimate tensile strength. ", + UserWarning, + stacklevel=2, + ) + + return stress_amp_arr / (1 - (mean_stress_arr / ult_stress_arr) ** 2) + + +def calc_stress_eq_amp_goodman( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + ult_stress: ArrayLike | np.float64, +) -> NDArray[np.float64]: + r"""Calculate equivalent stress amplitude using Goodman criterion. + + ??? info "Goodman Use-case" + The Goodman criterion accounts for mean stress effects in high-cycle fatigue + by modifying the stress amplitude based on the ultimate tensile strength using + a linear relationship. + + ??? abstract "Math Equations" + The Goodman equivalent stress amplitude is calculated as: + + $$ + \displaystyle\sigma_{aeq}=\frac{\sigma_a}{1-\frac{\sigma_m}{\sigma_{UTS}}} + $$ + + Args: + stress_amp: Array-like of stress amplitudes. Leading dimensions are preserved. + mean_stress: Array-like of mean stresses. Must be broadcastable with + stress_amp. Leading dimensions are preserved. + ult_stress: Array-like of ultimate tensile strengths. Must be broadcastable + with stress_amp and mean_stress. Leading dimensions are preserved. + + Returns: + Array of equivalent stress amplitudes. Shape follows NumPy broadcasting + rules for the input arrays. + + Raises: + Warning: If mean stress exceeds ultimate tensile strength. + ValueError: If ultimate tensile strength is not positive. + ValueError: If mean stress is equal to ultimate tensile strength, resulting in + infinite equivalent stress amplitude. + """ + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + ult_stress_arr = np.asarray(ult_stress, dtype=np.float64) + + if np.any(ult_stress_arr <= 0): + raise ValueError("Ultimate tensile strength must be positive") + + # Check if mean stress approaches or exceeds material parameter + ratio = mean_stress_arr / ult_stress_arr + + if np.any(ratio == 1.0): + raise ValueError( + "Mean stress equals ultimate tensile strength this would result in " + "infinite equivalent stress amplitude." + ) + elif np.any(ratio > 1.0): + warnings.warn( + "Mean stress magnitude exceeds ultimate tensile strength. ", + UserWarning, + stacklevel=2, + ) + + return stress_amp_arr / (1 - mean_stress_arr / ult_stress_arr) + + +def calc_stress_eq_amp_half_slope( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + ult_stress: ArrayLike | np.float64, +) -> NDArray[np.float64]: + r"""Calculate equivalent stress amplitude using a half-slope mean stress correction. + + ??? info "Half-slope Use-case" + A half-slope mean stress correction can be applied to account for mean + stress effects in high-cycle fatigue by modifying the stress amplitude based + on the ultimate tensile strength. + + ??? abstract "Math Equations" + The half-slope corrected equivalent stress amplitude is calculated as: + + $$ + \sigma_{aeq}=\frac{\sigma_a}{1 - \frac{\sigma_m}{2 \cdot \sigma_{UTS}}} + $$ + + Args: + stress_amp: Array-like of stress amplitudes. Leading dimensions are preserved. + mean_stress: Array-like of mean stresses. Must be broadcastable with + stress_amp. Leading dimensions are preserved. + ult_stress: Array-like of ultimate tensile strengths. Must be broadcastable + with stress_amp and mean_stress. Leading dimensions are preserved. + + Returns: + Array of equivalent stress amplitudes. Shape follows NumPy broadcasting + rules for the input arrays. + + Raises: + Warning: If mean stress exceeds half of the ultimate tensile strength. + ValueError: If ultimate tensile strength is not positive. + ValueError: If mean stress is equal to half of the ultimate tensile strength, + resulting in zero equivalent stress amplitude. + + """ + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + ult_stress_arr = np.asarray(ult_stress, dtype=np.float64) + + if np.any(ult_stress_arr <= 0): + raise ValueError("Ultimate tensile strength must be positive") + + # Check if mean stress approaches or exceeds material parameter + ratio = mean_stress_arr / (2 * ult_stress_arr) + if np.any(ratio == 1.0): + raise ValueError( + "Mean stress equals half of the ultimate tensile strength this would result" + "in zero equivalent stress amplitude." + ) + elif np.any(ratio > 1.0): + warnings.warn( + "Mean stress exceeds half of the ultimate tensile strength. ", + UserWarning, + stacklevel=2, + ) + return stress_amp_arr / (1 - mean_stress_arr / (2 * ult_stress_arr)) + + +def calc_stress_eq_amp_linear( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + stress_parameter_M: ArrayLike | np.float64, +) -> NDArray[np.float64]: + r"""Calculate equivalent stress amplitude using a linear mean stress correction. + + ??? info "Linear Use-case" + A simple linear mean stress correction can be applied to account for mean + stress effects in high-cycle fatigue by modifying the stress amplitude based + on the ultimate tensile strength. + + ??? abstract "Math Equations" + The linearly corrected equivalent stress amplitude is calculated as: + + $$ + \sigma_{aeq}=\frac{\sigma_a}{1 - \frac{\sigma_m}{M}} + $$ + Args: + stress_amp: Array-like of stress amplitudes. Leading dimensions are preserved. + mean_stress: Array-like of mean stresses. Must be broadcastable with + stress_amp. Leading dimensions are preserved. + stress_parameter_M: Array-like of material stress parameters M. + Must be broadcastable with stress_amp and mean_stress. + Leading dimensions are preserved. + + Returns: + Array of equivalent stress amplitudes. Shape follows NumPy broadcasting + rules for the input arrays. + + Raises: + Warning: If mean stress exceeds material stress parameter M. + ValueError: If material stress parameter M is not positive. + ValueError: If mean stress is equal to material stress parameter M, resulting in + zero equivalent stress amplitude. + + """ + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + stress_parameter_M_arr = np.asarray(stress_parameter_M, dtype=np.float64) + + if np.any(stress_parameter_M_arr <= 0): + raise ValueError("Material stress parameter M must be positive") + # Check if mean stress approaches or exceeds material parameter + ratio = mean_stress_arr / stress_parameter_M_arr + + if np.any(ratio == 1.0): + raise ValueError( + "Mean stress equals material stress parameter M this would result in " + "zero equivalent stress amplitude." + ) + elif np.any(ratio > 1.0): + warnings.warn( + "Mean stress exceeds material stress parameter M. ", + UserWarning, + stacklevel=2, + ) + + return stress_amp_arr / (1 - mean_stress_arr / stress_parameter_M_arr) + + +def calc_stress_eq_amp_morrow( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + true_fract_stress: ArrayLike | np.float64, +) -> NDArray[np.float64]: + r"""Calculate equivalent stress amplitude using Morrow criterion. + + ??? info "Morrow Use-case" + The Morrow criterion accounts for mean stress effects in high-cycle fatigue + by modifying the stress amplitude based on the true fracture strength. + + ??? abstract "Math Equations" + The Morrow equivalent stress amplitude is calculated as: + + $$ + \displaystyle\sigma_{aeq}=\frac{\sigma_a}{1-\frac{\sigma_m}{\sigma_{true}} } + $$ + + Args: + stress_amp: Array-like of stress amplitudes. Leading dimensions are preserved. + mean_stress: Array-like of mean stresses. Must be broadcastable with + stress_amp. Leading dimensions are preserved. + true_fract_stress: Array-like of true tensile fracture stress. Must be + broadcastable with stress_amp and mean_stress. Leading dimensions + are preserved. + + Returns: + Array of equivalent stress amplitudes. Shape follows NumPy broadcasting + rules for the input arrays. + + Raises: + Warning: If mean stress exceeds true fracture stress. + ValueError: If true fracture stress is not positive. + ValueError: If mean stress is equal to true fracture stress, resulting in + infinite equivalent stress amplitude. + """ + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + true_fract_stress_arr = np.asarray(true_fract_stress, dtype=np.float64) + + if np.any(true_fract_stress_arr <= 0): + raise ValueError("True fracture stress must be positive") + + # Check if mean stress approaches or exceeds material parameter + ratio = mean_stress_arr / true_fract_stress_arr + + if np.any(ratio == 1.0): + raise ValueError( + "Mean stress equals true fracture stress this would result in " + "infinite equivalent stress amplitude." + ) + elif np.any(ratio > 1.0): + warnings.warn( + "Mean stress exceeds true fracture stress. ", + UserWarning, + stacklevel=2, + ) + + return stress_amp_arr / (1 - mean_stress_arr / true_fract_stress_arr) + + +def calc_stress_eq_amp_soderberg( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + yield_strength: ArrayLike | np.float64, +) -> NDArray[np.float64]: + r"""Calculate equivalent stress amplitude using Soderberg criterion. + + ??? info "Soderberg Use-case" + The Soderberg criterion accounts for mean stress effects in high-cycle fatigue + by modifying the stress amplitude based on the yield strength using a linear + relationship. + + ??? abstract "Math Equations" + The Soderberg equivalent stress amplitude is calculated as: + + $$ + \sigma_{aeq}=\frac{\sigma_a}{1-\frac{\sigma_m}{R_e}} + $$ + + Args: + stress_amp: Array-like of stress amplitudes. Leading dimensions are preserved. + mean_stress: Array-like of mean stresses. Must be broadcastable with + stress_amp. Leading dimensions are preserved. + yield_strength: Array-like of yield strengths. Must be broadcastable with + stress_amp and mean_stress. Leading dimensions are preserved. + + Returns: + Array of equivalent stress amplitudes. Shape follows NumPy broadcasting + rules for the input arrays. + + Raises: + Warning: If mean stress exceeds yield strength. + ValueError: If yield strength is not positive. + ValueError: If mean stress is equal to yield strength, resulting in + infinite equivalent stress amplitude. + """ + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + yield_strength_arr = np.asarray(yield_strength, dtype=np.float64) + + if np.any(yield_strength_arr <= 0): + raise ValueError("Yield strength must be positive") + + # Check if mean stress approaches or exceeds material parameter + ratio = mean_stress_arr / yield_strength_arr + if np.any(ratio == 1.0): + raise ValueError( + "Mean stress equals yield strength this would result in " + "infinite equivalent stress amplitude." + ) + elif np.any(ratio > 1.0): + warnings.warn( + "Mean stress exceeds yield strength. ", + UserWarning, + stacklevel=2, + ) + + return stress_amp_arr / (1 - mean_stress_arr / yield_strength_arr) + + +def calc_stress_eq_amp_smith( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + ult_stress: ArrayLike | np.float64, +) -> NDArray[np.float64]: + r"""Calculate equivalent stress amplitude using Smith criterion. + + ??? info "Smith Use-case" + The Smith criterion accounts for mean stress effects in high-cycle fatigue + by modifying the stress amplitude based on the ultimate tensile strength. + + ??? abstract "Math Equations" + The Smith equivalent stress amplitude is calculated as: + + $$ + \sigma_{aeq}=\frac{\sigma_a \cdot \left(1 + \frac{\sigma_m}{\sigma_{UTS}} + \right)}{1-\left(\frac{\sigma_m}{\sigma_{UTS}}\right)} + $$ + + Args: + stress_amp: Array-like of stress amplitudes. Leading dimensions are preserved. + mean_stress: Array-like of mean stresses. Must be broadcastable with + stress_amp. Leading dimensions are preserved. + ult_stress: Array-like of ultimate tensile strengths. Must be broadcastable + with stress_amp and mean_stress. Leading dimensions are preserved. + + Returns: + Array of equivalent stress amplitudes. Shape follows NumPy broadcasting + rules for the input arrays. + + Raises: + Warning: If mean stress exceeds ultimate tensile strength. + ValueError: If ultimate tensile strength is not positive. + ValueError: If mean stress is equal to ultimate tensile strength, resulting in + infinite equivalent stress amplitude. + + """ + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + ult_stress_arr = np.asarray(ult_stress, dtype=np.float64) + + if np.any(ult_stress_arr <= 0): + raise ValueError("Ultimate tensile strength must be positive") + + # Check if mean stress approaches or exceeds material parameter + ratio = mean_stress_arr / ult_stress_arr + if np.any(ratio == 1.0): + raise ValueError( + "Mean stress equals ultimate tensile strength this would result in " + "infinite equivalent stress amplitude." + ) + elif np.any(ratio > 1.0): + warnings.warn( + "Mean stress exceeds ultimate tensile strength. ", + UserWarning, + stacklevel=2, + ) + return (stress_amp_arr * (1 + mean_stress_arr / ult_stress_arr)) / ( + 1 - mean_stress_arr / ult_stress_arr + ) + + +def calc_stress_eq_amp_swt( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, +) -> NDArray[np.float64]: + r"""Calculate equivalent stress amplitude using Smith-Watson-Topper parameter. + + ??? info "SWT Use-case" + The Smith-Watson-Topper (SWT) parameter accounts for mean stress effects in + high-cycle fatigue by combining stress amplitude and maximum stress in the cycle + + ??? abstract "Math Equations" + The SWT equivalent stress amplitude is calculated as: + + $$ + \sigma_{aeq} = \sqrt{\sigma_{a} \cdot (\sigma_{m} + \sigma_{a})} \\ + $$ + + Args: + stress_amp: Array-like of stress amplitudes. Leading dimensions are preserved. + mean_stress: Array-like of mean stresses. Must be broadcastable with + stress_amp. Leading dimensions are preserved. + + Returns: + Array of equivalent stress amplitudes. Shape follows NumPy broadcasting + rules for the input arrays. + + Raises: + ValueError: If stress amplitude is negative. + ValueError: If the validity condition σₐ > |σₘ| is not satisfied. + + ??? note "Validity Condition" + The SWT parameter is valid when $\sigma_a > |\sigma_m|$, ensuring that the + maximum stress in the cycle is positive (tensile). When this condition is + not met, a ValueError is raised. + + """ + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + + # Check for negative stress amplitudes + if np.any(stress_amp_arr < 0): + raise ValueError("Stress amplitude must be non-negative") + + # Check validity condition: σₐ > |σₘ| + abs_mean_stress = np.abs(mean_stress_arr) + invalid_condition = stress_amp_arr <= abs_mean_stress + + if np.any(invalid_condition): + raise ValueError( + "Smith-Watson-Topper parameter validity condition (σₐ > |σₘ|) not " + "satisfied for some data points. The SWT approach may not be " + "appropriate for compressive-dominated loading conditions." + ) + + return np.sqrt(stress_amp_arr * (mean_stress_arr + stress_amp_arr)) + + +def calc_stress_eq_amp_walker( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + walker_parameter: ArrayLike | np.float64, +) -> NDArray[np.float64]: + r"""Calculate equivalent stress amplitude using Walker criterion. + + ??? info "Walker Use-case" + The Walker criterion accounts for mean stress effects in high-cycle fatigue + by modifying by combining stress amplitude and maximum stress in the cycle and + utilizing a material specific exponent - the Walker parameter (γ'). + + ??? abstract "Math Equations" + The Walker equivalent stress amplitude is calculated as: + + $$ + \displaystyle\sigma_{aeq}=\left(\sigma_a+\sigma_m\right)^{1-\gamma'} \cdot + \sigma_a^{\gamma'} + $$ + Args: + stress_amp: Array-like of stress amplitudes. Leading dimensions are preserved. + mean_stress: Array-like of mean stresses. Must be broadcastable with + stress_amp. Leading dimensions are preserved. + walker_parameter: Array-like of Walker exponents (γ'). Must be broadcastable + with stress_amp and mean_stress. Leading dimensions are preserved. + + Returns: + Array of equivalent stress amplitudes. Shape follows NumPy broadcasting + rules for the input arrays. + + Raises: + ValueError: If input arrays cannot be broadcast together or when the + condition γ' in (0, 1) is not satisfied. + """ + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + walker_parameter_arr = np.asarray(walker_parameter, dtype=np.float64) + + # Check validity of Walker parameter: γ' in range (0, 1) + invalid_condition = (walker_parameter_arr < 0) | (walker_parameter_arr > 1) + if np.any(invalid_condition): + raise ValueError("Walker parameter (γ') must be in the range (0, 1). ") + + return (stress_amp_arr + mean_stress_arr) ** ( + 1 - walker_parameter_arr + ) * stress_amp_arr**walker_parameter_arr diff --git a/tests/core/stress_life/damage_params/test_uniaxial_stress_eq_amp.py b/tests/core/stress_life/damage_params/test_uniaxial_stress_eq_amp.py new file mode 100644 index 0000000..3326f5c --- /dev/null +++ b/tests/core/stress_life/damage_params/test_uniaxial_stress_eq_amp.py @@ -0,0 +1,330 @@ +"""Test functions for uniaxial stress equivalent amplitude calculations. + +Tests cover input validation, mathematical correctness, and edge cases for all +four equivalent stress amplitude calculation methods: SWT, Goodman, Gerber, and Morrow. +""" + +from typing import Tuple + +import numpy as np +import pytest +from numpy.testing import assert_allclose +from numpy.typing import NDArray + +from fatpy.core.stress_life.damage_params.uniaxial_stress_eq_amp import ( + calc_stress_eq_amp_ASME, + calc_stress_eq_amp_bagci, + calc_stress_eq_amp_gerber, + calc_stress_eq_amp_goodman, + calc_stress_eq_amp_half_slope, + calc_stress_eq_amp_linear, + calc_stress_eq_amp_morrow, + calc_stress_eq_amp_smith, + calc_stress_eq_amp_soderberg, + calc_stress_eq_amp_swt, + calc_stress_eq_amp_walker, +) + + +@pytest.fixture +def array_inputs() -> Tuple[NDArray[np.float64], NDArray[np.float64]]: + stress_amp = np.array([150.0, 500.0, 80.0, 200.0]) + mean_stress = np.array([100.0, 150.0, 30.0, 0.0]) + return stress_amp, mean_stress + + +class TestCalcStressEqAmpASME: + def test_basic_calculation(self) -> None: + result = calc_stress_eq_amp_ASME(180.0, 100.0, 500.0) + expected = 180.0 / np.sqrt(1.0 - (100.0 / 500.0) ** 2) + assert_allclose(result, expected) + + def test_array_inputs( + self, + array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + ) -> None: + stress_amp, mean_stress = array_inputs + result = calc_stress_eq_amp_ASME(stress_amp, mean_stress, 700.0) + assert result.shape == (4,) + + def test_invalid_yield_strength(self) -> None: + with pytest.raises(ValueError): + for ys in [0.0, -500.0]: + calc_stress_eq_amp_ASME(100.0, 50.0, ys) + + def test_mean_stress_yield_strength_comparison_error(self) -> None: + with pytest.raises(ValueError): + for ms in [500.0, -500.0, 600.0, -600.0]: + calc_stress_eq_amp_ASME(100.0, ms, 500.0) + + +class TestCalcStressEqAmpBagci: + def test_basic_calculation(self) -> None: + result = calc_stress_eq_amp_bagci(180.0, 100.0, 500.0) + expected = 180.0 / (1.0 - (100.0 / 500.0) ** 4) + assert_allclose(result, expected) + + def test_array_inputs( + self, + array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + ) -> None: + stress_amp, mean_stress = array_inputs + result = calc_stress_eq_amp_bagci(stress_amp, mean_stress, 700.0) + assert result.shape == (4,) + + def test_invalid_yield_strength(self) -> None: + with pytest.raises(ValueError): + for ys in [0.0, -500.0]: + calc_stress_eq_amp_bagci(100.0, 50.0, ys) + + def test_mean_stress_yield_strength_comparison_error(self) -> None: + with pytest.raises(ValueError): + for ms in [500.0, -500.0]: + calc_stress_eq_amp_bagci(100.0, ms, 500.0) + + def test_mean_stress_yield_strength_comparison_warning(self) -> None: + with pytest.warns(UserWarning): + for ms in [600.0, -600.0]: + calc_stress_eq_amp_bagci(100.0, ms, 500.0) + + +class TestCalcStressEqAmpGerber: + def test_basic_calculation(self) -> None: + result = calc_stress_eq_amp_gerber(180.0, 100.0, 500.0) + expected = 180.0 / (1.0 - (100.0 / 500.0) ** 2) + assert_allclose(result, expected) + + def test_array_inputs( + self, + array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + ) -> None: + stress_amp, mean_stress = array_inputs + result = calc_stress_eq_amp_gerber(stress_amp, mean_stress, 700.0) + assert result.shape == (4,) + + def test_invalid_uts(self) -> None: + with pytest.raises(ValueError): + for uts in [0.0, -500.0]: + calc_stress_eq_amp_gerber(100.0, 50.0, uts) + + def test_mean_stress_uts_comparison_error(self) -> None: + with pytest.raises(ValueError): + for ms in [500.0, -500.0]: + calc_stress_eq_amp_gerber(100.0, ms, 500.0) + + def test_mean_stress_uts_comparison_warning(self) -> None: + with pytest.warns(UserWarning): + for ms in [600.0, -600.0]: + calc_stress_eq_amp_gerber(100.0, ms, 500.0) + + +class TestCalcStressEqAmpGoodman: + def test_basic_calculation(self) -> None: + result = calc_stress_eq_amp_goodman(180.0, 100.0, 500.0) + expected = 180.0 / (1.0 - (100.0 / 500.0)) + assert_allclose(result, expected) + + def test_array_inputs( + self, + array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + ) -> None: + stress_amp, mean_stress = array_inputs + result = calc_stress_eq_amp_goodman(stress_amp, mean_stress, 700.0) + assert result.shape == (4,) + + def test_invalid_uts(self) -> None: + with pytest.raises(ValueError): + for uts in [0.0, -500.0]: + calc_stress_eq_amp_goodman(100.0, 50.0, uts) + + def test_mean_stress_uts_comparison_error(self) -> None: + with pytest.raises(ValueError): + calc_stress_eq_amp_goodman(100.0, 500.0, 500.0) + + def test_mean_stress_uts_comparison_warning(self) -> None: + with pytest.warns(UserWarning): + calc_stress_eq_amp_goodman(100.0, 600.0, 500.0) + + +class TestCalcStressEqHalfSlope: + def test_basic_calculation(self) -> None: + result = calc_stress_eq_amp_half_slope(180.0, 100.0, 500.0) + expected = 180.0 / (1.0 - (100.0 / (2 * 500.0))) + assert_allclose(result, expected) + + def test_array_inputs( + self, + array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + ) -> None: + stress_amp, mean_stress = array_inputs + result = calc_stress_eq_amp_half_slope(stress_amp, mean_stress, 700.0) + assert result.shape == (4,) + + def test_invalid_uts(self) -> None: + with pytest.raises(ValueError): + for uts in [0.0, -500.0]: + calc_stress_eq_amp_half_slope(100.0, 50.0, uts) + + def test_mean_stress_uts_comparison_error(self) -> None: + with pytest.raises(ValueError): + calc_stress_eq_amp_half_slope(100.0, 500.0, 250.0) + + def test_mean_stress_uts_comparison_warning(self) -> None: + with pytest.warns(UserWarning): + calc_stress_eq_amp_half_slope(100.0, 650.0, 300.0) + + +class TestCalcStressEqAmpLinear: + def test_basic_calculation(self) -> None: + result = calc_stress_eq_amp_linear(180.0, 100.0, 500.0) + expected = 180.0 / (1.0 - (100.0 / 500.0)) + assert_allclose(result, expected) + + def test_array_inputs( + self, + array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + ) -> None: + stress_amp, mean_stress = array_inputs + result = calc_stress_eq_amp_linear(stress_amp, mean_stress, 700.0) + assert result.shape == (4,) + + def test_invalid_material_param(self) -> None: + with pytest.raises(ValueError): + for mat_param in [0.0, -500.0]: + calc_stress_eq_amp_linear(100.0, 50.0, mat_param) + + def test_mean_stress_material_param_comparison_error(self) -> None: + with pytest.raises(ValueError): + calc_stress_eq_amp_linear(100.0, 500.0, 500.0) + + def test_mean_stress_material_param_comparison_warning(self) -> None: + with pytest.warns(UserWarning): + calc_stress_eq_amp_linear(100.0, 600.0, 500.0) + + +class TestCalcStressEqAmpMorrow: + def test_basic_calculation(self) -> None: + result = calc_stress_eq_amp_morrow(180.0, 100.0, 500.0) + expected = 180.0 / (1.0 - (100.0 / 500.0)) + assert_allclose(result, expected) + + def test_array_inputs( + self, + array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + ) -> None: + stress_amp, mean_stress = array_inputs + result = calc_stress_eq_amp_morrow(stress_amp, mean_stress, 700.0) + assert result.shape == (4,) + + def test_invalid_true_fracture_stress(self) -> None: + with pytest.raises(ValueError): + for true_fracture_stress in [0.0, -500.0]: + calc_stress_eq_amp_morrow(100.0, 50.0, true_fracture_stress) + + def test_mean_stress_true_fracture_stress_comparison_error(self) -> None: + with pytest.raises(ValueError): + calc_stress_eq_amp_morrow(100.0, 500.0, 500.0) + + def test_mean_stress_true_fracture_stress_comparison_warning(self) -> None: + with pytest.warns(UserWarning): + calc_stress_eq_amp_morrow(100.0, 600.0, 500.0) + + +class TestCalcStressEqAmpSoderberg: + def test_basic_calculation(self) -> None: + result = calc_stress_eq_amp_soderberg(180.0, 100.0, 500.0) + expected = 180.0 / (1.0 - (100.0 / 500.0)) + assert_allclose(result, expected) + + def test_array_inputs( + self, + array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + ) -> None: + stress_amp, mean_stress = array_inputs + result = calc_stress_eq_amp_soderberg(stress_amp, mean_stress, 700.0) + assert result.shape == (4,) + + def test_invalid_yield_strength(self) -> None: + with pytest.raises(ValueError): + for yield_strength in [0.0, -500.0]: + calc_stress_eq_amp_soderberg(100.0, 50.0, yield_strength) + + def test_mean_stress_yield_strength_comparison_error(self) -> None: + with pytest.raises(ValueError): + calc_stress_eq_amp_soderberg(100.0, 500.0, 500.0) + + def test_mean_stress_yield_strength_comparison_warning(self) -> None: + with pytest.warns(UserWarning): + calc_stress_eq_amp_soderberg(100.0, 600.0, 500.0) + + +class TestCalcStressEqAmpSmith: + def test_basic_calculation(self) -> None: + result = calc_stress_eq_amp_smith(180.0, 100.0, 500.0) + expected = (180.0 * (1 + (100.0 / 500.0))) / (1.0 - (100.0 / 500.0)) + assert_allclose(result, expected) + + def test_array_inputs( + self, + array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + ) -> None: + stress_amp, mean_stress = array_inputs + result = calc_stress_eq_amp_smith(stress_amp, mean_stress, 700.0) + assert result.shape == (4,) + + def test_invalid_uts(self) -> None: + with pytest.raises(ValueError): + for uts in [0.0, -500.0]: + calc_stress_eq_amp_smith(100.0, 50.0, uts) + + def test_mean_stress_uts_comparison_error(self) -> None: + with pytest.raises(ValueError): + calc_stress_eq_amp_smith(100.0, 500.0, 500.0) + + def test_mean_stress_uts_comparison_warning(self) -> None: + with pytest.warns(UserWarning): + calc_stress_eq_amp_smith(100.0, 600.0, 500.0) + + +class TestCalcStressEqAmpSwt: + def test_basic_calculation(self) -> None: + for mean_stress, stress_amp in [(-100.0, 180.0), (100.0, 180.0)]: + result = calc_stress_eq_amp_swt(stress_amp, mean_stress) + expected = np.sqrt((stress_amp + mean_stress) * stress_amp) + assert_allclose(result, expected) + + def test_array_inputs( + self, + array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + ) -> None: + stress_amp, mean_stress = array_inputs + result = calc_stress_eq_amp_swt(stress_amp, mean_stress) + assert result.shape == (4,) + + def test_negative_stress_amplitude(self) -> None: + with pytest.raises(ValueError): + calc_stress_eq_amp_swt(-100.0, 500.0) + + def test_swt_validity_condition(self) -> None: + with pytest.raises(ValueError): + calc_stress_eq_amp_swt(400.0, -500.0) + + +class TestCalcStressEqAmpWalker: + def test_basic_calculation(self) -> None: + result = calc_stress_eq_amp_walker(180.0, 100.0, 0.4) + expected = (180.0 + 100.0) ** 0.6 * 180.0**0.4 + assert_allclose(result, expected) + + def test_array_inputs( + self, + array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + ) -> None: + stress_amp, mean_stress = array_inputs + result = calc_stress_eq_amp_walker(stress_amp, mean_stress, 0.4) + assert result.shape == (4,) + + def test_invalid_walker_parameter(self) -> None: + with pytest.raises(ValueError): + for walker_param in [-1.0, 2.0]: + calc_stress_eq_amp_walker(100.0, 50.0, walker_param)