FKM Nonlinear demo

This jupyter notebook is the main example how to apply the FKM nonlinear, i.e., the local strain concept using pylife. The fatigue assessment is performed for a single point. Note that assessment for a mesh is also possible with pylife, by providing the respective meshes. If you have voilà installed (pip install voila), you can also open the notebook by clicking on the voila button in the top bar. This will hide the code blocks and make the plots better visible.

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". If you want to learn more about how the algorithm works, have a look at the notebook fkm_nonlinear_full.

Python module imports

[1]:
# standard modules
import pandas as pd
import numpy as np
import itertools
import timeit
import matplotlib.pyplot as plt
import matplotlib.patches
plt.rcParams.update({'font.size': 18})

# pylife
import pylife
import pylife.strength
import pylife.strength.fkm_nonlinear
import pylife.mesh.gradient

Input data

Specify material and assessment parameters

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.

[2]:
assessment_parameters = pd.Series({
        'MatGroupFKM': 'Steel',  # [Steel, SteelCast, Al_wrought] material group
        '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

        'c':   1.4,              # [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': 0.15,               # [mm^-1] (de: bezogener Spannungsgradient)
        'K_p': 3.5,              # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1)

        'P_A': 0.5,              # [-] failure probability (de: auszulegende Ausfallwahrscheinlichkeit), set to 0.5 to disable statistical assessment
        # beta: 0.5,             # damage index, specify this as an alternative to P_A
        'P_L': 50,               # [%] (one of 2.5%, 50%) (de: Auftretenswahrscheinlichkeit der Lastfolge), usually set to 50
        #'s_L': 10,              # [MPa] standard deviation of Gaussian distribution for load
        'n_bins': 200,           # optional (default: 200) number of bins or classes for P_RAJ computation. A larger value gives more accurate results but longer runtimes.
})

Specify load sequence

[3]:
load_sequence = pd.Series([100, -200, 100, -250, 200, 0, 200, -200])  # [N]
fig = plt.figure(figsize=(5,3))
plt.plot(load_sequence, "o-")
[3]:
[<matplotlib.lines.Line2D at 0x7f438b55af50>]
../_images/tutorials_fkm_nonlinear_7_1.png
[4]:
#%%script false --no-raise-error
result = pylife.strength.fkm_nonlinear.assessment_nonlinear_standard\
    .perform_fkm_nonlinear_assessment(assessment_parameters, load_sequence, calculate_P_RAM=True, calculate_P_RAJ=True)

Output resulting lifetimes

[5]:
print("P_RAM")
is_life_infinite = result["P_RAM_is_life_infinite"]
lifetime_n_cycles = result["P_RAM_lifetime_n_cycles"]
lifetime_n_times_load_sequence = result["P_RAM_lifetime_n_times_load_sequence"]
print(f"   Infinite life:                     {is_life_infinite}")
print(f"   Number of bearable cycles:         {lifetime_n_cycles:.0f}")
print(f"   Number of bearable load sequences: {lifetime_n_times_load_sequence:.0f}")

print("")
print("P_RAJ canonical (Miner Elementary)")
is_life_infinite = result["P_RAJ_is_life_infinite"] if "P_RAJ_is_life_infinite" in result else None
lifetime_n_cycles = result["P_RAJ_lifetime_n_cycles"] if "P_RAJ_lifetime_n_cycles" in result else -1
lifetime_n_times_load_sequence = result["P_RAJ_lifetime_n_times_load_sequence"] if "P_RAJ_lifetime_n_times_load_sequence" in result else -1

miner_is_life_infinite = result['P_RAJ_miner_is_life_infinite']

miner_lifetime_n_cycles = result['P_RAJ_miner_lifetime_n_cycles']
miner_lifetime_n_times_load_sequence = result['P_RAJ_miner_lifetime_n_times_load_sequence']
print(f"   Infinite life:                     {is_life_infinite} ({miner_is_life_infinite})")
print(f"   Number of bearable cycles:         {lifetime_n_cycles:.0f} ({miner_lifetime_n_cycles:.0f})")
print(f"   Number of bearable load sequences: {lifetime_n_times_load_sequence:.0f} "
      f"({miner_lifetime_n_times_load_sequence:.0f})")

P_RAM
   Infinite life:                     False
   Number of bearable cycles:         239646
   Number of bearable load sequences: 59911

P_RAJ canonical (Miner Elementary)
   Infinite life:                     False (False)
   Number of bearable cycles:         781480 (505343)
   Number of bearable load sequences: 195370 (126336)

Output collective

[6]:
# The following fields are available in the result:
print(result.keys())
result['P_RAM_recorder_collective']
dict_keys(['extended_neuber_binned', 'P_RAM_damage_parameter', 'P_RAM_is_life_infinite', 'P_RAM_lifetime_n_cycles', 'P_RAM_lifetime_n_times_load_sequence', 'P_RAM_lifetime_N_1ppm', 'P_RAM_lifetime_N_10', 'P_RAM_lifetime_N_50', 'P_RAM_lifetime_N_90', 'P_RAM_N_max_bearable', 'P_RAM_failure_probability', 'P_RAM_recorder_collective', 'P_RAM_collective', 'P_RAM_woehler_curve', 'P_RAM_damage_calculator', 'P_RAM_detector', 'P_RAM_detector_1st', 'seeger_beste_binned', 'P_RAJ_damage_parameter', 'P_RAJ_is_life_infinite', 'P_RAJ_lifetime_n_cycles', 'P_RAJ_lifetime_n_times_load_sequence', 'P_RAJ_miner_damage_calculator', 'P_RAJ_miner_is_life_infinite', 'P_RAJ_miner_lifetime_n_cycles', 'P_RAJ_miner_lifetime_n_times_load_sequence', 'P_RAJ_lifetime_N_1ppm', 'P_RAJ_lifetime_N_10', 'P_RAJ_lifetime_N_50', 'P_RAJ_lifetime_N_90', 'P_RAJ_N_max_bearable', 'P_RAJ_failure_probability', 'P_RAJ_recorder_collective', 'P_RAJ_collective', 'P_RAJ_woehler_curve', 'P_RAJ_damage_calculator', 'P_RAJ_detector', 'P_RAJ_detector_1st', 'assessment_parameters'])
[6]:
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 -140.0 140.0 -138.923720 138.923720 -1.000000 -0.000685 0.000685 138.923720 0.000000 0.000685 0.000000 0.000000 0.000685 False True 1
1 0 -280.0 140.0 -254.118717 150.149735 -1.692435 -0.001500 0.000619 202.134226 -51.984491 0.001060 -0.000440 -0.001500 0.000685 True False 1
2 0 0.0 280.0 -21.326590 256.520851 -0.083138 0.000109 0.001478 138.923720 117.597131 0.000685 0.000793 -0.002023 0.001478 True False 1
3 0 -280.0 140.0 -251.716583 152.551869 -1.650039 -0.001521 0.000598 202.134226 -49.582357 0.001060 -0.000462 -0.002023 0.001478 True False 2
4 0 -280.0 140.0 -251.716583 152.551869 -1.650039 -0.001521 0.000598 202.134226 -49.582357 0.001060 -0.000462 -0.002023 0.001478 True False 2
5 0 -350.0 280.0 -295.009952 256.520851 -1.150043 -0.002023 0.001478 275.765401 -19.244551 0.001751 -0.000272 -0.002023 0.001478 True False 2
6 0 0.0 280.0 -21.326590 256.520851 -0.083138 0.000109 0.001478 138.923720 117.597131 0.000685 0.000793 -0.002023 0.001478 True False 2

Lifetime \(N\) for given failure probability \(P_A\)

The dashed lines show the lifetime if the scaling factor \(\gamma_M\) is not clipped at 1.1 (P_RAM), respective 1.2 (P_RAJ).

[7]:
P_RAM_N_max_bearable = result['P_RAM_N_max_bearable']
P_RAJ_N_max_bearable = result['P_RAJ_N_max_bearable']
P_A_list = np.logspace(-6, -1, 20)
p = plt.plot(P_A_list, [P_RAJ_N_max_bearable(P_A,True) for P_A in P_A_list], label="P_RAJ")
p = plt.plot(P_A_list, [P_RAJ_N_max_bearable(P_A) for P_A in P_A_list], "--", color=p[0].get_color())
p = plt.plot(P_A_list, [P_RAM_N_max_bearable(P_A,True) for P_A in P_A_list], label="P_RAM")
p = plt.plot(P_A_list, [P_RAM_N_max_bearable(P_A) for P_A in P_A_list], "--", color=p[0].get_color())
plt.grid(which='both')
plt.legend()
plt.xscale("log")
plt.xlabel("Failure probability $P_A$ [-]")
plt.ylabel("Lifetime $N$ [-]")
[7]:
Text(0, 0.5, 'Lifetime $N$ [-]')
../_images/tutorials_fkm_nonlinear_15_1.png

Plot failure probability

The marked points have \(P_A\) = 50%.

[8]:
N_list = np.logspace(4, np.log10(result['P_RAJ_lifetime_N_90']), 20)

# P_RAJ
P_RAJ_failure_probability = result['P_RAJ_failure_probability']
p = plt.plot(N_list, [P_RAJ_failure_probability(N) for N in N_list], label="P_RAJ")
plt.plot(result['P_RAJ_lifetime_N_50'], 0.5, "o", color=p[0].get_color())

# P_RAM
P_RAM_failure_probability = result['P_RAM_failure_probability']
p = plt.plot(N_list, [P_RAM_failure_probability(N) for N in N_list], label="P_RAM")
plt.plot(result['P_RAM_lifetime_N_50'], 0.5, "o", color=p[0].get_color())
plt.grid(which='both')
plt.legend()
plt.xscale("log")
plt.yscale("log")
plt.xlabel("Lifetime $N$ [-]")
plt.ylabel("Failure probability $P_A$ [-]")
[8]:
Text(0, 0.5, 'Failure probability $P_A$ [-]')
../_images/tutorials_fkm_nonlinear_17_1.png

Woehler curves

[9]:
assessment_parameters = result["assessment_parameters"]
component_woehler_curve_P_RAM = result["P_RAM_woehler_curve"]
component_woehler_curve_P_RAJ = result["P_RAJ_woehler_curve"]

# plot component woehler curve P_RAM
n_list = np.logspace(2, 7, num=101, endpoint=True, base=10.0)
plt.rcParams.update({'font.size': 18})
fig,axes = plt.subplots(1,2,figsize=(12,6))

# P_RAM material woehler curve
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)

# P_RAJ material woehler curve
material_woehler_curve_parameters = assessment_parameters[["P_RAJ_Z_WS", "P_RAJ_D_WS", "d_RAJ"]]
material_woehler_curve_parameters["P_RAJ_Z"] = assessment_parameters["P_RAJ_Z_WS"]
material_woehler_curve_parameters["P_RAJ_D_0"] = assessment_parameters["P_RAJ_D_WS"]

material_woehler_curve_P_RAJ = pylife.strength.woehler_fkm_nonlinear\
    .WoehlerCurvePRAJ(material_woehler_curve_parameters)

# plot P_RAM material woehler curve
line1 = axes[0].plot(n_list, material_woehler_curve_P_RAM.calc_P_RAM(n_list), "--", lw=1, label="Material")
axes[0].plot(1e3, material_woehler_curve_P_RAM.P_RAM_Z,
         "o", color=line1[0].get_color())
N_D = material_woehler_curve_P_RAM.fatigue_life_limit
axes[0].plot(N_D, material_woehler_curve_P_RAM.fatigue_strength_limit,
         "o", color=line1[0].get_color())

# plot P_RAM component woehler curve
line = axes[0].plot(n_list, component_woehler_curve_P_RAM.calc_P_RAM(n_list), "-", lw=3,
                color=line1[0].get_color(), label="Component")
axes[0].plot(1e3, component_woehler_curve_P_RAM.P_RAM_Z,
         "o", color=line[0].get_color())
