Source code for pylife.strength.sn_curve

# Copyright (c) 2019-2021 - 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__ = "Cedric Philip Wagner"
__maintainer__ = "Johannes Mueller"

import numpy as np
import logging
import pandas as pd

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s\n')
ch.setFormatter(formatter)
ch.setLevel(logging.DEBUG)
logger.addHandler(ch)


[docs]class FiniteLifeBase: """Base class for SN curve calculations - either in logarithmic or regular scale""" def __init__(self, k_1, SD_50, ND_50): self._k = k_1 self.SD_50 = SD_50 self.ND_50 = ND_50 self._check_inputs_valid(k_1=self.k_1, SD_50=self.SD_50, ND_50=self.ND_50) @property def k_1(self): return self._k @k_1.setter def k_1(self, k_1): self._k = k_1 def _check_inputs_valid(self, **kwargs): try: for param, value in kwargs.items(): if not float(value) > 0: logger.error("Parameter '{}' is not a number greater than zero: {}".format( param, value, )) except Exception as e: raise ValueError("Not all inputs are valid numbers: {}".format(e))
[docs]class FiniteLifeLine(FiniteLifeBase): """Sample points on the finite life line - either N or S (LOGARITHMIC SCALE) The formula for calculation is taken from "Betriebsfestigkeit", Haibach, 3. Auflage 2006 Notes ----- In contrast to the case Parameters ---------- k : float slope of the SN-curve SD_50 : float lower stress limit in the finite life region ND_50 : float number of cycles at stress SD_50 """ def __init__(self, k, SD_50, ND_50): super(FiniteLifeLine, self).__init__(k, SD_50, ND_50) self.SD_50_log = np.log10(self.SD_50) self.ND_50_log = np.log10(self.ND_50) if self.k_1 < 0: logger.warning("Parameter k should be positive. Given k ('{}') has been " "assigned as absolute value.".format(self.k_1)) self.k_1 = abs(self.k_1)
[docs] def calc_S_log(self, N_log, ignore_limits=False): """Calculate stress logarithmic S_log for a given number of cycles N_log Parameters ---------- N_log : float logarithmic number of cycles ignore_limits : boolean ignores the upper limit of the number of cycles generally it should be smaller than ND_50 (=the limit of the finite life region) but some special evaluation methods (e.g. according to marquardt2004) require extrapolation to estimate an equivalent stress Returns ------- S_log : float logarithmic stress corresponding to the given number of cycles (point on the SN-curve) """ if N_log > self.ND_50_log: if ignore_limits: logger.warning("The limits to the interpolation of the finite life region " "have explicitly been ignored.") else: raise ValueError("The given N_log ('{}') must be smaller than " "the lifetime ('{}').".format( N_log, self.ND_50_log, )) elif N_log < 1: raise ValueError("Invalid input for parameter N_log: {}".format(N_log)) return self.SD_50_log + (self.ND_50_log - N_log) / self.k_1
[docs] def calc_N_log(self, S_log, ignore_limits=False): """Calculate number of cycles N_log for a given stress S_log Parameters ---------- S_log : float logarithmic stress (point on the SN-curve) ignore_limits : boolean ignores the upper limit of the number of cycles generally it should be smaller than ND_50_log (=the limit of the finite life region) but some special evaluation methods (e.g. according to marquardt2004) require extrapolation to estimate an equivalent stress Returns ------- N_log : float logarithmic number of cycles corresponding to the given stress value (point on the SN-curve) """ if S_log < self.SD_50_log: if ignore_limits: logger.warning("The limits to the interpolation of the finite life region " "have explicitly been ignored.") else: raise ValueError("The given stress S_log ('{}') must be larger than " "SD_50_log ('{}').".format( S_log, self.SD_50_log, )) return self.ND_50_log + (self.SD_50_log - S_log) * self.k_1
[docs]class FiniteLifeCurve(FiniteLifeBase): """Sample points on the finite life curve - either N or S (NOT logarithmic scale) The formula for calculation is taken from "Betriebsfestigkeit", Haibach, 3. Auflage 2006 **Consider:** load collective and life curve have to be consistent: * range vs range * amplitude vs amplitude Parameters ---------- k : float slope of the SN-curve SD_50 : float lower stress limit in the finite life region ND_50 : float number of cycles at stress SD_50 """ def __init__(self, k_1, SD_50, ND_50): super(FiniteLifeCurve, self).__init__(k_1, SD_50, ND_50)
[docs] def calc_S(self, N, ignore_limits=True): """Calculate stress S for a given number of cycles N Parameters ---------- N : float number of cycles ignore_limits : boolean ignores the upper limit of the number of cycles generally it should be smaller than ND_50 (=the limit of the finite life region) but some special evaluation methods (e.g. according to marquardt2004) require extrapolation to estimate an equivalent stress Returns ------- S : float stress corresponding to the given number of cycles (point on the SN-curve) """ if N > self.ND_50: if ignore_limits: logger.warning("The limits to the interpolation of the finite life region " "have explicitly been ignored.") else: raise ValueError("The given N ('{}') must be smaller than " "the lifetime ('{}').".format( N, self.ND_50, )) elif N < 1: raise ValueError("Invalid input for parameter N: {}".format(N)) return self.SD_50 * (N / self.ND_50)**(- 1/self.k_1)
[docs] def calc_N(self, S, ignore_limits=False): """Calculate number of cycles N for a given stress S Parameters ---------- S : array like Stress (point(s) on the SN-curve) ignore_limits : boolean ignores the upper limit of the number of cycles generally it should be smaller than ND_50 (=the limit of the finite life region) but some special evaluation methods (e.g. according to marquardt2004) require extrapolation to estimate an equivalent stress Returns ------- N : array like number of cycles corresponding to the given stress value (point on the SN-curve) """ if np.any(S < self.SD_50): if ignore_limits: logger.warning("Using all cycles") else: raise ValueError("The given stress S ('{}') must be larger than " "SD_50 ('{}').".format( S, self.SD_50, )) return self.ND_50 * (S / self.SD_50)**(-self.k_1)
[docs] def calc_damage(self, loads, method="elementar", index_name="range"): """Calculate the damage based on the methods * Miner elementar (k_2 = k) * Miner Haibach (k_2 = 2k-1) * Miner original (k_2 = -\inf) **Consider:** load collective and life curve have to be consistent: * range vs range * amplitude vs amplitude Parameters ---------- loads : pandas series histogram loads (index is the load, column the cycles) method : str * 'elementar': Miner elementar (k_2 = k) * 'MinerHaibach': Miner Haibach (k_2 = 2k-1) * 'original': Miner original (k_2 = -\inf) Returns ------- damage : pd.DataFrame damage for every load horizont based on the load collective and the method """ damage = pd.DataFrame(index=loads.index,columns=loads.columns, data=0) load_values = loads.index.get_level_values(index_name).mid.values # Miner elementar cycles_SN = self.calc_N(load_values, ignore_limits=True) if method == 'original': damage = loads.divide(cycles_SN,axis = 0) damage[load_values <= self.SD_50] = 0 elif method == 'elementar': damage = loads.divide(cycles_SN,axis = 0) elif method == 'MinerHaibach': # k2 cycles_SN[load_values <= self.SD_50] = self.ND_50 * (load_values[load_values <= self.SD_50] / self.SD_50)**(-(2*self.k_1-1)) damage = loads.divide(cycles_SN, axis=0) return damage