From c7ef1e84cb2654ee1d962656f0996be81e146000 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Wed, 12 Nov 2025 16:41:23 +0100 Subject: [PATCH 01/21] Initial version of SWt for HFC --- .../damage_params/uniaxial_stress_eq_amp.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/fatpy/core/stress_life/damage_params/uniaxial_stress_eq_amp.py 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..52171fa --- /dev/null +++ b/src/fatpy/core/stress_life/damage_params/uniaxial_stress_eq_amp.py @@ -0,0 +1,72 @@ +"""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. +""" + +import warnings + +import numpy as np +from numpy.typing import ArrayLike, NDArray + + +def calc_stress_eq_amp_swt( + stress_amp: ArrayLike | float, + mean_stress: ArrayLike | float, +) -> NDArray[np.float64]: + r"""Calculate equivalent stress amplitude using Smith-Watson-Topper parameter. + + 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. Tensor rank matches the broadcasted result + of the input arrays. + + Raises: + ValueError: If input arrays cannot be broadcast together. + + UserWarning: When the 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 warning is issued as the SWT approach may not be appropriate + for compressive-dominated loading conditions. + + """ + stress_amp = np.asarray(stress_amp) + mean_stress = np.asarray(mean_stress) + + # Check validity condition: σₐ > |σₘ| + abs_mean_stress = np.abs(mean_stress) + invalid_condition = stress_amp <= abs_mean_stress + + if np.any(invalid_condition): + warnings.warn( + "Smith-Watson-Topper parameter validity condition (σₐ > |σₘ|) not " + "satisfied for some data points. The SWT approach may not be " + "appropriate for compressive-dominated loading conditions.", + UserWarning, + ) + return + + stress_eq_amp = np.sqrt(stress_amp * (mean_stress + stress_amp)) + return stress_eq_amp From e5f37a2709335ff65cecb55950b89d9a209149dc Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Thu, 13 Nov 2025 10:15:20 +0100 Subject: [PATCH 02/21] other eq stress functions and input validation --- .../damage_params/uniaxial_stress_eq_amp.py | 200 ++++++++++++++++-- 1 file changed, 181 insertions(+), 19 deletions(-) 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 index 52171fa..8aaae7a 100644 --- 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 @@ -7,15 +7,63 @@ significantly affect material endurance. """ -import warnings - import numpy as np from numpy.typing import ArrayLike, NDArray +def _validate_stress_inputs( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + material_param: ArrayLike | np.float64 | None = None, + param_name: str = "material parameter", +) -> tuple[NDArray[np.float64], NDArray[np.float64]]: + """Validate stress inputs and material parameters for fatigue calculations. + + Args: + stress_amp: Stress amplitudes (must be non-negative) + mean_stress: Mean stresses (can be positive or negative) + material_param: Material strength parameter (must be positive) + param_name: Name of material parameter for error messages + + Returns: + Tuple of validated arrays (stress_amp, mean_stress) + + Raises: + ValueError: If validation fails + UserWarning: For questionable but not invalid conditions + """ + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + material_param_arr = ( + None if material_param is None else np.asarray(material_param, dtype=np.float64) + ) + + # Check for negative stress amplitudes + if np.any(stress_amp_arr < 0): + raise ValueError("Stress amplitude must be non-negative") + + # Validate material parameter if provided + if material_param_arr is not None: + if np.any(material_param_arr <= 0): + raise ValueError(f"{param_name} must be positive") + + # Check if mean stress approaches or exceeds material parameter + abs_mean = np.abs(mean_stress_arr) + ratio = abs_mean / material_param_arr + + if np.any(ratio >= 1.0): + raise ValueError( + f"Mean stress magnitude ({np.max(abs_mean):.1f}) exceeds or equals " + f"{param_name} ({np.min(material_param_arr):.1f}). This would result in" + " infinite or negative equivalent stress amplitude." + ) + + return stress_amp_arr, mean_stress_arr + + def calc_stress_eq_amp_swt( - stress_amp: ArrayLike | float, - mean_stress: ArrayLike | float, + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Smith-Watson-Topper parameter. @@ -37,13 +85,11 @@ def calc_stress_eq_amp_swt( Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting - rules for the input arrays. Tensor rank matches the broadcasted result - of the input arrays. + rules for the input arrays. Raises: - ValueError: If input arrays cannot be broadcast together. - - UserWarning: When the condition σₐ > |σₘ| is not satisfied. + ValueError: If input arrays cannot be broadcast together or when the + condition σₐ > |σₘ| is not satisfied. ??? note "Validity Condition" The SWT parameter is valid when $\sigma_a > |\sigma_m|$, ensuring that the @@ -52,21 +98,137 @@ def calc_stress_eq_amp_swt( for compressive-dominated loading conditions. """ - stress_amp = np.asarray(stress_amp) - mean_stress = np.asarray(mean_stress) + stress_amp_arr, mean_stress_arr = _validate_stress_inputs(stress_amp, mean_stress) # Check validity condition: σₐ > |σₘ| - abs_mean_stress = np.abs(mean_stress) - invalid_condition = stress_amp <= abs_mean_stress + abs_mean_stress = np.abs(mean_stress_arr) + invalid_condition = stress_amp_arr <= abs_mean_stress if np.any(invalid_condition): - warnings.warn( + 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.", - UserWarning, + "appropriate for compressive-dominated loading conditions." ) - return - stress_eq_amp = np.sqrt(stress_amp * (mean_stress + stress_amp)) - return stress_eq_amp + return np.sqrt(stress_amp_arr * (mean_stress_arr + stress_amp_arr)) + + +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. + + 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: + ValueError: If input arrays cannot be broadcast together. + """ + stress_amp_arr, mean_stress_arr = _validate_stress_inputs( + stress_amp, mean_stress, ult_stress, "Ultimate tensile strength" + ) + + ult_stress_arr = np.asarray(ult_stress, dtype=np.float64) + + return stress_amp_arr / (1 - mean_stress_arr / ult_stress_arr) + + +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. + + 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: + ValueError: If input arrays cannot be broadcast together. + + """ + stress_amp_arr, mean_stress_arr = _validate_stress_inputs( + stress_amp, mean_stress, ult_stress, "Ultimate tensile strength" + ) + ult_stress_arr = np.asarray(ult_stress, dtype=np.float64) + + return stress_amp_arr / (1 - (mean_stress_arr / ult_stress_arr) ** 2) + + +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. + + 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: + ValueError: If input arrays cannot be broadcast together + """ + stress_amp_arr, mean_stress_arr = _validate_stress_inputs( + stress_amp, mean_stress, true_fract_stress, "True tensile fracture stress" + ) + true_fract_stress_arr = np.asarray(true_fract_stress, dtype=np.float64) + + return stress_amp_arr / (1 - mean_stress_arr / true_fract_stress_arr) From c31397541d7075c3982484b6194a3f93cc4a0014 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Thu, 13 Nov 2025 11:20:11 +0100 Subject: [PATCH 03/21] tests proposition --- .../test_iniaxial_stress_eq_amp.py | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 tests/core/stress_life/damage_params/test_iniaxial_stress_eq_amp.py diff --git a/tests/core/stress_life/damage_params/test_iniaxial_stress_eq_amp.py b/tests/core/stress_life/damage_params/test_iniaxial_stress_eq_amp.py new file mode 100644 index 0000000..2ffee18 --- /dev/null +++ b/tests/core/stress_life/damage_params/test_iniaxial_stress_eq_amp.py @@ -0,0 +1,267 @@ +"""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 Union + +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 ( + _validate_stress_inputs, + calc_stress_eq_amp_gerber, + calc_stress_eq_amp_goodman, + calc_stress_eq_amp_morrow, + calc_stress_eq_amp_swt, +) + +# Type alias for stress calculation functions +callable = Union[ + type(calc_stress_eq_amp_swt), + type(calc_stress_eq_amp_goodman), + type(calc_stress_eq_amp_gerber), + type(calc_stress_eq_amp_morrow), +] + + +@pytest.fixture +def sample_stress_data() -> tuple[NDArray[np.float64], NDArray[np.float64]]: + """Fixture providing sample stress amplitude and mean stress data. + + Returns: + tuple: (stress_amplitudes, mean_stresses) arrays for testing + """ + stress_amplitudes = np.array([150.0, 500.0, 80.0]) + mean_stresses = np.array([100.0, 30.0, 0.0]) + return stress_amplitudes, mean_stresses + + +@pytest.fixture +def material_properties() -> dict[str, float]: + """Fixture providing sample material properties. + + Returns: + dict: Material properties for testing + """ + return { + "ult_stress": 700.0, + "true_fract_stress": 770.0, + } + + +@pytest.fixture +def zero_mean_stress_case() -> tuple[float, float]: + """Fixture providing stress case with zero mean stress (purely alternating). + + Returns: + tuple: (stress_amplitude, mean_stress) with mean_stress = 0 + """ + return 100.0, 0.0 + + +@pytest.fixture +def negative_mean_stress_case() -> tuple[float, float]: + """Fixture providing stress case with negative mean stress. + + Returns: + tuple: (stress_amplitude, mean_stress) with negative mean_stress + """ + return 150.0, -50.0 + + +@pytest.fixture +def validation_test_cases() -> dict[str, tuple[float, float, float, str]]: + """Fixture providing test cases for input validation. + + Returns: + dict: Test cases with (stress_amp, mean_stress, material_param, param_name) + """ + return { + "valid_case": (100.0, 50.0, 400.0, "test parameter"), + "negative_stress_amp": (-50.0, 30.0, 400.0, "test parameter"), + "negative_material_param": (100.0, 50.0, -400.0, "test parameter"), + "zero_material_param": (100.0, 50.0, 0.0, "ultimate tensile strength"), + "mean_exceeds_material": (100.0, 450.0, 400.0, "ultimate tensile strength"), + "mean_equals_material": (100.0, 400.0, 400.0, "ultimate tensile strength"), + } + + +def test_validate_stress_inputs_valid_no_material_param( + sample_stress_data: tuple[NDArray[np.float64], NDArray[np.float64]], +) -> None: + """Test validation with valid inputs and no material parameter.""" + stress_amp, mean_stress = sample_stress_data + + stress_amp_arr, mean_stress_arr = _validate_stress_inputs(stress_amp, mean_stress) + + assert_allclose(stress_amp_arr, stress_amp) + assert_allclose(mean_stress_arr, mean_stress) + assert stress_amp_arr.dtype == np.float64 + assert mean_stress_arr.dtype == np.float64 + + +@pytest.mark.parametrize( + "case_name,should_pass,expected_error", + [ + ("valid_case", True, None), + ("negative_stress_amp", False, "Stress amplitude must be non-negative"), + ("negative_material_param", False, "test parameter must be positive"), + ("zero_material_param", False, "ultimate tensile strength must be positive"), + ("mean_exceeds_material", False, "Mean stress magnitude.*exceeds or equals"), + ("mean_equals_material", False, "Mean stress magnitude.*exceeds or equals"), + ], +) +def test_validate_stress_inputs_parametrized( + validation_test_cases: dict[str, tuple[float, float, float, str]], + case_name: str, + should_pass: bool, + expected_error: str | None, +) -> None: + """Parametrized test for input validation with various cases.""" + test_case = validation_test_cases[case_name] + stress_amp, mean_stress, material_param, param_name = test_case + + if should_pass: + stress_amp_arr, mean_stress_arr = _validate_stress_inputs( + stress_amp, mean_stress, material_param, param_name + ) + assert stress_amp_arr == stress_amp + assert mean_stress_arr == mean_stress + else: + with pytest.raises(ValueError, match=expected_error): + _validate_stress_inputs(stress_amp, mean_stress, material_param, param_name) + + +def test_validate_stress_inputs_array_broadcasting() -> None: + """Test validation with different array shapes.""" + stress_amp = np.array([100.0, 200.0, 150.0]) + mean_stress = 50.0 + material_param = 500.0 + + stress_amp_arr, mean_stress_arr = _validate_stress_inputs( + stress_amp, mean_stress, material_param + ) + + assert stress_amp_arr.shape == (3,) + assert mean_stress_arr.shape == () + assert_allclose(stress_amp_arr, [100.0, 200.0, 150.0]) + assert mean_stress_arr == 50.0 + + +@pytest.mark.parametrize( + "method,stress_amp,mean_stress,material_param,expected_result", + [ + (calc_stress_eq_amp_swt, 290.0, 10.0, None, 294.958), + (calc_stress_eq_amp_goodman, 180.0, 100.0, 700.0, 210.0), + (calc_stress_eq_amp_gerber, 180.0, 100.0, 700.0, 183.8), + (calc_stress_eq_amp_morrow, 180.0, 100.0, 770.0, 206.9), + ], +) +def test_calc_stress_eq_amp_basic_calculations( + method: callable, + stress_amp: float, + mean_stress: float, + material_param: float | None, + expected_result: float, +) -> None: + """Test basic calculations for all equivalent stress amplitude methods.""" + if material_param is None: + result = method(stress_amp, mean_stress) + else: + result = method(stress_amp, mean_stress, material_param) + + assert_allclose(result, expected_result, rtol=1e-2) + + +@pytest.mark.parametrize( + "method,material_param_key", + [ + (calc_stress_eq_amp_swt, None), + (calc_stress_eq_amp_goodman, "ult_stress"), + (calc_stress_eq_amp_gerber, "ult_stress"), + (calc_stress_eq_amp_morrow, "true_fract_stress"), + ], +) +def test_calc_stress_eq_amp_array_inputs( + method: callable, + material_param_key: str | None, + sample_stress_data: tuple[NDArray[np.float64], NDArray[np.float64]], + material_properties: dict[str, float], +) -> None: + """Test all methods with array inputs.""" + stress_amp, mean_stress = sample_stress_data + + if material_param_key is None: + result = method(stress_amp, mean_stress) + expected = np.sqrt(stress_amp * (mean_stress + stress_amp)) + else: + material_param = material_properties[material_param_key] + result = method(stress_amp, mean_stress, material_param) + + if method == calc_stress_eq_amp_gerber: + expected = stress_amp / (1 - (mean_stress / material_param) ** 2) + else: # Goodman or Morrow + expected = stress_amp / (1 - mean_stress / material_param) + + assert_allclose(result, expected) + assert result.shape == (3,) + + +@pytest.mark.parametrize( + "method,material_param", + [ + (calc_stress_eq_amp_swt, None), + (calc_stress_eq_amp_goodman, 500.0), + (calc_stress_eq_amp_gerber, 500.0), + (calc_stress_eq_amp_morrow, 800.0), + ], +) +def test_calc_stress_eq_amp_zero_mean_stress( + method: callable, + material_param: float | None, + zero_mean_stress_case: tuple[float, float], +) -> None: + """Test all methods with zero mean stress (should equal stress amplitude).""" + stress_amp, mean_stress = zero_mean_stress_case + + if material_param is None: + result = method(stress_amp, mean_stress) + else: + result = method(stress_amp, mean_stress, material_param) + + assert_allclose(result, stress_amp) + + +def test_calc_stress_eq_amp_swt_negative_mean_stress( + negative_mean_stress_case: tuple[float, float], +) -> None: + """Test SWT with negative mean stress.""" + stress_amp, mean_stress = negative_mean_stress_case + result = calc_stress_eq_amp_swt(stress_amp, mean_stress) + expected = np.sqrt(stress_amp * (stress_amp + mean_stress)) + assert_allclose(result, expected) + + +def test_calc_stress_eq_amp_swt_validity_condition_violation() -> None: + """Test that SWT validity condition violation raises ValueError.""" + with pytest.raises( + ValueError, match="Smith-Watson-Topper parameter validity condition" + ): + calc_stress_eq_amp_swt(stress_amp=50.0, mean_stress=100.0) + + with pytest.raises( + ValueError, match="Smith-Watson-Topper parameter validity condition" + ): + calc_stress_eq_amp_swt(stress_amp=50.0, mean_stress=-100.0) + + +def test_calc_stress_eq_amp_swt_validity_condition_boundary() -> None: + """Test SWT validity condition at boundary.""" + with pytest.raises( + ValueError, match="Smith-Watson-Topper parameter validity condition" + ): + calc_stress_eq_amp_swt(stress_amp=100.0, mean_stress=100.0) From 94980614f4f091cb5616a378f4d0893af6025e1e Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Thu, 13 Nov 2025 11:24:35 +0100 Subject: [PATCH 04/21] ruff formating fix --- .../stress_life/damage_params/uniaxial_stress_eq_amp.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 index 8aaae7a..097751a 100644 --- 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 @@ -169,7 +169,8 @@ def calc_stress_eq_amp_gerber( The Gerber equivalent stress amplitude is calculated as: $$ - \displaystyle\sigma_{aeq}=\frac{\sigma_a}{1-\left(\frac{\sigma_m}{\sigma_{UTS}}\right)^2 } + \displaystyle\sigma_{aeq}=\frac{\sigma_a}{1-\left(\frac{\sigma_m}{\sigma_{UTS}} + \right)^2 } $$ Args: @@ -216,8 +217,9 @@ def calc_stress_eq_amp_morrow( 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. + 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 From 6ecfd77c3ac4acd59314462dc4067c7972c2fc64 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Thu, 1 Jan 2026 11:26:40 +0100 Subject: [PATCH 05/21] added walker --- .../damage_params/uniaxial_stress_eq_amp.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) 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 index 097751a..31215ad 100644 --- 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 @@ -234,3 +234,54 @@ def calc_stress_eq_amp_morrow( true_fract_stress_arr = np.asarray(true_fract_stress, dtype=np.float64) return stress_amp_arr / (1 - mean_stress_arr / true_fract_stress_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. + + 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, mean_stress_arr = _validate_stress_inputs( + stress_amp, mean_stress, None + ) + 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). " + "Invalid values detected in the input data." + ) + + return (stress_amp_arr + mean_stress_arr) ** ( + 1 - walker_parameter_arr + ) * stress_amp_arr**walker_parameter_arr From 1e2496e825fe04fc17203ac7fd805dc3d22e0d25 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Sat, 21 Feb 2026 16:50:42 +0100 Subject: [PATCH 06/21] Added ASME and other methods based on paper in ASME issue --- .../damage_params/uniaxial_stress_eq_amp.py | 555 +++++++++++++++--- 1 file changed, 466 insertions(+), 89 deletions(-) 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 index 31215ad..d0877e2 100644 --- 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 @@ -7,75 +7,31 @@ significantly affect material endurance. """ +import warnings + import numpy as np from numpy.typing import ArrayLike, NDArray -def _validate_stress_inputs( - stress_amp: ArrayLike | np.float64, - mean_stress: ArrayLike | np.float64, - material_param: ArrayLike | np.float64 | None = None, - param_name: str = "material parameter", -) -> tuple[NDArray[np.float64], NDArray[np.float64]]: - """Validate stress inputs and material parameters for fatigue calculations. - - Args: - stress_amp: Stress amplitudes (must be non-negative) - mean_stress: Mean stresses (can be positive or negative) - material_param: Material strength parameter (must be positive) - param_name: Name of material parameter for error messages - - Returns: - Tuple of validated arrays (stress_amp, mean_stress) - - Raises: - ValueError: If validation fails - UserWarning: For questionable but not invalid conditions - """ - stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) - mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) - material_param_arr = ( - None if material_param is None else np.asarray(material_param, dtype=np.float64) - ) - - # Check for negative stress amplitudes - if np.any(stress_amp_arr < 0): - raise ValueError("Stress amplitude must be non-negative") - - # Validate material parameter if provided - if material_param_arr is not None: - if np.any(material_param_arr <= 0): - raise ValueError(f"{param_name} must be positive") - - # Check if mean stress approaches or exceeds material parameter - abs_mean = np.abs(mean_stress_arr) - ratio = abs_mean / material_param_arr - - if np.any(ratio >= 1.0): - raise ValueError( - f"Mean stress magnitude ({np.max(abs_mean):.1f}) exceeds or equals " - f"{param_name} ({np.min(material_param_arr):.1f}). This would result in" - " infinite or negative equivalent stress amplitude." - ) - - return stress_amp_arr, mean_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. - 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. - + ??? 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. + # TODO: předpoklad nulové plastické deformace, pro LCF nutno použít metodu ze strain life, refernce na korektní funkci ??? abstract "Math Equations" The SWT equivalent stress amplitude is calculated as: $$ - \sigma_{aeq} = \sqrt{\sigma_{a} \cdot (\sigma_{m} + \sigma_{a})} + \begin{align*} + \sigma_{aeq} = \sqrt{\sigma_{a} \cdot (\sigma_{m} + \sigma_{a})} \\ + \text{where: } \sigma_{m} < 0 \rightarrow \sigma_{aeq} = \sigma_{a} \\ + \end{align*} $$ Args: @@ -88,17 +44,23 @@ def calc_stress_eq_amp_swt( rules for the input arrays. Raises: - ValueError: If input arrays cannot be broadcast together or when the - condition σₐ > |σₘ| is not satisfied. + Warning: If mean stress is compressive (σₘ < 0), a warning is issued and + the equivalent stress amplitude is set equal to the stress amplitude (σₐ) + 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 warning is issued as the SWT approach may not be appropriate - for compressive-dominated loading conditions. + not met, a ValueError is raised. """ - stress_amp_arr, mean_stress_arr = _validate_stress_inputs(stress_amp, mean_stress) + 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) @@ -111,6 +73,14 @@ def calc_stress_eq_amp_swt( "appropriate for compressive-dominated loading conditions." ) + if np.any(mean_stress_arr < 0): + warnings.warn( + r"Mean stress is compressive, $\sigma_{aeq} = \sigma_a$ was used!", + UserWarning, + stacklevel=2, + ) + return stress_amp_arr + return np.sqrt(stress_amp_arr * (mean_stress_arr + stress_amp_arr)) @@ -121,9 +91,10 @@ def calc_stress_eq_amp_goodman( ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Goodman criterion. - 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. + ??? 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: @@ -144,14 +115,33 @@ def calc_stress_eq_amp_goodman( rules for the input arrays. Raises: - ValueError: If input arrays cannot be broadcast together. + 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, mean_stress_arr = _validate_stress_inputs( - stress_amp, mean_stress, ult_stress, "Ultimate tensile strength" - ) - + 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) @@ -162,8 +152,9 @@ def calc_stress_eq_amp_gerber( ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Gerber criterion. - The Gerber criterion accounts for mean stress effects in high-cycle fatigue - by modifying the stress amplitude based on the ultimate tensile strength. + ??? 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: @@ -185,14 +176,34 @@ def calc_stress_eq_amp_gerber( rules for the input arrays. Raises: - ValueError: If input arrays cannot be broadcast together. + 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, mean_stress_arr = _validate_stress_inputs( - stress_amp, mean_stress, ult_stress, "Ultimate tensile strength" - ) + 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 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) @@ -203,8 +214,9 @@ def calc_stress_eq_amp_morrow( ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Morrow criterion. - The Morrow criterion accounts for mean stress effects in high-cycle fatigue - by modifying the stress amplitude based on the true fracture strength. + ??? 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: @@ -226,13 +238,33 @@ def calc_stress_eq_amp_morrow( rules for the input arrays. Raises: - ValueError: If input arrays cannot be broadcast together + 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, mean_stress_arr = _validate_stress_inputs( - stress_amp, mean_stress, true_fract_stress, "True tensile fracture stress" - ) + 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) @@ -243,9 +275,10 @@ def calc_stress_eq_amp_walker( ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Walker criterion. - 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 (γ'). + ??? 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: @@ -269,19 +302,363 @@ def calc_stress_eq_amp_walker( ValueError: If input arrays cannot be broadcast together or when the condition γ' in (0, 1) is not satisfied. """ - stress_amp_arr, mean_stress_arr = _validate_stress_inputs( - stress_amp, mean_stress, None - ) + 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). " - "Invalid values detected in the input data." - ) + 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 + + +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 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_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_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_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)) From 17dd2dd8f88962111bc89cac2e62ac90ea95d9f2 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Sat, 21 Feb 2026 16:55:46 +0100 Subject: [PATCH 07/21] Alphabetical order of functions --- .../damage_params/uniaxial_stress_eq_amp.py | 435 +++++++++--------- 1 file changed, 217 insertions(+), 218 deletions(-) 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 index d0877e2..cfc6f7f 100644 --- 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 @@ -13,136 +13,110 @@ from numpy.typing import ArrayLike, NDArray -def calc_stress_eq_amp_swt( +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 Smith-Watson-Topper parameter. + r"""Calculate equivalent stress amplitude using ASME criterion. - ??? 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. - # TODO: předpoklad nulové plastické deformace, pro LCF nutno použít metodu ze strain life, refernce na korektní funkci + ??? 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 SWT equivalent stress amplitude is calculated as: + The ASME equivalent stress amplitude is calculated as: $$ - \begin{align*} - \sigma_{aeq} = \sqrt{\sigma_{a} \cdot (\sigma_{m} + \sigma_{a})} \\ - \text{where: } \sigma_{m} < 0 \rightarrow \sigma_{aeq} = \sigma_{a} \\ - \end{align*} + \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. - - Returns: - Array of equivalent stress amplitudes. Shape follows NumPy broadcasting - rules for the input arrays. + yield_strength: Array-like of yield strengths. Must be broadcastable with + stress_amp and mean_stress. Leading dimensions are preserved. Raises: - Warning: If mean stress is compressive (σₘ < 0), a warning is issued and - the equivalent stress amplitude is set equal to the stress amplitude (σₐ) - 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. - + 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) - # 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." - ) + if np.any(yield_strength_arr <= 0): + raise ValueError("Yield strength must be positive") - if np.any(mean_stress_arr < 0): - warnings.warn( - r"Mean stress is compressive, $\sigma_{aeq} = \sigma_a$ was used!", - UserWarning, - stacklevel=2, - ) - return stress_amp_arr + # 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 np.sqrt(stress_amp_arr * (mean_stress_arr + stress_amp_arr)) + return stress_amp_arr / (1 - (mean_stress_arr / yield_strength_arr) ** 2) ** 0.5 -def calc_stress_eq_amp_goodman( +def calc_stress_eq_amp_bagci( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - ult_stress: ArrayLike | np.float64, + yield_strength: ArrayLike | np.float64, ) -> NDArray[np.float64]: - r"""Calculate equivalent stress amplitude using Goodman criterion. + r"""Calculate equivalent stress amplitude using Bagci 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. + ??? 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 Goodman equivalent stress amplitude is calculated as: + The Bagci equivalent stress amplitude is calculated as: $$ - \displaystyle\sigma_{aeq}=\frac{\sigma_a}{1-\frac{\sigma_m}{\sigma_{UTS}}} + \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. - ult_stress: Array-like of ultimate tensile strengths. Must be broadcastable - with stress_amp and mean_stress. 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 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. + 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) - ult_stress_arr = np.asarray(ult_stress, dtype=np.float64) + yield_strength_arr = np.asarray(yield_strength, dtype=np.float64) - if np.any(ult_stress_arr <= 0): - raise ValueError("Ultimate tensile strength must be positive") + 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 / ult_stress_arr - + ratio = np.abs(mean_stress_arr) / yield_strength_arr if np.any(ratio == 1.0): raise ValueError( - "Mean stress equals ultimate tensile strength this would result in " + "Mean stress equals yield strength this would result in " "infinite equivalent stress amplitude." ) elif np.any(ratio > 1.0): warnings.warn( - "Mean stress magnitude exceeds ultimate tensile strength. ", + "Mean stress magnitude exceeds yield strength.", UserWarning, stacklevel=2, ) - return stress_amp_arr / (1 - mean_stress_arr / ult_stress_arr) + return stress_amp_arr / (1 - (mean_stress_arr / yield_strength_arr) ** 4) def calc_stress_eq_amp_gerber( @@ -207,91 +181,91 @@ def calc_stress_eq_amp_gerber( return stress_amp_arr / (1 - (mean_stress_arr / ult_stress_arr) ** 2) -def calc_stress_eq_amp_morrow( +def calc_stress_eq_amp_goodman( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - true_fract_stress: ArrayLike | np.float64, + ult_stress: ArrayLike | np.float64, ) -> NDArray[np.float64]: - r"""Calculate equivalent stress amplitude using Morrow criterion. + r"""Calculate equivalent stress amplitude using Goodman 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. + ??? 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 Morrow equivalent stress amplitude is calculated as: + The Goodman equivalent stress amplitude is calculated as: $$ - \displaystyle\sigma_{aeq}=\frac{\sigma_a}{1-\frac{\sigma_m}{\sigma_{true}} } + \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. - true_fract_stress: Array-like of true tensile fracture stress. Must be - broadcastable with stress_amp and mean_stress. 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 true fracture stress. - ValueError: If true fracture stress is not positive. - ValueError: If mean stress is equal to true fracture stress, resulting in + 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) - true_fract_stress_arr = np.asarray(true_fract_stress, dtype=np.float64) + ult_stress_arr = np.asarray(ult_stress, dtype=np.float64) - if np.any(true_fract_stress_arr <= 0): - raise ValueError("True fracture stress must be positive") + 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 / true_fract_stress_arr + ratio = mean_stress_arr / ult_stress_arr if np.any(ratio == 1.0): raise ValueError( - "Mean stress equals true fracture stress this would result in " + "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 true fracture stress. ", + "Mean stress magnitude exceeds ultimate tensile strength. ", UserWarning, stacklevel=2, ) - return stress_amp_arr / (1 - mean_stress_arr / true_fract_stress_arr) + return stress_amp_arr / (1 - mean_stress_arr / ult_stress_arr) -def calc_stress_eq_amp_walker( +def calc_stress_eq_amp_half_slope( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - walker_parameter: ArrayLike | np.float64, + ult_stress: ArrayLike | np.float64, ) -> NDArray[np.float64]: - r"""Calculate equivalent stress amplitude using Walker criterion. + r"""Calculate equivalent stress amplitude using a half-slope mean stress correction. - ??? 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 (γ'). + ??? 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 Walker equivalent stress amplitude is calculated as: + The half-slope corrected equivalent stress amplitude is calculated as: $$ - \displaystyle\sigma_{aeq}=\left(\sigma_a+\sigma_m\right)^{1-\gamma'} \cdot - \sigma_a^{\gamma'} + \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. - walker_parameter: Array-like of Walker exponents (γ'). Must be broadcastable + ult_stress: Array-like of ultimate tensile strengths. Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. Returns: @@ -299,127 +273,154 @@ def calc_stress_eq_amp_walker( rules for the input arrays. Raises: - ValueError: If input arrays cannot be broadcast together or when the - condition γ' in (0, 1) is not satisfied. + 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) - walker_parameter_arr = np.asarray(walker_parameter, dtype=np.float64) + ult_stress_arr = np.asarray(ult_stress, 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). ") + if np.any(ult_stress_arr <= 0): + raise ValueError("Ultimate tensile strength must be positive") - return (stress_amp_arr + mean_stress_arr) ** ( - 1 - walker_parameter_arr - ) * stress_amp_arr**walker_parameter_arr + # 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_ASME( +def calc_stress_eq_amp_linear( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - yield_strength: ArrayLike | np.float64, + stress_parameter_M: ArrayLike | np.float64, ) -> NDArray[np.float64]: - r"""Calculate equivalent stress amplitude using ASME criterion. + r"""Calculate equivalent stress amplitude using a linear mean stress correction. - ??? 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. + ??? 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 ASME equivalent stress amplitude is calculated as: + The linearly corrected equivalent stress amplitude is calculated as: $$ - \sigma_{aeq}=\frac{\sigma_a}{\left[1-\left(\frac{\sigma_m}{R_e}\right)^2\right]^{1/2}} + \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. - yield_strength: Array-like of yield strengths. Must be broadcastable with - stress_amp and mean_stress. 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: - 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. + 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) - yield_strength_arr = np.asarray(yield_strength, dtype=np.float64) - - if np.any(yield_strength_arr <= 0): - raise ValueError("Yield strength must be positive") + 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 = 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.") + ratio = mean_stress_arr / stress_parameter_M_arr - return stress_amp_arr / (1 - (mean_stress_arr / yield_strength_arr) ** 2) ** 0.5 + 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_bagci( +def calc_stress_eq_amp_morrow( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - yield_strength: ArrayLike | np.float64, + true_fract_stress: ArrayLike | np.float64, ) -> NDArray[np.float64]: - r"""Calculate equivalent stress amplitude using Bagci criterion. + r"""Calculate equivalent stress amplitude using Morrow 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. + ??? 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 Bagci equivalent stress amplitude is calculated as: + The Morrow equivalent stress amplitude is calculated as: $$ - \sigma_{aeq}=\frac{\sigma_a}{1-\left(\frac{\sigma_m}{R_e}\right)^4} + \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. - yield_strength: Array-like of yield strengths. Must be broadcastable with - stress_amp and mean_stress. 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 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. + 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) - yield_strength_arr = np.asarray(yield_strength, dtype=np.float64) + true_fract_stress_arr = np.asarray(true_fract_stress, dtype=np.float64) - if np.any(yield_strength_arr <= 0): - raise ValueError("Yield strength must be positive") + 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 = np.abs(mean_stress_arr) / yield_strength_arr + ratio = mean_stress_arr / true_fract_stress_arr + if np.any(ratio == 1.0): raise ValueError( - "Mean stress equals yield strength this would result in " + "Mean stress equals true fracture stress this would result in " "infinite equivalent stress amplitude." ) elif np.any(ratio > 1.0): warnings.warn( - "Mean stress magnitude exceeds yield strength.", + "Mean stress exceeds true fracture stress. ", UserWarning, stacklevel=2, ) - return stress_amp_arr / (1 - (mean_stress_arr / yield_strength_arr) ** 4) + return stress_amp_arr / (1 - mean_stress_arr / true_fract_stress_arr) def calc_stress_eq_amp_soderberg( @@ -544,90 +545,100 @@ def calc_stress_eq_amp_smith( ) -def calc_stress_eq_amp_linear( +def calc_stress_eq_amp_swt( 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. + r"""Calculate equivalent stress amplitude using Smith-Watson-Topper parameter. - ??? 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. + ??? 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 linearly corrected equivalent stress amplitude is calculated as: + The SWT equivalent stress amplitude is calculated as: $$ - \sigma_{aeq}=\frac{\sigma_a}{1 - \frac{\sigma_m}{M}} + \begin{align*} + \sigma_{aeq} = \sqrt{\sigma_{a} \cdot (\sigma_{m} + \sigma_{a})} \\ + \text{where: } \sigma_{m} < 0 \rightarrow \sigma_{aeq} = \sigma_{a} \\ + \end{align*} $$ + 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. + 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. + Warning: If mean stress is compressive (σₘ < 0), a warning is issued and + the equivalent stress amplitude is set equal to the stress amplitude (σₐ) + 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) - 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 + # Check for negative stress amplitudes + if np.any(stress_amp_arr < 0): + raise ValueError("Stress amplitude must be non-negative") - if np.any(ratio == 1.0): + # 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( - "Mean stress equals material stress parameter M this would result in " - "zero equivalent stress amplitude." + "Smith-Watson-Topper parameter validity condition (σₐ > |σₘ|) not " + "satisfied for some data points. The SWT approach may not be " + "appropriate for compressive-dominated loading conditions." ) - elif np.any(ratio > 1.0): + + if np.any(mean_stress_arr < 0): warnings.warn( - "Mean stress exceeds material stress parameter M. ", + r"Mean stress is compressive, $\sigma_{aeq} = \sigma_a$ was used!", UserWarning, stacklevel=2, ) + return stress_amp_arr - return stress_amp_arr / (1 - mean_stress_arr / stress_parameter_M_arr) + return np.sqrt(stress_amp_arr * (mean_stress_arr + stress_amp_arr)) -def calc_stress_eq_amp_half_slope( +def calc_stress_eq_amp_walker( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - ult_stress: ArrayLike | np.float64, + walker_parameter: ArrayLike | np.float64, ) -> NDArray[np.float64]: - r"""Calculate equivalent stress amplitude using a half-slope mean stress correction. + r"""Calculate equivalent stress amplitude using Walker criterion. - ??? 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. + ??? 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 half-slope corrected equivalent stress amplitude is calculated as: + The Walker equivalent stress amplitude is calculated as: $$ - \sigma_{aeq}=\frac{\sigma_a}{1 - \frac{\sigma_m}{2 \cdot \sigma_{UTS}}} + \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. - ult_stress: Array-like of ultimate tensile strengths. Must be broadcastable + walker_parameter: Array-like of Walker exponents (γ'). Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. Returns: @@ -635,30 +646,18 @@ def calc_stress_eq_amp_half_slope( 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. - + 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) - ult_stress_arr = np.asarray(ult_stress, dtype=np.float64) + walker_parameter_arr = np.asarray(walker_parameter, dtype=np.float64) - if np.any(ult_stress_arr <= 0): - raise ValueError("Ultimate tensile strength must be positive") + # 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). ") - # 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)) + return (stress_amp_arr + mean_stress_arr) ** ( + 1 - walker_parameter_arr + ) * stress_amp_arr**walker_parameter_arr From f7f5f3d97e483956a24f35a3fc25c7a4d67b5c21 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Sat, 21 Feb 2026 17:00:26 +0100 Subject: [PATCH 08/21] reference added --- .../stress_life/damage_params/uniaxial_stress_eq_amp.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 index cfc6f7f..aa56da5 100644 --- 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 @@ -5,6 +5,9 @@ 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 @@ -324,8 +327,9 @@ def calc_stress_eq_amp_linear( 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. + 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 From 83f7322417facff600e5be32e8efd6d62c013cf9 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Tue, 17 Mar 2026 12:06:13 +0100 Subject: [PATCH 09/21] implementation of tests --- .../damage_params/uniaxial_stress_eq_amp.py | 6 +- .../test_iniaxial_stress_eq_amp.py | 267 -------------- .../test_uniaxial_stress_eq_amp.py | 338 ++++++++++++++++++ 3 files changed, 341 insertions(+), 270 deletions(-) delete mode 100644 tests/core/stress_life/damage_params/test_iniaxial_stress_eq_amp.py create mode 100644 tests/core/stress_life/damage_params/test_uniaxial_stress_eq_amp.py 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 index aa56da5..d3bf130 100644 --- 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 @@ -109,7 +109,7 @@ def calc_stress_eq_amp_bagci( ratio = np.abs(mean_stress_arr) / yield_strength_arr if np.any(ratio == 1.0): raise ValueError( - "Mean stress equals yield strength this would result in " + "Mean stress magnitude equals yield strength this would result in " "infinite equivalent stress amplitude." ) elif np.any(ratio > 1.0): @@ -171,8 +171,8 @@ def calc_stress_eq_amp_gerber( if np.any(ratio == 1.0): raise ValueError( - "Mean stress equals ultimate tensile strength this would result in " - "infinite equivalent stress amplitude." + "Mean stress magnitude equals ultimate tensile strength this would " + "result in infinite equivalent stress amplitude." ) elif np.any(ratio > 1.0): warnings.warn( diff --git a/tests/core/stress_life/damage_params/test_iniaxial_stress_eq_amp.py b/tests/core/stress_life/damage_params/test_iniaxial_stress_eq_amp.py deleted file mode 100644 index 2ffee18..0000000 --- a/tests/core/stress_life/damage_params/test_iniaxial_stress_eq_amp.py +++ /dev/null @@ -1,267 +0,0 @@ -"""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 Union - -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 ( - _validate_stress_inputs, - calc_stress_eq_amp_gerber, - calc_stress_eq_amp_goodman, - calc_stress_eq_amp_morrow, - calc_stress_eq_amp_swt, -) - -# Type alias for stress calculation functions -callable = Union[ - type(calc_stress_eq_amp_swt), - type(calc_stress_eq_amp_goodman), - type(calc_stress_eq_amp_gerber), - type(calc_stress_eq_amp_morrow), -] - - -@pytest.fixture -def sample_stress_data() -> tuple[NDArray[np.float64], NDArray[np.float64]]: - """Fixture providing sample stress amplitude and mean stress data. - - Returns: - tuple: (stress_amplitudes, mean_stresses) arrays for testing - """ - stress_amplitudes = np.array([150.0, 500.0, 80.0]) - mean_stresses = np.array([100.0, 30.0, 0.0]) - return stress_amplitudes, mean_stresses - - -@pytest.fixture -def material_properties() -> dict[str, float]: - """Fixture providing sample material properties. - - Returns: - dict: Material properties for testing - """ - return { - "ult_stress": 700.0, - "true_fract_stress": 770.0, - } - - -@pytest.fixture -def zero_mean_stress_case() -> tuple[float, float]: - """Fixture providing stress case with zero mean stress (purely alternating). - - Returns: - tuple: (stress_amplitude, mean_stress) with mean_stress = 0 - """ - return 100.0, 0.0 - - -@pytest.fixture -def negative_mean_stress_case() -> tuple[float, float]: - """Fixture providing stress case with negative mean stress. - - Returns: - tuple: (stress_amplitude, mean_stress) with negative mean_stress - """ - return 150.0, -50.0 - - -@pytest.fixture -def validation_test_cases() -> dict[str, tuple[float, float, float, str]]: - """Fixture providing test cases for input validation. - - Returns: - dict: Test cases with (stress_amp, mean_stress, material_param, param_name) - """ - return { - "valid_case": (100.0, 50.0, 400.0, "test parameter"), - "negative_stress_amp": (-50.0, 30.0, 400.0, "test parameter"), - "negative_material_param": (100.0, 50.0, -400.0, "test parameter"), - "zero_material_param": (100.0, 50.0, 0.0, "ultimate tensile strength"), - "mean_exceeds_material": (100.0, 450.0, 400.0, "ultimate tensile strength"), - "mean_equals_material": (100.0, 400.0, 400.0, "ultimate tensile strength"), - } - - -def test_validate_stress_inputs_valid_no_material_param( - sample_stress_data: tuple[NDArray[np.float64], NDArray[np.float64]], -) -> None: - """Test validation with valid inputs and no material parameter.""" - stress_amp, mean_stress = sample_stress_data - - stress_amp_arr, mean_stress_arr = _validate_stress_inputs(stress_amp, mean_stress) - - assert_allclose(stress_amp_arr, stress_amp) - assert_allclose(mean_stress_arr, mean_stress) - assert stress_amp_arr.dtype == np.float64 - assert mean_stress_arr.dtype == np.float64 - - -@pytest.mark.parametrize( - "case_name,should_pass,expected_error", - [ - ("valid_case", True, None), - ("negative_stress_amp", False, "Stress amplitude must be non-negative"), - ("negative_material_param", False, "test parameter must be positive"), - ("zero_material_param", False, "ultimate tensile strength must be positive"), - ("mean_exceeds_material", False, "Mean stress magnitude.*exceeds or equals"), - ("mean_equals_material", False, "Mean stress magnitude.*exceeds or equals"), - ], -) -def test_validate_stress_inputs_parametrized( - validation_test_cases: dict[str, tuple[float, float, float, str]], - case_name: str, - should_pass: bool, - expected_error: str | None, -) -> None: - """Parametrized test for input validation with various cases.""" - test_case = validation_test_cases[case_name] - stress_amp, mean_stress, material_param, param_name = test_case - - if should_pass: - stress_amp_arr, mean_stress_arr = _validate_stress_inputs( - stress_amp, mean_stress, material_param, param_name - ) - assert stress_amp_arr == stress_amp - assert mean_stress_arr == mean_stress - else: - with pytest.raises(ValueError, match=expected_error): - _validate_stress_inputs(stress_amp, mean_stress, material_param, param_name) - - -def test_validate_stress_inputs_array_broadcasting() -> None: - """Test validation with different array shapes.""" - stress_amp = np.array([100.0, 200.0, 150.0]) - mean_stress = 50.0 - material_param = 500.0 - - stress_amp_arr, mean_stress_arr = _validate_stress_inputs( - stress_amp, mean_stress, material_param - ) - - assert stress_amp_arr.shape == (3,) - assert mean_stress_arr.shape == () - assert_allclose(stress_amp_arr, [100.0, 200.0, 150.0]) - assert mean_stress_arr == 50.0 - - -@pytest.mark.parametrize( - "method,stress_amp,mean_stress,material_param,expected_result", - [ - (calc_stress_eq_amp_swt, 290.0, 10.0, None, 294.958), - (calc_stress_eq_amp_goodman, 180.0, 100.0, 700.0, 210.0), - (calc_stress_eq_amp_gerber, 180.0, 100.0, 700.0, 183.8), - (calc_stress_eq_amp_morrow, 180.0, 100.0, 770.0, 206.9), - ], -) -def test_calc_stress_eq_amp_basic_calculations( - method: callable, - stress_amp: float, - mean_stress: float, - material_param: float | None, - expected_result: float, -) -> None: - """Test basic calculations for all equivalent stress amplitude methods.""" - if material_param is None: - result = method(stress_amp, mean_stress) - else: - result = method(stress_amp, mean_stress, material_param) - - assert_allclose(result, expected_result, rtol=1e-2) - - -@pytest.mark.parametrize( - "method,material_param_key", - [ - (calc_stress_eq_amp_swt, None), - (calc_stress_eq_amp_goodman, "ult_stress"), - (calc_stress_eq_amp_gerber, "ult_stress"), - (calc_stress_eq_amp_morrow, "true_fract_stress"), - ], -) -def test_calc_stress_eq_amp_array_inputs( - method: callable, - material_param_key: str | None, - sample_stress_data: tuple[NDArray[np.float64], NDArray[np.float64]], - material_properties: dict[str, float], -) -> None: - """Test all methods with array inputs.""" - stress_amp, mean_stress = sample_stress_data - - if material_param_key is None: - result = method(stress_amp, mean_stress) - expected = np.sqrt(stress_amp * (mean_stress + stress_amp)) - else: - material_param = material_properties[material_param_key] - result = method(stress_amp, mean_stress, material_param) - - if method == calc_stress_eq_amp_gerber: - expected = stress_amp / (1 - (mean_stress / material_param) ** 2) - else: # Goodman or Morrow - expected = stress_amp / (1 - mean_stress / material_param) - - assert_allclose(result, expected) - assert result.shape == (3,) - - -@pytest.mark.parametrize( - "method,material_param", - [ - (calc_stress_eq_amp_swt, None), - (calc_stress_eq_amp_goodman, 500.0), - (calc_stress_eq_amp_gerber, 500.0), - (calc_stress_eq_amp_morrow, 800.0), - ], -) -def test_calc_stress_eq_amp_zero_mean_stress( - method: callable, - material_param: float | None, - zero_mean_stress_case: tuple[float, float], -) -> None: - """Test all methods with zero mean stress (should equal stress amplitude).""" - stress_amp, mean_stress = zero_mean_stress_case - - if material_param is None: - result = method(stress_amp, mean_stress) - else: - result = method(stress_amp, mean_stress, material_param) - - assert_allclose(result, stress_amp) - - -def test_calc_stress_eq_amp_swt_negative_mean_stress( - negative_mean_stress_case: tuple[float, float], -) -> None: - """Test SWT with negative mean stress.""" - stress_amp, mean_stress = negative_mean_stress_case - result = calc_stress_eq_amp_swt(stress_amp, mean_stress) - expected = np.sqrt(stress_amp * (stress_amp + mean_stress)) - assert_allclose(result, expected) - - -def test_calc_stress_eq_amp_swt_validity_condition_violation() -> None: - """Test that SWT validity condition violation raises ValueError.""" - with pytest.raises( - ValueError, match="Smith-Watson-Topper parameter validity condition" - ): - calc_stress_eq_amp_swt(stress_amp=50.0, mean_stress=100.0) - - with pytest.raises( - ValueError, match="Smith-Watson-Topper parameter validity condition" - ): - calc_stress_eq_amp_swt(stress_amp=50.0, mean_stress=-100.0) - - -def test_calc_stress_eq_amp_swt_validity_condition_boundary() -> None: - """Test SWT validity condition at boundary.""" - with pytest.raises( - ValueError, match="Smith-Watson-Topper parameter validity condition" - ): - calc_stress_eq_amp_swt(stress_amp=100.0, mean_stress=100.0) 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..7eceac2 --- /dev/null +++ b/tests/core/stress_life/damage_params/test_uniaxial_stress_eq_amp.py @@ -0,0 +1,338 @@ +"""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) + if mean_stress < 0.0: + expected = stress_amp + else: + 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) + + def test_swt_negative_mean_stress_warning(self) -> None: + with pytest.warns(UserWarning): + calc_stress_eq_amp_swt(100.0, -50.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) From 76015336aeeef640e1bc71b60ab5f97fd11a1d87 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Tue, 17 Mar 2026 12:13:17 +0100 Subject: [PATCH 10/21] excess condition removed for SWT --- .../damage_params/uniaxial_stress_eq_amp.py | 13 ------------- .../damage_params/test_uniaxial_stress_eq_amp.py | 10 +--------- 2 files changed, 1 insertion(+), 22 deletions(-) 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 index d3bf130..3e10c1b 100644 --- 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 @@ -563,10 +563,7 @@ def calc_stress_eq_amp_swt( The SWT equivalent stress amplitude is calculated as: $$ - \begin{align*} \sigma_{aeq} = \sqrt{\sigma_{a} \cdot (\sigma_{m} + \sigma_{a})} \\ - \text{where: } \sigma_{m} < 0 \rightarrow \sigma_{aeq} = \sigma_{a} \\ - \end{align*} $$ Args: @@ -579,8 +576,6 @@ def calc_stress_eq_amp_swt( rules for the input arrays. Raises: - Warning: If mean stress is compressive (σₘ < 0), a warning is issued and - the equivalent stress amplitude is set equal to the stress amplitude (σₐ) ValueError: If stress amplitude is negative. ValueError: If the validity condition σₐ > |σₘ| is not satisfied. @@ -608,14 +603,6 @@ def calc_stress_eq_amp_swt( "appropriate for compressive-dominated loading conditions." ) - if np.any(mean_stress_arr < 0): - warnings.warn( - r"Mean stress is compressive, $\sigma_{aeq} = \sigma_a$ was used!", - UserWarning, - stacklevel=2, - ) - return stress_amp_arr - return np.sqrt(stress_amp_arr * (mean_stress_arr + stress_amp_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 index 7eceac2..3326f5c 100644 --- 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 @@ -290,10 +290,7 @@ 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) - if mean_stress < 0.0: - expected = stress_amp - else: - expected = np.sqrt((stress_amp + mean_stress) * stress_amp) + expected = np.sqrt((stress_amp + mean_stress) * stress_amp) assert_allclose(result, expected) def test_array_inputs( @@ -309,14 +306,9 @@ def test_negative_stress_amplitude(self) -> None: 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) - def test_swt_negative_mean_stress_warning(self) -> None: - with pytest.warns(UserWarning): - calc_stress_eq_amp_swt(100.0, -50.0) - class TestCalcStressEqAmpWalker: def test_basic_calculation(self) -> None: From 56500bd8150f40d566e3ddd45a94451924cd386a Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Tue, 17 Mar 2026 12:21:34 +0100 Subject: [PATCH 11/21] documentation error fix --- .../core/stress_life/damage_params/uniaxial_stress_eq_amp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 3e10c1b..856ae70 100644 --- 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 @@ -332,8 +332,8 @@ def calc_stress_eq_amp_linear( Leading dimensions are preserved. Returns: - Array of equivalent stress amplitudes. Shape follows NumPy broadcasting - rules for the input arrays. + Array of equivalent stress amplitudes. Shape follows NumPy broadcasting + rules for the input arrays. Raises: Warning: If mean stress exceeds material stress parameter M. From 9d9e7988f4c580069181648f2212686dd4732f90 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Tue, 31 Mar 2026 16:26:49 +0200 Subject: [PATCH 12/21] variables renamed according to nomenclature excel --- .../damage_params/uniaxial_stress_eq_amp.py | 90 ++++++++++--------- 1 file changed, 47 insertions(+), 43 deletions(-) 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 index 856ae70..eccbb90 100644 --- 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 @@ -10,6 +10,9 @@ https://doi.org/10.1051/matecconf/201816510018 """ +# TODO: Excel calls the methods MEan Stress correction Methods - correction_method or mean_stress_correction_method do we change it here? +# TODO? USe case block can be deleted, do we add any details or should the mathematical formula be enough? + import warnings import numpy as np @@ -125,7 +128,7 @@ def calc_stress_eq_amp_bagci( def calc_stress_eq_amp_gerber( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - ult_stress: ArrayLike | np.float64, + ult_tensile_strength: ArrayLike | np.float64, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Gerber criterion. @@ -145,7 +148,7 @@ def calc_stress_eq_amp_gerber( 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 + ult_tensile_strength: Array-like of ultimate tensile strengths. Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. Returns: @@ -161,13 +164,13 @@ def calc_stress_eq_amp_gerber( """ 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) + ult_tensile_strength_arr = np.asarray(ult_tensile_strength, dtype=np.float64) - if np.any(ult_stress_arr <= 0): + if np.any(ult_tensile_strength_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 + ratio = np.abs(mean_stress_arr) / ult_tensile_strength_arr if np.any(ratio == 1.0): raise ValueError( @@ -181,13 +184,13 @@ def calc_stress_eq_amp_gerber( stacklevel=2, ) - return stress_amp_arr / (1 - (mean_stress_arr / ult_stress_arr) ** 2) + return stress_amp_arr / (1 - (mean_stress_arr / ult_tensile_strength_arr) ** 2) def calc_stress_eq_amp_goodman( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - ult_stress: ArrayLike | np.float64, + ult_tensile_strength: ArrayLike | np.float64, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Goodman criterion. @@ -207,7 +210,7 @@ def calc_stress_eq_amp_goodman( 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 + ult_tensile_strength: Array-like of ultimate tensile strengths. Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. Returns: @@ -222,13 +225,13 @@ def calc_stress_eq_amp_goodman( """ 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) + ult_tensile_strength_arr = np.asarray(ult_tensile_strength, dtype=np.float64) - if np.any(ult_stress_arr <= 0): + if np.any(ult_tensile_strength_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 + ratio = mean_stress_arr / ult_tensile_strength_arr if np.any(ratio == 1.0): raise ValueError( @@ -242,13 +245,13 @@ def calc_stress_eq_amp_goodman( stacklevel=2, ) - return stress_amp_arr / (1 - mean_stress_arr / ult_stress_arr) + return stress_amp_arr / (1 - mean_stress_arr / ult_tensile_strength_arr) def calc_stress_eq_amp_half_slope( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - ult_stress: ArrayLike | np.float64, + ult_tensile_strength: ArrayLike | np.float64, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using a half-slope mean stress correction. @@ -268,7 +271,7 @@ def calc_stress_eq_amp_half_slope( 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 + ult_tensile_strength: Array-like of ultimate tensile strengths. Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. Returns: @@ -284,13 +287,13 @@ def calc_stress_eq_amp_half_slope( """ 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) + ult_tensile_strength_arr = np.asarray(ult_tensile_strength, dtype=np.float64) - if np.any(ult_stress_arr <= 0): + if np.any(ult_tensile_strength_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) + ratio = mean_stress_arr / (2 * ult_tensile_strength_arr) if np.any(ratio == 1.0): raise ValueError( "Mean stress equals half of the ultimate tensile strength this would result" @@ -302,13 +305,13 @@ def calc_stress_eq_amp_half_slope( UserWarning, stacklevel=2, ) - return stress_amp_arr / (1 - mean_stress_arr / (2 * ult_stress_arr)) + return stress_amp_arr / (1 - mean_stress_arr / (2 * ult_tensile_strength_arr)) def calc_stress_eq_amp_linear( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - stress_parameter_M: ArrayLike | np.float64, + stress_param_M: ArrayLike | np.float64, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using a linear mean stress correction. @@ -327,7 +330,7 @@ def calc_stress_eq_amp_linear( 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. + stress_param_M: Array-like of material stress parameters M. Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. @@ -344,12 +347,12 @@ def calc_stress_eq_amp_linear( """ 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) + stress_param_M_arr = np.asarray(stress_param_M, dtype=np.float64) - if np.any(stress_parameter_M_arr <= 0): + if np.any(stress_param_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 + ratio = mean_stress_arr / stress_param_M_arr if np.any(ratio == 1.0): raise ValueError( @@ -363,13 +366,14 @@ def calc_stress_eq_amp_linear( stacklevel=2, ) - return stress_amp_arr / (1 - mean_stress_arr / stress_parameter_M_arr) + return stress_amp_arr / (1 - mean_stress_arr / stress_param_M_arr) +# todo! Check the name of the material parameter,issue calls it a true fracture stress but the paper calls it a fatigue strength coeficient, exel calls it a fat_strength_coef def calc_stress_eq_amp_morrow( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - true_fract_stress: ArrayLike | np.float64, + true_frac_stress: ArrayLike | np.float64, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Morrow criterion. @@ -388,7 +392,7 @@ def calc_stress_eq_amp_morrow( 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 + true_frac_stress: Array-like of true tensile fracture stress. Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. @@ -404,13 +408,13 @@ def calc_stress_eq_amp_morrow( """ 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) + true_frac_stress_arr = np.asarray(true_frac_stress, dtype=np.float64) - if np.any(true_fract_stress_arr <= 0): + if np.any(true_frac_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 + ratio = mean_stress_arr / true_frac_stress_arr if np.any(ratio == 1.0): raise ValueError( @@ -424,7 +428,7 @@ def calc_stress_eq_amp_morrow( stacklevel=2, ) - return stress_amp_arr / (1 - mean_stress_arr / true_fract_stress_arr) + return stress_amp_arr / (1 - mean_stress_arr / true_frac_stress_arr) def calc_stress_eq_amp_soderberg( @@ -490,7 +494,7 @@ def calc_stress_eq_amp_soderberg( def calc_stress_eq_amp_smith( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - ult_stress: ArrayLike | np.float64, + ult_tensile_strength: ArrayLike | np.float64, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Smith criterion. @@ -510,7 +514,7 @@ def calc_stress_eq_amp_smith( 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 + ult_tensile_strength: Array-like of ultimate tensile strengths. Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. Returns: @@ -526,13 +530,13 @@ def calc_stress_eq_amp_smith( """ 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) + ult_tensile_strength_arr = np.asarray(ult_tensile_strength, dtype=np.float64) - if np.any(ult_stress_arr <= 0): + if np.any(ult_tensile_strength_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 + ratio = mean_stress_arr / ult_tensile_strength_arr if np.any(ratio == 1.0): raise ValueError( "Mean stress equals ultimate tensile strength this would result in " @@ -544,8 +548,8 @@ def calc_stress_eq_amp_smith( UserWarning, stacklevel=2, ) - return (stress_amp_arr * (1 + mean_stress_arr / ult_stress_arr)) / ( - 1 - mean_stress_arr / ult_stress_arr + return (stress_amp_arr * (1 + mean_stress_arr / ult_tensile_strength_arr)) / ( + 1 - mean_stress_arr / ult_tensile_strength_arr ) @@ -609,7 +613,7 @@ def calc_stress_eq_amp_swt( def calc_stress_eq_amp_walker( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - walker_parameter: ArrayLike | np.float64, + walker_param: ArrayLike | np.float64, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Walker criterion. @@ -629,7 +633,7 @@ def calc_stress_eq_amp_walker( 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 + walker_param: Array-like of Walker exponents (γ'). Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. Returns: @@ -642,13 +646,13 @@ def calc_stress_eq_amp_walker( """ 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) + walker_param_arr = np.asarray(walker_param, dtype=np.float64) # Check validity of Walker parameter: γ' in range (0, 1) - invalid_condition = (walker_parameter_arr < 0) | (walker_parameter_arr > 1) + invalid_condition = (walker_param_arr < 0) | (walker_param_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 + 1 - walker_param_arr + ) * stress_amp_arr**walker_param_arr From e4649ef6b72012b28a1cf9e3021f6d3761871861 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Sat, 9 May 2026 16:18:00 +0200 Subject: [PATCH 13/21] negative mean stress wrapper try --- .../damage_params/uniaxial_stress_eq_amp.py | 99 ++++++++++++++++--- .../test_uniaxial_stress_eq_amp.py | 33 +++++++ 2 files changed, 118 insertions(+), 14 deletions(-) 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 index eccbb90..015bbb2 100644 --- 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 @@ -10,14 +10,89 @@ https://doi.org/10.1051/matecconf/201816510018 """ -# TODO: Excel calls the methods MEan Stress correction Methods - correction_method or mean_stress_correction_method do we change it here? -# TODO? USe case block can be deleted, do we add any details or should the mathematical formula be enough? - import warnings import numpy as np from numpy.typing import ArrayLike, NDArray +# TODO: wrapper functionality with allowing negative mean stresses + + +def _asme_correction_method( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + yield_strength: ArrayLike | np.float64, # TODO? Array or float? +) -> NDArray[np.float64]: + """Calculate equivalent stress amplitude using ASME criterion.""" + 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) + + asme_eq_amp = ( + stress_amp_arr / (1 - (mean_stress_arr / yield_strength_arr) ** 2) ** 0.5 + ) + + return asme_eq_amp + + +def calc_stress_eq_amp_asme( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + yield_strength: ArrayLike | np.float64, + allow_neg_mean_stress: bool = True, +) -> NDArray[np.float64]: + r"""Calculate equivalent stress amplitude using ASME criterion. + + ??? 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(ArrayLike): The stress amplitude values. + Leading dimensions are preserved. + mean_stress(ArrayLike): The mean stress values. Must be broadcastable with + stress_amp. Leading dimensions are preserved. + yield_strength(ArrayLike): The yield strength values. Must be broadcastable with + stress_amp and mean_stress. Leading dimensions are preserved. + allow_neg_mean_stress(bool, optional): A flag to control the calculation method. + Defaults to True. If set to False, the equivalent stress amplitude will be + set equal to the original stress amplitude for cases where the mean stress + is negative, ignoring the correction. + + 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. + + Returns: + NDArray[np.float64]: The calculated equivalent amplitude stress values. + """ + 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.") + + eq_stress_amp_arr = _asme_correction_method( + stress_amp_arr, mean_stress_arr, yield_strength_arr + ) + + if not allow_neg_mean_stress: + eq_stress_amp_arr = np.where( + mean_stress_arr < 0, stress_amp_arr, eq_stress_amp_arr + ) + + return eq_stress_amp_arr + def calc_stress_eq_amp_ASME( stress_amp: ArrayLike | np.float64, @@ -28,8 +103,8 @@ def calc_stress_eq_amp_ASME( ??? 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. + by modifying the stress amplitude based on the yield strength using a + quadratic, square-root denominator relationship. ??? abstract "Math Equations" The ASME equivalent stress amplitude is calculated as: @@ -72,11 +147,6 @@ def calc_stress_eq_amp_bagci( ) -> 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: @@ -271,17 +341,18 @@ def calc_stress_eq_amp_half_slope( 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_tensile_strength: Array-like of ultimate tensile strengths. Must be broadcastable - with stress_amp and mean_stress. Leading dimensions are preserved. + ult_tensile_strength: 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. + Warning: If mean stress exceeds double 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, + ValueError: If mean stress is equal to double of the ultimate tensile strength, resulting in zero equivalent stress amplitude. """ 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 index 3326f5c..78fd082 100644 --- 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 @@ -12,6 +12,7 @@ from numpy.typing import NDArray from fatpy.core.stress_life.damage_params.uniaxial_stress_eq_amp import ( + ASME_mean_stress_correction_method, calc_stress_eq_amp_ASME, calc_stress_eq_amp_bagci, calc_stress_eq_amp_gerber, @@ -33,6 +34,38 @@ def array_inputs() -> Tuple[NDArray[np.float64], NDArray[np.float64]]: return stress_amp, mean_stress +class TestASMEMeanStressCorrectionMethod: + def test_basic_calculation(self) -> None: + result = ASME_mean_stress_correction_method(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 = ASME_mean_stress_correction_method(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]: + ASME_mean_stress_correction_method(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]: + ASME_mean_stress_correction_method(100.0, ms, 500.0) + + def test_negative_mean_stress_no_correction(self) -> None: + result = ASME_mean_stress_correction_method( + 180.0, -100.0, 500.0, allow_neg_mean_stress=False + ) + expected = 180.0 + assert_allclose(result, expected) + + class TestCalcStressEqAmpASME: def test_basic_calculation(self) -> None: result = calc_stress_eq_amp_ASME(180.0, 100.0, 500.0) From 01f60c6fd4255c436a60c6a35dee45d95d938309 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Sat, 9 May 2026 17:30:52 +0200 Subject: [PATCH 14/21] review comments about error handling --- .../damage_params/uniaxial_stress_eq_amp.py | 135 +++++------------- 1 file changed, 34 insertions(+), 101 deletions(-) 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 index 015bbb2..3da7639 100644 --- 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 @@ -94,52 +94,6 @@ def calc_stress_eq_amp_asme( return eq_stress_amp_arr -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 - quadratic, square-root denominator 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, @@ -202,10 +156,6 @@ def calc_stress_eq_amp_gerber( ) -> 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: @@ -264,11 +214,6 @@ def calc_stress_eq_amp_goodman( ) -> 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: @@ -325,11 +270,6 @@ def calc_stress_eq_amp_half_slope( ) -> 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: @@ -350,7 +290,7 @@ def calc_stress_eq_amp_half_slope( rules for the input arrays. Raises: - Warning: If mean stress exceeds double of the ultimate tensile strength. + Warning: If mean stress exceeds the ultimate tensile strength. ValueError: If ultimate tensile strength is not positive. ValueError: If mean stress is equal to double of the ultimate tensile strength, resulting in zero equivalent stress amplitude. @@ -364,15 +304,15 @@ def calc_stress_eq_amp_half_slope( raise ValueError("Ultimate tensile strength must be positive") # Check if mean stress approaches or exceeds material parameter - ratio = mean_stress_arr / (2 * ult_tensile_strength_arr) - if np.any(ratio == 1.0): + ratio = mean_stress_arr / ult_tensile_strength_arr + if np.any(ratio == 2.0): raise ValueError( - "Mean stress equals half of the ultimate tensile strength this would result" - "in zero equivalent stress amplitude." + "Mean stress equals to double of the ultimate tensile strength this would " + "result in infinite equivalent stress amplitude." ) elif np.any(ratio > 1.0): warnings.warn( - "Mean stress exceeds half of the ultimate tensile strength. ", + "Mean stress exceeds the ultimate tensile strength. ", UserWarning, stacklevel=2, ) @@ -386,11 +326,6 @@ def calc_stress_eq_amp_linear( ) -> 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: @@ -448,10 +383,6 @@ def calc_stress_eq_amp_morrow( ) -> 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: @@ -509,11 +440,6 @@ def calc_stress_eq_amp_soderberg( ) -> 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: @@ -569,10 +495,6 @@ def calc_stress_eq_amp_smith( ) -> 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: @@ -630,10 +552,6 @@ def calc_stress_eq_amp_swt( ) -> 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: @@ -652,10 +570,10 @@ def calc_stress_eq_amp_swt( Raises: ValueError: If stress amplitude is negative. - ValueError: If the validity condition σₐ > |σₘ| is not satisfied. + ValueError: If the validity condition σₐ + σₘ > 0 is not satisfied. ??? note "Validity Condition" - The SWT parameter is valid when $\sigma_a > |\sigma_m|$, ensuring that the + The SWT parameter is valid when $\sigma_a + \sigma_m > 0$, ensuring that the maximum stress in the cycle is positive (tensile). When this condition is not met, a ValueError is raised. @@ -667,13 +585,12 @@ def calc_stress_eq_amp_swt( 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 + # Check validity condition: σₐ + σₘ > 0 + invalid_condition = stress_amp_arr + mean_stress_arr <= 0 if np.any(invalid_condition): raise ValueError( - "Smith-Watson-Topper parameter validity condition (σₐ > |σₘ|) not " + "Smith-Watson-Topper parameter validity condition (σₐ + σₘ > 0) not " "satisfied for some data points. The SWT approach may not be " "appropriate for compressive-dominated loading conditions." ) @@ -688,11 +605,6 @@ def calc_stress_eq_amp_walker( ) -> 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: @@ -712,17 +624,38 @@ def calc_stress_eq_amp_walker( rules for the input arrays. Raises: + ValueError: If stress amplitude is negative. + ValueError: If the validity condition σₐ + σₘ > 0 is not satisfied. ValueError: If input arrays cannot be broadcast together or when the condition γ' in (0, 1) is not satisfied. + + ??? note "Validity Condition" + The Walker method is valid when $\sigma_a + \sigma_m > 0$, 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) walker_param_arr = np.asarray(walker_param, 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: σₐ + σₘ > 0 + invalid_condition = stress_amp_arr + mean_stress_arr <= 0 + + if np.any(invalid_condition): + raise ValueError( + "Walker method validity condition (σₐ + σₘ > 0) not " + "satisfied for some data points. The Walker approach may not be " + "appropriate for compressive-dominated loading conditions." + ) + # Check validity of Walker parameter: γ' in range (0, 1) - invalid_condition = (walker_param_arr < 0) | (walker_param_arr > 1) + invalid_condition = (walker_param_arr <= 0) | (walker_param_arr >= 1) if np.any(invalid_condition): - raise ValueError("Walker parameter (γ') must be in the range (0, 1). ") + raise ValueError("Walker parameter (γ') must be in the range [0, 1]. ") return (stress_amp_arr + mean_stress_arr) ** ( 1 - walker_param_arr From e0eb32d1679c214fe677fe8b77cc4d6a728074db Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Sun, 10 May 2026 11:23:26 +0200 Subject: [PATCH 15/21] Negative mean stress wrapper implemented, error conditions updated --- .../damage_params/uniaxial_stress_eq_amp.py | 379 ++++++++++++++---- 1 file changed, 312 insertions(+), 67 deletions(-) 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 index 3da7639..64649a3 100644 --- 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 @@ -2,12 +2,14 @@ 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 +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 + +[PAPUGA, Jan, et al. Mean stress effect in stress-life fatigue prediction re-evaluated. +In: MATEC web of conferences. EDP Sciences, 2018. p. 10018.](https://doi.org/10.1051/matecconf/201816510018). """ import warnings @@ -15,13 +17,11 @@ import numpy as np from numpy.typing import ArrayLike, NDArray -# TODO: wrapper functionality with allowing negative mean stresses - def _asme_correction_method( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - yield_strength: ArrayLike | np.float64, # TODO? Array or float? + yield_strength: ArrayLike | np.float64, ) -> NDArray[np.float64]: """Calculate equivalent stress amplitude using ASME criterion.""" stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) @@ -51,13 +51,13 @@ def calc_stress_eq_amp_asme( $$ Args: - stress_amp(ArrayLike): The stress amplitude values. + stress_amp: The stress amplitude values. Leading dimensions are preserved. - mean_stress(ArrayLike): The mean stress values. Must be broadcastable with + mean_stress: The mean stress values. Must be broadcastable with stress_amp. Leading dimensions are preserved. - yield_strength(ArrayLike): The yield strength values. Must be broadcastable with + yield_strength: The yield strength values. Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. - allow_neg_mean_stress(bool, optional): A flag to control the calculation method. + allow_neg_mean_stress: A flag to control the calculation method. Defaults to True. If set to False, the equivalent stress amplitude will be set equal to the original stress amplitude for cases where the mean stress is negative, ignoring the correction. @@ -68,7 +68,8 @@ def calc_stress_eq_amp_asme( resulting in infinite equivalent stress amplitude. Returns: - NDArray[np.float64]: The calculated equivalent amplitude stress values. + Array of equivalent stress amplitudes. Shape follows NumPy broadcasting + rules for the input arrays. """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) @@ -79,6 +80,7 @@ def calc_stress_eq_amp_asme( # 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.") @@ -86,6 +88,7 @@ def calc_stress_eq_amp_asme( stress_amp_arr, mean_stress_arr, yield_strength_arr ) + # If allow_neg_mean_stress is False, set equivalent stress amplitude = to original if not allow_neg_mean_stress: eq_stress_amp_arr = np.where( mean_stress_arr < 0, stress_amp_arr, eq_stress_amp_arr @@ -94,10 +97,26 @@ def calc_stress_eq_amp_asme( return eq_stress_amp_arr +def _bagci_correction_method( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + yield_strength: ArrayLike | np.float64, +) -> NDArray[np.float64]: + """Calculate equivalent stress amplitude using Bagci criterion.""" + 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) + + bagci_eq_amp = stress_amp_arr / (1 - (mean_stress_arr / yield_strength_arr) ** 4) + + return bagci_eq_amp + + def calc_stress_eq_amp_bagci( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, yield_strength: ArrayLike | np.float64, + allow_neg_mean_stress: bool = True, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Bagci criterion. @@ -114,6 +133,10 @@ def calc_stress_eq_amp_bagci( 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. + allow_neg_mean_stress: A flag to control the calculation method. + Defaults to True. If set to False, the equivalent stress amplitude will be + set equal to the original stress amplitude for cases where the mean stress + is negative, ignoring the correction. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting @@ -134,6 +157,7 @@ def calc_stress_eq_amp_bagci( # 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 " @@ -146,13 +170,41 @@ def calc_stress_eq_amp_bagci( stacklevel=2, ) - return stress_amp_arr / (1 - (mean_stress_arr / yield_strength_arr) ** 4) + eq_stress_amp_arr = _bagci_correction_method( + stress_amp_arr, mean_stress_arr, yield_strength_arr + ) + + # If allow_neg_mean_stress is False, set equivalent stress amplitude = to original + if not allow_neg_mean_stress: + eq_stress_amp_arr = np.where( + mean_stress_arr < 0, stress_amp_arr, eq_stress_amp_arr + ) + + return eq_stress_amp_arr + + +def _gerber_correction_method( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + ult_tensile_strength: ArrayLike | np.float64, +) -> NDArray[np.float64]: + """Calculate equivalent stress amplitude using Gerber criterion.""" + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + ult_tensile_strength_arr = np.asarray(ult_tensile_strength, dtype=np.float64) + + gerber_eq_amp = stress_amp_arr / ( + 1 - (mean_stress_arr / ult_tensile_strength_arr) ** 2 + ) + + return gerber_eq_amp def calc_stress_eq_amp_gerber( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, ult_tensile_strength: ArrayLike | np.float64, + allow_neg_mean_stress: bool = True, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Gerber criterion. @@ -168,8 +220,13 @@ def calc_stress_eq_amp_gerber( 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_tensile_strength: Array-like of ultimate tensile strengths. Must be broadcastable - with stress_amp and mean_stress. Leading dimensions are preserved. + ult_tensile_strength: Array-like of ultimate tensile strengths. Must be + broadcastable with stress_amp and mean_stress. + Leading dimensions are preserved. + allow_neg_mean_stress: A flag to control the calculation method. + Defaults to True. If set to False, the equivalent stress amplitude will be + set equal to the original stress amplitude for cases where the mean stress + is negative, ignoring the correction. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting @@ -204,13 +261,39 @@ def calc_stress_eq_amp_gerber( stacklevel=2, ) - return stress_amp_arr / (1 - (mean_stress_arr / ult_tensile_strength_arr) ** 2) + eq_stress_amp_arr = _gerber_correction_method( + stress_amp_arr, mean_stress_arr, ult_tensile_strength_arr + ) + + # If allow_neg_mean_stress is False, set equivalent stress amplitude = to original + if not allow_neg_mean_stress: + eq_stress_amp_arr = np.where( + mean_stress_arr < 0, stress_amp_arr, eq_stress_amp_arr + ) + + return eq_stress_amp_arr + + +def _linear_correction_method( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + material_parameter: ArrayLike | np.float64, +) -> NDArray[np.float64]: + """Calculate equivalent stress amplitude using linear correction.""" + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + material_parameter_arr = np.asarray(material_parameter, dtype=np.float64) + + linear_eq_amp = stress_amp_arr / (1 - (mean_stress_arr / material_parameter_arr)) + + return linear_eq_amp def calc_stress_eq_amp_goodman( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, ult_tensile_strength: ArrayLike | np.float64, + allow_neg_mean_stress: bool = True, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Goodman criterion. @@ -225,15 +308,20 @@ def calc_stress_eq_amp_goodman( 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_tensile_strength: Array-like of ultimate tensile strengths. Must be broadcastable - with stress_amp and mean_stress. Leading dimensions are preserved. + ult_tensile_strength: Array-like of ultimate tensile strengths. Must be + broadcastable with stress_amp and mean_stress. + Leading dimensions are preserved. + allow_neg_mean_stress: A flag to control the calculation method. + Defaults to True. If set to False, the equivalent stress amplitude will be + set equal to the original stress amplitude for cases where the mean stress + is negative, ignoring the correction. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting rules for the input arrays. Raises: - Warning: If mean stress exceeds ultimate tensile strength. + Warning: If mean stress magnitude 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. @@ -253,20 +341,31 @@ def calc_stress_eq_amp_goodman( "Mean stress equals ultimate tensile strength this would result in " "infinite equivalent stress amplitude." ) - elif np.any(ratio > 1.0): + elif np.any(abs(ratio) > 1.0): warnings.warn( "Mean stress magnitude exceeds ultimate tensile strength. ", UserWarning, stacklevel=2, ) - return stress_amp_arr / (1 - mean_stress_arr / ult_tensile_strength_arr) + eq_stress_amp_arr = _linear_correction_method( + stress_amp_arr, mean_stress_arr, ult_tensile_strength_arr + ) + + # If allow_neg_mean_stress is False, set equivalent stress amplitude = to original + if not allow_neg_mean_stress: + eq_stress_amp_arr = np.where( + mean_stress_arr < 0, stress_amp_arr, eq_stress_amp_arr + ) + + return eq_stress_amp_arr def calc_stress_eq_amp_half_slope( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, ult_tensile_strength: ArrayLike | np.float64, + allow_neg_mean_stress: bool = True, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using a half-slope mean stress correction. @@ -284,16 +383,20 @@ def calc_stress_eq_amp_half_slope( ult_tensile_strength: Array-like of ultimate tensile strengths. Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. + allow_neg_mean_stress: A flag to control the calculation method. + Defaults to True. If set to False, the equivalent stress amplitude will be + set equal to the original stress amplitude for cases where the mean stress + is negative, ignoring the correction. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting rules for the input arrays. Raises: - Warning: If mean stress exceeds the ultimate tensile strength. + Warning: If mean stress magnitude exceeds the ultimate tensile strength. ValueError: If ultimate tensile strength is not positive. ValueError: If mean stress is equal to double of the ultimate tensile strength, - resulting in zero equivalent stress amplitude. + resulting in infinite equivalent stress amplitude. """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) @@ -305,24 +408,37 @@ def calc_stress_eq_amp_half_slope( # Check if mean stress approaches or exceeds material parameter ratio = mean_stress_arr / ult_tensile_strength_arr + if np.any(ratio == 2.0): raise ValueError( "Mean stress equals to double of the ultimate tensile strength this would " "result in infinite equivalent stress amplitude." ) - elif np.any(ratio > 1.0): + elif np.any(abs(ratio) > 1.0): warnings.warn( - "Mean stress exceeds the ultimate tensile strength. ", + "Mean stress magnitude exceeds the ultimate tensile strength. ", UserWarning, stacklevel=2, ) - return stress_amp_arr / (1 - mean_stress_arr / (2 * ult_tensile_strength_arr)) + + eq_stress_amp_arr = _linear_correction_method( + stress_amp_arr, mean_stress_arr, 2 * ult_tensile_strength_arr + ) + + # If allow_neg_mean_stress is False, set equivalent stress amplitude = to original + if not allow_neg_mean_stress: + eq_stress_amp_arr = np.where( + mean_stress_arr < 0, stress_amp_arr, eq_stress_amp_arr + ) + + return eq_stress_amp_arr def calc_stress_eq_amp_linear( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - stress_param_M: ArrayLike | np.float64, + stress_param_m: ArrayLike | np.float64, + allow_neg_mean_stress: bool = True, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using a linear mean stress correction. @@ -336,50 +452,69 @@ def calc_stress_eq_amp_linear( 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_param_M: Array-like of material stress parameters M. + stress_param_m: Array-like of material stress parameters M. Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. + allow_neg_mean_stress: A flag to control the calculation method. + Defaults to True. If set to False, the equivalent stress amplitude will be + set equal to the original stress amplitude for cases where the mean stress + is negative, ignoring the correction. 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. + Warning: If mean stress magnitude 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. - + infinite equivalent stress amplitude. """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) - stress_param_M_arr = np.asarray(stress_param_M, dtype=np.float64) + stress_param_m_arr = np.asarray(stress_param_m, dtype=np.float64) - if np.any(stress_param_M_arr <= 0): + if np.any(stress_param_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_param_M_arr + ratio = mean_stress_arr / stress_param_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." + "infinite equivalent stress amplitude." ) - elif np.any(ratio > 1.0): + elif np.any(abs(ratio) > 1.0): warnings.warn( - "Mean stress exceeds material stress parameter M. ", + "Mean stress magnitude exceeds material stress parameter M. ", UserWarning, stacklevel=2, ) - return stress_amp_arr / (1 - mean_stress_arr / stress_param_M_arr) + eq_stress_amp_arr = _linear_correction_method( + stress_amp_arr, mean_stress_arr, stress_param_m_arr + ) + + # If allow_neg_mean_stress is False, set equivalent stress amplitude = to original + if not allow_neg_mean_stress: + eq_stress_amp_arr = np.where( + mean_stress_arr < 0, stress_amp_arr, eq_stress_amp_arr + ) + + return eq_stress_amp_arr + + +# todo! Check the name of the material parameter,issue calls it a true fracture stress +# todo! but the paper calls it a fatigue strength coeficient, +# todo! exel calls it a fat_strength_coef -# todo! Check the name of the material parameter,issue calls it a true fracture stress but the paper calls it a fatigue strength coeficient, exel calls it a fat_strength_coef def calc_stress_eq_amp_morrow( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, - true_frac_stress: ArrayLike | np.float64, + fat_strength_coef: ArrayLike | np.float64, + allow_neg_mean_stress: bool = True, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Morrow criterion. @@ -394,49 +529,64 @@ def calc_stress_eq_amp_morrow( 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_frac_stress: Array-like of true tensile fracture stress. Must be + fat_strength_coef: Array-like of fatigue strength coefficients. Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. + allow_neg_mean_stress: A flag to control the calculation method. + Defaults to True. If set to False, the equivalent stress amplitude will be + set equal to the original stress amplitude for cases where the mean stress + is negative, ignoring the correction. 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 + Warning: If mean stress magnitude exceeds fatigue strength coefficient. + ValueError: If fatigue strength coefficient is not positive. + ValueError: If mean stress is equal to fatigue strength coefficient, 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_frac_stress_arr = np.asarray(true_frac_stress, dtype=np.float64) + fat_strength_coef_arr = np.asarray(fat_strength_coef, dtype=np.float64) - if np.any(true_frac_stress_arr <= 0): - raise ValueError("True fracture stress must be positive") + if np.any(fat_strength_coef_arr <= 0): + raise ValueError("Fatigue strength coefficient must be positive") # Check if mean stress approaches or exceeds material parameter - ratio = mean_stress_arr / true_frac_stress_arr + ratio = mean_stress_arr / fat_strength_coef_arr if np.any(ratio == 1.0): raise ValueError( - "Mean stress equals true fracture stress this would result in " + "Mean stress equals fatigue strength coefficient this would result in " "infinite equivalent stress amplitude." ) - elif np.any(ratio > 1.0): + elif np.any(np.abs(ratio) > 1.0): warnings.warn( - "Mean stress exceeds true fracture stress. ", + "Mean stress magnitude exceeds fatigue strength coefficient. ", UserWarning, stacklevel=2, ) - return stress_amp_arr / (1 - mean_stress_arr / true_frac_stress_arr) + eq_stress_amp_arr = _linear_correction_method( + stress_amp_arr, mean_stress_arr, fat_strength_coef_arr + ) + + # If allow_neg_mean_stress is False, set equivalent stress amplitude = to original + if not allow_neg_mean_stress: + eq_stress_amp_arr = np.where( + mean_stress_arr < 0, stress_amp_arr, eq_stress_amp_arr + ) + + return eq_stress_amp_arr def calc_stress_eq_amp_soderberg( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, yield_strength: ArrayLike | np.float64, + allow_neg_mean_stress: bool = True, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Soderberg criterion. @@ -453,13 +603,17 @@ def calc_stress_eq_amp_soderberg( 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. + allow_neg_mean_stress: A flag to control the calculation method. + Defaults to True. If set to False, the equivalent stress amplitude will be + set equal to the original stress amplitude for cases where the mean stress + is negative, ignoring the correction. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting rules for the input arrays. Raises: - Warning: If mean stress exceeds yield strength. + Warning: If mean stress magnitude 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. @@ -473,25 +627,54 @@ def calc_stress_eq_amp_soderberg( # 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): + elif np.any(np.abs(ratio) > 1.0): warnings.warn( - "Mean stress exceeds yield strength. ", + "Mean stress magnitude exceeds yield strength. ", UserWarning, stacklevel=2, ) - return stress_amp_arr / (1 - mean_stress_arr / yield_strength_arr) + eq_stress_amp_arr = _linear_correction_method( + stress_amp_arr, mean_stress_arr, yield_strength_arr + ) + + # If allow_neg_mean_stress is False, set equivalent stress amplitude = to original + if not allow_neg_mean_stress: + eq_stress_amp_arr = np.where( + mean_stress_arr < 0, stress_amp_arr, eq_stress_amp_arr + ) + + return eq_stress_amp_arr + + +def _smith_correction_method( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + ult_tensile_strength: ArrayLike | np.float64, +) -> NDArray[np.float64]: + """Calculate equivalent stress amplitude using Smith criterion.""" + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + ult_tensile_strength_arr = np.asarray(ult_tensile_strength, dtype=np.float64) + + smith_eq_amp = ( + stress_amp_arr * (1 + mean_stress_arr / ult_tensile_strength_arr) + ) / (1 - mean_stress_arr / ult_tensile_strength_arr) + + return smith_eq_amp def calc_stress_eq_amp_smith( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, ult_tensile_strength: ArrayLike | np.float64, + allow_neg_mean_stress: bool = True, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Smith criterion. @@ -507,15 +690,20 @@ def calc_stress_eq_amp_smith( 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_tensile_strength: Array-like of ultimate tensile strengths. Must be broadcastable - with stress_amp and mean_stress. Leading dimensions are preserved. + ult_tensile_strength: Array-like of ultimate tensile strengths. Must be + broadcastable with stress_amp and mean_stress. + Leading dimensions are preserved. + allow_neg_mean_stress: A flag to control the calculation method. + Defaults to True. If set to False, the equivalent stress amplitude will be + set equal to the original stress amplitude for cases where the mean stress + is negative, ignoring the correction. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting rules for the input arrays. Raises: - Warning: If mean stress exceeds ultimate tensile strength. + Warning: If mean stress magnitude 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. @@ -535,20 +723,44 @@ def calc_stress_eq_amp_smith( "Mean stress equals ultimate tensile strength this would result in " "infinite equivalent stress amplitude." ) - elif np.any(ratio > 1.0): + elif np.any(np.abs(ratio) > 1.0): warnings.warn( - "Mean stress exceeds ultimate tensile strength. ", + "Mean stress magnitude exceeds ultimate tensile strength. ", UserWarning, stacklevel=2, ) - return (stress_amp_arr * (1 + mean_stress_arr / ult_tensile_strength_arr)) / ( - 1 - mean_stress_arr / ult_tensile_strength_arr + + eq_stress_amp_arr = _smith_correction_method( + stress_amp_arr, mean_stress_arr, ult_tensile_strength_arr ) + # If allow_neg_mean_stress is False, set equivalent stress amplitude = to original + if not allow_neg_mean_stress: + eq_stress_amp_arr = np.where( + mean_stress_arr < 0, stress_amp_arr, eq_stress_amp_arr + ) + + return eq_stress_amp_arr + + +# TODO use Walker instead and set the gamma parameter to 0.5? +def _swt_correction_method( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, +) -> NDArray[np.float64]: + """Calculate equivalent stress amplitude using Smith-Watson-Topper criterion.""" + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + + swt_eq_amp = np.sqrt(stress_amp_arr * (mean_stress_arr + stress_amp_arr)) + + return swt_eq_amp + def calc_stress_eq_amp_swt( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, + allow_neg_mean_stress: bool = True, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Smith-Watson-Topper parameter. @@ -563,6 +775,10 @@ def calc_stress_eq_amp_swt( 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. + allow_neg_mean_stress: A flag to control the calculation method. + Defaults to True. If set to False, the equivalent stress amplitude will be + set equal to the original stress amplitude for cases where the mean stress + is negative, ignoring the correction. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting @@ -595,13 +811,39 @@ def calc_stress_eq_amp_swt( "appropriate for compressive-dominated loading conditions." ) - return np.sqrt(stress_amp_arr * (mean_stress_arr + stress_amp_arr)) + eq_stress_amp_arr = _swt_correction_method(stress_amp_arr, mean_stress_arr) + + # If allow_neg_mean_stress is False, set equivalent stress amplitude = to original + if not allow_neg_mean_stress: + eq_stress_amp_arr = np.where( + mean_stress_arr < 0, stress_amp_arr, eq_stress_amp_arr + ) + + return eq_stress_amp_arr + + +def _walker_correction_method( + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, + walker_param: ArrayLike | np.float64, +) -> NDArray[np.float64]: + """Calculate equivalent stress amplitude using Walker criterion.""" + stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) + mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) + walker_param_arr = np.asarray(walker_param, dtype=np.float64) + + walker_eq_amp = (stress_amp_arr + mean_stress_arr) ** ( + 1 - walker_param_arr + ) * stress_amp_arr**walker_param_arr + + return walker_eq_amp def calc_stress_eq_amp_walker( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, walker_param: ArrayLike | np.float64, + allow_neg_mean_stress: bool = True, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Walker criterion. @@ -618,6 +860,10 @@ def calc_stress_eq_amp_walker( stress_amp. Leading dimensions are preserved. walker_param: Array-like of Walker exponents (γ'). Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. + allow_neg_mean_stress: A flag to control the calculation method. + Defaults to True. If set to False, the equivalent stress amplitude will be + set equal to the original stress amplitude for cases where the mean stress + is negative, ignoring the correction. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting @@ -626,8 +872,7 @@ def calc_stress_eq_amp_walker( Raises: ValueError: If stress amplitude is negative. ValueError: If the validity condition σₐ + σₘ > 0 is not satisfied. - ValueError: If input arrays cannot be broadcast together or when the - condition γ' in (0, 1) is not satisfied. + ValueError: When the condition γ' in [0, 1] is not satisfied. ??? note "Validity Condition" The Walker method is valid when $\sigma_a + \sigma_m > 0$, ensuring that the @@ -652,8 +897,8 @@ def calc_stress_eq_amp_walker( "appropriate for compressive-dominated loading conditions." ) - # Check validity of Walker parameter: γ' in range (0, 1) - invalid_condition = (walker_param_arr <= 0) | (walker_param_arr >= 1) + # Check validity of Walker parameter: γ' in range [0, 1] + invalid_condition = (walker_param_arr < 0) | (walker_param_arr > 1) if np.any(invalid_condition): raise ValueError("Walker parameter (γ') must be in the range [0, 1]. ") From fefc7cad16efd115976be9a0394ea09a9d929494 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Sun, 10 May 2026 11:54:15 +0200 Subject: [PATCH 16/21] doc string updated --- .../damage_params/uniaxial_stress_eq_amp.py | 86 +++++++++++-------- 1 file changed, 48 insertions(+), 38 deletions(-) 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 index 64649a3..ad2c742 100644 --- 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 @@ -63,9 +63,9 @@ def calc_stress_eq_amp_asme( is negative, ignoring the correction. Raises: - ValueError: If yield strength is not positive. + ValueError: If yield strength is not positive ($R_e > 0$). ValueError: If mean stress magnitude is equal or greater to yield strength, - resulting in infinite equivalent stress amplitude. + resulting in infinite equivalent stress amplitude ($\sigma_m = R_e$). Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting @@ -143,10 +143,10 @@ def calc_stress_eq_amp_bagci( rules for the input arrays. Raises: - Warning: If mean stress magnitude exceeds yield strength. - ValueError: If yield strength is not positive. + Warning: If mean stress magnitude exceeds yield strength ($|\sigma_m| > R_e$). + ValueError: If yield strength is not positive ($R_e > 0$). ValueError: If mean stress magnitude is equal to yield strength, - resulting in infinite equivalent stress amplitude. + resulting in infinite equivalent stress amplitude ($\sigma_m = R_e$). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) @@ -233,10 +233,12 @@ def calc_stress_eq_amp_gerber( rules for the input arrays. Raises: - Warning: If mean stress magnitude exceeds ultimate tensile strength. - ValueError: If ultimate tensile strength is not positive. + Warning: If mean stress magnitude exceeds ultimate tensile strength + ($|\sigma_m| > \sigma_{UTS}$). + ValueError: If ultimate tensile strength is not positive ($\sigma_{UTS} > 0$). ValueError: If mean stress magnitude is equal to ultimate tensile strength, - resulting in infinite equivalent stress amplitude. + resulting in infinite equivalent stress amplitude + ($\sigma_m = \sigma_{UTS}$). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) @@ -321,10 +323,11 @@ def calc_stress_eq_amp_goodman( rules for the input arrays. Raises: - Warning: If mean stress magnitude exceeds ultimate tensile strength. - ValueError: If ultimate tensile strength is not positive. + Warning: If mean stress magnitude exceeds ultimate tensile strength + ($|\sigma_m| > \sigma_{UTS}$). + ValueError: If ultimate tensile strength is not positive ($\sigma_{UTS} > 0$). ValueError: If mean stress is equal to ultimate tensile strength, resulting in - infinite equivalent stress amplitude. + infinite equivalent stress amplitude ($\sigma_m = \sigma_{UTS}$). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) @@ -393,10 +396,12 @@ def calc_stress_eq_amp_half_slope( rules for the input arrays. Raises: - Warning: If mean stress magnitude exceeds the ultimate tensile strength. - ValueError: If ultimate tensile strength is not positive. + Warning: If mean stress magnitude exceeds the ultimate tensile strength + ($|\sigma_m| > \sigma_{UTS}$). + ValueError: If ultimate tensile strength is not positive ($\sigma_{UTS} > 0$). ValueError: If mean stress is equal to double of the ultimate tensile strength, - resulting in infinite equivalent stress amplitude. + resulting in infinite equivalent stress amplitude + ($\sigma_m = 2 \cdot \sigma_{UTS}$). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) @@ -448,6 +453,7 @@ def calc_stress_eq_amp_linear( $$ \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 @@ -465,10 +471,11 @@ def calc_stress_eq_amp_linear( rules for the input arrays. Raises: - Warning: If mean stress magnitude exceeds material stress parameter M. - ValueError: If material stress parameter M is not positive. + Warning: If mean stress magnitude exceeds material stress parameter M + ($|\sigma_m| > M$). + ValueError: If material stress parameter M is not positive ($M > 0$). ValueError: If mean stress is equal to material stress parameter M, resulting in - infinite equivalent stress amplitude. + infinite equivalent stress amplitude ($\sigma_m = M$). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) @@ -542,10 +549,11 @@ def calc_stress_eq_amp_morrow( rules for the input arrays. Raises: - Warning: If mean stress magnitude exceeds fatigue strength coefficient. - ValueError: If fatigue strength coefficient is not positive. - ValueError: If mean stress is equal to fatigue strength coefficient, resulting in - infinite equivalent stress amplitude. + Warning: If mean stress magnitude exceeds fatigue strength coefficient + ($|\sigma_m| > \sigma_{f}'$). + ValueError: If fatigue strength coefficient is not positive ($\sigma_{f}' > 0$). + ValueError: If mean stress is equal to fatigue strength coefficient, resulting + in infinite equivalent stress amplitude ($\sigma_m = \sigma_{f}'$). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) @@ -613,10 +621,10 @@ def calc_stress_eq_amp_soderberg( rules for the input arrays. Raises: - Warning: If mean stress magnitude exceeds yield strength. - ValueError: If yield strength is not positive. + Warning: If mean stress magnitude exceeds yield strength ($|\sigma_m| > R_e$). + ValueError: If yield strength is not positive ($R_e > 0$). ValueError: If mean stress is equal to yield strength, resulting in - infinite equivalent stress amplitude. + infinite equivalent stress amplitude ($\sigma_m = R_e$). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) @@ -703,10 +711,11 @@ def calc_stress_eq_amp_smith( rules for the input arrays. Raises: - Warning: If mean stress magnitude exceeds ultimate tensile strength. - ValueError: If ultimate tensile strength is not positive. + Warning: If mean stress magnitude exceeds ultimate tensile strength + ($\sigma_m > \sigma_{UTS}$). + ValueError: If ultimate tensile strength is not positive ($\sigma_{UTS} > 0$). ValueError: If mean stress is equal to ultimate tensile strength, resulting in - infinite equivalent stress amplitude. + infinite equivalent stress amplitude ($\sigma_{m} = \sigma_{UTS}$). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) @@ -775,7 +784,7 @@ def calc_stress_eq_amp_swt( 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. - allow_neg_mean_stress: A flag to control the calculation method. + allow_neg_mean_stress: A flag to control the calculation method. Defaults to True. If set to False, the equivalent stress amplitude will be set equal to the original stress amplitude for cases where the mean stress is negative, ignoring the correction. @@ -785,8 +794,8 @@ def calc_stress_eq_amp_swt( rules for the input arrays. Raises: - ValueError: If stress amplitude is negative. - ValueError: If the validity condition σₐ + σₘ > 0 is not satisfied. + ValueError: If stress amplitude is negative ($\sigma_a < 0$). + ValueError: If the validity condition $\sigma_a + \sigma_m >0$ is not satisfied. ??? note "Validity Condition" The SWT parameter is valid when $\sigma_a + \sigma_m > 0$, ensuring that the @@ -806,8 +815,8 @@ def calc_stress_eq_amp_swt( if np.any(invalid_condition): raise ValueError( - "Smith-Watson-Topper parameter validity condition (σₐ + σₘ > 0) not " - "satisfied for some data points. The SWT approach may not be " + r"Smith-Watson-Topper parameter validity condition $\sigma_a + \sigma_m >0$" + " not satisfied for some data points. The SWT approach may not be " "appropriate for compressive-dominated loading conditions." ) @@ -854,11 +863,12 @@ def calc_stress_eq_amp_walker( \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_param: Array-like of Walker exponents (γ'). Must be broadcastable + walker_param: Array-like of Walker exponents ($\gamma'$'). Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. allow_neg_mean_stress: A flag to control the calculation method. Defaults to True. If set to False, the equivalent stress amplitude will be @@ -870,9 +880,9 @@ def calc_stress_eq_amp_walker( rules for the input arrays. Raises: - ValueError: If stress amplitude is negative. - ValueError: If the validity condition σₐ + σₘ > 0 is not satisfied. - ValueError: When the condition γ' in [0, 1] is not satisfied. + ValueError: If stress amplitude is negative ($\sigma_a < 0$). + ValueError: If the validity condition $\sigma_a + \sigma_m >0$ is not satisfied. + ValueError: When the condition $\gamma'$ in [0, 1] is not satisfied. ??? note "Validity Condition" The Walker method is valid when $\sigma_a + \sigma_m > 0$, ensuring that the @@ -892,7 +902,7 @@ def calc_stress_eq_amp_walker( if np.any(invalid_condition): raise ValueError( - "Walker method validity condition (σₐ + σₘ > 0) not " + r"Walker method validity condition $\sigma_a + \sigma_m >0$ not " "satisfied for some data points. The Walker approach may not be " "appropriate for compressive-dominated loading conditions." ) @@ -900,7 +910,7 @@ def calc_stress_eq_amp_walker( # Check validity of Walker parameter: γ' in range [0, 1] invalid_condition = (walker_param_arr < 0) | (walker_param_arr > 1) if np.any(invalid_condition): - raise ValueError("Walker parameter (γ') must be in the range [0, 1]. ") + raise ValueError(r"Walker parameter ($\gamma'$') must be in the range [0, 1]. ") return (stress_amp_arr + mean_stress_arr) ** ( 1 - walker_param_arr From 406887a2e2263aaf69581be8137716374a7d780a Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Thu, 21 May 2026 09:53:14 +0200 Subject: [PATCH 17/21] asme method final? --- .../damage_params/uniaxial_stress_eq_amp.py | 44 +++-- .../test_uniaxial_stress_eq_amp.py | 153 +++++++++++------- 2 files changed, 129 insertions(+), 68 deletions(-) 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 index ad2c742..c28d389 100644 --- 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 @@ -1,12 +1,11 @@ """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. +This module contains criteria for uniaxial high-cycle fatigue that incorporate +mean-stress effects via equivalent stress amplitudes. It adjusts the stress amplitude +using models such as Goodman, Gerber, and Soderberg to provide more accurate +fatigue-life predictions when mean stresses significantly affect material endurance. -For more information you can refer to the following resource: +For more information, you can refer to the following resource: [PAPUGA, Jan, et al. Mean stress effect in stress-life fatigue prediction re-evaluated. In: MATEC web of conferences. EDP Sciences, 2018. p. 10018.](https://doi.org/10.1051/matecconf/201816510018). @@ -40,6 +39,8 @@ def calc_stress_eq_amp_asme( mean_stress: ArrayLike | np.float64, yield_strength: ArrayLike | np.float64, allow_neg_mean_stress: bool = True, + rtol: float = 0.0001, + atol: float = 1e-7, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using ASME criterion. @@ -61,11 +62,19 @@ def calc_stress_eq_amp_asme( Defaults to True. If set to False, the equivalent stress amplitude will be set equal to the original stress amplitude for cases where the mean stress is negative, ignoring the correction. + rtol: Relative tolerance for checking if mean stress magnitude is close to + yield strength. + atol: Absolute tolerance for checking if mean stress magnitude is close to + yield strength. Raises: ValueError: If yield strength is not positive ($R_e > 0$). - ValueError: If mean stress magnitude is equal or greater to yield strength, - resulting in infinite equivalent stress amplitude ($\sigma_m = R_e$). + ValueError: If mean stress magnitude exceeds yield strength, + which would produce a negative value under the square root. + ($|\sigma_m| > R_e$) + ValueError: If mean stress magnitude is close to yield strength + (within tolerance), the equivalent stress amplitude tends to infinity. + ($\left|\frac{\sigma_m}{R_e}\right| \approx 1.0$ within tolerance). Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting @@ -81,8 +90,17 @@ def calc_stress_eq_amp_asme( # 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.") + if np.any(ratio > 1.0): + raise ValueError( + "Mean stress magnitude exceeds yield strength, which produces a negative" + " value under the square root." + ) + + if np.any(np.isclose(ratio, 1.0, rtol=rtol, atol=atol)): + raise ValueError( + "Mean stress magnitude is close to yield strength, this results in " + "infinite equivalent stress amplitude." + ) eq_stress_amp_arr = _asme_correction_method( stress_amp_arr, mean_stress_arr, yield_strength_arr @@ -263,9 +281,9 @@ def calc_stress_eq_amp_gerber( stacklevel=2, ) - eq_stress_amp_arr = _gerber_correction_method( - stress_amp_arr, mean_stress_arr, ult_tensile_strength_arr - ) + eq_stress_amp_arr = _gerber_correction_method( + stress_amp_arr, mean_stress_arr, ult_tensile_strength_arr + ) # If allow_neg_mean_stress is False, set equivalent stress amplitude = to original if not allow_neg_mean_stress: 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 index 78fd082..e15c801 100644 --- 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 @@ -9,11 +9,10 @@ import numpy as np import pytest from numpy.testing import assert_allclose -from numpy.typing import NDArray +from numpy.typing import ArrayLike, NDArray from fatpy.core.stress_life.damage_params.uniaxial_stress_eq_amp import ( - ASME_mean_stress_correction_method, - calc_stress_eq_amp_ASME, + calc_stress_eq_amp_asme, calc_stress_eq_amp_bagci, calc_stress_eq_amp_gerber, calc_stress_eq_amp_goodman, @@ -26,69 +25,113 @@ 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 TestASMEMeanStressCorrectionMethod: - def test_basic_calculation(self) -> None: - result = ASME_mean_stress_correction_method(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( +# Broadcasting test variables - different shapes +_SA_1D = np.array([150.0, 500.0, 80.0, 200.0]) +_SM_1D = np.array([100.0, 150.0, 30.0, 0.0]) +_MAT_PARAM_1D = np.array([600.0, 700.0, 800.0, 900.0]) +_SA_2D = np.array([[100.0, 200.0], [150.0, 250.0]]) +_SM_2D = np.array([[0.0, 50.0], [100.0, 150.0]]) +# Shape (2, 4): last dim matches _SA_1D so they broadcast to (2, 4) +_SM_2D_COMPAT = np.array([[0.0, 50.0, 75.0, 100.0], [10.0, 25.0, 30.0, 0.0]]) + +_ASME_EXPECTED = 180.0 / np.sqrt(1.0 - (100.0 / 500.0) ** 2) + + +class TestCalcStressEqAmpAsme: + # Test numerical calculation for scalar inputs, and negative mean stress control + @pytest.mark.parametrize( + "sa, sm, ys, allow_neg_mean_stress, expected", + [ + pytest.param( + 180.0, + 100.0, + 500.0, + True, + _ASME_EXPECTED, + id="positive_mean_stress", + ), + pytest.param( + 180.0, + -100.0, + 500.0, + True, + _ASME_EXPECTED, + id="negative_mean_stress_with_correction", + ), + pytest.param(180.0, 0.0, 500.0, True, 180.0, id="zero_mean_stress"), + pytest.param( + 180.0, + -100.0, + 500.0, + False, + 180.0, + id="negative_mean_stress_no_correction", + ), + pytest.param( + 180.0, + 100.0, + 500.0, + False, + _ASME_EXPECTED, + id="positive_mean_stress_allow_neg_false", + ), + ], + ) + def test_calculation( self, - array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + sa: float, + sm: float, + ys: float, + allow_neg_mean_stress: bool, + expected: float, ) -> None: - stress_amp, mean_stress = array_inputs - result = ASME_mean_stress_correction_method(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]: - ASME_mean_stress_correction_method(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]: - ASME_mean_stress_correction_method(100.0, ms, 500.0) - - def test_negative_mean_stress_no_correction(self) -> None: - result = ASME_mean_stress_correction_method( - 180.0, -100.0, 500.0, allow_neg_mean_stress=False + result = calc_stress_eq_amp_asme( + sa, sm, ys, allow_neg_mean_stress=allow_neg_mean_stress ) - expected = 180.0 - assert_allclose(result, expected) - - -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( + # Test broadcasting behavior for various combinations of 1D and 2D array inputs + @pytest.mark.parametrize( + "sa, sm, mp", + [ + pytest.param(_SA_1D, 100.0, 700.0, id="sa_1d-sm_scalar-mp_scalar"), + pytest.param(150.0, _SM_1D, 700.0, id="sa_scalar-sm_1d-mp_scalar"), + pytest.param(150.0, 100.0, _MAT_PARAM_1D, id="sa_scalar-sm_scalar-mp_1d"), + pytest.param(_SA_1D, _SM_1D, _MAT_PARAM_1D, id="all_1d"), + pytest.param(_SA_2D, _SM_2D, 700.0, id="sa_2d-sm_2d-mp_scalar"), + pytest.param(150.0, _SM_2D, 700.0, id="sa_scalar-sm_2d-mp_scalar"), + pytest.param(_SA_1D, _SM_2D_COMPAT, 700.0, id="sa_1d-sm_2d-mp_scalar"), + ], + ) + def test_broadcasting( self, - array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + sa: ArrayLike | np.float64, + sm: ArrayLike | np.float64, + mp: ArrayLike | 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,) + result = calc_stress_eq_amp_asme(sa, sm, mp) + expected_shape = np.broadcast_shapes( + np.asarray(sa).shape, + np.asarray(sm).shape, + np.asarray(mp).shape, + ) + assert result.shape == expected_shape - def test_invalid_yield_strength(self) -> None: + # Test ValueError and Warning conditions + @pytest.mark.parametrize("ys", [0.0, -500.0]) + def test_invalid_yield_strength(self, ys: float) -> None: with pytest.raises(ValueError): - for ys in [0.0, -500.0]: - calc_stress_eq_amp_ASME(100.0, 50.0, ys) + calc_stress_eq_amp_asme(100.0, 50.0, ys) - def test_mean_stress_yield_strength_comparison_error(self) -> None: + @pytest.mark.parametrize("ms", [600.0, -600.0]) + def test_mean_stress_exceeds_yield_strength_error(self, ms: float) -> None: + with pytest.raises(ValueError): + calc_stress_eq_amp_asme(100.0, ms, 500.0) + + @pytest.mark.parametrize("ms", [500.0, -500.0]) + def test_mean_stress_close_to_yield_strength_error(self, ms: float) -> 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) + calc_stress_eq_amp_asme(100.0, ms, 500.0) class TestCalcStressEqAmpBagci: From 7f46ef63adf5bf52a772c4940df968dace3de3a3 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Sat, 30 May 2026 11:35:08 +0200 Subject: [PATCH 18/21] updated methods to use np.isclose --- .../damage_params/uniaxial_stress_eq_amp.py | 209 ++++++++++++------ 1 file changed, 138 insertions(+), 71 deletions(-) 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 index c28d389..bc3c13a 100644 --- 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 @@ -34,13 +34,18 @@ def _asme_correction_method( return asme_eq_amp +# Preset tolerances for all np.isclose() functions in mean stress correction methods +_RTOL = 0.001 +_ATOL = 1e-5 + + def calc_stress_eq_amp_asme( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, yield_strength: ArrayLike | np.float64, allow_neg_mean_stress: bool = True, - rtol: float = 0.0001, - atol: float = 1e-7, + rtol: float = _RTOL, + atol: float = _ATOL, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using ASME criterion. @@ -135,6 +140,8 @@ def calc_stress_eq_amp_bagci( mean_stress: ArrayLike | np.float64, yield_strength: ArrayLike | np.float64, allow_neg_mean_stress: bool = True, + rtol: float = _RTOL, + atol: float = _ATOL, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Bagci criterion. @@ -155,6 +162,10 @@ def calc_stress_eq_amp_bagci( Defaults to True. If set to False, the equivalent stress amplitude will be set equal to the original stress amplitude for cases where the mean stress is negative, ignoring the correction. + rtol: Relative tolerance for checking if mean stress magnitude is close to + yield strength. + atol: Absolute tolerance for checking if mean stress magnitude is close to + yield strength. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting @@ -163,8 +174,9 @@ def calc_stress_eq_amp_bagci( Raises: Warning: If mean stress magnitude exceeds yield strength ($|\sigma_m| > R_e$). ValueError: If yield strength is not positive ($R_e > 0$). - ValueError: If mean stress magnitude is equal to yield strength, - resulting in infinite equivalent stress amplitude ($\sigma_m = R_e$). + ValueError: If mean stress magnitude is close to yield strength + (within tolerance), the equivalent stress amplitude tends to infinity. + ($\left|\frac{\sigma_m}{R_e}\right| \approx 1.0$ within tolerance). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) @@ -176,12 +188,13 @@ def calc_stress_eq_amp_bagci( # Check if mean stress approaches or exceeds material parameter ratio = np.abs(mean_stress_arr) / yield_strength_arr - if np.any(ratio == 1.0): + if np.any(np.isclose(ratio, 1.0, rtol=rtol, atol=atol)): raise ValueError( - "Mean stress magnitude equals yield strength this would result in " + "Mean stress magnitude is close to yield strength, this results in " "infinite equivalent stress amplitude." ) - elif np.any(ratio > 1.0): + + if np.any(ratio > 1.0): warnings.warn( "Mean stress magnitude exceeds yield strength.", UserWarning, @@ -223,6 +236,8 @@ def calc_stress_eq_amp_gerber( mean_stress: ArrayLike | np.float64, ult_tensile_strength: ArrayLike | np.float64, allow_neg_mean_stress: bool = True, + rtol: float = _RTOL, + atol: float = _ATOL, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Gerber criterion. @@ -245,6 +260,10 @@ def calc_stress_eq_amp_gerber( Defaults to True. If set to False, the equivalent stress amplitude will be set equal to the original stress amplitude for cases where the mean stress is negative, ignoring the correction. + rtol: Relative tolerance for checking if mean stress magnitude is close to + ultimate tensile strength. + atol: Absolute tolerance for checking if mean stress magnitude is close to + ultimate tensile strength. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting @@ -254,9 +273,9 @@ def calc_stress_eq_amp_gerber( Warning: If mean stress magnitude exceeds ultimate tensile strength ($|\sigma_m| > \sigma_{UTS}$). ValueError: If ultimate tensile strength is not positive ($\sigma_{UTS} > 0$). - ValueError: If mean stress magnitude is equal to ultimate tensile strength, - resulting in infinite equivalent stress amplitude - ($\sigma_m = \sigma_{UTS}$). + ValueError: If mean stress magnitude is close to ultimate tensile strength + (within tolerance), the equivalent stress amplitude tends to infinity. + ($\left|\frac{\sigma_m}{\sigma_{UTS}}\right| \approx 1.0$ within tolerance). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) @@ -269,12 +288,13 @@ def calc_stress_eq_amp_gerber( # Check if mean stress approaches or exceeds material parameter ratio = np.abs(mean_stress_arr) / ult_tensile_strength_arr - if np.any(ratio == 1.0): + if np.any(np.isclose(ratio, 1.0, rtol=rtol, atol=atol)): raise ValueError( - "Mean stress magnitude equals ultimate tensile strength this would " - "result in infinite equivalent stress amplitude." + "Mean stress magnitude is close to ultimate tensile strength, " + "this results in infinite equivalent stress amplitude." ) - elif np.any(ratio > 1.0): + + if np.any(ratio > 1.0): warnings.warn( "Mean stress magnitude exceeds ultimate tensile strength. ", UserWarning, @@ -314,6 +334,8 @@ def calc_stress_eq_amp_goodman( mean_stress: ArrayLike | np.float64, ult_tensile_strength: ArrayLike | np.float64, allow_neg_mean_stress: bool = True, + rtol: float = _RTOL, + atol: float = _ATOL, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Goodman criterion. @@ -335,6 +357,10 @@ def calc_stress_eq_amp_goodman( Defaults to True. If set to False, the equivalent stress amplitude will be set equal to the original stress amplitude for cases where the mean stress is negative, ignoring the correction. + rtol: Relative tolerance for checking if mean stress magnitude is close to + ultimate tensile strength. + atol: Absolute tolerance for checking if mean stress magnitude is close to + ultimate tensile strength. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting @@ -344,8 +370,9 @@ def calc_stress_eq_amp_goodman( Warning: If mean stress magnitude exceeds ultimate tensile strength ($|\sigma_m| > \sigma_{UTS}$). ValueError: If ultimate tensile strength is not positive ($\sigma_{UTS} > 0$). - ValueError: If mean stress is equal to ultimate tensile strength, resulting in - infinite equivalent stress amplitude ($\sigma_m = \sigma_{UTS}$). + ValueError: If mean stress magnitude is close to ultimate tensile strength + (within tolerance), the equivalent stress amplitude tends to infinity. + ($\left|\frac{\sigma_m}{\sigma_{UTS}}\right| \approx 1.0$ within tolerance). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) @@ -355,14 +382,15 @@ def calc_stress_eq_amp_goodman( raise ValueError("Ultimate tensile strength must be positive") # Check if mean stress approaches or exceeds material parameter - ratio = mean_stress_arr / ult_tensile_strength_arr + ratio = abs(mean_stress_arr) / ult_tensile_strength_arr - if np.any(ratio == 1.0): + if np.any(np.isclose(ratio, 1.0, rtol=rtol, atol=atol)): raise ValueError( - "Mean stress equals ultimate tensile strength this would result in " - "infinite equivalent stress amplitude." + "Mean stress magnitude is close to ultimate tensile strength, " + "this results in infinite equivalent stress amplitude." ) - elif np.any(abs(ratio) > 1.0): + + if np.any(ratio > 1.0): warnings.warn( "Mean stress magnitude exceeds ultimate tensile strength. ", UserWarning, @@ -387,6 +415,8 @@ def calc_stress_eq_amp_half_slope( mean_stress: ArrayLike | np.float64, ult_tensile_strength: ArrayLike | np.float64, allow_neg_mean_stress: bool = True, + rtol: float = _RTOL, + atol: float = _ATOL, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using a half-slope mean stress correction. @@ -408,6 +438,10 @@ def calc_stress_eq_amp_half_slope( Defaults to True. If set to False, the equivalent stress amplitude will be set equal to the original stress amplitude for cases where the mean stress is negative, ignoring the correction. + rtol: Relative tolerance for checking if mean stress magnitude is close to + ultimate tensile strength. + atol: Absolute tolerance for checking if mean stress magnitude is close to + ultimate tensile strength. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting @@ -417,9 +451,10 @@ def calc_stress_eq_amp_half_slope( Warning: If mean stress magnitude exceeds the ultimate tensile strength ($|\sigma_m| > \sigma_{UTS}$). ValueError: If ultimate tensile strength is not positive ($\sigma_{UTS} > 0$). - ValueError: If mean stress is equal to double of the ultimate tensile strength, - resulting in infinite equivalent stress amplitude - ($\sigma_m = 2 \cdot \sigma_{UTS}$). + ValueError: If mean stress magnitude is close to double of the ultimate tensile + strength, (within tolerance), the equivalent stress amplitude tends to + infinity. ($\left|\frac{\sigma_m}{\sigma_{UTS}}\right| \approx 2.0$ within + tolerance). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) @@ -430,14 +465,14 @@ def calc_stress_eq_amp_half_slope( raise ValueError("Ultimate tensile strength must be positive") # Check if mean stress approaches or exceeds material parameter - ratio = mean_stress_arr / ult_tensile_strength_arr + ratio = np.abs(mean_stress_arr) / ult_tensile_strength_arr - if np.any(ratio == 2.0): + if np.any(np.isclose(ratio, 2.0, rtol=rtol, atol=atol)): raise ValueError( - "Mean stress equals to double of the ultimate tensile strength this would " - "result in infinite equivalent stress amplitude." + "Mean stress magnitude is close to double of the ultimate tensile strength," + " this results in infinite equivalent stress amplitude." ) - elif np.any(abs(ratio) > 1.0): + elif np.any(ratio > 1.0): warnings.warn( "Mean stress magnitude exceeds the ultimate tensile strength. ", UserWarning, @@ -462,6 +497,8 @@ def calc_stress_eq_amp_linear( mean_stress: ArrayLike | np.float64, stress_param_m: ArrayLike | np.float64, allow_neg_mean_stress: bool = True, + rtol: float = _RTOL, + atol: float = _ATOL, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using a linear mean stress correction. @@ -483,6 +520,10 @@ def calc_stress_eq_amp_linear( Defaults to True. If set to False, the equivalent stress amplitude will be set equal to the original stress amplitude for cases where the mean stress is negative, ignoring the correction. + rtol: Relative tolerance for checking if mean stress magnitude is close to + stress parameter M. + atol: Absolute tolerance for checking if mean stress magnitude is close to + stress parameter M. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting @@ -492,8 +533,9 @@ def calc_stress_eq_amp_linear( Warning: If mean stress magnitude exceeds material stress parameter M ($|\sigma_m| > M$). ValueError: If material stress parameter M is not positive ($M > 0$). - ValueError: If mean stress is equal to material stress parameter M, resulting in - infinite equivalent stress amplitude ($\sigma_m = M$). + ValueError: If mean stress magnitude is close to stress parameter M + (within tolerance), the equivalent stress amplitude tends to infinity. + ($\left|\frac{\sigma_m}{M}\right| \approx 1.0$ within tolerance). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) @@ -503,14 +545,15 @@ def calc_stress_eq_amp_linear( raise ValueError("Material stress parameter M must be positive") # Check if mean stress approaches or exceeds material parameter - ratio = mean_stress_arr / stress_param_m_arr + ratio = abs(mean_stress_arr) / stress_param_m_arr - if np.any(ratio == 1.0): + if np.any(np.isclose(ratio, 1.0, rtol=rtol, atol=atol)): raise ValueError( - "Mean stress equals material stress parameter M this would result in " - "infinite equivalent stress amplitude." + "Mean stress magnitude is close to stress parameter M, " + "this results in infinite equivalent stress amplitude." ) - elif np.any(abs(ratio) > 1.0): + + if np.any(ratio > 1.0): warnings.warn( "Mean stress magnitude exceeds material stress parameter M. ", UserWarning, @@ -530,16 +573,13 @@ def calc_stress_eq_amp_linear( return eq_stress_amp_arr -# todo! Check the name of the material parameter,issue calls it a true fracture stress -# todo! but the paper calls it a fatigue strength coeficient, -# todo! exel calls it a fat_strength_coef - - def calc_stress_eq_amp_morrow( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, fat_strength_coef: ArrayLike | np.float64, allow_neg_mean_stress: bool = True, + rtol: float = _RTOL, + atol: float = _ATOL, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Morrow criterion. @@ -560,7 +600,11 @@ def calc_stress_eq_amp_morrow( allow_neg_mean_stress: A flag to control the calculation method. Defaults to True. If set to False, the equivalent stress amplitude will be set equal to the original stress amplitude for cases where the mean stress - is negative, ignoring the correction. + is negative, ignoring the correction + rtol: Relative tolerance for checking if mean stress magnitude is close to + fatigue strength coefficient. + atol: Absolute tolerance for checking if mean stress magnitude is close to + fatigue strength coefficient. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting @@ -570,8 +614,9 @@ def calc_stress_eq_amp_morrow( Warning: If mean stress magnitude exceeds fatigue strength coefficient ($|\sigma_m| > \sigma_{f}'$). ValueError: If fatigue strength coefficient is not positive ($\sigma_{f}' > 0$). - ValueError: If mean stress is equal to fatigue strength coefficient, resulting - in infinite equivalent stress amplitude ($\sigma_m = \sigma_{f}'$). + ValueError: If mean stress magnitude is close to fatigue strength coefficient + (within tolerance), the equivalent stress amplitude tends to infinity. + ($\left|\frac{\sigma_m}{\sigma_{f}'}\right| \approx 1.0$ within tolerance). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) @@ -581,14 +626,14 @@ def calc_stress_eq_amp_morrow( raise ValueError("Fatigue strength coefficient must be positive") # Check if mean stress approaches or exceeds material parameter - ratio = mean_stress_arr / fat_strength_coef_arr + ratio = np.abs(mean_stress_arr) / fat_strength_coef_arr - if np.any(ratio == 1.0): + if np.any(np.isclose(ratio, 1.0, rtol=rtol, atol=atol)): raise ValueError( - "Mean stress equals fatigue strength coefficient this would result in " - "infinite equivalent stress amplitude." + "Mean stress magnitude is close to fatigue strength coefficient, " + "this results in infinite equivalent stress amplitude." ) - elif np.any(np.abs(ratio) > 1.0): + elif np.any(ratio > 1.0): warnings.warn( "Mean stress magnitude exceeds fatigue strength coefficient. ", UserWarning, @@ -613,6 +658,8 @@ def calc_stress_eq_amp_soderberg( mean_stress: ArrayLike | np.float64, yield_strength: ArrayLike | np.float64, allow_neg_mean_stress: bool = True, + rtol: float = _RTOL, + atol: float = _ATOL, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Soderberg criterion. @@ -633,6 +680,10 @@ def calc_stress_eq_amp_soderberg( Defaults to True. If set to False, the equivalent stress amplitude will be set equal to the original stress amplitude for cases where the mean stress is negative, ignoring the correction. + rtol: Relative tolerance for checking if mean stress magnitude is close to + yield strength. + atol: Absolute tolerance for checking if mean stress magnitude is close to + yield strength. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting @@ -641,8 +692,9 @@ def calc_stress_eq_amp_soderberg( Raises: Warning: If mean stress magnitude exceeds yield strength ($|\sigma_m| > R_e$). ValueError: If yield strength is not positive ($R_e > 0$). - ValueError: If mean stress is equal to yield strength, resulting in - infinite equivalent stress amplitude ($\sigma_m = R_e$). + ValueError: If mean stress magnitude is close to yield strength + (within tolerance), the equivalent stress amplitude tends to infinity. + ($\left|\frac{\sigma_m}{R_e}\right| \approx 1.0$ within tolerance). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) mean_stress_arr = np.asarray(mean_stress, dtype=np.float64) @@ -652,14 +704,14 @@ def calc_stress_eq_amp_soderberg( raise ValueError("Yield strength must be positive") # Check if mean stress approaches or exceeds material parameter - ratio = mean_stress_arr / yield_strength_arr + ratio = np.abs(mean_stress_arr) / yield_strength_arr - if np.any(ratio == 1.0): + if np.any(np.isclose(ratio, 1.0, rtol=rtol, atol=atol)): raise ValueError( - "Mean stress equals yield strength this would result in " + "Mean stress magnitude is close to yield strength, this results in " "infinite equivalent stress amplitude." ) - elif np.any(np.abs(ratio) > 1.0): + elif np.any(ratio > 1.0): warnings.warn( "Mean stress magnitude exceeds yield strength. ", UserWarning, @@ -701,6 +753,8 @@ def calc_stress_eq_amp_smith( mean_stress: ArrayLike | np.float64, ult_tensile_strength: ArrayLike | np.float64, allow_neg_mean_stress: bool = True, + rtol: float = _RTOL, + atol: float = _ATOL, ) -> NDArray[np.float64]: r"""Calculate equivalent stress amplitude using Smith criterion. @@ -723,6 +777,10 @@ def calc_stress_eq_amp_smith( Defaults to True. If set to False, the equivalent stress amplitude will be set equal to the original stress amplitude for cases where the mean stress is negative, ignoring the correction. + rtol: Relative tolerance for checking if mean stress magnitude is close to + ultimate tensile strength. + atol: Absolute tolerance for checking if mean stress magnitude is close to + ultimate tensile strength. Returns: Array of equivalent stress amplitudes. Shape follows NumPy broadcasting @@ -732,8 +790,9 @@ def calc_stress_eq_amp_smith( Warning: If mean stress magnitude exceeds ultimate tensile strength ($\sigma_m > \sigma_{UTS}$). ValueError: If ultimate tensile strength is not positive ($\sigma_{UTS} > 0$). - ValueError: If mean stress is equal to ultimate tensile strength, resulting in - infinite equivalent stress amplitude ($\sigma_{m} = \sigma_{UTS}$). + ValueError: If mean stress magnitude is close to ultimate tensile strength + (within tolerance), the equivalent stress amplitude tends to infinity. + ($\left|\frac{\sigma_m}{\sigma_{UTS}}\right| \approx 1.0$ within tolerance). """ stress_amp_arr = np.asarray(stress_amp, dtype=np.float64) @@ -744,13 +803,14 @@ def calc_stress_eq_amp_smith( raise ValueError("Ultimate tensile strength must be positive") # Check if mean stress approaches or exceeds material parameter - ratio = mean_stress_arr / ult_tensile_strength_arr - if np.any(ratio == 1.0): + ratio = np.abs(mean_stress_arr / ult_tensile_strength_arr) + + if np.any(np.isclose(ratio, 1.0, rtol=rtol, atol=atol)): raise ValueError( - "Mean stress equals ultimate tensile strength this would result in " - "infinite equivalent stress amplitude." + "Mean stress magnitude is close to ultimate tensile strength, " + "this results in infinite equivalent stress amplitude." ) - elif np.any(np.abs(ratio) > 1.0): + elif np.any(ratio > 1.0): warnings.warn( "Mean stress magnitude exceeds ultimate tensile strength. ", UserWarning, @@ -770,7 +830,6 @@ def calc_stress_eq_amp_smith( return eq_stress_amp_arr -# TODO use Walker instead and set the gamma parameter to 0.5? def _swt_correction_method( stress_amp: ArrayLike | np.float64, mean_stress: ArrayLike | np.float64, @@ -878,15 +937,15 @@ def calc_stress_eq_amp_walker( The Walker equivalent stress amplitude is calculated as: $$ - \displaystyle\sigma_{aeq}=\left(\sigma_a+\sigma_m\right)^{1-\gamma'} \cdot - \sigma_a^{\gamma'} + \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_param: Array-like of Walker exponents ($\gamma'$'). Must be broadcastable + walker_param: Array-like of Walker exponents ($\gamma$). Must be broadcastable with stress_amp and mean_stress. Leading dimensions are preserved. allow_neg_mean_stress: A flag to control the calculation method. Defaults to True. If set to False, the equivalent stress amplitude will be @@ -900,7 +959,7 @@ def calc_stress_eq_amp_walker( Raises: ValueError: If stress amplitude is negative ($\sigma_a < 0$). ValueError: If the validity condition $\sigma_a + \sigma_m >0$ is not satisfied. - ValueError: When the condition $\gamma'$ in [0, 1] is not satisfied. + ValueError: When the condition $\gamma$ in [0, 1] is not satisfied. ??? note "Validity Condition" The Walker method is valid when $\sigma_a + \sigma_m > 0$, ensuring that the @@ -928,8 +987,16 @@ def calc_stress_eq_amp_walker( # Check validity of Walker parameter: γ' in range [0, 1] invalid_condition = (walker_param_arr < 0) | (walker_param_arr > 1) if np.any(invalid_condition): - raise ValueError(r"Walker parameter ($\gamma'$') must be in the range [0, 1]. ") + raise ValueError(r"Walker parameter ($\gamma$) must be in the range [0, 1]. ") - return (stress_amp_arr + mean_stress_arr) ** ( - 1 - walker_param_arr - ) * stress_amp_arr**walker_param_arr + eq_stress_amp_arr = _walker_correction_method( + stress_amp_arr, mean_stress_arr, walker_param_arr + ) + + # If allow_neg_mean_stress is False, set equivalent stress amplitude = to original + if not allow_neg_mean_stress: + eq_stress_amp_arr = np.where( + mean_stress_arr < 0, stress_amp_arr, eq_stress_amp_arr + ) + + return eq_stress_amp_arr From bc407a023ee095386559ea4f24512303a3cb855c Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Sat, 30 May 2026 17:11:18 +0200 Subject: [PATCH 19/21] Tests revised an refactored --- .../test_uniaxial_stress_eq_amp.py | 652 ++++++++++-------- 1 file changed, 358 insertions(+), 294 deletions(-) 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 index e15c801..e7c27f6 100644 --- 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 @@ -4,12 +4,12 @@ four equivalent stress amplitude calculation methods: SWT, Goodman, Gerber, and Morrow. """ -from typing import Tuple +from typing import ClassVar import numpy as np import pytest from numpy.testing import assert_allclose -from numpy.typing import ArrayLike, NDArray +from numpy.typing import ArrayLike from fatpy.core.stress_life.damage_params.uniaxial_stress_eq_amp import ( calc_stress_eq_amp_asme, @@ -25,382 +25,446 @@ calc_stress_eq_amp_walker, ) -# Broadcasting test variables - different shapes -_SA_1D = np.array([150.0, 500.0, 80.0, 200.0]) -_SM_1D = np.array([100.0, 150.0, 30.0, 0.0]) +# --- Shared test fixtures / constants --- + +# Broadcasting test variables +_STRESS_AMP_1D = np.array([150.0, 500.0, 80.0, 200.0]) +_MEAN_STRESS_1D = np.array([100.0, 150.0, 30.0, 0.0]) _MAT_PARAM_1D = np.array([600.0, 700.0, 800.0, 900.0]) -_SA_2D = np.array([[100.0, 200.0], [150.0, 250.0]]) -_SM_2D = np.array([[0.0, 50.0], [100.0, 150.0]]) -# Shape (2, 4): last dim matches _SA_1D so they broadcast to (2, 4) -_SM_2D_COMPAT = np.array([[0.0, 50.0, 75.0, 100.0], [10.0, 25.0, 30.0, 0.0]]) +_STRESS_AMP_2D = np.array([[100.0, 200.0], [150.0, 250.0]]) +_MEAN_STRESS_2D = np.array([[0.0, 50.0], [100.0, 150.0]]) +_MEAN_STRESS_2D_COMPAT = np.array([[0.0, 50.0, 75.0, 100.0], [10.0, 25.0, 30.0, 0.0]]) + +# Scalar inputs for calculation tests +_STRESS_AMP = 180.0 +_MEAN_STRESS = 100.0 +_MAT_PARAM = 500.0 +_MEAN_STRESS_SWT = _MEAN_STRESS_WALKER = 80.0 +_STRESS_AMP_SWT = _STRESS_AMP_WALKER = 150.0 + +# Walker exponent (dimensionless, ∈ [0, 1]) +_WALKER_PARAM = 0.5 + +# --- Base class --- + + +class _BaseEqAmpTests: + """Shared tests for all calc_stress_eq_amp_* methods that: + - accept (stress_amp, mean_stress, material_param, allow_neg_mean_stress) + - raise ValueError for invalid (≤0) material param + - raise ValueError when |mean_stress| is close to material param (isclose) + - raise ValueError or Warn when |mean_stress| exceeds material param + """ + + correction_method: ClassVar + expected: ClassVar[float] # expected result for (_SA, +_SM, _MAT_PARAM) + neg_sm_expected: ClassVar[float] # expected result for (_SA, -_SM, _MAT_PARAM) + + # --- Correct caLculation tests --- + + # Trivial case: positive mean stress is corrected → expected result + def test_positive_mean_stress(self) -> None: + assert_allclose( + self.correction_method(_STRESS_AMP, _MEAN_STRESS, _MAT_PARAM), self.expected + ) + + # Negative mean stress case: + def test_negative_mean_stress_with_correction(self) -> None: + assert_allclose( + self.correction_method(_STRESS_AMP, -_MEAN_STRESS, _MAT_PARAM), + self.neg_sm_expected, + ) + + # Zero mean stress: no correction + def test_zero_mean_stress(self) -> None: + assert_allclose( + self.correction_method(_STRESS_AMP, 0.0, _MAT_PARAM), _STRESS_AMP + ) + + # allow_neg_mean_stress=False: + def test_negative_mean_stress_no_correction(self) -> None: + assert_allclose( + self.correction_method( + _STRESS_AMP, -_MEAN_STRESS, _MAT_PARAM, allow_neg_mean_stress=False + ), + _STRESS_AMP, + ) -_ASME_EXPECTED = 180.0 / np.sqrt(1.0 - (100.0 / 500.0) ** 2) + # Positive sm: always corrected regardless of the flag + def test_positive_mean_stress_allow_neg_false(self) -> None: + assert_allclose( + self.correction_method( + _STRESS_AMP, _MEAN_STRESS, _MAT_PARAM, allow_neg_mean_stress=False + ), + self.expected, + ) + + # Array with mixed signs: negative entries return sa, positive entries corrected + def test_mixed_sign_array_allow_neg_false(self) -> None: + result = self.correction_method( + _STRESS_AMP, + [-_MEAN_STRESS, 0.0, _MEAN_STRESS], + _MAT_PARAM, + allow_neg_mean_stress=False, + ) + assert_allclose(result, [_STRESS_AMP, _STRESS_AMP, self.expected]) + + # --- Broadcasting --- -class TestCalcStressEqAmpAsme: - # Test numerical calculation for scalar inputs, and negative mean stress control @pytest.mark.parametrize( - "sa, sm, ys, allow_neg_mean_stress, expected", + "stress_amp, mean_stress, mp", [ pytest.param( - 180.0, + _STRESS_AMP_1D, 100.0, - 500.0, - True, - _ASME_EXPECTED, - id="positive_mean_stress", + 700.0, + id="stress_amp_1d-mean_stress_scalar-mp_scalar", ), pytest.param( - 180.0, - -100.0, - 500.0, - True, - _ASME_EXPECTED, - id="negative_mean_stress_with_correction", + 150.0, + _MEAN_STRESS_1D, + 700.0, + id="stress_amp_scalar-mean_stress_1d-mp_scalar", ), - pytest.param(180.0, 0.0, 500.0, True, 180.0, id="zero_mean_stress"), pytest.param( - 180.0, - -100.0, - 500.0, - False, - 180.0, - id="negative_mean_stress_no_correction", + 150.0, + 100.0, + _MAT_PARAM_1D, + id="stress_amp_scalar-mean_stress_scalar-mp_1d", ), + pytest.param(_STRESS_AMP_1D, _MEAN_STRESS_1D, _MAT_PARAM_1D, id="all_1d"), pytest.param( - 180.0, - 100.0, - 500.0, - False, - _ASME_EXPECTED, - id="positive_mean_stress_allow_neg_false", + _STRESS_AMP_2D, + _MEAN_STRESS_2D, + 700.0, + id="stress_amp_2d-mean_stress_2d-mp_scalar", + ), + pytest.param( + 150.0, + _MEAN_STRESS_2D, + 700.0, + id="stress_amp_scalar-mean_stress_2d-mp_scalar", + ), + pytest.param( + _STRESS_AMP_1D, + _MEAN_STRESS_2D_COMPAT, + 700.0, + id="stress_amp_1d-mean_stress_2d-mp_scalar", ), - ], - ) - def test_calculation( - self, - sa: float, - sm: float, - ys: float, - allow_neg_mean_stress: bool, - expected: float, - ) -> None: - result = calc_stress_eq_amp_asme( - sa, sm, ys, allow_neg_mean_stress=allow_neg_mean_stress - ) - assert_allclose(result, expected) - - # Test broadcasting behavior for various combinations of 1D and 2D array inputs - @pytest.mark.parametrize( - "sa, sm, mp", - [ - pytest.param(_SA_1D, 100.0, 700.0, id="sa_1d-sm_scalar-mp_scalar"), - pytest.param(150.0, _SM_1D, 700.0, id="sa_scalar-sm_1d-mp_scalar"), - pytest.param(150.0, 100.0, _MAT_PARAM_1D, id="sa_scalar-sm_scalar-mp_1d"), - pytest.param(_SA_1D, _SM_1D, _MAT_PARAM_1D, id="all_1d"), - pytest.param(_SA_2D, _SM_2D, 700.0, id="sa_2d-sm_2d-mp_scalar"), - pytest.param(150.0, _SM_2D, 700.0, id="sa_scalar-sm_2d-mp_scalar"), - pytest.param(_SA_1D, _SM_2D_COMPAT, 700.0, id="sa_1d-sm_2d-mp_scalar"), ], ) def test_broadcasting( self, - sa: ArrayLike | np.float64, - sm: ArrayLike | np.float64, + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, mp: ArrayLike | np.float64, ) -> None: - result = calc_stress_eq_amp_asme(sa, sm, mp) + result = self.correction_method(stress_amp, mean_stress, mp) expected_shape = np.broadcast_shapes( - np.asarray(sa).shape, - np.asarray(sm).shape, + np.asarray(stress_amp).shape, + np.asarray(mean_stress).shape, np.asarray(mp).shape, ) assert result.shape == expected_shape + assert result.dtype == np.float64 - # Test ValueError and Warning conditions - @pytest.mark.parametrize("ys", [0.0, -500.0]) - def test_invalid_yield_strength(self, ys: float) -> None: - with pytest.raises(ValueError): - calc_stress_eq_amp_asme(100.0, 50.0, ys) + # --- Input validation --- - @pytest.mark.parametrize("ms", [600.0, -600.0]) - def test_mean_stress_exceeds_yield_strength_error(self, ms: float) -> None: + @pytest.mark.parametrize("mp", [0.0, -500.0]) + def test_invalid_material_param(self, mp: float) -> None: with pytest.raises(ValueError): - calc_stress_eq_amp_asme(100.0, ms, 500.0) + self.correction_method(100.0, 50.0, mp) - @pytest.mark.parametrize("ms", [500.0, -500.0]) - def test_mean_stress_close_to_yield_strength_error(self, ms: float) -> None: + @pytest.mark.parametrize("ms", [500.0, -500.0, 499.97, -499.97]) + def test_mean_stress_close_to_param_error(self, ms: float) -> None: with pytest.raises(ValueError): - 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,) + self.correction_method(100.0, ms, 500.0) - 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: + # Default behaviour: warn when |sm| > param. + @pytest.mark.parametrize("ms", [600.0, -600.0]) + def test_mean_stress_exceeds_param(self, ms: float) -> None: with pytest.warns(UserWarning): - for ms in [600.0, -600.0]: - calc_stress_eq_amp_bagci(100.0, ms, 500.0) + self.correction_method(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) +# --- Concrete test classes --- - 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) +class TestCalcStressEqAmpAsme(_BaseEqAmpTests): + correction_method = staticmethod(calc_stress_eq_amp_asme) + expected = _STRESS_AMP / np.sqrt(1.0 - (_MEAN_STRESS / _MAT_PARAM) ** 2) + neg_sm_expected = _STRESS_AMP / np.sqrt(1.0 - (-_MEAN_STRESS / _MAT_PARAM) ** 2) - def test_mean_stress_uts_comparison_error(self) -> None: + # Override: ASME method raises ValueError + @pytest.mark.parametrize("ms", [600.0, -600.0]) + def test_mean_stress_exceeds_param(self, ms: float) -> None: with pytest.raises(ValueError): - for ms in [500.0, -500.0]: - calc_stress_eq_amp_gerber(100.0, ms, 500.0) + self.correction_method(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 TestCalcStressEqAmpBagci(_BaseEqAmpTests): + correction_method = staticmethod(calc_stress_eq_amp_bagci) + expected = _BAGCI_EXPECTED = _STRESS_AMP / (1.0 - (_MEAN_STRESS / _MAT_PARAM) ** 4) + neg_sm_expected = _BAGCI_EXPECTED = _STRESS_AMP / ( + 1.0 - (-_MEAN_STRESS / _MAT_PARAM) ** 4 + ) -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,) +class TestCalcStressEqAmpGerber(_BaseEqAmpTests): + correction_method = staticmethod(calc_stress_eq_amp_gerber) + expected = _STRESS_AMP / (1.0 - (_MEAN_STRESS / _MAT_PARAM) ** 2) + neg_sm_expected = _STRESS_AMP / (1.0 - (-_MEAN_STRESS / _MAT_PARAM) ** 2) - 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) +class TestCalcStressEqAmpGoodman(_BaseEqAmpTests): + correction_method = staticmethod(calc_stress_eq_amp_goodman) + expected = _STRESS_AMP / (1.0 - _MEAN_STRESS / _MAT_PARAM) + neg_sm_expected = _STRESS_AMP / (1.0 - (-_MEAN_STRESS / _MAT_PARAM)) - 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 TestCalcStressEqAmpHalfSlope(_BaseEqAmpTests): + correction_method = staticmethod(calc_stress_eq_amp_half_slope) + expected = _STRESS_AMP / (1.0 - _MEAN_STRESS / (2.0 * _MAT_PARAM)) + neg_sm_expected = _STRESS_AMP / (1.0 - (-_MEAN_STRESS) / (2.0 * _MAT_PARAM)) -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) + # Half-slope singularity is at |sm| ≈ 2*UTS → override boundary test + @pytest.mark.parametrize("ms", [1000.0, -1000.0, 999.97, -999.97]) + def test_mean_stress_close_to_param_error(self, ms: float) -> None: + with pytest.raises(ValueError): + self.correction_method(100.0, ms, 500.0) - 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) +class TestCalcStressEqAmpLinear(_BaseEqAmpTests): + correction_method = staticmethod(calc_stress_eq_amp_linear) + expected = _STRESS_AMP / (1.0 - _MEAN_STRESS / _MAT_PARAM) + neg_sm_expected = _STRESS_AMP / (1.0 - (-_MEAN_STRESS / _MAT_PARAM)) - 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 TestCalcStressEqAmpMorrow(_BaseEqAmpTests): + correction_method = staticmethod(calc_stress_eq_amp_morrow) + expected = _STRESS_AMP / (1.0 - _MEAN_STRESS / _MAT_PARAM) + neg_sm_expected = _STRESS_AMP / (1.0 - (-_MEAN_STRESS / _MAT_PARAM)) -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) +class TestCalcStressEqAmpSoderberg(_BaseEqAmpTests): + correction_method = staticmethod(calc_stress_eq_amp_soderberg) + expected = _STRESS_AMP / (1.0 - _MEAN_STRESS / _MAT_PARAM) + neg_sm_expected = _STRESS_AMP / (1.0 - (-_MEAN_STRESS / _MAT_PARAM)) - 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) +class TestCalcStressEqAmpSmith(_BaseEqAmpTests): + correction_method = staticmethod(calc_stress_eq_amp_smith) + expected = ( + _STRESS_AMP + * (1.0 + _MEAN_STRESS / _MAT_PARAM) + / (1.0 - _MEAN_STRESS / _MAT_PARAM) + ) + neg_sm_expected = ( + _STRESS_AMP + * (1.0 + (-_MEAN_STRESS) / _MAT_PARAM) + / (1.0 - (-_MEAN_STRESS) / _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) +# --- SWT and Walker: standalone classes (different signatures and validation) --- +_SWT_EXPECTED = np.sqrt(_STRESS_AMP_SWT * (_MEAN_STRESS_SWT + _STRESS_AMP_SWT)) +_SWT_NEG_SM_EXPECTED = np.sqrt(_STRESS_AMP_SWT * (-_MEAN_STRESS_SWT + _STRESS_AMP_SWT)) +_WALKER_EXPECTED = (_STRESS_AMP_WALKER + _MEAN_STRESS_WALKER) ** ( + 1 - _WALKER_PARAM +) * _STRESS_AMP_WALKER**_WALKER_PARAM -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) +_WALKER_NEG_SM_EXPECTED = (_STRESS_AMP_WALKER - _MEAN_STRESS_WALKER) ** ( + 1 - _WALKER_PARAM +) * _STRESS_AMP_WALKER**_WALKER_PARAM - 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) +class TestCalcStressEqAmpSwt: + """Tests for SWT: sqrt(sa * (sm + sa)). No material param.""" - 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) + # Trivial case + def test_positive_mean_stress(self) -> None: + assert_allclose( + calc_stress_eq_amp_swt(_STRESS_AMP_SWT, _MEAN_STRESS_SWT), _SWT_EXPECTED + ) - 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) + # Negative mean stress: + def test_negative_mean_stress_with_correction(self) -> None: + assert_allclose( + calc_stress_eq_amp_swt(_STRESS_AMP_SWT, -_MEAN_STRESS_SWT), + _SWT_NEG_SM_EXPECTED, + ) + + # Zero mean stress: + def test_zero_mean_stress(self) -> None: + assert_allclose(calc_stress_eq_amp_swt(_STRESS_AMP_SWT, 0.0), _STRESS_AMP_SWT) + # Negative mean stress with allow_neg_mean_stress=False: + def test_negative_mean_stress_no_correction(self) -> None: + result = calc_stress_eq_amp_swt( + _STRESS_AMP_SWT, -_MEAN_STRESS_SWT, allow_neg_mean_stress=False + ) + assert_allclose(result, _STRESS_AMP_SWT) -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) + # Positive mean stress with allow_neg_mean_stress=False: + def test_positive_mean_stress_allow_neg_false(self) -> None: + result = calc_stress_eq_amp_swt( + _STRESS_AMP_SWT, _MEAN_STRESS_SWT, allow_neg_mean_stress=False + ) + assert_allclose(result, _SWT_EXPECTED) + + # Array with mixed signs: negative entries return sa, positive entries corrected + def test_mixed_sign_array_allow_neg_false(self) -> None: + result = calc_stress_eq_amp_swt( + _STRESS_AMP_SWT, + [-_MEAN_STRESS_SWT, 0.0, _MEAN_STRESS_SWT], + allow_neg_mean_stress=False, + ) + assert_allclose(result, [_STRESS_AMP_SWT, _STRESS_AMP_SWT, _SWT_EXPECTED]) - def test_array_inputs( + @pytest.mark.parametrize( + "stress_amp, mean_stress", + [ + pytest.param(_STRESS_AMP_1D, 100.0, id="sa_1d-sm_scalar"), + pytest.param(150.0, _MEAN_STRESS_1D, id="sa_scalar-sm_1d"), + pytest.param(_STRESS_AMP_1D, _MEAN_STRESS_1D, id="all_1d"), + pytest.param(_STRESS_AMP_2D, _MEAN_STRESS_2D, id="sa_2d-sm_2d"), + pytest.param(150.0, _MEAN_STRESS_2D, id="sa_scalar-sm_2d"), + pytest.param(_STRESS_AMP_1D, _MEAN_STRESS_2D_COMPAT, id="sa_1d-sm_2d"), + ], + ) + def test_broadcasting( self, - array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | 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,) + result = calc_stress_eq_amp_swt(stress_amp, mean_stress) + expected_shape = np.broadcast_shapes( + np.asarray(stress_amp).shape, + np.asarray(mean_stress).shape, + ) + assert result.shape == expected_shape + assert result.dtype == np.float64 - def test_invalid_yield_strength(self) -> None: + @pytest.mark.parametrize("sa", [-_STRESS_AMP_SWT, -1.0, -0.001]) + def test_negative_stress_amp_raises(self, sa: float) -> None: with pytest.raises(ValueError): - for yield_strength in [0.0, -500.0]: - calc_stress_eq_amp_soderberg(100.0, 50.0, yield_strength) + calc_stress_eq_amp_swt(sa, 100.0) - def test_mean_stress_yield_strength_comparison_error(self) -> None: + @pytest.mark.parametrize( + "sa, sm", + [ + pytest.param(100.0, -100.0, id="sa_plus_sm_equals_zero"), + pytest.param(100.0, -200.0, id="sa_plus_sm_negative"), + ], + ) + def test_validity_condition_violated(self, sa: float, sm: float) -> 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) - + calc_stress_eq_amp_swt(sa, sm) -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) +class TestCalcStressEqAmpWalker: + """Tests for Walker: (sa + sm)^(1-γ) * sa^γ. walker_param = γ ∈ [0, 1].""" - def test_mean_stress_uts_comparison_error(self) -> None: - with pytest.raises(ValueError): - calc_stress_eq_amp_smith(100.0, 500.0, 500.0) + # Trivial case + def test_positive_mean_stress(self) -> None: + assert_allclose( + calc_stress_eq_amp_walker( + _STRESS_AMP_WALKER, _MEAN_STRESS_WALKER, _WALKER_PARAM + ), + _WALKER_EXPECTED, + ) - def test_mean_stress_uts_comparison_warning(self) -> None: - with pytest.warns(UserWarning): - calc_stress_eq_amp_smith(100.0, 600.0, 500.0) + # Negative mean stress: + def test_negative_mean_stress_with_correction(self) -> None: + assert_allclose( + calc_stress_eq_amp_walker( + _STRESS_AMP_WALKER, -_MEAN_STRESS_WALKER, _WALKER_PARAM + ), + _WALKER_NEG_SM_EXPECTED, + ) + # Zero mean stress: + def test_zero_mean_stress(self) -> None: + assert_allclose( + calc_stress_eq_amp_walker(_STRESS_AMP_WALKER, 0.0, _WALKER_PARAM), + _STRESS_AMP_WALKER, + ) -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) + # Negative mean stress with allow_neg_mean_stress=False: + def test_negative_mean_stress_no_correction(self) -> None: + result = calc_stress_eq_amp_walker( + _STRESS_AMP_WALKER, + -_MEAN_STRESS_WALKER, + _WALKER_PARAM, + allow_neg_mean_stress=False, + ) + assert_allclose(result, _STRESS_AMP_WALKER) + + # Positive mean stress with allow_neg_mean_stress=False: + def test_positive_mean_stress_allow_neg_false(self) -> None: + result = calc_stress_eq_amp_walker( + _STRESS_AMP_WALKER, + _MEAN_STRESS_WALKER, + _WALKER_PARAM, + allow_neg_mean_stress=False, + ) + assert_allclose(result, _WALKER_EXPECTED) + + # Array with mixed signs: negative entries return sa, positive entries corrected + def test_mixed_sign_array_allow_neg_false(self) -> None: + result = calc_stress_eq_amp_walker( + _STRESS_AMP_WALKER, + [-_MEAN_STRESS_WALKER, 0.0, _MEAN_STRESS_WALKER], + _WALKER_PARAM, + allow_neg_mean_stress=False, + ) + assert_allclose( + result, [_STRESS_AMP_WALKER, _STRESS_AMP_WALKER, _WALKER_EXPECTED] + ) - def test_array_inputs( + @pytest.mark.parametrize( + "stress_amp, mean_stress", + [ + pytest.param(_STRESS_AMP_1D, 100.0, id="sa_1d-sm_scalar"), + pytest.param(150.0, _MEAN_STRESS_1D, id="sa_scalar-sm_1d"), + pytest.param(_STRESS_AMP_1D, _MEAN_STRESS_1D, id="all_1d"), + pytest.param(_STRESS_AMP_2D, _MEAN_STRESS_2D, id="sa_2d-sm_2d"), + pytest.param(150.0, _MEAN_STRESS_2D, id="sa_scalar-sm_2d"), + pytest.param(_STRESS_AMP_1D, _MEAN_STRESS_2D_COMPAT, id="sa_1d-sm_2d"), + ], + ) + def test_broadcasting( self, - array_inputs: Tuple[NDArray[np.float64], NDArray[np.float64]], + stress_amp: ArrayLike | np.float64, + mean_stress: ArrayLike | np.float64, ) -> None: - stress_amp, mean_stress = array_inputs - result = calc_stress_eq_amp_swt(stress_amp, mean_stress) - assert result.shape == (4,) + result = calc_stress_eq_amp_walker(stress_amp, mean_stress, _WALKER_PARAM) + expected_shape = np.broadcast_shapes( + np.asarray(stress_amp).shape, + np.asarray(mean_stress).shape, + ) + assert result.shape == expected_shape + assert result.dtype == np.float64 - def test_negative_stress_amplitude(self) -> None: + @pytest.mark.parametrize("sa", [-_STRESS_AMP_WALKER, -1.0, -0.001]) + def test_negative_stress_amp_raises(self, sa: float) -> None: with pytest.raises(ValueError): - calc_stress_eq_amp_swt(-100.0, 500.0) + calc_stress_eq_amp_walker(sa, 100.0, _WALKER_PARAM) - def test_swt_validity_condition(self) -> None: + @pytest.mark.parametrize( + "sa, sm", + [ + pytest.param(100.0, -100.0, id="sa_plus_sm_equals_zero"), + pytest.param(100.0, -200.0, id="sa_plus_sm_negative"), + ], + ) + def test_validity_condition_violated(self, sa: float, sm: float) -> 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,) + calc_stress_eq_amp_walker(sa, sm, _WALKER_PARAM) - def test_invalid_walker_parameter(self) -> None: + @pytest.mark.parametrize("gamma", [-0.1, 1.1, -1.0, 2.0]) + def test_invalid_walker_param(self, gamma: float) -> None: with pytest.raises(ValueError): - for walker_param in [-1.0, 2.0]: - calc_stress_eq_amp_walker(100.0, 50.0, walker_param) + calc_stress_eq_amp_walker(_STRESS_AMP_WALKER, _MEAN_STRESS_WALKER, gamma) From 309ce2ed8c890cc293113524561aa79ae7000cff Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Sun, 31 May 2026 10:05:14 +0200 Subject: [PATCH 20/21] test type error fixed --- .../stress_life/damage_params/test_uniaxial_stress_eq_amp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index e7c27f6..ebc8c57 100644 --- 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 @@ -4,7 +4,7 @@ four equivalent stress amplitude calculation methods: SWT, Goodman, Gerber, and Morrow. """ -from typing import ClassVar +from typing import Any, Callable, ClassVar import numpy as np import pytest @@ -56,7 +56,7 @@ class _BaseEqAmpTests: - raise ValueError or Warn when |mean_stress| exceeds material param """ - correction_method: ClassVar + correction_method: ClassVar[Callable[..., Any]] expected: ClassVar[float] # expected result for (_SA, +_SM, _MAT_PARAM) neg_sm_expected: ClassVar[float] # expected result for (_SA, -_SM, _MAT_PARAM) From 1c153554397064f7245b9b6c04a7744338a3d7f9 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Thu, 11 Jun 2026 09:50:19 +0200 Subject: [PATCH 21/21] added warning for negative stress_amp --- .../damage_params/uniaxial_stress_eq_amp.py | 84 ++++++++++++++++++- .../test_uniaxial_stress_eq_amp.py | 8 +- 2 files changed, 87 insertions(+), 5 deletions(-) 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 index bc3c13a..b8e2e9d 100644 --- 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 @@ -73,6 +73,7 @@ def calc_stress_eq_amp_asme( yield strength. Raises: + Warning: If stress amplitude is negative ($\sigma_a < 0$). ValueError: If yield strength is not positive ($R_e > 0$). ValueError: If mean stress magnitude exceeds yield strength, which would produce a negative value under the square root. @@ -107,6 +108,13 @@ def calc_stress_eq_amp_asme( "infinite equivalent stress amplitude." ) + if np.any(stress_amp_arr < 0): + warnings.warn( + "Stress amplitude is negative.", + UserWarning, + stacklevel=2, + ) + eq_stress_amp_arr = _asme_correction_method( stress_amp_arr, mean_stress_arr, yield_strength_arr ) @@ -173,6 +181,7 @@ def calc_stress_eq_amp_bagci( Raises: Warning: If mean stress magnitude exceeds yield strength ($|\sigma_m| > R_e$). + Warning: If stress amplitude is negative ($\sigma_a < 0$). ValueError: If yield strength is not positive ($R_e > 0$). ValueError: If mean stress magnitude is close to yield strength (within tolerance), the equivalent stress amplitude tends to infinity. @@ -201,6 +210,13 @@ def calc_stress_eq_amp_bagci( stacklevel=2, ) + if np.any(stress_amp_arr < 0): + warnings.warn( + "Stress amplitude is negative.", + UserWarning, + stacklevel=2, + ) + eq_stress_amp_arr = _bagci_correction_method( stress_amp_arr, mean_stress_arr, yield_strength_arr ) @@ -272,6 +288,7 @@ def calc_stress_eq_amp_gerber( Raises: Warning: If mean stress magnitude exceeds ultimate tensile strength ($|\sigma_m| > \sigma_{UTS}$). + Warning: If stress amplitude is negative ($\sigma_a < 0$). ValueError: If ultimate tensile strength is not positive ($\sigma_{UTS} > 0$). ValueError: If mean stress magnitude is close to ultimate tensile strength (within tolerance), the equivalent stress amplitude tends to infinity. @@ -301,6 +318,13 @@ def calc_stress_eq_amp_gerber( stacklevel=2, ) + if np.any(stress_amp_arr < 0): + warnings.warn( + "Stress amplitude is negative.", + UserWarning, + stacklevel=2, + ) + eq_stress_amp_arr = _gerber_correction_method( stress_amp_arr, mean_stress_arr, ult_tensile_strength_arr ) @@ -369,6 +393,7 @@ def calc_stress_eq_amp_goodman( Raises: Warning: If mean stress magnitude exceeds ultimate tensile strength ($|\sigma_m| > \sigma_{UTS}$). + Warning: If stress amplitude is negative ($\sigma_a < 0$). ValueError: If ultimate tensile strength is not positive ($\sigma_{UTS} > 0$). ValueError: If mean stress magnitude is close to ultimate tensile strength (within tolerance), the equivalent stress amplitude tends to infinity. @@ -397,6 +422,13 @@ def calc_stress_eq_amp_goodman( stacklevel=2, ) + if np.any(stress_amp_arr < 0): + warnings.warn( + "Stress amplitude is negative.", + UserWarning, + stacklevel=2, + ) + eq_stress_amp_arr = _linear_correction_method( stress_amp_arr, mean_stress_arr, ult_tensile_strength_arr ) @@ -450,6 +482,7 @@ def calc_stress_eq_amp_half_slope( Raises: Warning: If mean stress magnitude exceeds the ultimate tensile strength ($|\sigma_m| > \sigma_{UTS}$). + Warning: If stress amplitude is negative ($\sigma_a < 0$). ValueError: If ultimate tensile strength is not positive ($\sigma_{UTS} > 0$). ValueError: If mean stress magnitude is close to double of the ultimate tensile strength, (within tolerance), the equivalent stress amplitude tends to @@ -472,13 +505,21 @@ def calc_stress_eq_amp_half_slope( "Mean stress magnitude is close to double of the ultimate tensile strength," " this results in infinite equivalent stress amplitude." ) - elif np.any(ratio > 1.0): + + if np.any(ratio > 1.0): warnings.warn( "Mean stress magnitude exceeds the ultimate tensile strength. ", UserWarning, stacklevel=2, ) + if np.any(stress_amp_arr < 0): + warnings.warn( + "Stress amplitude is negative.", + UserWarning, + stacklevel=2, + ) + eq_stress_amp_arr = _linear_correction_method( stress_amp_arr, mean_stress_arr, 2 * ult_tensile_strength_arr ) @@ -532,6 +573,7 @@ def calc_stress_eq_amp_linear( Raises: Warning: If mean stress magnitude exceeds material stress parameter M ($|\sigma_m| > M$). + Warning: If stress amplitude is negative ($\sigma_a < 0$). ValueError: If material stress parameter M is not positive ($M > 0$). ValueError: If mean stress magnitude is close to stress parameter M (within tolerance), the equivalent stress amplitude tends to infinity. @@ -560,6 +602,13 @@ def calc_stress_eq_amp_linear( stacklevel=2, ) + if np.any(stress_amp_arr < 0): + warnings.warn( + "Stress amplitude is negative.", + UserWarning, + stacklevel=2, + ) + eq_stress_amp_arr = _linear_correction_method( stress_amp_arr, mean_stress_arr, stress_param_m_arr ) @@ -613,6 +662,7 @@ def calc_stress_eq_amp_morrow( Raises: Warning: If mean stress magnitude exceeds fatigue strength coefficient ($|\sigma_m| > \sigma_{f}'$). + Warning: If stress amplitude is negative ($\sigma_a < 0$). ValueError: If fatigue strength coefficient is not positive ($\sigma_{f}' > 0$). ValueError: If mean stress magnitude is close to fatigue strength coefficient (within tolerance), the equivalent stress amplitude tends to infinity. @@ -633,13 +683,21 @@ def calc_stress_eq_amp_morrow( "Mean stress magnitude is close to fatigue strength coefficient, " "this results in infinite equivalent stress amplitude." ) - elif np.any(ratio > 1.0): + + if np.any(ratio > 1.0): warnings.warn( "Mean stress magnitude exceeds fatigue strength coefficient. ", UserWarning, stacklevel=2, ) + if np.any(stress_amp_arr < 0): + warnings.warn( + "Stress amplitude is negative.", + UserWarning, + stacklevel=2, + ) + eq_stress_amp_arr = _linear_correction_method( stress_amp_arr, mean_stress_arr, fat_strength_coef_arr ) @@ -691,6 +749,7 @@ def calc_stress_eq_amp_soderberg( Raises: Warning: If mean stress magnitude exceeds yield strength ($|\sigma_m| > R_e$). + Warning: If stress amplitude is negative ($\sigma_a < 0$). ValueError: If yield strength is not positive ($R_e > 0$). ValueError: If mean stress magnitude is close to yield strength (within tolerance), the equivalent stress amplitude tends to infinity. @@ -711,13 +770,21 @@ def calc_stress_eq_amp_soderberg( "Mean stress magnitude is close to yield strength, this results in " "infinite equivalent stress amplitude." ) - elif np.any(ratio > 1.0): + + if np.any(ratio > 1.0): warnings.warn( "Mean stress magnitude exceeds yield strength. ", UserWarning, stacklevel=2, ) + if np.any(stress_amp_arr < 0): + warnings.warn( + "Stress amplitude is negative.", + UserWarning, + stacklevel=2, + ) + eq_stress_amp_arr = _linear_correction_method( stress_amp_arr, mean_stress_arr, yield_strength_arr ) @@ -789,6 +856,7 @@ def calc_stress_eq_amp_smith( Raises: Warning: If mean stress magnitude exceeds ultimate tensile strength ($\sigma_m > \sigma_{UTS}$). + Warning: If stress amplitude is negative ($\sigma_a < 0$). ValueError: If ultimate tensile strength is not positive ($\sigma_{UTS} > 0$). ValueError: If mean stress magnitude is close to ultimate tensile strength (within tolerance), the equivalent stress amplitude tends to infinity. @@ -810,13 +878,21 @@ def calc_stress_eq_amp_smith( "Mean stress magnitude is close to ultimate tensile strength, " "this results in infinite equivalent stress amplitude." ) - elif np.any(ratio > 1.0): + + if np.any(ratio > 1.0): warnings.warn( "Mean stress magnitude exceeds ultimate tensile strength. ", UserWarning, stacklevel=2, ) + if np.any(stress_amp_arr < 0): + warnings.warn( + "Stress amplitude is negative.", + UserWarning, + stacklevel=2, + ) + eq_stress_amp_arr = _smith_correction_method( stress_amp_arr, mean_stress_arr, ult_tensile_strength_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 index ebc8c57..ea0b3df 100644 --- 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 @@ -53,7 +53,8 @@ class _BaseEqAmpTests: - accept (stress_amp, mean_stress, material_param, allow_neg_mean_stress) - raise ValueError for invalid (≤0) material param - raise ValueError when |mean_stress| is close to material param (isclose) - - raise ValueError or Warn when |mean_stress| exceeds material param + - raise ValueError or Warning when |mean_stress| exceeds material param + - raise Warning when stress_amp is negative """ correction_method: ClassVar[Callable[..., Any]] @@ -187,6 +188,11 @@ def test_mean_stress_exceeds_param(self, ms: float) -> None: with pytest.warns(UserWarning): self.correction_method(100.0, ms, 500.0) + @pytest.mark.parametrize("sa", [-100.0, -0.001]) + def test_negative_stress_amp_warning(self, sa: float) -> None: + with pytest.warns(UserWarning): + self.correction_method(sa, 50.0, 500.0) + # --- Concrete test classes ---