FKM Nonlinear - full algorithm step-through
The algorithm follows the document “RICHTLINIE NICHTLINEAR / Rechnerischer Festigkeitsnachweis unter expliziter Erfassung nichtlinearen Werkstoffverformungsverhaltens / Für Bauteile aus Stahl, Stahlguss und Aluminiumknetlegierungen / 1.Auflage, 2019”
The used values are according to “Akademisches Beispiel”, chapter 2.7.1.
Note that this notebook is for demonstration of the algorithm, for actual assessment use other notebooks, e.g.,fkm_nonlinear
Python module imports
[1]:
# standard modules
import pandas as pd
import numpy as np
import itertools
import matplotlib.pyplot as plt
import copy
# pylife
import pylife
import pylife.vmap
import pylife.stress.equistress
import pylife.strength.fkm_load_distribution
import pylife.strength.fkm_nonlinear
import pylife.strength.fkm_nonlinear.damage_calculator
import pylife.strength.damage_parameter
import pylife.strength.woehler_fkm_nonlinear
import pylife.materiallaws
import pylife.stress.rainflow
import pylife.stress.rainflow.recorders
import pylife.stress.rainflow.fkm_nonlinear
import pylife.materiallaws.notch_approximation_law
import pylife.materiallaws.notch_approximation_law_seegerbeste
import pylife.strength.fkm_nonlinear.parameter_calculations as parameter_calculations
2.6.1 Collect all input data
Material parameters
[2]:
assessment_parameters = pd.Series({
'MatGroupFKM': 'Steel', # [Steel, SteelCast, Al_wrought] material group
'FinishingFKM': 'none', # type of surface finisihing
'R_m': 600, # [MPa] ultimate tensile strength (de: Zugfestigkeit)
#'K_RP': 1, # [-] surface roughness factor, set to 1 for polished surfaces or determine from the diagrams below
'R_z': 250, # [um] average roughness (de: mittlere Rauheit), only required if K_RP is not specified directly
'P_A': 7.2e-5, # [-] failure probability, set to 0.5 to not consider additional safety (de: auszulegende Ausfallwahrscheinlichkeit)
# beta: 0.5, # damage index, specify this as an alternative to P_A
'P_L': 2.5, # [%] (one of 2.5%, 50%) (de: Auftretenswahrscheinlilchkeit der Lastfolge)
'c': 3, # [MPa/N] (de: Übertragungsfaktor Vergleichsspannung zu Referenzlast im Nachweispunkt, c = sigma_I / L_REF)
'A_sigma': 339.4, # [mm^2] (de: Hochbeanspruchte Oberfläche des Bauteils)
'A_ref': 500, # [mm^2] (de: Hochbeanspruchte Oberfläche eines Referenzvolumens), usually set to 500
'G': 2/15, # [mm^-1] (de: bezogener Spannungsgradient)
's_L': 10, # [MPa] standard deviation of Gaussian distribution
'K_p': 3.5, # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1)
'r': 15, # [mm] radius (?)
})
Notes on the parameters
The highly loaded surface \(A_\sigma\)
The highly loaded surface parameter \(A_\sigma\) is needed when the expected fracture starts at the component’s surface. It can be computed using the algorithm “SPIEL” as described in the FKM guideline nonlinear. (cf. chapter 3.1.2). For simple common geometries like notched plates and shafts, table 2.6 of the FKM guideline nonlinear presents formulas for \(A_\sigma\).
The relative stress gradient \(G\)
The relative stress gradient \(G\) is usually determined from finite element calculations, but can also be estimated by heuristic, cf. eq. (2.5-34) in the FKM guideline nonlinear.
The surface roughness factor \(K_{R,P}\)
The factor of surface roughness can either be manually specified in the assessment_parameters
variable. If it is omitted, the FKM formula is used to estimate it from ultimate tensile strength, \(R_m\), and surface roughness, \(R_z\). For polished components, the factor should be set to \(K_{R,P}=1\). For other materials, it can be retrieved from the diagrams in Fig. 2.8 in the FKM guideline nonlinear.
The safety factor of the component and the failure probability \(P_A\)
For calculations that should be compared with cyclic experiments, set the failure probability to \(P_A=50\%\). For added safety in assessment concepts, the FKM guideline nonlinear suggests the following failure probabilities P_A
: * Redundant components: \(P_A=2.3\cdot 10^{-1}\), \(P_A=10^{-3}\), and \(P_A=7.2\cdot 10^{-5}\) for moderate, severe, and very severe failure consequences, respectively. * Non-redundant components: \(P_A=10^{-3}\), \(P_A=7.2\cdot 10^{-5}\),
and \(P_A=10^{-5}\) for moderate, severe, and very severe failure consequences, respectively.
You can additionally specify the parameter beta
(de: Zuverlässigkeitsindex) for the material, which then will be used to compute the material safety factor \(\gamma_M\). For \(P_A=50\%\) set gamma_M=1
.
Load sequence
[3]:
load_sequence = pd.Series([160, -200, 250, -250, 230, 0, 260]) # [N]
fig = plt.figure(figsize=(5,3))
plt.plot(load_sequence, "o-")
plt.ylabel("F")
[3]:
Text(0, 0.5, 'F')
There are three possible methods to consider the statistical distribution of the load: * A normal distribution with given standard deviation, \(s_L\) * A logarithmic-normal distribution with given standard deviation \(LSD_s\) * An unknown distribution, instead use a constant factor \(P_L = 2.5\%\)
[4]:
# Choose one of the following three lines:
# FKMLoadDistributionNormal, uses assessment_parameters.s_L, assessment_parameters.P_L, assessment_parameters.P_A
scaled_load_sequence = load_sequence.fkm_safety_normal_from_stddev.scaled_load_sequence(assessment_parameters)
gamma_L = load_sequence.fkm_safety_normal_from_stddev.gamma_L(assessment_parameters)
print(f"scaling factor gamma_L: {gamma_L:.5f}")
# FKMLoadDistributionLognormal, uses assessment_parameters.LSD_s, assessment_parameters.P_L, assessment_parameters.P_A
assessment_parameters["LSD_s"] = 1
#scaled_load_sequence = load_sequence.fkm_safety_lognormal_from_stddev.scaled_load_sequence(assessment_parameters)
# FKMLoadDistributionBlanket, uses input_parameters.P_L
#scaled_load_sequence = load_sequence.fkm_safety_blanket.scaled_load_sequence(assessment_parameters)
# 79
scaling factor gamma_L: 1.02538
[5]:
# scale load sequence by reference load
#c = 1/L_REF
c = assessment_parameters.c
scaled_load_sequence = c * gamma_L * load_sequence
display(scaled_load_sequence)
# plot load sequence
plt.rcParams.update({'font.size': 22})
plt.plot(range(len(scaled_load_sequence)), load_sequence, "o-", c="g", lw=3)
plt.grid()
plt.ylabel("$\sigma_V$ [MPa]")
0 492.184615
1 -615.230769
2 769.038462
3 -769.038462
4 707.515385
5 0.000000
6 799.800000
dtype: float64
[5]:
Text(0, 0.5, '$\\sigma_V$ [MPa]')
2.6.3 Determine cyclic material parameters according to Ramberg-Osgood
This can be done by experiments or by the formulas given in the guideline. This Jupyter notebook uses the implemented formulas. So far, we have specified the following quantities.
[6]:
assessment_parameters
[6]:
MatGroupFKM Steel
FinishingFKM none
R_m 600
R_z 250
P_A 0.000072
P_L 2.5
c 3
A_sigma 339.4
A_ref 500
G 0.133333
s_L 10
K_p 3.5
r 15
max_load_independently_for_nodes False
LSD_s 1
dtype: object
[7]:
assessment_parameters = pylife.strength.fkm_nonlinear.parameter_calculations.calculate_cyclic_assessment_parameters(assessment_parameters)
2.6.4 Estimate material SN-curve (Werkstoff-Wöhlerlinie)
2.5.5.1 Determine from experiments
Conduct strain-driven experiments with \(R_\varepsilon = -1\). Record values for \(\sigma_a, \varepsilon_{a,\text{ges}}\) and \(N_\text{Werkstoff}\).
Compute \(P_{RAM}=\sqrt{\sigma_a\cdot \varepsilon_{a,\text{ges}} \cdot E}\) for every single experiment.
Use the maximum-likelihood method to infer the parameters \(d_1, d_2, P_{RAM,Z,WS}\). For details, refer to the FKM nonlinear document, Sec. 2.5.5.1, p. 40.
2.5.5.2 Estimate from formulas
Alternatively, estimate the material SN-curve from the ultimate tensile strength \(R_m\).
[8]:
# calculate the parameters for the material woehler curve
# (for both P_RAM and P_RAJ, the variable names do not interfere)
assessment_parameters = parameter_calculations.calculate_material_woehler_parameters_P_RAM(assessment_parameters)
assessment_parameters = parameter_calculations.calculate_material_woehler_parameters_P_RAJ(assessment_parameters)
2.6.5 Estimate component SN-curve (Bauteil-Wöhlerlinie)
Consideration of non-local effects on the lifetime of the component.
2.6.5.1, 2.5.6.1 Size and geometry factor \(n_P\), Spannungsgradient \(G\), \(A_\sigma\)
Compute the factor for non-local influences, \(n_P = n_{bm}(R_m, G) \cdot n_{st}(A_\sigma)\), where \(n_{bm}\) is the fracture mechanics factor (de: bruchmechanische Stützzahl) and \(n_{st}\) is the statistic factor (de: statistische Stützzahl). The factors depend on the stress gradient, \(G\), and the highly loaded surface, \(A_\sigma\), respectively.
[9]:
assessment_parameters = parameter_calculations.calculate_nonlocal_parameters(assessment_parameters)
2.6.5.2, 2.5.6.2 Roughness factor \(K_{R,P}\)
Compute the influence factor of the roughness, \(K_{R,P}\), which is estimated based on the ultimate tensile strength, \(R_m\), and the surface roughness, \(R_z\). See also the diagrams “Abbildung 2.21” show above.
[10]:
assessment_parameters = parameter_calculations.calculate_roughness_parameter(assessment_parameters)
[11]:
assessment_parameters = parameter_calculations.calculate_failure_probability_factor_P_RAM(assessment_parameters)
assessment_parameters = parameter_calculations.calculate_failure_probability_factor_P_RAJ(assessment_parameters)
2.6.5.3, 2.5.6.3 safety factor \(\gamma_M\)
Compute the factors to derive the component Wöhler curve from the material Wöhler curve. The factors for both P_RAM and P_RAJ are computed.
[12]:
assessment_parameters = parameter_calculations.calculate_component_woehler_parameters_P_RAM(assessment_parameters)
assessment_parameters = parameter_calculations.calculate_component_woehler_parameters_P_RAJ(assessment_parameters)
2.6.5.4, 2.5.6 formula for component SN-curve
Woehler curve for P_RAM
[13]:
component_woehler_curve_parameters = assessment_parameters[["P_RAM_Z", "P_RAM_D", "d_1", "d_2"]]
component_woehler_curve_P_RAM = component_woehler_curve_parameters.woehler_P_RAM
Wöhler curve for P_RAJ
[14]:
component_woehler_curve_parameters = assessment_parameters[["P_RAJ_Z", "P_RAJ_D_0", "d_RAJ"]]
component_woehler_curve_P_RAJ = component_woehler_curve_parameters.woehler_P_RAJ
[15]:
assessment_parameters
[15]:
MatGroupFKM Steel
FinishingFKM none
R_m 600
R_z 250
P_A 0.000072
P_L 2.5
c 3
A_sigma 339.4
A_ref 500
G 0.133333
s_L 10
K_p 3.5
r 15
max_load_independently_for_nodes False
LSD_s 1
n_prime 0.187
E 206000.0
K_prime 1184.470952
notes P_A not 0.5 (but 7.2e-05): scale P_RAM woehler...
P_RAM_Z_WS 606.82453
P_RAM_D_WS 209.397432
d_1 -0.302
d_2 -0.197
P_RAJ_Z_WS 768.807373
P_RAJ_D_WS 0.262811
d_RAJ -0.63
n_st 1.012998
n_bm_ 0.719783
n_bm 1.0
n_P 1.012998
K_RP 0.8531
beta 3.801195
gamma_M_RAM 1.211369
gamma_M_RAJ 1.449934
f_RAM 1.401741
P_RAM_Z 432.907751
P_RAM_D 149.383828
f_RAJ 1.94147
P_RAJ_Z 395.99234
P_RAJ_D_0 0.135367
P_RAJ_D 0.135367
dtype: object
[16]:
1/assessment_parameters.d_1, 1/assessment_parameters.d_2, 1/assessment_parameters.d_RAJ
[16]:
(-3.3112582781456954, -5.0761421319796955, -1.5873015873015872)
[17]:
assessment_parameters[["P_RAM_Z_WS", "P_RAM_D_WS", "d_1", "d_2"]]
[17]:
P_RAM_Z_WS 606.82453
P_RAM_D_WS 209.397432
d_1 -0.302
d_2 -0.197
dtype: object
[18]:
# plot P_RAM material Woehler curve
n_list = np.logspace(2, 7, num=101, endpoint=True, base=10.0)
plt.rcParams.update({'font.size': 24})
fig,ax = plt.subplots(figsize=(8,6))
# for core
material_woehler_curve_parameters = assessment_parameters[["P_RAM_Z_WS", "P_RAM_D_WS", "d_1", "d_2"]]
material_woehler_curve_parameters["P_RAM_Z"] = assessment_parameters["P_RAM_Z_WS"]
material_woehler_curve_parameters["P_RAM_D"] = assessment_parameters["P_RAM_D_WS"]
material_woehler_curve_P_RAM = pylife.strength.woehler_fkm_nonlinear\
.WoehlerCurvePRAM(material_woehler_curve_parameters)
line = plt.plot(n_list, material_woehler_curve_P_RAM.calc_P_RAM(n_list), "-", lw=4)
plt.plot(1e3, material_woehler_curve_P_RAM.P_RAM_Z,
"o", color=line[0].get_color(), markersize=10)
N_D = material_woehler_curve_P_RAM.fatigue_life_limit
plt.plot(N_D, material_woehler_curve_P_RAM.fatigue_strength_limit,
"o", color=line[0].get_color(), markersize=10)
plt.annotate(f"$N_D$ = {N_D:.1e}", (N_D, material_woehler_curve_P_RAM.fatigue_strength_limit),
textcoords="offset points", xytext=(0,10), color=line[0].get_color())
plt.xscale('log')
plt.yscale('log')
plt.xlabel('N')
plt.ylabel('P_RAM')
plt.title("P_RAM material Woehler curve")
plt.grid(which='both')
[19]:
# plot P_RAM component Woehler curve
n_list = np.logspace(2, 7, num=101, endpoint=True, base=10.0)
plt.rcParams.update({'font.size': 24})
fig,ax = plt.subplots(figsize=(8,6))
# for core
component_woehler_curve_parameters = assessment_parameters[["P_RAM_Z", "P_RAM_D", "d_1", "d_2"]]
component_woehler_curve_P_RAM = pylife.strength.woehler_fkm_nonlinear\
.WoehlerCurvePRAM(component_woehler_curve_parameters)
# material
line = plt.plot(n_list, material_woehler_curve_P_RAM.calc_P_RAM(n_list), "-", lw=4, label="material")
plt.plot(1e3, material_woehler_curve_P_RAM.P_RAM_Z,
"o", color=line[0].get_color(), markersize=10)
N_D = material_woehler_curve_P_RAM.fatigue_life_limit
plt.plot(N_D, material_woehler_curve_P_RAM.fatigue_strength_limit,
"o", color=line[0].get_color(), markersize=10)
plt.annotate(f"$N_D$ = {N_D:.1e}", (N_D, material_woehler_curve_P_RAM.fatigue_strength_limit),
textcoords="offset points", xytext=(0,10), color=line[0].get_color())
# component
line = plt.plot(n_list, component_woehler_curve_P_RAM.calc_P_RAM(n_list), "-", lw=4, label="component")
plt.plot(1e3, component_woehler_curve_P_RAM.P_RAM_Z,
"o", color=line[0].get_color(), markersize=10)
N_D = component_woehler_curve_P_RAM.fatigue_life_limit
plt.plot(N_D, component_woehler_curve_P_RAM.fatigue_strength_limit,
"o", color=line[0].get_color(), markersize=10)
plt.annotate(f"$N_D$ = {N_D:.1e}", (N_D, component_woehler_curve_P_RAM.fatigue_strength_limit),
textcoords="offset points", xytext=(0,10), color=line[0].get_color())
plt.legend(loc="best")
plt.xscale('log')
plt.yscale('log')
plt.xlabel('N')
plt.ylabel('P_RAM')
plt.title("P_RAM component Woehler curve")
plt.grid(which='both')
2.6.7, 2.5.8 Perform HCM rainflow counting
2.6.6, 2.5.7 Compute stresses and strains, classification, PFAD and AST matrices
2.6.7, 2.5.8.1 HCM algorithm, output (S_a, S_m and epsilon_a) for every hysteresis
[20]:
# initialize notch approximation law
E, K_prime, n_prime, K_p = assessment_parameters[["E", "K_prime", "n_prime", "K_p"]]
extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K_prime, n_prime, K_p)
load_sequence_list = scaled_load_sequence
print(load_sequence_list)
# wrap the notch approximation law by a binning class, which precomputes the values
maximum_absolute_load = max(np.abs(load_sequence_list))
print(f"maximum_absolute_load: {maximum_absolute_load}")
extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned(
extended_neuber, maximum_absolute_load, 100)
# create recorder object
recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder()
# create detector object
detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector(
recorder=recorder, notch_approximation_law=extended_neuber_binned)
# perform HCM algorithm, first run
detector.process(load_sequence_list, flush=False)
detector_1st = copy.deepcopy(detector)
# perform HCM algorithm, second run
detector.process(load_sequence_list, flush=True)
0 492.184615
1 -615.230769
2 769.038462
3 -769.038462
4 707.515385
5 0.000000
6 799.800000
dtype: float64
maximum_absolute_load: 799.8000000000002
[20]:
<pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector at 0x7f2ae3d0c350>
[21]:
# plot resulting stress-strain curve
sampling_parameter = 50 # choose larger for smoother plot
plotting_data = detector_1st.interpolated_stress_strain_data(sampling_parameter)
strain_values_primary = plotting_data["strain_values_primary"]
stress_values_primary = plotting_data["stress_values_primary"]
hysteresis_index_primary = plotting_data["hysteresis_index_primary"]
strain_values_secondary = plotting_data["strain_values_secondary"]
stress_values_secondary = plotting_data["stress_values_secondary"]
hysteresis_index_secondary = plotting_data["hysteresis_index_secondary"]
fig, axes = plt.subplots(1, 2, figsize=(12,6))
plt.subplots_adjust(wspace=0.4,
hspace=0.4)
# load-time diagram
import matplotlib
matplotlib.rcParams.update({'font.size': 14})
axes[0].plot(load_sequence_list, "o-", lw=3)
axes[0].grid()
axes[0].set_xlabel("t [s]")
axes[0].set_ylabel("L [N]")
axes[0].set_title("Scaled load sequence")
# stress-strain diagram
axes[1].plot(0,0,"ok")
axes[1].plot(strain_values_primary, stress_values_primary, "k-", lw=2)
axes[1].plot(strain_values_secondary, stress_values_secondary, "k--", lw=1)
axes[1].grid()
axes[1].set_xlabel("$\epsilon$")
axes[1].set_ylabel("$\sigma$ [MPa]")
axes[1].set_title("Material response")
# stress-strain diagram
plt.rcParams.update({'font.size': 22})
fig, ax = plt.subplots(1, 1, figsize=(4,4))
ax.plot(0,0,"ok")
ax.plot([e*1e2 for e in strain_values_primary], stress_values_primary, "k-", lw=3)
ax.plot([e*1e2 for e in strain_values_secondary], stress_values_secondary, "k--", lw=1)
ax.grid()
ax.set_xlabel("$\epsilon$ [%]")
ax.set_ylabel("$\sigma$ [MPa]")
[21]:
Text(0, 0.5, '$\\sigma$ [MPa]')
2.6.8, 2.5.9 Compute \(P_{RAM}\) and damage sum
2.6.8.1, 2.5.9 Mittelspannungsempfindlichkeit
[22]:
recorder.collective
[22]:
loads_min | loads_max | S_min | S_max | R | epsilon_min | epsilon_max | S_a | S_m | epsilon_a | epsilon_m | epsilon_min_LF | epsilon_max_LF | is_closed_hysteresis | is_zero_mean_stress_and_strain | run_index | debug_output | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
hysteresis_index | assessment_point_index | |||||||||||||||||
0 | 0 | -615.230769 | 615.230769 | -397.356662 | 397.356662 | -1.000000 | -0.004836 | 0.004836 | 397.356662 | 0.000000 | 0.004836 | 0.000000 | -0.004836 | 0.000000 | False | True | 1 | |
1 | 0 | 0.000000 | 707.515385 | -170.559791 | 425.547755 | -0.400801 | 0.002082 | 0.006225 | 298.053773 | 127.493982 | 0.002071 | 0.004154 | -0.007225 | 0.007375 | True | False | 2 | |
2 | 0 | -769.038462 | 769.038462 | -441.153888 | 443.371282 | -0.994999 | -0.007225 | 0.007375 | 442.262585 | 1.108697 | 0.007300 | 0.000075 | -0.007225 | 0.007375 | True | False | 2 | |
3 | 0 | -615.230769 | 769.038462 | -398.658704 | 443.169842 | -0.899562 | -0.004541 | 0.007456 | 420.914273 | 22.255569 | 0.005998 | 0.001458 | -0.007225 | 0.007839 | True | False | 2 | |
4 | 0 | 0.000000 | 707.515385 | -172.783251 | 423.324295 | -0.408158 | 0.001939 | 0.006082 | 298.053773 | 125.270522 | 0.002071 | 0.004011 | -0.007368 | 0.007839 | True | False | 2 | |
5 | 0 | -769.038462 | 799.800000 | -443.377349 | 450.007178 | -0.985267 | -0.007368 | 0.007839 | 446.692263 | 3.314914 | 0.007604 | 0.000235 | -0.007368 | 0.007839 | True | False | 2 |
[23]:
# define damage parameter
damage_parameter = pylife.strength.damage_parameter.P_RAM(recorder.collective, assessment_parameters)
#display(damage_parameter.collective)
# compute the effect of the damage parameter with the woehler curve
damage_calculator = pylife.strength.fkm_nonlinear.damage_calculator\
.DamageCalculatorPRAM(damage_parameter.collective, component_woehler_curve_P_RAM)
display(damage_calculator.collective)
loads_min | loads_max | S_min | S_max | R | epsilon_min | epsilon_max | S_a | S_m | epsilon_a | ... | epsilon_min_LF | epsilon_max_LF | is_closed_hysteresis | is_zero_mean_stress_and_strain | run_index | debug_output | P_RAM | N | D | cumulative_damage | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
hysteresis_index | assessment_point_index | |||||||||||||||||||||
0 | 0 | -615.230769 | 615.230769 | -397.356662 | 397.356662 | -1.000000 | -0.004836 | 0.004836 | 397.356662 | 0.000000 | 0.004836 | ... | -0.004836 | 0.000000 | False | True | 1 | 629.144128 | 290.002252 | 0.001724 | 0.001724 | |
1 | 0 | 0.000000 | 707.515385 | -170.559791 | 425.547755 | -0.400801 | 0.002082 | 0.006225 | 298.053773 | 127.493982 | 0.002071 | ... | -0.007225 | 0.007375 | True | False | 2 | 373.910895 | 2103.686027 | 0.000475 | 0.002199 | |
2 | 0 | -769.038462 | 769.038462 | -441.153888 | 443.371282 | -0.994999 | -0.007225 | 0.007375 | 442.262585 | 1.108697 | 0.007300 | ... | -0.007225 | 0.007375 | True | False | 2 | 815.754848 | 122.704341 | 0.008150 | 0.010349 | |
3 | 0 | -615.230769 | 769.038462 | -398.658704 | 443.169842 | -0.899562 | -0.004541 | 0.007456 | 420.914273 | 22.255569 | 0.005998 | ... | -0.007225 | 0.007839 | True | False | 2 | 725.599898 | 180.833395 | 0.005530 | 0.015879 | |
4 | 0 | 0.000000 | 707.515385 | -172.783251 | 423.324295 | -0.408158 | 0.001939 | 0.006082 | 298.053773 | 125.270522 | 0.002071 | ... | -0.007368 | 0.007839 | True | False | 2 | 373.616310 | 2112.119313 | 0.000473 | 0.016353 | |
5 | 0 | -769.038462 | 799.800000 | -443.377349 | 450.007178 | -0.985267 | -0.007368 | 0.007839 | 446.692263 | 3.314914 | 0.007604 | ... | -0.007368 | 0.007839 | True | False | 2 | 837.180202 | 112.610146 | 0.008880 | 0.025233 |
6 rows × 21 columns
2.6.9, 2.5.10 Safety assessment with \(P_{RAM}\)
2.6.9.1, 2.5.10.1 Infinite life assessment (Dauerfestigkeitsnachweis)
[24]:
# Infinite life assessment
is_life_infinite = damage_calculator.is_life_infinite
print(f"Infinite life: {is_life_infinite}")
Infinite life: False
2.6.9.3, 2.5.10.3 Finite life assessment (Betriebsfestigkeitsnachweis)
[25]:
# finite life assessment
lifetime_n_cycles = damage_calculator.lifetime_n_cycles
lifetime_n_times_load_sequence = damage_calculator.lifetime_n_times_load_sequence
print(f"Number of bearable cycles: {lifetime_n_cycles:.0f}")
print(f"Number of bearable load sequences: {lifetime_n_times_load_sequence:.0f}")
Number of bearable cycles: 217
Number of bearable load sequences: 43
Assessment with \(P_{RAJ}\)
Repeat HCM algorithm with Seeger-Beste notch approximation
[26]:
# initialize notch approximation law
E, K_prime, n_prime, K_p = assessment_parameters[["E", "K_prime", "n_prime", "K_p"]]
seeger_beste = pylife.materiallaws.notch_approximation_law_seegerbeste.SeegerBeste(E, K_prime, n_prime, K_p)
load_sequence_list = scaled_load_sequence
print(load_sequence_list)
# wrap the notch approximation law by a binning class, which precomputes the values
maximum_absolute_load = max(np.abs(load_sequence_list))
print(f"maximum_absolute_load: {maximum_absolute_load}")
seeger_beste_binned = pylife.materiallaws.notch_approximation_law.Binned(
seeger_beste, maximum_absolute_load, 100)
# create recorder object
recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder()
# create detector object
detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector(
recorder=recorder, notch_approximation_law=seeger_beste_binned)
# perform HCM algorithm, first run
detector.process(load_sequence_list, flush=False)
# perform HCM algorithm, second run
detector.process(load_sequence_list, flush=True)
0 492.184615
1 -615.230769
2 769.038462
3 -769.038462
4 707.515385
5 0.000000
6 799.800000
dtype: float64
maximum_absolute_load: 799.8000000000002
[26]:
<pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector at 0x7f2ae1999590>
2.9.8, 2.8.9 Compute \(P_{RAJ}\) and damage sum
2.9.9, 2.8.10 Safety assessment with \(P_{RAJ}\)
[27]:
# define damage parameter
damage_parameter = pylife.strength.damage_parameter.P_RAJ(recorder.collective, assessment_parameters,\
component_woehler_curve_P_RAJ)
#display(damage_parameter.collective)
# compute the effect of the damage parameter with the woehler curve
damage_calculator = pylife.strength.fkm_nonlinear.damage_calculator\
.DamageCalculatorPRAJ(damage_parameter.collective, assessment_parameters, component_woehler_curve_P_RAJ)
display(damage_calculator.collective)
# show all columns
pd.set_option('display.max_columns', None)
display(damage_calculator.collective)
loads_min | loads_max | S_min | S_max | R | epsilon_min | epsilon_max | S_a | S_m | epsilon_a | ... | D | S_close | case_name | epsilon_min_alt_SP | epsilon_max_alt_SP | delta_S_eff | delta_epsilon_eff | is_damage_in_current_hysteresis | P_RAJ_D | cumulative_damage | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
hysteresis_index | assessment_point_index | |||||||||||||||||||||
0 | 0 | -615.230769 | 615.230769 | -377.064063 | 377.064063 | -1.000000 | -0.004027 | 0.004027 | 377.064063 | 0.000000 | 0.004027 | ... | 0.000970 | -321.509046 | 2 | -0.004027 | 0.000000 | 698.573109 | 0.006308 | True | 0.135321 | 0.000970 |
1 | 0 | 0.000000 | 707.515385 | -158.985707 | 405.679822 | -0.391899 | 0.001510 | 0.005186 | 282.332765 | 123.347058 | 0.001838 | ... | 0.000156 | -51.318171 | 2 | -0.006046 | 0.006176 | 456.997994 | 0.002520 | True | 0.135313 | 0.001126 |
2 | 0 | -769.038462 | 769.038462 | -421.792044 | 424.086847 | -0.994589 | -0.006046 | 0.006176 | 422.939446 | 1.147402 | 0.006111 | ... | 0.006483 | -388.848190 | 4 | -0.006046 | 0.006176 | 812.935037 | 0.010509 | True | 0.135004 | 0.007609 |
3 | 0 | -615.230769 | 769.038462 | -377.964838 | 423.959578 | -0.891511 | -0.003739 | 0.006255 | 400.962208 | 22.997370 | 0.004997 | ... | 0.003862 | -339.437342 | 2 | -0.006046 | 0.006579 | 763.396920 | 0.008395 | True | 0.134820 | 0.011471 |
4 | 0 | 0.000000 | 707.515385 | -161.282987 | 403.382543 | -0.399826 | 0.001386 | 0.005062 | 282.332765 | 121.049778 | 0.001838 | ... | 0.000155 | -52.697611 | 2 | -0.006170 | 0.006579 | 456.080154 | 0.002512 | True | 0.134812 | 0.011626 |
5 | 0 | -769.038462 | 799.800000 | -424.089324 | 430.963765 | -0.984049 | -0.006170 | 0.006579 | 427.526545 | 3.437221 | 0.006374 | ... | 0.007271 | -393.077075 | 4 | -0.006170 | 0.006579 | 824.040841 | 0.011057 | True | 0.134466 | 0.018897 |
6 rows × 33 columns
loads_min | loads_max | S_min | S_max | R | epsilon_min | epsilon_max | S_a | S_m | epsilon_a | epsilon_m | epsilon_min_LF | epsilon_max_LF | is_closed_hysteresis | is_zero_mean_stress_and_strain | run_index | debug_output | A_m | S_open | epsilon_open_ein | epsilon_open_alt | epsilon_open | P_RAJ | D | S_close | case_name | epsilon_min_alt_SP | epsilon_max_alt_SP | delta_S_eff | delta_epsilon_eff | is_damage_in_current_hysteresis | P_RAJ_D | cumulative_damage | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
hysteresis_index | assessment_point_index | |||||||||||||||||||||||||||||||||
0 | 0 | -615.230769 | 615.230769 | -377.064063 | 377.064063 | -1.000000 | -0.004027 | 0.004027 | 377.064063 | 0.000000 | 0.004027 | 0.000000 | -0.004027 | 0.000000 | False | True | 1 | 0.000000 | -31.571122 | -0.002282 | -0.002282 | -0.002282 | 7.744399 | 0.000970 | -321.509046 | 2 | -0.004027 | 0.000000 | 698.573109 | 0.006308 | True | 0.135321 | 0.000970 | |
1 | 0 | 0.000000 | 707.515385 | -158.985707 | 405.679822 | -0.391899 | 0.001510 | 0.005186 | 282.332765 | 123.347058 | 0.001838 | 0.003348 | -0.006046 | 0.006176 | True | False | 2 | 0.274572 | 77.308334 | 0.002666 | 0.002666 | 0.002666 | 1.582240 | 0.000156 | -51.318171 | 2 | -0.006046 | 0.006176 | 456.997994 | 0.002520 | True | 0.135313 | 0.001126 | |
2 | 0 | -769.038462 | 769.038462 | -421.792044 | 424.086847 | -0.994589 | -0.006046 | 0.006176 | 422.939446 | 1.147402 | 0.006111 | 0.000065 | -0.006046 | 0.006176 | True | False | 2 | 0.214872 | -81.675363 | -0.004333 | -0.004333 | -0.004333 | 16.561924 | 0.006483 | -388.848190 | 4 | -0.006046 | 0.006176 | 812.935037 | 0.010509 | True | 0.135004 | 0.007609 | |
3 | 0 | -615.230769 | 769.038462 | -377.964838 | 423.959578 | -0.891511 | -0.003739 | 0.006255 | 400.962208 | 22.997370 | 0.004997 | 0.001258 | -0.006046 | 0.006579 | True | False | 2 | 0.241561 | -57.769696 | -0.002140 | -0.002140 | -0.002140 | 11.950868 | 0.003862 | -339.437342 | 2 | -0.006046 | 0.006579 | 763.396920 | 0.008395 | True | 0.134820 | 0.011471 | |
4 | 0 | 0.000000 | 707.515385 | -161.282987 | 403.382543 | -0.399826 | 0.001386 | 0.005062 | 282.332765 | 121.049778 | 0.001838 | 0.003224 | -0.006170 | 0.006579 | True | False | 2 | 0.273100 | 76.529635 | 0.002550 | 0.002550 | 0.002550 | 1.573073 | 0.000155 | -52.697611 | 2 | -0.006170 | 0.006579 | 456.080154 | 0.002512 | True | 0.134812 | 0.011626 | |
5 | 0 | -769.038462 | 799.800000 | -424.089324 | 430.963765 | -0.984049 | -0.006170 | 0.006579 | 427.526545 | 3.437221 | 0.006374 | 0.000205 | -0.006170 | 0.006579 | True | False | 2 | 0.233037 | -87.643774 | -0.004478 | -0.004478 | -0.004478 | 17.803251 | 0.007271 | -393.077075 | 4 | -0.006170 | 0.006579 | 824.040841 | 0.011057 | True | 0.134466 | 0.018897 |
[28]:
print("Tabelle 2.48, stimmt")
display(damage_calculator.collective[["run_index","epsilon_open_alt","epsilon_min_alt_SP", "epsilon_max_alt_SP", "epsilon_min_LF", "epsilon_max_LF"]])
Tabelle 2.48, stimmt
run_index | epsilon_open_alt | epsilon_min_alt_SP | epsilon_max_alt_SP | epsilon_min_LF | epsilon_max_LF | ||
---|---|---|---|---|---|---|---|
hysteresis_index | assessment_point_index | ||||||
0 | 0 | 1 | -0.002282 | -0.004027 | 0.000000 | -0.004027 | 0.000000 |
1 | 0 | 2 | 0.002666 | -0.006046 | 0.006176 | -0.006046 | 0.006176 |
2 | 0 | 2 | -0.004333 | -0.006046 | 0.006176 | -0.006046 | 0.006176 |
3 | 0 | 2 | -0.002140 | -0.006046 | 0.006579 | -0.006046 | 0.006579 |
4 | 0 | 2 | 0.002550 | -0.006170 | 0.006579 | -0.006170 | 0.006579 |
5 | 0 | 2 | -0.004478 | -0.006170 | 0.006579 | -0.006170 | 0.006579 |
[29]:
print("Tabelle 2.49, stimmt")
display(damage_calculator.collective[["run_index", "is_closed_hysteresis", "S_open", "case_name", "epsilon_open_ein", "epsilon_open", "S_close", "P_RAJ", "P_RAJ_D"]])
Tabelle 2.49, stimmt
run_index | is_closed_hysteresis | S_open | case_name | epsilon_open_ein | epsilon_open | S_close | P_RAJ | P_RAJ_D | ||
---|---|---|---|---|---|---|---|---|---|---|
hysteresis_index | assessment_point_index | |||||||||
0 | 0 | 1 | False | -31.571122 | 2 | -0.002282 | -0.002282 | -321.509046 | 7.744399 | 0.135321 |
1 | 0 | 2 | True | 77.308334 | 2 | 0.002666 | 0.002666 | -51.318171 | 1.582240 | 0.135313 |
2 | 0 | 2 | True | -81.675363 | 4 | -0.004333 | -0.004333 | -388.848190 | 16.561924 | 0.135004 |
3 | 0 | 2 | True | -57.769696 | 2 | -0.002140 | -0.002140 | -339.437342 | 11.950868 | 0.134820 |
4 | 0 | 2 | True | 76.529635 | 2 | 0.002550 | 0.002550 | -52.697611 | 1.573073 | 0.134812 |
5 | 0 | 2 | True | -87.643774 | 4 | -0.004478 | -0.004478 | -393.077075 | 17.803251 | 0.134466 |
Debug crack opening strain of hystereses
[30]:
# plot resulting stress-strain curve
sampling_parameter = 50 # choose larger for smoother plot
plotting_data = detector.interpolated_stress_strain_data(sampling_parameter)
strain_values_primary = plotting_data["strain_values_primary"]
stress_values_primary = plotting_data["stress_values_primary"]
hysteresis_index_primary = plotting_data["hysteresis_index_primary"]
strain_values_secondary = plotting_data["strain_values_secondary"]
stress_values_secondary = plotting_data["stress_values_secondary"]
hysteresis_index_secondary = plotting_data["hysteresis_index_secondary"]
# all hystereses in stress-strain diagram
fig, axes = plt.subplots(1, 2, figsize=(18,6))
# load-time diagram
import matplotlib
matplotlib.rcParams.update({'font.size': 14})
axes[0].plot(load_sequence_list, "o-", lw=3)
axes[0].grid()
axes[0].set_xlabel("t [s]")
axes[0].set_ylabel("L [N]")
axes[0].set_title("Scaled load sequence")
# stress-strain diagram
axes[1].plot(strain_values_primary, stress_values_primary, "k-", lw=2)
axes[1].plot(strain_values_secondary, stress_values_secondary, "k--", lw=1)
axes[1].grid()
axes[1].set_xlabel("$\epsilon$")
axes[1].set_ylabel("$\sigma$ [MPa]")
axes[1].set_title("Material response")
# all hystereses in stress-strain diagram
strain_previous = []
stress_previous = []
n_hystereses = max(max(hysteresis_index_primary), max(hysteresis_index_secondary))
for hysteresis_index in range(n_hystereses):
fig, ax = plt.subplots(figsize=(12,6))
strain_primary_subset = np.where(hysteresis_index_primary == hysteresis_index, strain_values_primary, np.nan)
stress_primary_subset = np.where(hysteresis_index_primary == hysteresis_index, stress_values_primary, np.nan)
strain_secondary_subset = np.where(hysteresis_index_secondary == hysteresis_index, strain_values_secondary, np.nan)
stress_secondary_subset = np.where(hysteresis_index_secondary == hysteresis_index, stress_values_secondary, np.nan)
run_index = damage_parameter.collective.iloc[hysteresis_index, damage_parameter.collective.columns.get_loc("run_index")]
debug_output = damage_parameter.collective.iloc[hysteresis_index, damage_parameter.collective.columns.get_loc("debug_output")]
ax.set_title(f"Hysteresis {hysteresis_index}, Run {run_index}:\n{debug_output}")
ax.plot(strain_previous, stress_previous, "gray", lw=1)
ax.plot(strain_primary_subset, stress_primary_subset, "b-", lw=2)
ax.plot(strain_secondary_subset, stress_secondary_subset, "b--", lw=1)
ax.grid()
ax.set_xlabel("$\epsilon$")
ax.set_ylabel("$\sigma$ [MPa]")
# add the new hysteresis to every next plot
strain_previous += list(strain_primary_subset)
stress_previous += list(stress_primary_subset)
strain_previous += list(strain_secondary_subset)
stress_previous += list(stress_secondary_subset)
[31]:
# plot all hystereses
# get all graph data
sampling_parameter = 50 # choose larger for smoother plot
plotting_data = detector.interpolated_stress_strain_data(sampling_parameter, only_hystereses=False)
strain_values_primary = plotting_data["strain_values_primary"]
stress_values_primary = plotting_data["stress_values_primary"]
hysteresis_index_primary = plotting_data["hysteresis_index_primary"]
strain_values_secondary = plotting_data["strain_values_secondary"]
stress_values_secondary = plotting_data["stress_values_secondary"]
hysteresis_index_secondary = plotting_data["hysteresis_index_secondary"]
fig, ax = plt.subplots(figsize=(12,6))
ax.plot(strain_values_primary, stress_values_primary, "b-", lw=2)
ax.plot(strain_values_secondary, stress_values_secondary, "b--", lw=1)
ax.grid()
ax.set_xlabel("$\epsilon$")
ax.set_ylabel("$\sigma$ [MPa]")
ax.set_title("All hystereses")
collective = damage_parameter.collective
# get graph data of only hystereses
sampling_parameter = 50 # choose larger for smoother plot
plotting_data = detector.interpolated_stress_strain_data(sampling_parameter, only_hystereses=True)
strain_values_primary = plotting_data["strain_values_primary"]
stress_values_primary = plotting_data["stress_values_primary"]
hysteresis_index_primary = plotting_data["hysteresis_index_primary"]
strain_values_secondary = plotting_data["strain_values_secondary"]
stress_values_secondary = plotting_data["stress_values_secondary"]
hysteresis_index_secondary = plotting_data["hysteresis_index_secondary"]
# plot only hystereses
strain_previous = []
stress_previous = []
n_hystereses = max(max(hysteresis_index_primary), max(hysteresis_index_secondary))
for hysteresis_index in range(n_hystereses):
fig, ax = plt.subplots(figsize=(12,6))
# plot hysteresis
strain_primary_subset = np.where(hysteresis_index_primary == hysteresis_index, strain_values_primary, np.nan)
stress_primary_subset = np.where(hysteresis_index_primary == hysteresis_index, stress_values_primary, np.nan)
strain_secondary_subset = np.where(hysteresis_index_secondary == hysteresis_index, strain_values_secondary, np.nan)
stress_secondary_subset = np.where(hysteresis_index_secondary == hysteresis_index, stress_values_secondary, np.nan)
if len([_ for _ in strain_primary_subset if not np.isnan(_)]) == 0 and \
len([_ for _ in strain_secondary_subset if not np.isnan(_)]) == 0:
strain_primary_subset = np.array(strain_values_primary_all)
stress_primary_subset = np.array(stress_values_primary_all)
strain_secondary_subset = np.array(strain_values_secondary_all)
stress_secondary_subset = np.array(stress_values_secondary_all)
run_index = collective.iloc[hysteresis_index, collective.columns.get_loc("run_index")]
#case_debugging = collective.iloc[hysteresis_index, collective.columns.get_loc("case_debugging")]
case_debugging = ""
ax.set_title(f"Hysteresis {hysteresis_index}, Run {run_index}:\n{case_debugging}")
ax.plot(strain_previous, stress_previous, "gray", lw=1)
ax.plot(strain_primary_subset, stress_primary_subset, "b-", lw=4)
ax.plot(strain_secondary_subset, stress_secondary_subset, "b--", lw=2)
# plot crack opening points
S_open = collective.iloc[hysteresis_index, collective.columns.get_loc("S_open")]
S_close = collective.iloc[hysteresis_index, collective.columns.get_loc("S_close")]
epsilon_open = collective.iloc[hysteresis_index, collective.columns.get_loc("epsilon_open")]
epsilon_open_ein = collective.iloc[hysteresis_index, collective.columns.get_loc("epsilon_open_ein")]
ax.plot(epsilon_open_ein, S_open, "+", markersize=20, markeredgewidth=4, color="b", label="ε_open,ein")
ax.plot(epsilon_open_ein, S_close, "+", markersize=20, markeredgewidth=4, color="b")
ax.plot(epsilon_open, S_open, "+", markersize=20, markeredgewidth=2, color="m", label="ε_open")
ax.plot(epsilon_open, S_close, "+", markersize=20, markeredgewidth=2, color="m")
ax.plot([epsilon_open, epsilon_open],
[-400,400], "-", color="m")
# plot epsilons
S_min = collective.iloc[hysteresis_index, collective.columns.get_loc("S_min")]
S_max = collective.iloc[hysteresis_index, collective.columns.get_loc("S_max")]
epsilon_min_alt_SP = collective.iloc[hysteresis_index, collective.columns.get_loc("epsilon_min_alt_SP")]
epsilon_max_alt_SP = collective.iloc[hysteresis_index, collective.columns.get_loc("epsilon_max_alt_SP")]
epsilon_min_LF = collective.iloc[hysteresis_index, collective.columns.get_loc("epsilon_min_LF")]
epsilon_max_LF = collective.iloc[hysteresis_index, collective.columns.get_loc("epsilon_max_LF")]
# add the new hysteresis to every next plot
strain_previous += list(strain_primary_subset)
stress_previous += list(stress_primary_subset)
strain_previous += list(strain_secondary_subset)
stress_previous += list(stress_secondary_subset)
ax.plot([epsilon_min_alt_SP,epsilon_min_alt_SP], [S_min*0.9,S_max*1.1], ":", label="ε_min,alt,SP")
ax.plot([epsilon_max_alt_SP,epsilon_max_alt_SP], [S_min*0.9,S_max*1.1], "-", label="ε_max,alt,SP")
ax.plot([epsilon_min_LF,epsilon_min_LF], [S_min,S_max], ":", label="ε_min,LF")
ax.plot([epsilon_max_LF,epsilon_max_LF], [S_min,S_max], "-", label="ε_max,LF")
ax.grid()
ax.legend(bbox_to_anchor=(1.1,1), loc="upper left")
ax.set_xlabel("$\epsilon$")
ax.set_ylabel("$\sigma$ [MPa]")
[32]:
hysteresis_index_primary
[32]:
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6])
2.9.9.1, 2.8.10.1 Infinite life assessment (Dauerfestigkeitsnachweis)
[33]:
# Infinite life assessment
is_life_infinite = damage_calculator.is_life_infinite
print(f"Infinite life: {is_life_infinite}")
Infinite life: False
2.9.8.4, 2.8.10.2 Finite life assessment (Betriebsfestigkeitsnachweis)
[34]:
# finite life assessment
lifetime_n_cycles = damage_calculator.lifetime_n_cycles
lifetime_n_times_load_sequence = damage_calculator.lifetime_n_times_load_sequence
print(f"Number of bearable cycles: {lifetime_n_cycles:.0f}")
print(f"Number of bearable load sequences: {lifetime_n_times_load_sequence:.0f}")
Number of bearable cycles: 273
Number of bearable load sequences: 55