N_D = component_woehler_curve_P_RAM.fatigue_life_limit
axes[0].plot(N_D, component_woehler_curve_P_RAM.fatigue_strength_limit,
         "o", color=line[0].get_color())
axes[0].annotate(f"$N_D$ = {N_D:.1e}", (N_D, component_woehler_curve_P_RAM.fatigue_strength_limit),
             textcoords="offset points", xytext=(0,20), color=line[0].get_color())

axes[0].legend()
axes[0].set_xscale('log')
axes[0].set_yscale('log')
axes[0].set_xlabel('$N$')
axes[0].set_ylabel('$P_{RAM}$')
axes[0].set_title("$P_{RAM}$ Woehler curves")
axes[0].grid(which='both')

# plot P_RAJ material woehler curve
line1 = axes[1].plot(n_list, material_woehler_curve_P_RAJ.calc_P_RAJ(n_list), "--", lw=1, label="Material")
N_D = material_woehler_curve_P_RAJ.fatigue_life_limit
axes[1].plot(N_D, material_woehler_curve_P_RAJ.fatigue_strength_limit,
         "o", color=line1[0].get_color())

# plot P_RAJ component woehler curve
line = axes[1].plot(n_list, component_woehler_curve_P_RAJ.calc_P_RAJ(n_list), "-", lw=3,
                color=line1[0].get_color(), label="Component")
N_D = component_woehler_curve_P_RAJ.fatigue_life_limit
axes[1].plot(N_D, component_woehler_curve_P_RAJ.fatigue_strength_limit,
         "o", color=line[0].get_color())
axes[1].annotate(f"$N_D$ = {N_D:.1e}", (N_D, component_woehler_curve_P_RAJ.fatigue_strength_limit),
             textcoords="offset points", xytext=(0,20), color=line[0].get_color())

axes[1].legend()
axes[1].set_xscale('log')
axes[1].set_yscale('log')
axes[1].set_xlabel('$N$')
axes[1].set_ylabel('$P_{RAJ}$')
axes[1].set_title("$P_{RAJ}$ Woehler curves")
axes[1].grid(which='both')
plt.tight_layout()

../_images/tutorials_fkm_nonlinear_19_0.png

Hystereses

[10]:
fig, axes = plt.subplots(2, 2, figsize=(12,10))

# set font size
import matplotlib
matplotlib.rcParams.update({'font.size': 14})

# prepare first plot
axes[0,0].plot(load_sequence, "o-", lw=2)
axes[0,0].grid()
axes[0,0].set_xlabel("t [s]")
axes[0,0].set_ylabel("L [N]")
axes[0,0].set_title("Load sequence")

# plot hystereses for P_RAM
# ---------------------------
parameter_name = "RAM"
detector = result[f"P_{parameter_name}_detector"]
detector_1st = result[f"P_{parameter_name}_detector_1st"]

# 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"]

sampling_parameter = 50    # choose larger for smoother plot
plotting_data_1st = detector_1st.interpolated_stress_strain_data(sampling_parameter)

strain_values_primary_1st = plotting_data_1st["strain_values_primary"]
stress_values_primary_1st = plotting_data_1st["stress_values_primary"]
hysteresis_index_primary_1st = plotting_data_1st["hysteresis_index_primary"]
strain_values_secondary_1st = plotting_data_1st["strain_values_secondary"]
stress_values_secondary_1st = plotting_data_1st["stress_values_secondary"]
hysteresis_index_secondary_1st = plotting_data_1st["hysteresis_index_secondary"]


# stress-strain diagram
axes[1,0].plot(strain_values_primary, stress_values_primary, "y-", lw=2, label="HCM second run")
axes[1,0].plot(strain_values_secondary, stress_values_secondary, "y-.", lw=2)
axes[1,0].plot(strain_values_primary_1st, stress_values_primary_1st, "g-", lw=2, label="HCM first run")
axes[1,0].plot(strain_values_secondary_1st, stress_values_secondary_1st, "g-.", lw=2)
axes[1,0].grid()
axes[1,0].set_xlabel("$\epsilon$")
axes[1,0].set_ylabel("$\sigma$ [MPa]")
axes[1,0].set_title(f"P_{parameter_name} material response")


# show only legend in field axes[0,1]
handles, labels = axes[1,0].get_legend_handles_labels()
axes[0,1].axis("off")
axes[0,1].legend(handles, labels, bbox_to_anchor=(0.0,0), loc='lower left')

# plot hystereses for P_RAJ
# ---------------------------
parameter_name = "RAJ"
detector = result[f"P_{parameter_name}_detector"]
detector_1st = result[f"P_{parameter_name}_detector_1st"]

# 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"]

sampling_parameter = 50    # choose larger for smoother plot
plotting_data_1st = detector_1st.interpolated_stress_strain_data(sampling_parameter)

strain_values_primary_1st = plotting_data_1st["strain_values_primary"]
stress_values_primary_1st = plotting_data_1st["stress_values_primary"]
hysteresis_index_primary_1st = plotting_data_1st["hysteresis_index_primary"]
strain_values_secondary_1st = plotting_data_1st["strain_values_secondary"]
stress_values_secondary_1st = plotting_data_1st["stress_values_secondary"]
hysteresis_index_secondary_1st = plotting_data_1st["hysteresis_index_secondary"]

# stress-strain diagram
axes[1,1].plot(strain_values_primary, stress_values_primary, "y-", lw=2, label="HCM second run")
axes[1,1].plot(strain_values_secondary, stress_values_secondary, "y-.", lw=2)
axes[1,1].plot(strain_values_primary_1st, stress_values_primary_1st, "g-", lw=2, label="HCM first run")
axes[1,1].plot(strain_values_secondary_1st, stress_values_secondary_1st, "g-.", lw=2)
axes[1,1].grid()
axes[1,1].set_xlabel("$\epsilon$")
axes[1,1].set_ylabel("$\sigma$ [MPa]")
axes[1,1].set_title(f"P_{parameter_name} material response")

plt.tight_layout()
../_images/tutorials_fkm_nonlinear_21_0.png

Damaging effects of hystereses

[11]:
# P_RAM
damage_calculator = result["P_RAM_damage_calculator"]

P_RAM_D = component_woehler_curve_P_RAM.P_RAM_D
plt.figure()
plt.bar(damage_calculator.collective.index.get_level_values("hysteresis_index"),
        damage_calculator.collective.P_RAM,
        color=np.where(damage_calculator.collective.P_RAM>P_RAM_D, "b", "gray"))
plt.plot([-0.5,len(damage_calculator.collective)-0.5], [P_RAM_D,P_RAM_D],
         label="fatigue strength limit", color='k')
plt.legend(bbox_to_anchor=(1.1,1), loc='upper left')
plt.grid()
plt.xlabel("Hysteresis no.")
plt.ylabel("Damage value P_RAM")
plt.title("P_RAM contribution per hysteresis")

# P_RAJ
damage_calculator = result["P_RAJ_damage_calculator"]
P_RAJ_D_last = damage_calculator.collective.P_RAJ_D.values[-1]
hysteresis_index = damage_calculator.collective.index.get_level_values("hysteresis_index")
plt.figure()
plt.bar(hysteresis_index,
        damage_calculator.collective.P_RAJ,
        color=np.where(damage_calculator.collective.P_RAJ>damage_calculator.collective.P_RAJ_D, "b", "gray"))
plt.plot(hysteresis_index, damage_calculator.collective.P_RAJ_D,
         label="fatigue strength limit", color='k', marker="o")
plt.legend(bbox_to_anchor=(1.1,1), loc='upper left')
plt.grid()
plt.xlabel("Hysteresis no.")
plt.ylabel("Damage value P_RAJ")
plt.title("P_RAJ contribution per hysteresis")

[11]:
Text(0.5, 1.0, 'P_RAJ contribution per hysteresis')
../_images/tutorials_fkm_nonlinear_23_1.png
../_images/tutorials_fkm_nonlinear_23_2.png
[ ]: