Skip to content

Commit

Permalink
Merge pull request #110 from boschresearch/fix-change-fatigue-limit-name
Browse files Browse the repository at this point in the history
Fix change fatigue limit name
  • Loading branch information
johannes-mueller authored Oct 1, 2024
2 parents 1d65dcc + 8f26536 commit 072b316
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 61 deletions.
24 changes: 11 additions & 13 deletions demos/woehler_analyzer.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,22 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Before we do the actual analysis, we make some preparations like, guessing the initial `fatigue_limit`, distinguishing between `runouts` and `fractures` and between `infinite_zone` and `finite_zone`."
"Before we do the actual analysis, we make some preparations like, guessing the initial `finite_infinite_transition` load level, distinguishing between `runouts` and `fractures` and between `infinite_zone` and `finite_zone`."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fatigue_data = df.fatigue_data\n",
"fatigue_data.fatigue_limit"
"fatigue_data.finite_infinite_transition"
]
},
{
Expand All @@ -146,7 +151,7 @@
"go.Figure([\n",
" go.Scatter(x=finite_zone.cycles, y=finite_zone.load, mode='markers', name='finite'),\n",
" go.Scatter(x=infinite_zone.cycles, y=infinite_zone.load, mode='markers', name='infinite'),\n",
" go.Scatter(x=[df.cycles.min(), df.cycles.max()], y=[fatigue_data.fatigue_limit]*2, mode='lines', name='fatigue limit')\n",
" go.Scatter(x=[df.cycles.min(), df.cycles.max()], y=[fatigue_data.finite_infinite_transition]*2, mode='lines', name='fatigue limit')\n",
"]).update_xaxes(type='log').update_yaxes(type='log').update_layout(xaxis_title='Cycles', yaxis_title='Load')"
]
},
Expand All @@ -169,7 +174,7 @@
"fig = go.Figure([\n",
" go.Scatter(x=fractures.cycles, y=fractures.load, mode='markers', name='fractures'),\n",
" go.Scatter(x=runouts.cycles, y=runouts.load, mode='markers', name='runouts'),\n",
" go.Scatter(x=[df.cycles.min(), df.cycles.max()], y=[fatigue_data.fatigue_limit]*2, mode='lines', name='fatigue limit')\n",
" go.Scatter(x=[df.cycles.min(), df.cycles.max()], y=[fatigue_data.finite_infinite_transition]*2, mode='lines', name='fatigue limit')\n",
"]).update_xaxes(type='log').update_yaxes(type='log').update_layout(xaxis_title='Cycles', yaxis_title='Load')\n",
"fig"
]
Expand Down Expand Up @@ -207,9 +212,9 @@
"elementary_fig = copy.deepcopy(fig)\n",
"\n",
"elementary_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles), mode='lines', name='Elementary 50%')\n",
"elementary_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles, failure_probability=0.1), \n",
"elementary_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles, failure_probability=0.1),\n",
" mode='lines', name='Elementary 10%')\n",
"elementary_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles, failure_probability=0.9), \n",
"elementary_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles, failure_probability=0.9),\n",
" mode='lines', name='Elementary 90%')\n",
"\n",
"elementary_fig"
Expand Down Expand Up @@ -376,13 +381,6 @@
"\n",
"maxlike_fixed_fig"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
Expand Down
14 changes: 7 additions & 7 deletions src/pylife/materialdata/woehler/elementary.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ def _common_analysis(self):
TN, TS = self._pearl_chain_method()
return pd.Series({
'k_1': -self._slope,
'ND': self._transition_cycles(self._fd.fatigue_limit),
'SD': self._fd.fatigue_limit,
'ND': self._transition_cycles(self._fd.finite_infinite_transition),
'SD': self._fd.finite_infinite_transition,
'TN': TN,
'TS': TS
})
Expand Down Expand Up @@ -148,11 +148,11 @@ def _fit_slope(self):

return slope, lg_intercept

def _transition_cycles(self, fatigue_limit):
# FIXME Elementary means fatigue_limit == 0 -> np.inf
if fatigue_limit == 0:
fatigue_limit = 0.1
return 10**(self._lg_intercept + self._slope * (np.log10(fatigue_limit)))
def _transition_cycles(self, finite_infinite_transition):
# FIXME Elementary means finite_infinite_transition == 0 -> np.inf
if finite_infinite_transition == 0:
finite_infinite_transition = 0.1
return 10**(self._lg_intercept + self._slope * (np.log10(finite_infinite_transition)))

def _pearl_chain_method(self):
self._pearl_chain_estimator = PearlChainProbability(self._finite_fractures, self._slope)
Expand Down
38 changes: 19 additions & 19 deletions src/pylife/materialdata/woehler/fatigue_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class FatigueData(PylifeSignal):

def _validate(self):
self.fail_if_key_missing(['load', 'cycles', 'fracture'])
self._fatigue_limit = None
self._finite_infinite_transition = None

@property
def num_tests(self):
Expand Down Expand Up @@ -74,30 +74,30 @@ def cycles(self):
return self._obj.cycles

@property
def fatigue_limit(self):
def finite_infinite_transition(self):
'''The start value of the load endurance limit.
It is determined by searching for the lowest load level before the
appearance of a runout data point, and the first load level where a
runout appears. Then the median of the two load levels is the start
value.
'''
if self._fatigue_limit is None:
self._calc_fatigue_limit()
return self._fatigue_limit
if self._finite_infinite_transition is None:
self._calc_finite_infinite_transition()
return self._finite_infinite_transition

@property
def finite_zone(self):
'''All the tests with load levels above ``fatigue_limit``, i.e. the finite zone'''
if self._fatigue_limit is None:
self._calc_fatigue_limit()
'''All the tests with load levels above ``finite_infinite_transition``, i.e. the finite zone'''
if self._finite_infinite_transition is None:
self._calc_finite_infinite_transition()
return self._finite_zone

@property
def infinite_zone(self):
'''All the tests with load levels below ``fatigue_limit``, i.e. the infinite zone'''
if self._fatigue_limit is None:
self._calc_fatigue_limit()
'''All the tests with load levels below ``finite_infinite_transition``, i.e. the infinite zone'''
if self._finite_infinite_transition is None:
self._calc_finite_infinite_transition()
return self._infinite_zone

@property
Expand All @@ -120,7 +120,7 @@ def mixed_loads(self):
def pure_runout_loads(self):
return np.setxor1d(self.runout_loads, self.mixed_loads)

