Skip to content

Uniaxial equivalent stress amplitude functionality#55

Open
KarasTomas wants to merge 17 commits into
faberorg:mainfrom
KarasTomas:uniaxial_eq_stress
Open

Uniaxial equivalent stress amplitude functionality#55
KarasTomas wants to merge 17 commits into
faberorg:mainfrom
KarasTomas:uniaxial_eq_stress

Conversation

@KarasTomas
Copy link
Copy Markdown
Collaborator

@KarasTomas KarasTomas commented Nov 12, 2025

Pull request following issue #60 and is prepared for other stress based uniaxial fatigue criteria like #40, #44 , #45 and #46.

@codecov
Copy link
Copy Markdown

codecov Bot commented Nov 12, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@KarasTomas KarasTomas added the WIP Work In Progress label Nov 24, 2025
@KarasTomas KarasTomas requested a review from Vybornak2 November 24, 2025 15:22
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo in the file name iniaxial

from numpy.typing import ArrayLike, NDArray


def _validate_stress_inputs(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function seems to implement DRY principle, but to me seems a bit clunky and hard to extend

reason might be, that it is trying to do multiple things, so it does not have 1 responsibility only, thus it branches in its logic and is harder to fit to use cases.

think it through if it could not be improved

for compressive-dominated loading conditions.

"""
stress_amp_arr, mean_stress_arr = _validate_stress_inputs(stress_amp, mean_stress)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would convert ArrayLike to np.ndarray here and check for positive values
It is more explicit and does not create too much boilerplate code

In this case validate function seems as overkill to me

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not saying that we have to do that. Just going through cases of usage of validate function

"appropriate for compressive-dominated loading conditions."
)

return np.sqrt(stress_amp_arr * (mean_stress_arr + stress_amp_arr))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

** 0.5 might be more readable than np.sqrt but it does not matter as much, just is more consistent with the mathematical formulas

ratio = abs_mean / material_param_arr

if np.any(ratio >= 1.0):
raise ValueError(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about case, where user inputs real values, where some are simply too high, so some results are negative?
Is it okay to disallow user from calculating all values ?

I personally would rather raise warning and exclude it from this validation function

("mean_equals_material", False, "Mean stress magnitude.*exceeds or equals"),
],
)
def test_validate_stress_inputs_parametrized(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate function requires more then it brings value

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend using test classes to separate different test baste on function tested

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test class than would be more generic, where each test case within the test class repeats for each tested function

class TestFunctioon1:
    def test_feature1():
        pass
    
    def test_feature2(): ...
 
# now same thing for class TestFunction2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it puts a bit more structure into the test file, so it is easier to navigate
also you can run tests based on the test class and more

@Vybornak2
Copy link
Copy Markdown
Collaborator

Additionally the things should be added / considered:

  • additional eq methods (please contact @MartinNesladek)
  • STW for HCF docstring should include usage requirements and reference to SWT LCF (for now create issue placeholder until implementation)
  • for each equivalent stress function should be specified what is it equivalent of (R = -1 standard or R=0, ....) and potentially other descriptive information
  • consider/consult whether we should raise error for some conditions when mean stress is lower then zero or rather return stress amp as is

@KarasTomas
Copy link
Copy Markdown
Collaborator Author

On top of equivalent stress amplitude functionality requested by issues, several other functions were added based on this source: Mean stress effect in stress-life fatigue prediction re-evaluated

Use-case info blocks in docstrings still need to be revised.

@pavkukula pavkukula self-requested a review April 25, 2026 12:46
Copy link
Copy Markdown
Collaborator

@pavkukula pavkukula left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SWT's validity condition σ_a > |σ_m| is incorrect — it rejects R≥0 loading, which is the canonical SWT case; should be σ_a + σ_m > 0.

invalid = (stress_amp_arr + mean_stress_arr) <= 0

rules for the input arrays.

Raises:
Warning: If mean stress exceeds half of the ultimate tensile strength.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If mean stress exceeds DOUBLE of the ultimate tensile strength.

Comment thread src/fatpy/core/stress_life/damage_params/uniaxial_stress_eq_amp.py Outdated
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"
Copy link
Copy Markdown
Collaborator

@pavkukula pavkukula Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • not half, but DOUBLE
  • it will result in infinite eq stress amp, not zero
  • Adjacent string literals concatenate without inserting a space, so the actual message reads "…would resultin zero…"

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). ")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Walker is missing the σ_max > 0 guard — silently produces NaN
Walker is a generalization of SWT (it reduces to SWT at γ=0.5), so the same physical validity should hold.
Since γ is a non-integer, raising a negative (σ_a + σ_m) to a fractional power yields NaN. SWT guards against this; Walker doesn't.
Add the same σ_a + σ_m > 0 check as SWT.

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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

misplaced assertion

result = calc_stress_eq_amp_ASME(stress_amp, mean_stress, 700.0)
assert result.shape == (4,)

def test_invalid_yield_strength(self) -> None:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with pytest.raises: outside for loop only tests the first input
When ys=0.0 raises, the context manager exits and the loop is abandoned — ys=-500.0 is never executed.
Fix:
@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)

Comment thread src/fatpy/core/stress_life/damage_params/uniaxial_stress_eq_amp.py Outdated
Comment thread src/fatpy/core/stress_life/damage_params/uniaxial_stress_eq_amp.py Outdated
from numpy.typing import ArrayLike, NDArray


def calc_stress_eq_amp_ASME(
Copy link
Copy Markdown
Collaborator

@pavkukula pavkukula Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP 8 — calc_stress_eq_amp_ASME should be lowercase
Every other function uses _lowercase. ASME breaks both PEP 8 and the file's own convention. calc_stress_eq_amp_asme is consistent with calc_stress_eq_amp_swt.

On the other hand, ASME in upper letters is highly established abbreviation... I don't have strong opinion on that.

Comment thread src/fatpy/core/stress_life/damage_params/uniaxial_stress_eq_amp.py
@pavkukula
Copy link
Copy Markdown
Collaborator

ASME is more strict than Bagci/Gerber for no good reason
ASME raises for ratio >= 1.0 (combined). Bagci and Gerber raise only for ratio == 1.0 and merely warn for ratio > 1.0. The trouble: Bagci with σ_a=100, σ_m=600, R_e=500 returns σ_aeq ≈ -93 MPa with a warning — a non-physical negative equivalent amplitude that the caller can easily miss. Either align ASME's strict policy across all three (preferred — Papuga's §4 explicitly excludes such cases anyway, requiring σ_m < 0.75·R_e for these methods), or document why they differ.

@pavkukula
Copy link
Copy Markdown
Collaborator

ratio == 1.0 is a fragile float comparison
Exact float equality is brittle when inputs come from upstream computation rather than test literals.
np.isclose(ratio, 1.0) or ratio >= 1.0 (then warn separately for > 1.0) would be more robust.

expected = 180.0 / np.sqrt(1.0 - (100.0 / 500.0) ** 2)
assert_allclose(result, expected)

def test_array_inputs(
Copy link
Copy Markdown
Collaborator

@pavkukula pavkukula Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That tests one specific combination: two arrays of identical shape (4,) plus a scalar. It exercises a sliver of broadcasting — the trivial case where the two arrays already match. But the docstring promises much more.
Things that should work according to the docstring but are never tested:

  1. Scalar σ_a, array σ_m → result shape (3,)
    calc_stress_eq_amp_goodman(150.0, np.array([0.0, 100.0, 200.0]), 700.0)

  2. 1-D σ_a, 2-D σ_m → broadcasts to 2-D result
    sa = np.array([100.0, 200.0]) # shape (2,)
    sm = np.array([[0.0, 50.0], [100.0, 150.0]]) # shape (2, 2)
    calc_stress_eq_amp_goodman(sa, sm, 700.0) # should give shape (2, 2)

  3. Array of UTS values too (e.g., comparing materials)
    calc_stress_eq_amp_goodman(150.0, 100.0, np.array([500.0, 600.0, 700.0]))

@pavkukula
Copy link
Copy Markdown
Collaborator

Strongly compressive σ_m is silently accepted by Goodman/Linear/Morrow/Soderberg/Smith
E.g., Goodman with σ_a=100, σ_m=-1000, R_m=500 returns σ_aeq = 33.3 MPa (less than σ_a) — mathematically defined, physically dubious. Worth at least a Note: in the docstrings; an optional σ_m > -R_m-style sanity check would also be reasonable.

mean_stress: ArrayLike | np.float64,
yield_strength: ArrayLike | np.float64,
allow_neg_mean_stress: bool = True,
rtol: float = 0.0001,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Define module-level _RTOL / _ATOL constants and use them in the np.isclose check in every method (including Bagci/Gerber/etc. when you convert them off == 1.0). I'd suggest dropping rtol/atol from the function signatures entirely — they're an internal numerical guard, not something a user should tune — so all 11 functions keep an identical signature. If we later find a real need to tune them, we can add the keyword back compatibly.

The Walker equivalent stress amplitude is calculated as:

$$
\displaystyle\sigma_{aeq}=\left(\sigma_a+\sigma_m\right)^{1-\gamma'} \cdot
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe (based on the paper) the parameter is gamma, not gamma primed. There are more typos like this, for example: ($\gamma'$')

# Check if mean stress approaches or exceeds material parameter
ratio = np.abs(mean_stress_arr) / yield_strength_arr

if np.any(ratio == 1.0):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use np.isclose

@pavkukula
Copy link
Copy Markdown
Collaborator

The structure is good — approved as the template, with three additions to bake in before you propagate, so all 11 get them:

  1. Add a near-singularity test. test_mean_stress_close_to_yield_strength_error uses ms=500, ys=500 — that's ratio exactly 1.0, which a plain == 1.0 would also catch. Add a case with ms=499.97, ys=500 (ratio ≈ 0.99994) so the np.isclose tolerance is actually exercised — right now it isn't.
  2. Assert the return dtype (np.float64) somewhere in test_broadcasting — assert_allclose only checks values.
  3. Add a mixed-sign array case for allow_neg_mean_stress=False — e.g. σ_m=[-100,0,100] — the elementwise np.where is the point of the flag and no test covers it.

Also note for the warning-based methods (Bagci/Gerber/etc.): they'll each need a pytest.warns(UserWarning) test, which the ASME template doesn't include since ASME raises instead of warns (Bagci and Gerber do have a warning path, and that path needs to be tested.)

Question is whether Bagci and Gerber should warn-and-continue at all, or whether they should raise like ASME. Returning a negative equivalent stress amplitude with only a warning is arguably worse than just stopping, because a caller can easily miss a warning but can't miss an exception. That's a design decision for us: either make all three exponential methods raise (consistent, and then no pytest.warns test is needed anywhere), or keep the warn-vs-raise split deliberately (and then make sure each warning path gets a pytest.warns test). Either is defensible — it just needs to be decided rather than left as an accident of how the original code happened to be written.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

WIP Work In Progress

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants