Source code for pylife.strength.woehler_fkm_nonlinear

# 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 pandas as pd
import numpy as np
import scipy.stats as stats
import warnings
import copy

from pylife import PylifeSignal


[docs] @pd.api.extensions.register_series_accessor('woehler_P_RAM') @pd.api.extensions.register_dataframe_accessor('woehler_P_RAM') class WoehlerCurvePRAM(PylifeSignal): """This class represents the type of (component) Wöhler curve that is used in the FKM nonlinear fatigue assessment with damage parameter P_RAM. The Wöhler Curve (aka SN-curve) determines after how many load cycles at a certain load amplitude the component is expected to fail. This Wöhler curve is defined piecewise with three sections: Two sections with slopes :math:`d_1`, :math:`d_2` and then a horizontal section at the endurance limit (cf. Sec. 2.5.6 of the FKM nonlinear document). The signal has the following mandatory keys: * ``d_1`` : The slope of the Wöhler Curve in the first section, for N < 1e3 * ``d_2`` : The slope of the Wöhler Curve in the second section, for N >= 1e3 * ``P_RAM_Z`` : The damage parameter value that separates the first and second section, corresponding to N = 1e3 * ``P_RAM_D`` : The damage parameter value of the endurance limit of the component, computed as P_RAM_D_WS / f_RAM (FKM nonlinear, eq. 2.6-89) """ def __init__(self, pandas_obj): self._obj = pandas_obj.copy() self._validate() def _validate(self): self.fail_if_key_missing(['P_RAM_Z', 'P_RAM_D', 'd_1', 'd_2']) is_not_nan = ~np.isnan(self._obj.P_RAM_Z) if not np.all(np.where(is_not_nan, self._obj.P_RAM_Z, 1) > np.where(is_not_nan, self._obj.P_RAM_D, 0)): raise ValueError(f"P_RAM_Z ({self._obj.P_RAM_Z}) has to be larger than P_RAM_D ({self._obj.P_RAM_D})!") if self._obj.d_1 >= 0: raise ValueError(f"d_1 ({self._obj.d_1}) has to be negative!") if self._obj.d_2 >= 0: raise ValueError(f"d_2 ({self._obj.d_2}) has to be negative!")
[docs] def get_woehler_curve_minimum_lifetime(self): """If this woehler curve is vectorized, i.e., holds values for multiple assessment points at once, get a version of this woehler curve for the assessment point with the minimum lifetime. This function is usually needed after an FKM nonlinear assessment for multiple points (e.g, o whole mesh at once), if the woehler curve should be plotted afterwards. Plotting is only possible for a specific woehler curve of a single point. If the woehler curve was for a single assessment point before, nothing is changed. Returns ------- woehler_curve_minimum_lifetime : WoehlerCurvePRAJ A deep copy of the current woehler curve object, but with scalar values. The resulting values are the minimum of the stored vectorized values. """ # compute the minimum/maximum for the vectorized items woehler_curve_minimum_lifetime = copy.deepcopy(self) woehler_curve_minimum_lifetime._obj.P_RAM_Z = np.min(woehler_curve_minimum_lifetime._obj.P_RAM_Z) woehler_curve_minimum_lifetime._obj.P_RAM_D = np.min(woehler_curve_minimum_lifetime._obj.P_RAM_D) return woehler_curve_minimum_lifetime
@property def d_1(self): """The slope of the Wöhler Curve in the first section, for N < 1e3""" return self._obj.d_1 @property def d_2(self): """The slope of the Wöhler Curve in the second section, for N >= 1e3""" return self._obj.d_2 @property def P_RAM_Z(self): """The damage parameter value that separates the first and second section, corresponding to N = 1e3""" return self._obj.P_RAM_Z @property def P_RAM_D(self): """The damage parameter value of the endurance limit""" return self._obj.P_RAM_D
[docs] def calc_N(self, P_RAM): """Evaluate the woehler curve at the given damage paramater value, P_RAM. Parameters ---------- P_RAM : float The damage parameter value where to evaluate the woehler curve. Returns ------- N : float The number of cycles for the given P_RAM value. """ # silence warning "divide by zero in np.power. This happens for P_RAM=0, but then it will use the second branch with N=np.inf anyways with np.errstate(divide='ignore'): N = np.where(P_RAM > self.fatigue_strength_limit, np.where(P_RAM >= self.P_RAM_Z, 1e3 * np.power(P_RAM / self.P_RAM_Z, 1/self.d_1), 1e3 * np.power(P_RAM / self.P_RAM_Z, 1/self.d_2)), np.inf) return N
[docs] def calc_P_RAM(self, N): """Evaluate the woehler curve at the specified number of cycles. Parameters ---------- N : array-like Number of cycles where to evaluate the woehler curve. Returns ------- array-like The P_RAM values that correspond to the given N values. """ N = np.array(N) # Note, this formula was derived visually from the figure 2.5 on page 43 of the FKM nonlinear document return np.where(N < 1e3, self.P_RAM_Z * np.power(N * 1e-3, self.d_1), np.where(N < self.fatigue_life_limit, self.P_RAM_Z * np.power(N * 1e-3, self.d_2), self.fatigue_strength_limit) )
@property def fatigue_strength_limit(self): """The fatigue strength limit of the component, i.e., the P_RAM value below which we have infinite life.""" return self.P_RAM_D @property def fatigue_life_limit(self): """The fatigue life limit N_D of the component, i.e., the number of cycles at the fatigue strength limit P_RAM_D.""" # exp(log(P_RAM_Z) + (log(N) - log(1e3))*d_2) # P_RAM_Z * exp((log(N) - log(1e3))*d_2) = fatigue_strength_limit for N=fatigue_life_limit # => P_RAM_Z * exp((log(fatigue_life_limit) - log(1e3))*d_2) = fatigue_strength_limit # => fatigue_life_limit = exp(log(fatigue_strength_limit / P_RAM_Z) / d_2 + log(1e3)) = 1e3 * (fatigue_strength_limit / P_RAM_Z)^(1/d_2) return 1e3 * (self.fatigue_strength_limit / self.P_RAM_Z) ** (1/self._obj.d_2)
[docs] @pd.api.extensions.register_series_accessor('woehler_P_RAJ') @pd.api.extensions.register_dataframe_accessor('woehler_P_RAJ') class WoehlerCurvePRAJ(PylifeSignal): """This class represents the type of (component) Wöhler curve that is used in the FKM nonlinear fatigue assessment with damage parameter P_RAJ. The Wöhler Curve (aka SN-curve) determines after how many load cycles at a certain load amplitude the component is expected to fail. This Wöhler curve is defined piecewise with two sections for finite and infinite life: The sloped section with slope :math:`d` and the horizontal section at the endurance limit (cf. Sec. 2.8.6 of the FKM nonlinear document). The signal has the following mandatory keys: * ``d_RAJ`` : The slope of the Wöhler Curve in the finite life section. * ``P_RAJ_Z`` : The load limit at N = 1 * ``P_RAJ_D_0`` : The initial load level of the endurance limit """ def __init__(self, pandas_obj): self._obj = pandas_obj.copy() self._validate() # eq. (2.9-27) self._P_RAJ_D = self._obj.P_RAJ_D_0 def _validate(self): self.fail_if_key_missing(['P_RAJ_Z', 'P_RAJ_D_0', 'd_RAJ']) is_not_nan = ~np.isnan(self._obj.P_RAJ_Z) if not np.all(np.where(is_not_nan, self._obj.P_RAJ_Z, 1) > np.where(is_not_nan, self._obj.P_RAJ_D_0, 0)): raise ValueError(f"P_RAJ_Z ({self._obj.P_RAJ_Z}) has to be larger than P_RAJ_D_0 ({self._obj.P_RAJ_D_0})!") if self._obj.d_RAJ >= 0: raise ValueError(f"d_RAJ ({self._obj.d_RAJ}) has to be negative!")
[docs] def update_P_RAJ_D(self, P_RAJ_D): """This method is used to update the fatigue strength P_RAJ_D, which is stored in this woehler curve. Parameters ---------- P_RAJ_D : pandas Series The new fatigue strength values that will be set for the woehler curve for multiple assessment points. """ self._P_RAJ_D = P_RAJ_D
[docs] def get_woehler_curve_minimum_lifetime(self): """If this woehler curve is vectorized, i.e., holds values for multiple assessment points at once, get a version of this woehler curve for the assessment point with the minimum lifetime. This function is usually needed after an FKM nonlinear assessment for multiple points (e.g, o whole mesh at once), if the woehler curve should be plotted afterwards. Plotting is only possible for a specific woehler curve of a single point. If the woehler curve was for a single assessment point before, nothing is changed. Returns ------- woehler_curve_minimum_lifetime : WoehlerCurvePRAJ A deep copy of the current woehler curve object, but with scalar values. The resulting values are the minimum of the stored vectorized values. """ woehler_curve_minimum_lifetime = copy.deepcopy(self) # get the minimum values for all vectorized values woehler_curve_minimum_lifetime._obj.P_RAJ_Z = np.min(woehler_curve_minimum_lifetime._obj.P_RAJ_Z) woehler_curve_minimum_lifetime._obj.P_RAJ_D_0 = np.min(woehler_curve_minimum_lifetime._obj.P_RAJ_D_0) return woehler_curve_minimum_lifetime
@property def d(self): """The slope of the Wöhler Curve in the first section""" return self._obj.d_RAJ @property def P_RAJ_D(self): """The fatigue strength for multiple assessment points.""" return self._P_RAJ_D @property def P_RAJ_Z(self): """The P_RAJ value for N=1.""" return self._obj.P_RAJ_Z
[docs] def calc_P_RAJ(self, N): """Evaluate the woehler curve at the specified number of cycles. Parameters ---------- N : array-like Number of cycles where to evaluate the woehler curve. Returns ------- array-like The P_RAJ values that correspond to the given N values. """ N = np.array(N) # Note, this formula was derived visually from the figure 2.18 on page 93 of the FKM nonlinear document # N = (P_RAJ / P_RAJ_Z) ^ (1/d) # N^d = P_RAJ / P_RAJ_Z # P_RAJ = P_RAJ_Z * N^d return np.where(N < self.fatigue_life_limit, self.P_RAJ_Z * np.power(N, self.d), self.fatigue_strength_limit)
[docs] def calc_N(self, P_RAJ, P_RAJ_D=None): """Evaluate the woehler curve at the given damage paramater value, P_RAJ. Parameters ---------- P_RAJ : float The damage parameter value where to evaluate the woehler curve. P_RAJ_D : float (optional) A different fatigue strength limit P_RAJ_D, if not set, the normal fatigue strength limit is used. Returns ------- N : float The number of cycles for the given P_RAJ value. """ if P_RAJ_D is None: P_RAJ_D = self._P_RAJ_D # silence warning "divide by zero in np.power. This happens for P_RAJ=0, but then it will use the second branch with N=np.inf anyways with np.errstate(divide='ignore'): N = np.where(P_RAJ > P_RAJ_D, np.power(P_RAJ / self._obj.P_RAJ_Z, 1/self._obj.d_RAJ), np.inf) return N
@property def fatigue_strength_limit(self): """The fatigue strength limit of the component.""" return self._obj.P_RAJ_D_0 @property def fatigue_strength_limit_final(self): """The fatigue strength limit of the component, after the FKM algorithm.""" return self._P_RAJ_D @property def fatigue_life_limit(self): """The fatigue strength limit N_D of the component, i.e., the number of cycles at the fatigue strength limit.""" # ND = (P_RAJ_D / P_RAJ_Z) ^ (1/d) return (self.fatigue_strength_limit / self.P_RAJ_Z) ** (1/self.d) @property def fatigue_life_limit_final(self): """The fatigue strength limit N_D of the component, i.e., the number of cycles at the fatigue strength limit, after the FKM algorithm.""" return (self.fatigue_strength_limit_final / self.P_RAJ_Z) ** (1/self.d)