def conservative_fatigue_limit(self):
def conservative_finite_infinite_transition(self):
"""
Sets a lower fatigue limit that what is expected from the algorithm given by Mustafa Kassem.
For calculating the fatigue limit, all amplitudes where runouts and fractures are present are collected.
Expand All @@ -141,26 +141,26 @@ def conservative_fatigue_limit(self):
amps_to_consider = np.concatenate((amps_to_consider, [self.non_fractured_loads.max()]))

if len(amps_to_consider) > 0:
self._fatigue_limit = amps_to_consider.mean()
self._finite_infinite_transition = amps_to_consider.mean()
self._calc_finite_zone()

return self

def set_fatigue_limit(self, fatigue_limit):
def set_finite_infinite_transition(self, finite_infinite_transition):
"""
Allows the user to set an arbitrary fatigue limit.
Parameters
----------
fatigue_limit : float
finite_infinite_transition : float
The fatigue limit for separating the finite and infinite zone is set.
Returns
-------
self
"""
self._fatigue_limit = fatigue_limit
self._calc_finite_zone_manual(fatigue_limit)
self._finite_infinite_transition = finite_infinite_transition
self._calc_finite_zone_manual(finite_infinite_transition)

return self

Expand All @@ -178,9 +178,9 @@ def irrelevant_runouts_dropped(self):
def max_runout_load(self):
return self.runouts.load.max()

def _calc_fatigue_limit(self):
def _calc_finite_infinite_transition(self):
self._calc_finite_zone()
self._fatigue_limit = 0.0 if len(self.runouts) == 0 else self._half_level_above_highest_runout()
self._finite_infinite_transition = 0.0 if len(self.runouts) == 0 else self._half_level_above_highest_runout()

def _half_level_above_highest_runout(self):
if len(self._finite_zone) > 0:
Expand Down
2 changes: 1 addition & 1 deletion src/pylife/materialdata/woehler/maxlike.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def fail_if_less_than_three_fractures():
fail_if_less_than_two_mixed_levels()
fail_if_less_than_three_fractures()

SD_start = self._fd.fatigue_limit
SD_start = self._fd.finite_infinite_transition
TS_start = 1.2

var_opt = optimize.fmin(lambda p: -self._lh.likelihood_infinite(p[0], p[1]),
Expand Down
42 changes: 21 additions & 21 deletions tests/materialdata/woehler/test_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ def test_fatigue_data_finite_infinite_zone(data, finite_zone_expected, infinite_
no_runouts_infinite_expected)
])
def test_fatigue_data_finite_infinite_zone_conservative(data, finite_zone_expected, infinite_zone_expected):
fd = woehler.determine_fractures(data, 1e7).sort_index().fatigue_data.conservative_fatigue_limit()
fd = woehler.determine_fractures(data, 1e7).sort_index().fatigue_data.conservative_finite_infinite_transition()
pd.testing.assert_frame_equal(sort_fatigue_data(fd.finite_zone)[['load', 'cycles']],
sort_fatigue_data(finite_zone_expected))
fd = woehler.determine_fractures(data, 1e7).sort_index().fatigue_data.conservative_fatigue_limit()
fd = woehler.determine_fractures(data, 1e7).sort_index().fatigue_data.conservative_finite_infinite_transition()
pd.testing.assert_frame_equal(sort_fatigue_data(fd.infinite_zone)[['load', 'cycles']],
sort_fatigue_data(infinite_zone_expected))

Expand All @@ -103,7 +103,7 @@ def test_fatigue_data_finite_infinite_zone_conservative(data, finite_zone_expect
fat_limit_min)
])
def test_fatigue_data_finite_infinite_zone_manual_setting(data, finite_zone_expected, infinite_zone_expected, fat_limit):
fd = woehler.determine_fractures(data, 1e7).sort_index().fatigue_data.set_fatigue_limit(fatigue_limit=fat_limit)
fd = woehler.determine_fractures(data, 1e7).sort_index().fatigue_data.set_finite_infinite_transition(finite_infinite_transition=fat_limit)
pd.testing.assert_frame_equal(sort_fatigue_data(fd.finite_zone)[['load', 'cycles']],
sort_fatigue_data(finite_zone_expected))
pd.testing.assert_frame_equal(sort_fatigue_data(fd.infinite_zone)[['load', 'cycles']],
Expand Down Expand Up @@ -157,35 +157,35 @@ def test_woehler_fracture_determination_infered():
pd.testing.assert_frame_equal(test, expected)


@pytest.mark.parametrize("data, fatigue_limit_expected", [
@pytest.mark.parametrize("data, finite_infinite_transition_expected", [
(data, 362.5),
(data_no_mixed_horizons, 362.5),
(data_pure_runout_horizon_and_mixed_horizons, 362.5),
(data_no_runouts, 0.0),
(data_only_runout_levels, 362.5)
])
def test_woehler_endur_zones(data, fatigue_limit_expected):
def test_woehler_endur_zones(data, finite_infinite_transition_expected):
fd = woehler.determine_fractures(data, 1e7).fatigue_data
assert fd.fatigue_limit == fatigue_limit_expected
assert fd.finite_infinite_transition == finite_infinite_transition_expected


@pytest.mark.parametrize("data, fatigue_limit_expected", [
@pytest.mark.parametrize("data, finite_infinite_transition_expected", [
(data, 325.0),
(data_no_mixed_horizons, 350.0),
(data_pure_runout_horizon_and_mixed_horizons, 325.0),
(data_no_runouts, 0.0),
(data_only_runout_levels, 325.0)
])
def test_woehler_endur_zones_conservative(data, fatigue_limit_expected):
def test_woehler_endur_zones_conservative(data, finite_infinite_transition_expected):
fd = woehler.determine_fractures(data, 1e7).fatigue_data
fd = fd.conservative_fatigue_limit()
assert fd.fatigue_limit == fatigue_limit_expected
fd = fd.conservative_finite_infinite_transition()
assert fd.finite_infinite_transition == finite_infinite_transition_expected


def test_woehler_endure_zones_no_runouts():
df = data[data.cycles < 1e7]
fd = woehler.determine_fractures(df, 1e7).fatigue_data
assert fd.fatigue_limit == 0.0
assert fd.finite_infinite_transition == 0.0


def test_woehler_elementary():
Expand Down Expand Up @@ -287,7 +287,7 @@ def test_woehler_elementary_no_load_level_in_finite_region():
pd.testing.assert_series_equal(wc, expected)


def test_woehler_elementary_set_fatigue_limit_low():
def test_woehler_elementary_set_finite_infinite_transition_low():
expected = pd.Series({
'SD': 350.0,
'k_1': 9.88,
Expand All @@ -296,12 +296,12 @@ def test_woehler_elementary_set_fatigue_limit_low():
'TS': 1.21,
'failure_probability': 0.5
}).sort_index()
fd = woehler.determine_fractures(data, 1e7).sort_index().fatigue_data.set_fatigue_limit(fatigue_limit=350.)
fd = woehler.determine_fractures(data, 1e7).sort_index().fatigue_data.set_finite_infinite_transition(finite_infinite_transition=350.)
wc = woehler.Elementary(fd).analyze().sort_index()
pd.testing.assert_series_equal(wc, expected, rtol=1e-1)


def test_woehler_elementary_set_fatigue_limit_high():
def test_woehler_elementary_set_finite_infinite_transition_high():
expected = pd.Series({
'SD': 376.,
'k_1': 7.07,
Expand All @@ -310,7 +310,7 @@ def test_woehler_elementary_set_fatigue_limit_high():
'TS': 1.22,
'failure_probability': 0.5
}).sort_index()
fd = woehler.determine_fractures(data, 1e7).sort_index().fatigue_data.set_fatigue_limit(fatigue_limit=376.)
fd = woehler.determine_fractures(data, 1e7).sort_index().fatigue_data.set_finite_infinite_transition(finite_infinite_transition=376.)
wc = woehler.Elementary(fd).analyze().sort_index()
pd.testing.assert_series_equal(wc, expected, rtol=1e-1)

Expand All @@ -330,7 +330,7 @@ def test_woehler_probit():
pd.testing.assert_series_equal(wc, expected, rtol=1e-1)


def test_woehler_probit_set_fatigue_limit():
def test_woehler_probit_set_finite_infinite_transition():
expected = pd.Series({
'SD': 340.4,
'TS': 1.21,
Expand All @@ -340,7 +340,7 @@ def test_woehler_probit_set_fatigue_limit():
'failure_probability': 0.5
}).sort_index()

fd = woehler.determine_fractures(data, 1e7).sort_index().fatigue_data.set_fatigue_limit(fatigue_limit=376.)
fd = woehler.determine_fractures(data, 1e7).sort_index().fatigue_data.set_finite_infinite_transition(finite_infinite_transition=376.)
wc = woehler.Probit(fd).analyze().sort_index()
pd.testing.assert_series_equal(wc, expected, rtol=1e-1)

Expand Down Expand Up @@ -398,7 +398,7 @@ def test_woehler_probit_no_finite_zone_data():
}).sort_index()
data_no_finite_zone = data[data['load']<376.].copy(deep=True)

fd = woehler.determine_fractures(data_no_finite_zone, 1e7).sort_index().fatigue_data.set_fatigue_limit(fatigue_limit=376.)
fd = woehler.determine_fractures(data_no_finite_zone, 1e7).sort_index().fatigue_data.set_finite_infinite_transition(finite_infinite_transition=376.)
with pytest.warns(UserWarning, match=r"Need at least two different load levels in the finite zone to do a Wöhler slope analysis."):
wc = woehler.Probit(fd).analyze().sort_index()
pd.testing.assert_series_equal(wc, expected, rtol=1e-1)
Expand All @@ -419,7 +419,7 @@ def test_woehler_max_likelihood_inf_limit():
pd.testing.assert_series_equal(wc, expected, rtol=1e-1)


def test_woehler_max_likelihood_inf_limit_set_fatigue_limit():
def test_woehler_max_likelihood_inf_limit_set_finite_infinite_transition():
expected = pd.Series({
'SD': 333.6,
'TS': 1.22,
Expand All @@ -429,7 +429,7 @@ def test_woehler_max_likelihood_inf_limit_set_fatigue_limit():
'failure_probability': 0.5
}).sort_index()

fd = woehler.determine_fractures(data, 1e7).fatigue_data.set_fatigue_limit(fatigue_limit=376.)
fd = woehler.determine_fractures(data, 1e7).fatigue_data.set_finite_infinite_transition(finite_infinite_transition=376.)
wc = woehler.MaxLikeInf(fd).analyze().sort_index()
pd.testing.assert_series_equal(wc, expected, rtol=1e-1)

Expand All @@ -445,7 +445,7 @@ def test_woehler_max_likelihood_inf_limit_no_finite_zone_data():
}).sort_index()
data_no_finite_zone = data[data['load']<376.].copy(deep=True)

fd = woehler.determine_fractures(data_no_finite_zone, 1e7).sort_index().fatigue_data.set_fatigue_limit(fatigue_limit=376.)
fd = woehler.determine_fractures(data_no_finite_zone, 1e7).sort_index().fatigue_data.set_finite_infinite_transition(finite_infinite_transition=376.)
with pytest.warns(UserWarning, match=r"Need at least two different load levels in the finite zone to do a Wöhler slope analysis."):
wc = woehler.MaxLikeInf(fd).analyze().sort_index()
pd.testing.assert_series_equal(wc, expected, rtol=1e-1)
Expand Down

0 comments on commit 072b316

Please sign in to comment.