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
[ ]: