The Signal Broadcaster

Motivation

pyLife tries to provide a flexible API for its functionality with respect to sizes of the datasets involved. No matter if you want to perform some calculation on just a single value or on a whole FEM-mesh. No matter if you want to calculate the damage that a certain load amplitude does on a certain material, or if you have a FEM-mesh with different materials associated with to element and every node has its own rainflow matrix.

Example

Take for example the function cycles(). Imagine you have a single Wöhler curve dataset like

import pandas as pd
from pylife.materiallaws import WoehlerCurve

woehler_curve_data = pd.Series({
    'k_1': 7.0,
    'ND': 2e5,
    'SD': 320.0,
    'TN': 2.3,
    'TS': 1.25
})

woehler_curve_data
k_1         7.00
ND     200000.00
SD        320.00
TN          2.30
TS          1.25
dtype: float64

Now you can calculate the cycles along the Basquin equation for a single load value:

woehler_curve_data.woehler.cycles(load=350.)
array(106807.93865297)

Now let’s say, you have different loads for each element_id if your FEM-mesh:

amplitude = pd.Series([320., 340., 330., 320.], index=pd.Index([1, 2, 3, 4], name='element_id'))
amplitude
element_id
1    320.0
2    340.0
3    330.0
4    320.0
dtype: float64

cycles() now gives you a result for every element_id.

woehler_curve_data.woehler.cycles(load=amplitude)
element_id
1    200000.000000
2    130836.050152
3    161243.517913
4    200000.000000
dtype: float64

In the next step, even the Wöhler curve data is different for every element, like for example for a hardness gradient in your component:

woehler_curve_data = pd.DataFrame({
    'k_1': 7.0,
    'ND': 2e5,
    'SD': [370., 320., 280, 280],
    'TN': 2.3,
    'TS': 1.25
}, index=pd.Index([1, 2, 3, 4], name='element_id'))

woehler_curve_data
k_1 ND SD TN TS
element_id
1 7.0 200000.0 370.0 2.3 1.25
2 7.0 200000.0 320.0 2.3 1.25
3 7.0 200000.0 280.0 2.3 1.25
4 7.0 200000.0 280.0 2.3 1.25

In this case the broadcaster determines from the identical index name element_id that the two structures can be aligned, so every element is associated with its load and with its Wöhler curve:

woehler_curve_data.woehler.cycles(load=amplitude)
element_id
1             inf
2    1.308361e+05
3    6.331967e+04
4    7.853918e+04
dtype: float64

In another case we assume that you have a Wöhler curve associated to every element, and the loads are constant throughout the component but different for different load scenarios.

amplitude_scenarios = pd.Series([320., 340., 330., 320.], index=pd.Index([1, 2, 3, 4], name='scenario'))
amplitude_scenarios
scenario
1    320.0
2    340.0
3    330.0
4    320.0
dtype: float64

In this case the broadcaster makes a cross product of load scenario and element_id, i.e. for every element_id for every load scenario the allowable cycles are calculated:

woehler_curve_data.woehler.cycles(load=amplitude_scenarios)
element_id  scenario
1           1                    inf
            2                    inf
            3                    inf
            4                    inf
2           1           2.000000e+05
            2           1.308361e+05
            3           1.612435e+05
            4           2.000000e+05
3           1           7.853918e+04
            2           5.137878e+04
            3           6.331967e+04
            4           7.853918e+04
4           1           7.853918e+04
            2           5.137878e+04
            3           6.331967e+04
            4           7.853918e+04
dtype: float64

As is very uncommon that the load is constant all over the component like in the previous example we now consider an even more complex one. Let’s say we have a different load scenarios, which give us for every element_id multiple load scenarios:

amplitude_scenarios = pd.Series(
    [320., 340., 330., 320, 220., 240., 230., 220, 420., 440., 430., 420],
    index=pd.MultiIndex.from_tuples([
        (1, 1), (1, 2), (1, 3), (1, 4),
        (2, 1), (2, 2), (2, 3), (2, 4),
        (3, 1), (3, 2), (3, 3), (3, 4)
    ], names=['scenario', 'element_id']))
amplitude_scenarios
scenario  element_id
1         1             320.0
          2             340.0
          3             330.0
          4             320.0
2         1             220.0
          2             240.0
          3             230.0
          4             220.0
3         1             420.0
          2             440.0
          3             430.0
          4             420.0
dtype: float64

Now the broadcaster still aligns the element_id:

woehler_curve_data.woehler.cycles(load=amplitude_scenarios)
scenario  element_id
1         1                      inf
          2             1.308361e+05
          3             6.331967e+04
          4             7.853918e+04
2         1                      inf
          2                      inf
          3                      inf
          4                      inf
3         1             8.235634e+04
          2             2.152341e+04
          3             9.927892e+03
          4             1.170553e+04
dtype: float64

Note that in the above examples the call was always identical

woehler_curve_data.woehler.cycles(load=...)

That means that when you write a module for a certain functionality you don’t need to know if your code later on receives a single value parameter or a whole FEM-mesh. Your code will take both and handle them.

Usage

As you might have seen, we did not call the pylife.Broadcaster in the above code snippets directly. And that’s the way it’s meant to be. When you are on the level that you simply want to use pyLife’s functionality to perform calculations, you should not be required to think about how to broadcast your datasets to one another. It should simply happen automatically. In our example the the calls to the pylife.Broadcaster are done inside cycles().

You do need to deal with the pylife.Broadcaster when you implement new calculation methods. Let’s go through an example.

Todo

Sorry, this is still to be written.