Load Collectives and Load Histograms
From the load (stress) side pyLife provides the classes LoadCollective
and LoadHistogram
to deal with load collectives. LoadCollective
contains individal hysteresis loops whereas LoadHistogram
contains a 2D-histogram of classes of hysteresis loops and the number of cycles with which they occur.
[1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pylife.stress.timesignal as TS
import pylife.stress.rainflow as RF
import pylife.strength.meanstress as MS
import pylife.strength.fatigue
plt.rcParams['figure.figsize'] = [12, 7.5]
A simple load signal
Let’s take a look at a really simple load signal:
[2]:
load_signal = np.array([0., 2.0, -2.0, 1.0, -1.0, 2.0, -2.0, 1.0, -1.0, 2.0, -2.0, 1.0, -1.0, 2.0, 0.])
plt.plot(load_signal)
[2]:
[<matplotlib.lines.Line2D at 0x7f72169fe990>]
Now let’s perform a rainflow analysis.
[3]:
detector = RF.FourPointDetector(recorder=RF.LoopValueRecorder())
detector.process(load_signal)
[3]:
<pylife.stress.rainflow.fourpoint.FourPointDetector at 0x7f7216a53390>
The detector now contains the recorder which recorded the hysteresis loops for us. The simple load collective comes as a attribute of the detector:
[4]:
collective = detector.recorder.collective
collective
[4]:
from | to | |
---|---|---|
0 | 1.0 | -1.0 |
1 | -2.0 | 2.0 |
2 | 1.0 | -1.0 |
3 | -2.0 | 2.0 |
4 | 1.0 | -1.0 |
As you can see, the rainflow analysis found five histresis loops, three from 1.0 to -1.0 and two from -2.0 to 2.0. Alternatively you can ask the recorder for a load histogram:
[5]:
histogram = detector.recorder.histogram(bins=6)
histogram
[5]:
from to
(-2.0, -1.5] (-1.0, -0.5] 0.0
(-0.5, 0.0] 0.0
(0.0, 0.5] 0.0
(0.5, 1.0] 0.0
(1.0, 1.5] 0.0
(1.5, 2.0] 2.0
(-1.5, -1.0] (-1.0, -0.5] 0.0
(-0.5, 0.0] 0.0
(0.0, 0.5] 0.0
(0.5, 1.0] 0.0
(1.0, 1.5] 0.0
(1.5, 2.0] 0.0
(-1.0, -0.5] (-1.0, -0.5] 0.0
(-0.5, 0.0] 0.0
(0.0, 0.5] 0.0
(0.5, 1.0] 0.0
(1.0, 1.5] 0.0
(1.5, 2.0] 0.0
(-0.5, 0.0] (-1.0, -0.5] 0.0
(-0.5, 0.0] 0.0
(0.0, 0.5] 0.0
(0.5, 1.0] 0.0
(1.0, 1.5] 0.0
(1.5, 2.0] 0.0
(0.0, 0.5] (-1.0, -0.5] 0.0
(-0.5, 0.0] 0.0
(0.0, 0.5] 0.0
(0.5, 1.0] 0.0
(1.0, 1.5] 0.0
(1.5, 2.0] 0.0
(0.5, 1.0] (-1.0, -0.5] 3.0
(-0.5, 0.0] 0.0
(0.0, 0.5] 0.0
(0.5, 1.0] 0.0
(1.0, 1.5] 0.0
(1.5, 2.0] 0.0
dtype: float64
This is a bit hard to read. What you see is a pands.Series
that has a two dimensional IntervalIndex
as index. The histogram is all empty except the two classes from
: (-2.0, 1.5] to
: (1.5, 2.0] has 2.0 cycles and from
: (0.5, 1.0] to
: (-1.0, -1.5] has 3.0 cycles. Tose correspond to the two loops from -2.0 to 2.0 and the three loops 1.0 to -1.0.
Working with load collectives and load histograms
A load collective and a load histogram can be processed by the two classes LoadCollective
and LoadHistogram
. Both inherit from the common base class AbstractLoadCollective
. There is the common accessor attribute load_collective
that convert a pandas object with the load collective resp. load histogram data into the corresponding class.
First let’s look at a load collective. You can easily calculate the amplitude of each hysteresis loop:
[6]:
cl = collective.load_collective
cl.amplitude
[6]:
0 1.0
1 2.0
2 1.0
3 2.0
4 1.0
Name: amplitude, dtype: float64
Same for the mean stress and the R-value:
[7]:
cl.meanstress, cl.R
[7]:
(0 0.0
1 0.0
2 0.0
3 0.0
4 0.0
Name: meanstress, dtype: float64,
0 -1.0
1 -1.0
2 -1.0
3 -1.0
4 -1.0
Name: R, dtype: float64)
There is also the attribute cycles
:
[8]:
cl.cycles
[8]:
0 1.0
1 1.0
2 1.0
3 1.0
4 1.0
Name: cycles, dtype: float64
As you can see, the cycles are all 1.0 because we have an entry for each indivudual hysteresis loop which by definition occurs only once.
Now let’s take a look at the histogram:
[9]:
hi = histogram.load_collective
hi.amplitude, hi.meanstress, hi.R
[9]:
(from to
(-2.0, -1.5] (-1.0, -0.5] 0.50
(-0.5, 0.0] 0.75
(0.0, 0.5] 1.00
(0.5, 1.0] 1.25
(1.0, 1.5] 1.50
(1.5, 2.0] 1.75
(-1.5, -1.0] (-1.0, -0.5] 0.25
(-0.5, 0.0] 0.50
(0.0, 0.5] 0.75
(0.5, 1.0] 1.00
(1.0, 1.5] 1.25
(1.5, 2.0] 1.50
(-1.0, -0.5] (-1.0, -0.5] 0.00
(-0.5, 0.0] 0.25
(0.0, 0.5] 0.50
(0.5, 1.0] 0.75
(1.0, 1.5] 1.00
(1.5, 2.0] 1.25
(-0.5, 0.0] (-1.0, -0.5] 0.25
(-0.5, 0.0] 0.00
(0.0, 0.5] 0.25
(0.5, 1.0] 0.50
(1.0, 1.5] 0.75
(1.5, 2.0] 1.00
(0.0, 0.5] (-1.0, -0.5] 0.50
(-0.5, 0.0] 0.25
(0.0, 0.5] 0.00
(0.5, 1.0] 0.25
(1.0, 1.5] 0.50
(1.5, 2.0] 0.75
(0.5, 1.0] (-1.0, -0.5] 0.75
(-0.5, 0.0] 0.50
(0.0, 0.5] 0.25
(0.5, 1.0] 0.00
(1.0, 1.5] 0.25
(1.5, 2.0] 0.50
Name: amplitude, dtype: float64,
from to
(-2.0, -1.5] (-1.0, -0.5] -1.25
(-0.5, 0.0] -1.00
(0.0, 0.5] -0.75
(0.5, 1.0] -0.50
(1.0, 1.5] -0.25
(1.5, 2.0] 0.00
(-1.5, -1.0] (-1.0, -0.5] -1.00
(-0.5, 0.0] -0.75
(0.0, 0.5] -0.50
(0.5, 1.0] -0.25
(1.0, 1.5] 0.00
(1.5, 2.0] 0.25
(-1.0, -0.5] (-1.0, -0.5] -0.75
(-0.5, 0.0] -0.50
(0.0, 0.5] -0.25
(0.5, 1.0] 0.00
(1.0, 1.5] 0.25
(1.5, 2.0] 0.50
(-0.5, 0.0] (-1.0, -0.5] -0.50
(-0.5, 0.0] -0.25
(0.0, 0.5] 0.00
(0.5, 1.0] 0.25
(1.0, 1.5] 0.50
(1.5, 2.0] 0.75
(0.0, 0.5] (-1.0, -0.5] -0.25
(-0.5, 0.0] 0.00
(0.0, 0.5] 0.25
(0.5, 1.0] 0.50
(1.0, 1.5] 0.75
(1.5, 2.0] 1.00
(0.5, 1.0] (-1.0, -0.5] 0.00
(-0.5, 0.0] 0.25
(0.0, 0.5] 0.50
(0.5, 1.0] 0.75
(1.0, 1.5] 1.00
(1.5, 2.0] 1.25
Name: meanstress, dtype: float64,
from to
(-2.0, -1.5] (-1.0, -0.5] 2.333333
(-0.5, 0.0] 7.000000
(0.0, 0.5] -7.000000
(0.5, 1.0] -2.333333
(1.0, 1.5] -1.400000
(1.5, 2.0] -1.000000
(-1.5, -1.0] (-1.0, -0.5] 1.666667
(-0.5, 0.0] 5.000000
(0.0, 0.5] -5.000000
(0.5, 1.0] -1.666667
(1.0, 1.5] -1.000000
(1.5, 2.0] -0.714286
(-1.0, -0.5] (-1.0, -0.5] 1.000000
(-0.5, 0.0] 3.000000
(0.0, 0.5] -3.000000
(0.5, 1.0] -1.000000
(1.0, 1.5] -0.600000
(1.5, 2.0] -0.428571
(-0.5, 0.0] (-1.0, -0.5] 3.000000
(-0.5, 0.0] 1.000000
(0.0, 0.5] -1.000000
(0.5, 1.0] -0.333333
(1.0, 1.5] -0.200000
(1.5, 2.0] -0.142857
(0.0, 0.5] (-1.0, -0.5] -3.000000
(-0.5, 0.0] -1.000000
(0.0, 0.5] 1.000000
(0.5, 1.0] 0.333333
(1.0, 1.5] 0.200000
(1.5, 2.0] 0.142857
(0.5, 1.0] (-1.0, -0.5] -1.000000
(-0.5, 0.0] -0.333333
(0.0, 0.5] 0.333333
(0.5, 1.0] 1.000000
(1.0, 1.5] 0.600000
(1.5, 2.0] 0.428571
Name: R, dtype: float64)
This might look a bit confusing as this only shows the amplitudes, meanstresses and R-values correspond to the bins of the histogram. Remember, that they were all except two empty. So let’s restrict the histogram to bins that are not empty:
[10]:
not_empty = histogram > 0.0
hi.amplitude[not_empty], hi.cycles[not_empty]
[10]:
(from to
(-2.0, -1.5] (1.5, 2.0] 1.75
(0.5, 1.0] (-1.0, -0.5] 0.75
Name: amplitude, dtype: float64,
from to
(-2.0, -1.5] (1.5, 2.0] 2.0
(0.5, 1.0] (-1.0, -0.5] 3.0
Name: cycles, dtype: float64)
The amplitude values 1.75 and 0.75 correspond to 2.0 and 1.0. They are in the middle of the histogram bins.
A more complex example
Now let’s take a look at a more complex load collective. We use the TimeSignalGenerator
to generate a load signal.
[11]:
load_signal = TS.TimeSignalGenerator(
10,
{
'number': 50,
'amplitude_median': 1.0, 'amplitude_std_dev': 0.5,
'frequency_median': 4, 'frequency_std_dev': 3,
'offset_median': 0, 'offset_std_dev': 0.4
}, None, None
).query(50000)
plt.plot(load_signal)
[11]:
[<matplotlib.lines.Line2D at 0x7f7214548150>]
Again we perform a rainflow analysis to obtain the load histogram.
[12]:
detector = RF.FourPointDetector(recorder=RF.LoopValueRecorder())
detector.process(load_signal)
histogram = detector.recorder.histogram(64)
We can plot the histogram with a bit of processing.
[13]:
fr, to = histogram.index.levels[0], histogram.index.levels[1]
numpy_hist = np.flipud(histogram.values.reshape(len(fr),len(to)))
X, Y = np.meshgrid(fr.left, to.left)
plt.pcolormesh(X, Y, numpy_hist)
[13]:
<matplotlib.collections.QuadMesh at 0x7f7214581410>
We can also plot the cumulated version of the histogram. Therefor we put the amplitude and the cycles into a dataframe.
[14]:
df = pd.DataFrame({
'cycles': histogram.load_collective.cycles,
'amplitude': histogram.load_collective.amplitude,
}).sort_values('amplitude', ascending=False)
Now we can plot the amplitude against the cumulated sum of the cycles:
[15]:
plt.plot(np.cumsum(df.cycles), df.amplitude)
plt.loglog()
[15]:
[]