# Copyright (c) 2019-2023 - for information on the respective copyright owner
# see the NOTICE file and/or the repository
# https://github.com/boschresearch/pylife
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__author__ = "Benjamin Maier"
__maintainer__ = __author__
import copy
import numpy as np
import pandas as pd
# pylife
import pylife
import pylife.strength.fkm_load_distribution
import pylife.strength.damage_parameter
import pylife.strength.woehler_fkm_nonlinear
import pylife.strength.fkm_nonlinear.damage_calculator
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.damage_calculator_praj_miner
import pylife.strength.fkm_nonlinear.parameter_calculations as parameter_calculations
''' Collection of functions for computational proof of the strength
for machine elements considering their non-linear material deformation
(FKM non-linear guideline 2019)
'''
def _assert_G_is_in_correct_format(assessment_parameters):
"""Check that the related stress gradient G is given in the correct format,
either as a single float or as a pandas Series with values for each node
of the mesh. The check is performed by an assertion.
"""
# check that gradient G is in the correct format
assert isinstance(assessment_parameters.G, float) \
or (isinstance(assessment_parameters.G, pd.Series) and not isinstance(assessment_parameters.G.index, pd.MultiIndex)), \
"stress gradient G is in a wrong format (should be either float or pd.Series indexed by node)"
def _check_K_p_is_in_range(assessment_parameters):
"""Check that the load shape factor (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1)
is larger than 1.
"""
# check that gradient G is in the correct format
assert assessment_parameters.K_p >= 1, \
"K_p should be at least 1"
if assessment_parameters.K_p == 1:
print("Note, K_p is set to 1 which means only P_RAM can be calculated. "
f"To use P_RAJ set K_p > 1, e.g. try K_p = 1.001.")
def _scale_load_sequence_according_to_probability(assessment_parameters, load_sequence):
r"""Scales the given load sequence according to one of three methods defined in the FKM nonlinear guideline.
The FKM nonlinear guideline defines three possible methods to consider the statistical distribution of the load:
1. a normal distribution with given standard deviation, :math:`s_L`
2. a logarithmic-normal distribution with given standard deviation :math:`LSD_s`
3. an unknown distribution, use the constant factor :math:`\gamma_L=1.1` for :math:`P_L = 2.5\%`
or :math:`\gamma_L=1` for :math:`P_L = 50\%` or
If the ``assessment_parameters`̀` contain a value for ``s_L``, the first approach is used (normal distribution).
Else, if the ``assessment_parameters``̀ contain a value for ``LSD_s``, the second approach is used (log-normal distribution).
Else, if only ``P_L`̀ is given a scaling with the according factor is used. The statistical assessment can be skipped
by settings ``P_A = 0.5`` and ``P_L = 50``.
Parameters
----------
assessment_parameters : :class:`pandas.Series`
All parameters to the FKM algorithm, given in a series.
load_sequence : :class:`pandas.Series`
The load-time series for the assessment.
Returns
-------
:class:`pandas.Series`
The scaled load sequence.
"""
# add an empty "notes" entry in assessment_parameters
if "notes" not in assessment_parameters:
assessment_parameters["notes"] = ""
# FKMLoadDistributionNormal, uses assessment_parameters.s_L, assessment_parameters.P_L, assessment_parameters.P_A
if "s_L" in assessment_parameters:
scaled_load_sequence = load_sequence.fkm_safety_normal_from_stddev.scaled_load_sequence(assessment_parameters)
# add a note
assessment_parameters["notes"] += f"s_L was defined (s_L={assessment_parameters.s_L}), P_L={assessment_parameters.P_L}, "\
f" P_A={assessment_parameters.P_A}, using normal distribution "\
f"for load, factor gamma_L={load_sequence.fkm_safety_normal_from_stddev.gamma_L(assessment_parameters)}.\n"
elif "LSD_s" in assessment_parameters:
# FKMLoadDistributionLognormal, uses assessment_parameters.LSD_s, assessment_parameters.P_L, assessment_parameters.P_A
scaled_load_sequence = load_sequence.fkm_safety_lognormal_from_stddev.scaled_load_sequence(assessment_parameters)
# add a note
assessment_parameters["notes"] += f"LSD_s was defined (LSD_s={assessment_parameters.LSD_s}), "\
f" P_L={assessment_parameters.P_L}, P_A={assessment_parameters.P_A}, using lognormal distribution "\
f"for load, factor gamma_L={load_sequence.fkm_safety_lognormal_from_stddev.gamma_L(assessment_parameters)}.\n"
else:
# FKMLoadDistributionBlanket, uses input_parameters.P_L
scaled_load_sequence = load_sequence.fkm_safety_blanket.scaled_load_sequence(assessment_parameters)
# add a note
assessment_parameters["notes"] += f"none of s_L, LSD_s was defined, P_L={assessment_parameters.P_L}, "\
f"factor gamma_L={load_sequence.fkm_safety_blanket.gamma_L(assessment_parameters)}.\n"
return scaled_load_sequence
def _scale_load_sequence_by_c_factor(assessment_parameters, scaled_load_sequence):
"""Scale the load sequence by the given transfer factor c from the
linear elastic FE result to the given magnitude.
The factor c in defined as :math:`1/L_{REF}` with the reference load :math:`L_{REF}`.
"""
# scale load sequence by reference load
c = assessment_parameters.c
scaled_load_sequence = scaled_load_sequence.fkm_load_sequence.scaled_by_constant(c)
return scaled_load_sequence
def _calculate_local_parameters(assessment_parameters):
r"""Calculate several intermediate parameters as described in the FKM nonlinear guideline:
* The cyclic parameters :math:`n', K'`, and :math:`E`, used to
describe the cyclic material behavior (Sec. 2.5.3 of FKM nonlinear)
* The material woehler curve parameters for both the P_RAM and P_RAJ woehler curves
(Sec. 2.5.5 of FKM nonlinear)
* The factor for non-local influences, :math:`n_P = n_{bm}(R_m, G) \cdot n_{st}(A_\sigma)`,
where :math:`n_{bm}` is the fracture mechanics factor (de: bruchmechanische Stützzahl)
and :math:`n_{st}` is the statistic factor (de: statistische Stützzahl).
The factors depend on the stress gradient, :math:`G`, and the highly loaded surface,
:math:`A_\sigma`, respectively.
* The roughness factor :math:`K_{R,P}` which is estimated based on the ultimate tensile strength.
"""
# compute intermediate values
assessment_parameters = parameter_calculations.calculate_cyclic_assessment_parameters(assessment_parameters)
# 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)
# Size and geometry factor $n_P$, Spannungsgradient $G$, $A_\sigma$
assessment_parameters = parameter_calculations.calculate_nonlocal_parameters(assessment_parameters)
# Roughness factor $K_{R,P}$
assessment_parameters = parameter_calculations.calculate_roughness_parameter(assessment_parameters)
return assessment_parameters
def _compute_component_woehler_curves(assessment_parameters):
r"""Compute the PRAM and PRAJ component woehler curves.
At first, the safety factors :math:`\gamma_M` and :math:`f_\text{RAM}, f_\text{RAJ}`
are calculated. Then, the woehler curve objects are created.
"""
# Compute the safety factors to derive the component Woehler curve from the material Woehler curve.
# Compute gamma_M
assessment_parameters = parameter_calculations.calculate_failure_probability_factor_P_RAM(assessment_parameters)
assessment_parameters = parameter_calculations.calculate_failure_probability_factor_P_RAJ(assessment_parameters)
# Compute the component woehler curve parameters
assessment_parameters = parameter_calculations.calculate_component_woehler_parameters_P_RAM(assessment_parameters)
assessment_parameters = parameter_calculations.calculate_component_woehler_parameters_P_RAJ(assessment_parameters)
# Wöhler curve for P_RAM
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
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
return assessment_parameters, component_woehler_curve_P_RAM, component_woehler_curve_P_RAJ
def _compute_hcm_RAM(assessment_parameters, scaled_load_sequence):
"""Perform the HCM rainflow counting with the extended Neuber notch approximation.
The HCM algorithm is executed twice, as described in the FKM nonlinear guideline."""
# 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)
# 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
)
# perform HCM algorithm, first run
detector.process_hcm_first(scaled_load_sequence)
detector_1st = copy.deepcopy(detector)
# perform HCM algorithm, second run
detector.process_hcm_second(scaled_load_sequence)
return detector_1st, detector, extended_neuber, recorder
def _compute_damage_and_lifetimes_RAM(assessment_parameters, recorder, component_woehler_curve_P_RAM, result):
"""For P_RAM, calculate the damage and the lifetime and store in result dict."""
# define damage parameter
damage_parameter = pylife.strength.damage_parameter.P_RAM(recorder.collective, assessment_parameters)
# 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)
result["P_RAM_damage_parameter"] = damage_parameter
# Infinite life assessment
result["P_RAM_is_life_infinite"] = damage_calculator.is_life_infinite
# finite life assessment
result["P_RAM_lifetime_n_cycles"] = damage_calculator.lifetime_n_cycles
result["P_RAM_lifetime_n_times_load_sequence"] = damage_calculator.lifetime_n_times_load_sequence
return result, damage_calculator
def _compute_lifetimes_for_failure_probabilities_RAM(assessment_parameters, result, damage_calculator):
"""If P_A is set to 0.5, i.e., no explicit statistical assessment is performed, do
some statistical assessment as post-processing.
The lifetimes for 1ppm, 10%, 50%, and 90% are calculated using the given assessment concept
defined by the FKM nonlinear guideline. Further, two python functions for arbitrary lifetimes
and failure probabilities are created. Everything is stored in the result dict."""
if "P_A" in assessment_parameters and np.isclose(assessment_parameters.P_A, 0.5):
N_max_bearable, failure_probability = damage_calculator.get_lifetime_functions(assessment_parameters)
N_1ppm = N_max_bearable(1e-6)
N_10 = N_max_bearable(0.1)
N_50 = N_max_bearable(0.5)
N_90 = N_max_bearable(0.9)
# add lifetime and failure probability results
result["P_RAM_lifetime_N_1ppm"] = N_1ppm
result["P_RAM_lifetime_N_10"] = N_10
result["P_RAM_lifetime_N_50"] = N_50
result["P_RAM_lifetime_N_90"] = N_90
result["P_RAM_N_max_bearable"] = N_max_bearable
result["P_RAM_failure_probability"] = failure_probability
return result
def _store_additional_objects_in_result_RAM(result, recorder, damage_calculator, component_woehler_curve_P_RAM, detector, detector_1st):
"""Store the given objects in the results dict. The ``result`` variable gets
returned back to the user. These additional variables an be used for certain plots,
e.g. to plot the woehler curve."""
result["P_RAM_recorder_collective"] = recorder.collective
result["P_RAM_collective"] = damage_calculator.collective
result["P_RAM_woehler_curve"] = component_woehler_curve_P_RAM
result["P_RAM_damage_calculator"] = damage_calculator
result["P_RAM_detector"] = detector
result["P_RAM_detector_1st"] = detector_1st
return result
def _compute_hcm_RAJ(assessment_parameters, scaled_load_sequence):
"""Perform the HCM rainflow counting with the Seeger-Beste notch approximation.
The HCM algorithm is executed twice, as described in the FKM nonlinear guideline."""
# 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)
# 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
)
detector_1st = copy.deepcopy(detector)
# perform HCM algorithm, first run
detector.process_hcm_first(scaled_load_sequence)
# perform HCM algorithm, second run
detector.process_hcm_second(scaled_load_sequence)
return detector_1st, detector, seeger_beste, recorder
def _compute_damage_and_lifetimes_RAJ(assessment_parameters, recorder, component_woehler_curve_P_RAJ, result):
"""For P_RAJ, calculate the damage and the lifetime and store in result dict."""
# define damage parameter
damage_parameter = pylife.strength.damage_parameter.P_RAJ(recorder.collective, assessment_parameters,\
component_woehler_curve_P_RAJ)
# 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)
result["P_RAJ_damage_parameter"] = damage_parameter
# Infinite life assessment
result["P_RAJ_is_life_infinite"] = damage_calculator.is_life_infinite
# finite life assessment
result["P_RAJ_lifetime_n_cycles"] = damage_calculator.lifetime_n_cycles
result["P_RAJ_lifetime_n_times_load_sequence"] = damage_calculator.lifetime_n_times_load_sequence
return result, damage_calculator
def _compute_damage_and_lifetimes_RAJ_miner(assessment_parameters, recorder, component_woehler_curve_P_RAJ, result):
"""For P_RAJ, calculate the damage and the lifetime using the woehler curve directly, and store in result dict."""
# define damage parameter
damage_parameter = pylife.strength.damage_parameter.P_RAJ(recorder.collective, assessment_parameters,\
component_woehler_curve_P_RAJ)
# compute the effect of the damage parameter with the woehler curve
damage_calculator = pylife.strength.fkm_nonlinear.damage_calculator_praj_miner\
.DamageCalculatorPRAJMinerElementary(damage_parameter.collective, component_woehler_curve_P_RAJ)
result["P_RAJ_miner_damage_calculator"] = damage_calculator
# Infinite life assessment
result["P_RAJ_miner_is_life_infinite"] = damage_calculator.is_life_infinite
# finite life assessment
result["P_RAJ_miner_lifetime_n_cycles"] = damage_calculator.lifetime_n_cycles
result["P_RAJ_miner_lifetime_n_times_load_sequence"] = damage_calculator.lifetime_n_times_load_sequence
return result
def _compute_lifetimes_for_failure_probabilities_RAJ(assessment_parameters, result, damage_calculator):
"""If P_A is set to 0.5, i.e., no explicit statistical assessment is performed, do
some statistical assessment as post-processing.
The lifetimes for 1ppm, 10%, 50%, and 90% are calculated using the given assessment concept
defined by the FKM nonlinear guideline. Further, two python functions for arbitrary lifetimes
and failure probabilities are created. Everything is stored in the result dict."""
if "P_A" in assessment_parameters and np.isclose(assessment_parameters.P_A, 0.5):
N_max_bearable, failure_probability = damage_calculator.get_lifetime_functions()
N_1ppm = N_max_bearable(1e-6)
N_10 = N_max_bearable(0.1)
N_50 = N_max_bearable(0.5)
N_90 = N_max_bearable(0.9)
# add lifetime and failure probability results
result["P_RAJ_lifetime_N_1ppm"] = N_1ppm
result["P_RAJ_lifetime_N_10"] = N_10
result["P_RAJ_lifetime_N_50"] = N_50
result["P_RAJ_lifetime_N_90"] = N_90
result["P_RAJ_N_max_bearable"] = N_max_bearable
result["P_RAJ_failure_probability"] = failure_probability
return result
def _store_additional_objects_in_result_RAJ(result, recorder, damage_calculator, component_woehler_curve_P_RAJ, detector, detector_1st):
"""Store the given objects in the results dict. The ``result`` variable gets
returned back to the user. These additional variables an be used for certain plots,
e.g. to plot the woehler curve."""
# add collectives and objects
result["P_RAJ_recorder_collective"] = recorder.collective
result["P_RAJ_collective"] = damage_calculator.collective
result["P_RAJ_woehler_curve"] = component_woehler_curve_P_RAJ
result["P_RAJ_damage_calculator"] = damage_calculator
result["P_RAJ_detector"] = detector
result["P_RAJ_detector_1st"] = detector_1st
return result
def _compute_lifetimes_P_RAJ(assessment_parameters, result, scaled_load_sequence, component_woehler_curve_P_RAJ):
"""Compute the lifetimes using the given parameters and woehler curve, with P_RAJ.
* Execute the HCM algorithm to detect closed hysteresis.
* Use the woehler curve and the damage parameter to predict lifetimes.
* Do statistical assessment and store all results in a dict.
"""
detector_1st, detector, seeger_beste_binned, recorder = _compute_hcm_RAJ(assessment_parameters, scaled_load_sequence)
result["seeger_beste_binned"] = seeger_beste_binned
result, damage_calculator = _compute_damage_and_lifetimes_RAJ(assessment_parameters, recorder, component_woehler_curve_P_RAJ, result)
result = _compute_damage_and_lifetimes_RAJ_miner(assessment_parameters, recorder, component_woehler_curve_P_RAJ, result)
result = _compute_lifetimes_for_failure_probabilities_RAJ(assessment_parameters, result, damage_calculator)
result = _store_additional_objects_in_result_RAJ(result, recorder, damage_calculator, component_woehler_curve_P_RAJ, detector, detector_1st)
return result
def _compute_lifetimes_P_RAM(assessment_parameters, result, scaled_load_sequence, component_woehler_curve_P_RAM):
"""Compute the lifetimes using the given parameters and woehler curve, with P_RAM.
* Execute the HCM algorithm to detect closed hysteresis.
* Use the woehler curve and the damage parameter to predict lifetimes.
* Do statistical assessment and store all results in a dict.
"""
detector_1st, detector, extended_neuber_binned, recorder = _compute_hcm_RAM(assessment_parameters, scaled_load_sequence)
result["extended_neuber_binned"] = extended_neuber_binned
result, damage_calculator = _compute_damage_and_lifetimes_RAM(assessment_parameters, recorder, component_woehler_curve_P_RAM, result)
result = _compute_lifetimes_for_failure_probabilities_RAM(assessment_parameters, result, damage_calculator)
result = _store_additional_objects_in_result_RAM(result, recorder, damage_calculator, component_woehler_curve_P_RAM, detector, detector_1st)
return result