Wöhler analyzing functions
Developed by Mustapha Kassem in scope of a master thesis at TU München
[1]:
import copy
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import pylife.materialdata.woehler as woehler
from pylife.materiallaws import WoehlerCurve
The Wöhler analysis module takes fatigue data, i.e., values of the form cycles load fracture that have been measured by a fatigue testing lab and analyzes it to return the parameters of a Wöhler curve.
These are:
the slope
k_1the cycle number of the endurance limit
NDthe load level of the endurance limit
SDthe scatter line cycle (lifetime) direction
TNthe scatter in load direction
TS
We will see several methods to perform the analysis below.
Data import
Data is made up of two columns:
The first column is made up of the load values
The scond column is made up of the load-cycle values
[2]:
file_name = 'data/woehler/fatigue-data-plain.csv'
[3]:
df = pd.read_csv(file_name, sep='\t')
df.columns=['load', 'cycles']
px.scatter(df, x='cycles', y='load', log_x=True, log_y=True)
Guessing the fractures
In this case there is no information if the specimen was a runout or a fracture. We can guess it based on the value for the load_cycle_limit which defaults to 1e7.
[4]:
load_cycle_limit = None # or for example 1e7
df = woehler.determine_fractures(df, load_cycle_limit)
px.scatter(df, x='cycles', y='load', color='fracture', log_x=True, log_y=True)
Analysis
General preparations
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.
[5]:
fatigue_data = df.fatigue_data
fatigue_data.finite_infinite_transition
[5]:
np.float64(308.909475)
We can distinguish between the finite and infinite zones.
[6]:
infinite_zone = fatigue_data.infinite_zone
finite_zone = fatigue_data.finite_zone
go.Figure([
go.Scatter(x=finite_zone.cycles, y=finite_zone.load, mode='markers', name='finite'),
go.Scatter(x=infinite_zone.cycles, y=infinite_zone.load, mode='markers', name='infinite'),
go.Scatter(x=[df.cycles.min(), df.cycles.max()], y=[fatigue_data.finite_infinite_transition]*2, mode='lines', name='fatigue limit')
]).update_xaxes(type='log').update_yaxes(type='log').update_layout(xaxis_title='Cycles', yaxis_title='Load')
We can separate fractures from runouts.
[7]:
fractures = fatigue_data.fractures
runouts = fatigue_data.runouts
fig = go.Figure([
go.Scatter(x=fractures.cycles, y=fractures.load, mode='markers', name='fractures'),
go.Scatter(x=runouts.cycles, y=runouts.load, mode='markers', name='runouts'),
go.Scatter(x=[df.cycles.min(), df.cycles.max()], y=[fatigue_data.finite_infinite_transition]*2, mode='lines', name='fatigue limit')
]).update_xaxes(type='log').update_yaxes(type='log').update_layout(xaxis_title='Cycles', yaxis_title='Load')
fig
Elementary analysis
The Elementary analysis is the first step of the analysis. It determines the slope k_1 in the finite region and the scatter in cycle region TN using the pearl chain method.
The endurance limit in load direction SN is guessed from the tentative fatigue limit. The scatter in load direction TS is transformed from TN using the slope k_1.
[8]:
elementary_result = woehler.Elementary(fatigue_data).analyze()
elementary_result
[8]:
k_1 1.138923e+01
ND 1.002814e+06
SD 3.089095e+02
TN 1.076244e+01
TS 1.231981e+00
failure_probability 5.000000e-01
dtype: float64
[9]:
wc = elementary_result.woehler
cycles = np.logspace(np.log10(df.cycles.min()), np.log10(df.cycles.max()), 100)
elementary_fig = copy.deepcopy(fig)
elementary_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles), mode='lines', name='Elementary 50%')
elementary_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles, failure_probability=0.1),
mode='lines', name='Elementary 10%')
elementary_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles, failure_probability=0.9),
mode='lines', name='Elementary 90%')
elementary_fig
Probit
[10]:
probit_result = woehler.Probit(fatigue_data).analyze()
probit_result
[10]:
k_1 1.138923e+01
ND 1.469346e+06
SD 2.987202e+02
TN 1.076244e+01
TS 1.110052e+00
failure_probability 5.000000e-01
dtype: float64
[11]:
wc = probit_result.woehler
cycles = np.logspace(np.log10(df.cycles.min()), np.log10(df.cycles.max()), 100)
probit_fig = copy.deepcopy(fig)
probit_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles), mode='lines', name='Probit 50%')
probit_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles, failure_probability=0.1), mode='lines', name='Probit 10%')
probit_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles, failure_probability=0.9), mode='lines', name='Probit 90%')
probit_fig
Maximum Likelihood Infinite
The Maximum Likelihood Infinite method takes the parameters for the finite regime fitted by Elementary (k_1, TN) and fits the ones for the infinite regime (SD, ND, TS), hence the name.
[12]:
maxlike_inf_result = woehler.MaxLikeInf(fatigue_data).analyze()
maxlike_inf_result
[12]:
k_1 1.138923e+01
ND 1.718864e+06
SD 2.946345e+02
TN 1.076244e+01
TS 1.088856e+00
failure_probability 5.000000e-01
dtype: float64
[13]:
wc = maxlike_inf_result.woehler
cycles = np.logspace(np.log10(df.cycles.min()), np.log10(df.cycles.max()), 100)
maxlike_inf_fig = copy.deepcopy(fig)
maxlike_inf_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles), mode='lines', name='MaxLikeInf 50%')
maxlike_inf_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles, failure_probability=0.1), mode='lines', name='MaxLikeInf 10%')
maxlike_inf_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles, failure_probability=0.9), mode='lines', name='MaxLikeInf 90%')
maxlike_inf_fig
Maximum Likelihood Full
The Maximum Likelihood Full method just takes the elementary result as starting values but fits all the parameters.
[14]:
maxlike_full_result = woehler.MaxLikeFull(fatigue_data).analyze()
maxlike_full_result
[14]:
k_1 1.283005e+01
ND 1.994922e+06
SD 2.946346e+02
TN 9.984331e+00
TS 1.088856e+00
failure_probability 5.000000e-01
dtype: float64
[15]:
wc = maxlike_full_result.woehler
cycles = np.logspace(np.log10(df.cycles.min()), np.log10(df.cycles.max()), 100)
maxlike_full_fig = copy.deepcopy(fig)
maxlike_full_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles), mode='lines', name='MaxLikeFull 50%')
maxlike_full_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles, failure_probability=0.1), mode='lines', name='MaxLikeFull 10%')
maxlike_full_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles, failure_probability=0.9), mode='lines', name='MaxLikeFull 90%')
maxlike_full_fig
Maximum Likelihood Full with fixed parameters
Sometimes, it is desirable to pin one or more parameters of the Wöhler curve to a predefined value based on assumptions on the Wöhler curve. You can achieve that by handing the fixed parameters to the MaxLikeFull object using the fixed_parameters argument.
[16]:
fixed_parameters = {
'k_1': 7.,
'ND': 1e6
}
maxlike_fixed_result = woehler.MaxLikeFull(fatigue_data).analyze(fixed_parameters=fixed_parameters)
maxlike_fixed_result
[16]:
k_1 7.000000
ND 1000000.000000
SD 295.751917
TN 11.107539
TS 1.088086
failure_probability 0.500000
dtype: float64
[17]:
wc = maxlike_fixed_result.woehler
cycles = np.logspace(np.log10(df.cycles.min()), np.log10(df.cycles.max()), 100)
maxlike_fixed_fig = copy.deepcopy(fig)
maxlike_fixed_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles), mode='lines', name='MaxLikefixed 50%')
maxlike_fixed_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles, failure_probability=0.1), mode='lines', name='MaxLikeFixed 10%')
maxlike_fixed_fig.add_scatter(x=cycles, y=wc.basquin_load(cycles, failure_probability=0.9), mode='lines', name='MaxLikeFixed 90%')
maxlike_fixed_fig