From c7ef1e84cb2654ee1d962656f0996be81e146000 Mon Sep 17 00:00:00 2001 From: Tomas Karas Date: Wed, 12 Nov 2025 16:41:23 +0100 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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