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 0x7f0af469b490>]
../_images/demos_load_collective_3_1.png

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 0x7f0af46fc390>

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 0x7f0af1f74210>]
../_images/demos_load_collective_23_1.png

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 0x7f0af1f688d0>
../_images/demos_load_collective_27_1.png

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]:
[]
../_images/demos_load_collective_31_1.png