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
WARNING (pytensor.tensor.blas): Using NumPy C-API based implementation for BLAS functions.
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_1
* the cycle number of the endurance limit ND
* the load level of the endurance limit SD
* the scatter line cycle (lifetime) direction TN
* the 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 fatigue_limit
, distinguishing between runouts
and fractures
and between infinite_zone
and finite_zone
.
[5]:
fatigue_data = df.fatigue_data
fatigue_data.fatigue_limit
[5]:
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.fatigue_limit]*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.fatigue_limit]*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 8.626165
ND 898426.345672
SD 308.909475
TN 12.059468
TS 1.334610
failure_probability 0.500000
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 8.626165e+00
ND 1.199879e+06
SD 2.987202e+02
TN 1.205947e+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 8.626165e+00
ND 1.326971e+06
SD 2.952540e+02
TN 1.205947e+01
TS 1.106812e+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 8.626165e+00
ND 1.326971e+06
SD 2.952540e+02
TN 9.862007e+00
TS 1.106811e+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 296.971820
TN 10.201474
TS 1.114568
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
[ ]: