Source code for pylife.strength.meanstress
# 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.
"""
Meanstress transformation
=========================
Meanstress transformation is used to take the influence of the meanstress of a
load hysteresis into account. The fatigue of a cyclically loaded component is
not only influenced by the amplitude but also by the mean stress. The usual
way to take this into account is to compute a substitute amplitude that has the
same fatigue result for the known mean stress. The mean stress sensitivity
is usually defined by a haigh diagram, depending on the R-value regime.
In pyLife we have the class :class:`~pylife.strength.meanstress.HaighDiagram`
that lets you define an arbitrary haigh diagram, i.e. a mean stress sensitivity
for each interval of `R`. There are also convenience functions
:class:`~pylife.strength.meanstress.HaighDiagram.fkm_goodman` and
:class:`~pylife.strength.meanstress.HaighDiagram.five_segment` that lets you
define the more common haigh diagrams more easily.
* FKM Goodman
* Five Segment Correction
"""
__author__ = "Johannes Mueller, Lena Rapp"
__maintainer__ = "Johannes Mueller"
from collections.abc import Iterable
import operator as op
import numpy as np
import pandas as pd
from pylife import PylifeSignal, Broadcaster
import pylife.stress.collective as CL
[docs]
@pd.api.extensions.register_series_accessor("haigh_diagram")
class HaighDiagram(PylifeSignal):
"""Model for a Haigh diagram in order to perform meanstress transformations.
A Haigh diagram a set of meanstress sensitivity slopes $M$ that is changing
with the R-values. The values of the ```pd.Series`` represents that slopes
$M$ and the `pd.IntervalIndex` represents the R-ranges.
"""
[docs]
@classmethod
def from_dict(cls, segments_dict):
"""Create a Haigh diagram from a dict.
Parameters
----------
segments_dict : dict
dict resolving the R-value intervals to the meanstress slope
Example
-------
>>> HaighDiagram.from_dict({
... (1.0, np.inf): 0.0,
... (-np.inf, 0.0): 0.5,
... (0.0, 1.0): 0.167
... }).to_pandas()
R
(1.0, inf] 0.000
(-inf, 0.0] 0.500
(0.0, 1.0] 0.167
dtype: float64
sets up a FKM Goodman like Haigh diagram.
"""
vals = np.array(list(segments_dict.values()))
idx = pd.IntervalIndex.from_tuples(list(segments_dict.keys()), name="R")
return HaighDiagram(pd.Series(vals, index=idx))
[docs]
@classmethod
def fkm_goodman(cls, haigh_fkm_goodman):
"""Create a Haigh diagram according to FKM Goodman.
Parameters
----------
haigh_fkm_goodman : pd.Series or pd.DataFrame
a series containing one or a dataframe containing multiple values for
`M` and optionally `M2`.
Notes
-----
The Haigh diagram according to FKM Goodman comes with the slope ``M``
which is valid between ``R==-inf`` and ``R==0``. Beyond ``R==0`` the slope
is ``M2` if ``M2`` is given or ``M/3`` if not.
Examples
--------
A FKM Goodman diagram with default ``M2``
>>> HaighDiagram.fkm_goodman(pd.Series({"M": 0.5})).to_pandas()
R
(1.0, inf] 0.000000
(-inf, 0.0] 0.500000
(0.0, 1.0] 0.166667
dtype: float64
A FKM Goodman diagram with manual ``M2``
>>> HaighDiagram.fkm_goodman(pd.Series({"M": 0.5, "M2": 0.2})).to_pandas()
R
(1.0, inf] 0.0
(-inf, 0.0] 0.5
(0.0, 1.0] 0.2
dtype: float64
>>> collective = pd.DataFrame(
... {
... "range": [600.0, 300.0, 500.0],
... "mean": [100.0, 50.0, 80.0],
... "cycles": [1.0, 10.0, 100.0],
... }
... )
>>> HaighDiagram.fkm_goodman(pd.Series({"M": 0.5})).transform(collective, -1.0)
range mean cycles
0 700.0 0.0 1.0
1 350.0 0.0 10.0
2 580.0 0.0 100.0
"""
if "M2" not in haigh_fkm_goodman:
haigh_fkm_goodman["M2"] = haigh_fkm_goodman["M"] / 3.0
M = haigh_fkm_goodman.M
M2 = haigh_fkm_goodman.M2
interval_index = pd.IntervalIndex.from_tuples(
[(1.0, np.inf), (-np.inf, 0.0), (0.0, 1.0)], name="R"
)
if isinstance(haigh_fkm_goodman, pd.Series):
haigh_index = interval_index
dummy_index = pd.Index([0, 1, 2], name="R")
else:
haigh_frame, _ = Broadcaster(haigh_fkm_goodman.index.to_frame()).broadcast(
interval_index.to_frame()
)
haigh_index = haigh_frame.index
dummy_index = pd.Index([0, 1, 2] * len(haigh_fkm_goodman), name="R")
haigh = pd.Series(0.0, index=dummy_index)
R_index = haigh.index.get_level_values("R")
haigh.iloc[R_index.get_indexer_for([1])] = M
haigh.iloc[R_index.get_indexer_for([2])] = M2
haigh.index = haigh_index
return cls(haigh)
[docs]
@classmethod
def five_segment(cls, five_segment_haigh_diagram):
"""Create a five segment slope Haigh diagram.
Parameters
----------
five_segment_haigh_diagram : :class:`pandas.Series` or :class:`pandas.DataFrame`
The five segment meanstress slope data.
Notes
-----
``five_segment_hagih_diagram`` has to provide the following keys:
* ``M0``: the mean stress sensitivity between ``R==-inf`` and ``R==0``
* ``M1``: the mean stress sensitivity between ``R==0`` and ``R==R12``
* ``M2``: the mean stress sensitivity betwenn ``R==R12`` and ``R==R23``
* ``M3``: the mean stress sensitivity between ``R==R23`` and ``R==1``
* ``M4``: the mean stress sensitivity beyond ``R==1``
* ``R12``: R-value between ``M1`` and ``M2``
* ``R23``: R-value between ``M2`` and ``M3``
Examples
--------
>>> haigh = HaighDiagram.five_segment(
... pd.Series(
... {"M0": 0.5, "M1": 0.25, "M2": 0.125, "M3": 1.0, "M4": -2.0, "R12": 0.2, "R23": 0.8}
... )
... )
>>> haigh.to_pandas()
R
(1.0, inf] -2.000
(-inf, 0.0] 0.500
(0.0, 0.2] 0.250
(0.2, 0.8] 0.125
(0.8, 1.0] 1.000
dtype: float64
>>> collective = pd.DataFrame(
... {
... "range": [600.0, 300.0, 500.0],
... "mean": [400.0, -150.0, 0.0],
... "cycles": [1.0, 10.0, 100.0],
... }
... )
>>> haigh.transform(collective, 0.0)
range mean cycles
0 640.000000 320.000000 1.0
1 100.000000 50.000000 10.0
2 333.333333 166.666667 100.0
"""
was_series = isinstance(five_segment_haigh_diagram, pd.Series)
if was_series:
five_segment_haigh_diagram = pd.DataFrame(five_segment_haigh_diagram).T
index_names = five_segment_haigh_diagram.index.names + ["R"]
def make_index(h):
orig_index = [h.name] if not isinstance(h.name, Iterable) else list(h.name)
return pd.MultiIndex.from_tuples(
[
tuple(orig_index + [pd.Interval(1.0, np.inf)]),
tuple(orig_index + [pd.Interval(-np.inf, 0.0)]),
tuple(orig_index + [pd.Interval(0.0, h.R12)]),
tuple(orig_index + [pd.Interval(h.R12, h.R23)]),
tuple(orig_index + [pd.Interval(h.R23, 1.0)]),
],
names=index_names,
).to_frame()
haigh_index = pd.concat(
list(five_segment_haigh_diagram.apply(make_index, axis=1))
).index
haigh = pd.Series(0.0, index=haigh_index)
R_index = haigh.index.get_level_values("R")
h, _ = Broadcaster(haigh).broadcast(five_segment_haigh_diagram)
M4_locs = R_index.get_indexer_for([pd.Interval(1.0, np.inf)])
M0_locs = R_index.get_indexer_for([pd.Interval(-np.inf, 0.0)])
M1_locs = R_index.get_indexer_for([pd.Interval(0.0, R12) for R12 in h.R12])
M2_locs = R_index.get_indexer_for(
[pd.Interval(R12, R23) for R12, R23 in zip(h.R12, h.R23)]
)
M3_locs = R_index.get_indexer_for([pd.Interval(R23, 1.0) for R23 in h.R23])
haigh.iloc[M4_locs] = h.M4.iloc[M4_locs]
haigh.iloc[M0_locs] = h.M0.iloc[M0_locs]
haigh.iloc[M1_locs] = h.M1.iloc[M1_locs]
haigh.iloc[M2_locs] = h.M2.iloc[M2_locs]
haigh.iloc[M3_locs] = h.M3.iloc[M3_locs]
if was_series:
haigh = haigh.xs(0)
return cls(haigh)
[docs]
def transform(self, collective, R_goal):
"""Transform a load collective to defined R-value.
Parameters
----------
collective : pd.DataFrame
The load collective data to transform containing either `range` and
`mean` or `from` and `to` columns to describe the load cycles. All other
columns e.g. `cycles` are copied to the result.
R_goal : float
The target R-value for the transformation.
Returns
-------
pd.DataFrame
DataFrame with columns:
- 'range': transformed amplitude range
- 'mean': transformed mean value
- 'cycles': copied unchanged from the input if present
Examples
--------
>>> collective = pd.DataFrame(
... {
... "from": [300.0, -150.0, -250.0],
... "to": [-300.0, 150.0, 250.0],
... "cycles": [1.0, 10.0, 100.0],
... }
... )
>>> HaighDiagram.from_dict({(-np.inf, np.inf): 0.5}).transform(collective, 0.0)
range mean cycles
0 400.000000 200.000000 1.0
1 200.000000 100.000000 10.0
2 333.333333 166.666667 100.0
>>> collective = pd.DataFrame(
... {
... "range": [600.0, 300.0, 500.0],
... "mean": [100.0, 50.0, 80.0],
... "cycles": [1.0, 10.0, 100.0],
... }
... )
>>> HaighDiagram.from_dict({(-np.inf, np.inf): 0.5}).transform(collective, -1.0)
range mean cycles
0 700.0 0.0 1.0
1 350.0 0.0 10.0
2 580.0 0.0 100.0
"""
broadcasted_coll, haigh = self.broadcast(collective, droplevel=["R"])
coll = CL.LoadCollective(broadcasted_coll)
transformer = _SegmentTransformer(coll, haigh, self._R_index, R_goal)
for interval in transformer.segments_left_from_R_goal():
interval_boundary = (
interval.right if interval.right < 1.0 else interval.left
)
transformer.transform_cycles_in_interval(interval, interval_boundary)
for interval in transformer.segments_right_from_R_goal():
transformer.transform_cycles_in_interval(interval, interval.left)
for interval in transformer.segments_containing_R_goal():
transformer.transform_cycles_in_interval(interval, R_goal)
transformed_cycles = transformer.transformed_cycles
res = pd.DataFrame(
{
"range": 2.0 * transformed_cycles.amplitude,
"mean": transformed_cycles.amplitude
* ((1.0 + transformed_cycles.R) / (1.0 - transformed_cycles.R)).fillna(
-1.0
),
},
index=broadcasted_coll.index,
)
for col in collective:
if col not in coll.columns:
res[col] = collective[col]
return res
def _validate(self):
def has_gaps(idx):
if len(idx) <= 1:
return False
return (
pd.DataFrame({"l": idx.left[1:], "r": idx.right[:-1]})
.apply(
lambda r: r.l != r.r and not (r.l == -np.inf and r.r == np.inf),
axis=1,
)
.any()
)
self._R_index = self._find_R_index()
if self._check_if_R_index(lambda idx: idx.is_overlapping):
raise AttributeError(
"The intervals of the 'R' IntervalIndex must not overlap."
)
if self._check_if_R_index(has_gaps):
raise AttributeError(
"The intervals of the 'R' IntervalIndex must not have gaps."
)
def _find_R_index(self):
if "R" not in self._obj.index.names:
raise AttributeError("A Haigh Diagram needs an index level 'R'.")
if isinstance(self._obj.index, pd.MultiIndex):
R_index = self._obj.index.unique("R")
else:
R_index = self._obj.index
if not isinstance(R_index, pd.IntervalIndex):
raise AttributeError("The 'R' index must be an IntervalIndex.")
return R_index
def _check_if_R_index(self, check_func):
if isinstance(self._obj.index, pd.IntervalIndex):
return check_func(self._obj.index)
all_but_R = [n or 0 for n in self._obj.index.names if n != "R"]
return (
self._obj.index.to_frame(index=False)
.groupby(all_but_R)
.apply(lambda g: check_func(g.set_index("R").index), include_groups=False)
.any()
)
class _SegmentTransformer:
def __init__(self, collective, haigh, R_segments, R_goal):
self.transformed_cycles = pd.DataFrame(
{"amplitude": collective.amplitude, "R": collective.R},
index=collective.to_pandas().index,
)
self._haigh = haigh
self._R_index = R_segments
self._R_goal = R_goal
self._distances = self._distance_from_R_goal()
def segments_left_from_R_goal(self):
return self._distances[self._distances < 0.0].sort_values(ascending=True).index
def segments_right_from_R_goal(self):
return self._distances[self._distances > 0.0].sort_values(ascending=False).index
def segments_containing_R_goal(self):
goal_segments = self._R_index.contains(self._R_goal)
if not goal_segments.any():
goal_segments = self._R_index.set_closed("left").contains(self._R_goal)
return self._R_index[goal_segments]
def _distance_from_R_goal(self):
def fake_meanstress(R):
return (1.0 + R) / (1.0 - R)
meanstress = fake_meanstress(self._R_index.mid).fillna(-1.0)
meanstress_goal = (
-1.0 if self._R_goal == -np.inf else fake_meanstress(self._R_goal)
)
return pd.Series(meanstress.values - meanstress_goal, index=self._R_index)
def transform_cycles_in_interval(self, interval, R_goal):
def push_over_flipping_point(R):
if R == -np.inf and R_goal > 1.0:
return np.inf
if R == np.inf and R_goal < 1.0:
return -np.inf
return R
def cycles_in_current_interval():
R = self.transformed_cycles.R.apply(push_over_flipping_point)
test_interval = pd.Interval(interval.left, interval.right, closed="both")
return R.apply(lambda R: R in test_interval)
def cycles_in_current_segments(in_test_interval, segments_index):
in_segments_index = pd.Series(False, index=self.transformed_cycles.index)
in_segments_index[segments_index] = True
return in_test_interval & in_segments_index
def meanstress_sensitivity_segments_of_current_interval():
return self._haigh.xs(interval, level="R")
def transformed_amplitude():
rf = self.transformed_cycles.loc[to_shift]
amp = rf.amplitude
mean = amp * (1.0 + rf.R) / (1.0 - rf.R)
mean[rf.R == -np.inf] = -amp[rf.R == -np.inf]
mean[rf.R == 1.0] = -amp[rf.R == 1.0]
if R_goal == -np.inf:
trans_amp = (amp + M * mean) / (1.0 - M)
else:
trans_amp = (
(1.0 - R_goal)
* (amp + M * mean)
/ (1.0 - R_goal + M * (1.0 + R_goal))
)
return trans_amp.fillna(0.0)
to_shift = cycles_in_current_interval()
if not to_shift.any():
return
M = meanstress_sensitivity_segments_of_current_interval()
to_shift = cycles_in_current_segments(to_shift, M.index)
if not to_shift.any():
return
if R_goal == 1.0:
R_goal = -np.inf
self.transformed_cycles.loc[to_shift, "amplitude"] = transformed_amplitude()
self.transformed_cycles.loc[to_shift, "R"] = R_goal
[docs]
def experimental_mean_stress_sensitivity(sn_curve_R0, sn_curve_Rn1, N_c=np.inf):
r"""Estimate the mean stress sensitivity from two `FiniteLifeCurve` objects for the same amount of cycles `N_c`.
The formula for calculation is taken from: "Betriebsfestigkeit", Haibach, 3. Auflage 2006
Formula (2.1-24):
.. math::
M_{\sigma} = {S_a}^{R=-1}(N_c) / {S_a}^{R=0}(N_c) - 1
Alternatively the mean stress sensitivity is calculated based on both SD values
(if N_c is not given).
Parameters
----------
sn_curve_R0: pylife.strength.sn_curve.FiniteLifeCurve
Instance of FiniteLifeCurve for R == 0
sn_curve_Rn1: pylife.strength.sn_curve.FiniteLifeCurve
Instance of FiniteLifeCurve for R == -1
N_c: float, (default=np.inf)
Amount of cycles where the amplitudes should be compared.
If N_c is higher than a fatigue transition point (ND) for the SN-Curves, SD is taken.
If N_c is None, SD values are taken as stress amplitudes instead.
Returns
-------
float
Mean stress sensitivity M_sigma
Raises
------
ValueError
if the resulting M_sigma doesn't lie in the range from 0 to 1 a ValueError is raised, as this value would
suggest higher strength with additional loads.
"""
S_a_R0 = (
sn_curve_R0.woehler.basquin_load(N_c)
if N_c < sn_curve_R0.ND
else sn_curve_R0.SD
)
S_a_Rn1 = (
sn_curve_Rn1.woehler.basquin_load(N_c)
if N_c < sn_curve_Rn1.ND
else sn_curve_Rn1.SD
)
M_sigma = S_a_Rn1 / S_a_R0 - 1
if not 0 <= M_sigma <= 1:
raise ValueError(
"M_sigma: %.2f exceeds the interval [0, 1] which is not plausible."
% M_sigma
)
return M_sigma
[docs]
@pd.api.extensions.register_dataframe_accessor("meanstress_transform")
class MeanstressTransformCollective(CL.LoadCollective):
"""Meanstress transformer class for a load collective."""
[docs]
def fkm_goodman(self, goodman, R_goal):
""" Perform a FKM Goodman transformation on a load collective
Parameters
----------
goodman: pd.Series or pd.DataFrame
The meanstress sensitivity data needs `M` and optionally `M2`
R_goal: float
The R-value to transform to
Returns
-------
transformed_collective: LoadCollective
The transformed load collective
Examples
--------
>>> collective = pd.DataFrame(
... {
... "from": [300.0, -150.0, -250.0],
... "to": [-300.0, 150.0, 250.0],
... "cycles": [1.0, 10.0, 100.0],
... }
... )
>>> collective.meanstress_transform.fkm_goodman(pd.Series({"M": 0.5}), 0.0).amplitude
0 200.000000
1 100.000000
2 166.666667
Name: amplitude, dtype: float64
"""
res = HaighDiagram.fkm_goodman(goodman).transform(self._obj, R_goal)
return res.load_collective
[docs]
def five_segment(self, five_segment, R_goal):
""" Perform a Five segment transformation on a load collective
Parameters
----------
five_segment: pd.Series or pd.DataFrame
The meanstress sensitivities and R transition values (see :meth:`HaighDiagram.five_segment`)
R_goal: float
The R-value to transform to
Returns
-------
transformed_collective: LoadCollective
The transformed load collective. After the meanstress transformation,
the resulting ``(range, mean)`` interval bins may no longer be
continuous.
Examples
--------
>>> collective = pd.DataFrame(
... {
... "range": [600.0, 300.0, 500.0],
... "mean": [400.0, -150.0, 0.0],
... "cycles": [1.0, 10.0, 100.0],
... }
... )
>>> haigh = pd.Series(
... {"M0": 0.5, "M1": 0.25, "M2": 0.125, "M3": 1.0, "M4": -2.0, "R12": 0.2, "R23": 0.8}
... )
>>> transformed = collective.meanstress_transform.five_segment(haigh, 0.0)
>>> transformed.amplitude
0 320.000000
1 50.000000
2 166.666667
Name: amplitude, dtype: float64
>>> transformed.meanstress
0 320.000000
1 50.000000
2 166.666667
Name: meanstress, dtype: float64
"""
hd = HaighDiagram.five_segment(five_segment)
res = hd.transform(self._obj, R_goal)
return res.load_collective
[docs]
@pd.api.extensions.register_series_accessor("meanstress_transform")
class MeanstressTransformMatrix(CL.LoadHistogram):
"""Meanstress transformer class for a load histogram."""
def _validate(self):
super()._validate()
if set(self._obj.index.names).issuperset({"from", "to"}):
f = self._obj.index.get_level_values("from").mid
t = self._obj.index.get_level_values("to").mid
self._Sa = np.abs(f - t) / 2.0
self._Sm = (f + t) / 2.0
self._binsize_x = self._obj.index.get_level_values("from").length.min()
self._binsize_y = self._obj.index.get_level_values("to").length.min()
self._remaining_names = list(
filter(lambda n: n not in ["from", "to"], self._obj.index.names)
)
else:
self._Sa = self._obj.index.get_level_values("range").mid / 2.0
self._Sm = self._obj.index.get_level_values("mean").mid
self._binsize_x = self._obj.index.get_level_values("range").length.min()
self._binsize_y = self._obj.index.get_level_values("mean").length.min()
self._remaining_names = list(
filter(lambda n: n not in ["range", "mean"], self._obj.index.names)
)
[docs]
def fkm_goodman(self, goodman, R_goal):
""" Perform a FKM Goodman transformation on a load histogram
Parameters
----------
goodman: pd.Series or pd.DataFrame
The meanstress sensitivity data needs `M` and optionally `M2`.
R_goal: float
The R-value to transform to
Returns
-------
transformed_histogram: LoadHistogram
The transformed load histogram. After the meanstress transformation,
the resulting ``(range, mean)`` interval bins may no longer be
continuous.
Notes
-----
If continuous bins are required afterwards, e.g. for visualization, the
transformed histogram can optionally be rebinned. Be aware that such a
rebinning is a post-processing step for presentation purposes and may
reduce the accuracy of the transformed histogram.
Examples
--------
>>> histogram = pd.Series(
... [10, 90, 900, 9000, 90000, 900000],
... index=pd.MultiIndex.from_arrays(
... [
... pd.IntervalIndex.from_arrays(
... [650, 550, 450, 350, 250, 150], [750, 650, 550, 450, 350, 250]
... ),
... pd.IntervalIndex.from_arrays([0, 0, 0, 0 ,0, 0], [0, 0, 0, 0, 0, 0]),
... ],
... names=["range", "mean"],
... ),
... name="cycles"
... )
>>> transformed = histogram.meanstress_transform.fkm_goodman(pd.Series({"M": 0.0}), 0)
>>> transformed.amplitude
range mean
(650.0, 750.0] (325.0, 375.0] 350.0
(550.0, 650.0] (275.0, 325.0] 300.0
(450.0, 550.0] (225.0, 275.0] 250.0
(350.0, 450.0] (175.0, 225.0] 200.0
(250.0, 350.0] (125.0, 175.0] 150.0
(150.0, 250.0] (75.0, 125.0] 100.0
Name: amplitude, dtype: float64
"""
transformer = HaighDiagram.fkm_goodman(goodman)
return self._perform_transformation(transformer, R_goal)
[docs]
def five_segment(self, five_segment, R_goal):
""" Perform a Five segment transformation on a load histogram
Parameters
----------
five_segment: pd.Series or pd.DataFrame
The meanstress sensitivities and R transition values (see :meth:`HaighDiagram.five_segment`)
R_goal: float
The R-value to transform to
Returns
-------
transformed_histogram: LoadHistogram
The transformed load histogram. After the meanstress transformation,
the resulting ``(range, mean)`` interval bins may no longer be
continuous.
Notes
-----
If continuous bins are required afterwards, e.g. for visualization, the
transformed histogram can optionally be rebinned. Be aware that such a
rebinning is a post-processing step for presentation purposes and may
reduce the accuracy of the transformed histogram.
Examples
--------
>>> histogram = pd.Series(
... [10, 90, 900, 9000, 90000, 900000],
... index=pd.MultiIndex.from_arrays(
... [
... pd.IntervalIndex.from_arrays(
... [650, 550, 450, 350, 250, 150], [750, 650, 550, 450, 350, 250]
... ),
... pd.IntervalIndex.from_arrays([0, 0, 0, 0 ,0, 0], [0, 0, 0, 0, 0, 0]),
... ],
... names=["range", "mean"],
... ),
... name="cycles"
... )
>>> five_segments = pd.Series(
... {
... "M0": 0.5,
... "M1": 0.2,
... "M2": 0.1,
... "M3": 1.0,
... "M4": 2.0,
... "R12": 0.2,
... "R23": 0.8,
... }
... )
>>> transformed = histogram.meanstress_transform.five_segment(five_segments, 0)
>>> transformed.amplitude
range mean
(433.3333333333333, 500.0] (216.66666666666666, 250.0] 233.333333
(366.6666666666667, 433.3333333333333] (183.33333333333334, 216.66666666666666] 200.000000
(300.0, 366.6666666666667] (150.0, 183.33333333333334] 166.666667
(233.33333333333334, 300.0] (116.66666666666667, 150.0] 133.333333
(166.66666666666669, 233.33333333333334] (83.33333333333334, 116.66666666666667] 100.000000
(100.0, 166.66666666666669] (50.0, 83.33333333333334] 66.666667
Name: amplitude, dtype: float64
"""
transformer = HaighDiagram.five_segment(five_segment)
return self._perform_transformation(transformer, R_goal)
def _perform_transformation(self, transformer, R_goal):
mean_left = self.use_class_left().meanstress.reset_index(drop=True)
mean_right = self.use_class_right().meanstress.reset_index(drop=True)
range_index = self.amplitude_histogram.index
range_left = 2.0 * range_index.left.to_series().reset_index(drop=True)
range_right = 2.0 * range_index.right.to_series().reset_index(drop=True)
orig_lele = pd.DataFrame({"range": range_left, "mean": mean_left})
orig_lere = pd.DataFrame({"range": range_left, "mean": mean_right})
orig_rele = pd.DataFrame({"range": range_right, "mean": mean_left})
orig_rere = pd.DataFrame({"range": range_right, "mean": mean_right})
transformed_lele = transformer.transform(orig_lele, R_goal)
transformed_lere = transformer.transform(orig_lere, R_goal)
transformed_rele = transformer.transform(orig_rele, R_goal)
transformed_rere = transformer.transform(orig_rere, R_goal)
transformed = self._obj.to_frame()
have_additional_indeces = len(transformed.index.names) > 2
if have_additional_indeces:
transformed.index = transformed.index.droplevel(self.index_levels)
range = pd.DataFrame(
{
0: transformed_lele["range"],
1: transformed_lere["range"],
2: transformed_rele["range"],
3: transformed_rere["range"],
}
)
transformed["range"] = pd.IntervalIndex.from_arrays(
range.min(axis=1), range.max(axis=1), name="range"
)
mean = pd.DataFrame(
{
0: transformed_lele["mean"],
1: transformed_lere["mean"],
2: transformed_rele["mean"],
3: transformed_rere["mean"],
}
)
transformed["mean"] = pd.IntervalIndex.from_arrays(
mean.min(axis=1), mean.max(axis=1), name="mean"
)
result = transformed.set_index(["range", "mean"], append=have_additional_indeces, drop=True).iloc[:, 0]
new_names = self._obj.index.to_frame().rename(columns={"from": "range", "to": "mean"}).columns
result.index = result.index.reorder_levels(new_names)
result.name = self._obj.name
return CL.LoadHistogram(result)
[docs]
def fkm_goodman(amplitude, meanstress, M, M2, R_goal):
cycles = pd.DataFrame({"range": 2.0 * amplitude, "mean": meanstress})
haigh_fkm_goodman = pd.Series({"M": M, "M2": M2})
hd = HaighDiagram.fkm_goodman(haigh_fkm_goodman)
res = hd.transform(cycles, R_goal)
return res.load_collective.amplitude.to_numpy()
[docs]
def five_segment_correction(
amplitude, meanstress, M0, M1, M2, M3, M4, R12, R23, R_goal
):
"""Performs a mean stress transformation to R_goal according to the
Five Segment Mean Stress Correction
:param Sa: the stress amplitude
:param Sm: the mean stress
:param Rgoal: the R-value to transform to
:param M: the mean stress sensitivity between R=-inf and R=0
:param M1: the mean stress sensitivity between R=0 and R=R12
:param M2: the mean stress sensitivity betwenn R=R12 and R=R23
:param M3: the mean stress sensitivity between R=R23 and R=1
:param M4: the mean stress sensitivity beyond R=1
:param R12: R-value between M1 and M2
:param R23: R-value between M2 and M3
:returns: the transformed stress range
"""
cycles = pd.DataFrame({"range": 2.0 * amplitude, "mean": meanstress})
haigh_five_segment = pd.Series(
{"M0": M0, "M1": M1, "M2": M2, "M3": M3, "M4": M4, "R12": R12, "R23": R23}
)
hd = HaighDiagram.five_segment(haigh_five_segment)
res = hd.transform(cycles, R_goal)
return res.load_collective.amplitude.to_numpy()