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.

Lets assume we have a collective of seasonal flood events on the river Vitava in Prague. This is an oversimplified damage model, which assumes that we multiply the water level of a flood event with a sensitivity value of a bridge to calculate the damage that the flood events causes to the bridge.

from pylife import Broadcaster

flood_events = pd.Series(
    [10., 13., 9., 5.],
    name="water_level",
    index=pd.Index(
        ["spring", "summer", "autumn", "winter"],
        name="flood_event"
    )
)
flood_events
flood_event
spring    10.0
summer    13.0
autumn     9.0
winter     5.0
Name: water_level, dtype: float64

Lets assume some sensitivity value for the bridges of Prague.

sensitivities = pd.Series([2.3, 0.7, 2.7, 6.4, 3.9, 0.8],
    name="sensitivity",
    index=pd.Index(
        [
            "Palackého most",
            "Jiraskův most",
            "Most Legií",
            "Karlův most",
            "Mánesův most",
            "Centrův most"
        ],
        name="bridge"
    )
)
sensitivities
bridge
Palackého most    2.3
Jiraskův most     0.7
Most Legií        2.7
Karlův most       6.4
Mánesův most      3.9
Centrův most      0.8
Name: sensitivity, dtype: float64

Now we want to multiply the water levels with the sensitivity value in order to get a damage value:

damage = flood_events * sensitivities
damage
flood_event
Centrův most     NaN
Jiraskův most    NaN
Karlův most      NaN
Most Legií       NaN
Mánesův most     NaN
Palackého most   NaN
autumn           NaN
spring           NaN
summer           NaN
winter           NaN
dtype: float64

As we can see, this multiplication failed, as the indices of our two series do not match. First we need to broadcast the two indices to a mapped hierarchical index.

sens_mapped, flood_mapped = Broadcaster(flood_events).broadcast(sensitivities)

Now we have a mapped flood values

flood_mapped
flood_event  bridge        
spring       Palackého most    10.0
             Jiraskův most     10.0
             Most Legií        10.0
             Karlův most       10.0
             Mánesův most      10.0
             Centrův most      10.0
summer       Palackého most    13.0
             Jiraskův most     13.0
             Most Legií        13.0
             Karlův most       13.0
             Mánesův most      13.0
             Centrův most      13.0
autumn       Palackého most     9.0
             Jiraskův most      9.0
             Most Legií         9.0
             Karlův most        9.0
             Mánesův most       9.0
             Centrův most       9.0
winter       Palackého most     5.0
             Jiraskův most      5.0
             Most Legií         5.0
             Karlův most        5.0
             Mánesův most       5.0
             Centrův most       5.0
Name: water_level, dtype: float64

and the mapped sensitivity values

sens_mapped
flood_event  bridge        
spring       Palackého most    2.3
             Jiraskův most     0.7
             Most Legií        2.7
             Karlův most       6.4
             Mánesův most      3.9
             Centrův most      0.8
summer       Palackého most    2.3
             Jiraskův most     0.7
             Most Legií        2.7
             Karlův most       6.4
             Mánesův most      3.9
             Centrův most      0.8
autumn       Palackého most    2.3
             Jiraskův most     0.7
             Most Legií        2.7
             Karlův most       6.4
             Mánesův most      3.9
             Centrův most      0.8
winter       Palackého most    2.3
             Jiraskův most     0.7
             Most Legií        2.7
             Karlův most       6.4
             Mánesův most      3.9
             Centrův most      0.8
Name: sensitivity, dtype: float64

These mapped series we can multiply.

damage = flood_mapped * sens_mapped
damage
flood_event  bridge        
spring       Palackého most    23.0
             Jiraskův most      7.0
             Most Legií        27.0
             Karlův most       64.0
             Mánesův most      39.0
             Centrův most       8.0
summer       Palackého most    29.9
             Jiraskův most      9.1
             Most Legií        35.1
             Karlův most       83.2
             Mánesův most      50.7
             Centrův most      10.4
autumn       Palackého most    20.7
             Jiraskův most      6.3
             Most Legií        24.3
             Karlův most       57.6
             Mánesův most      35.1
             Centrův most       7.2
winter       Palackého most    11.5
             Jiraskův most      3.5
             Most Legií        13.5
             Karlův most       32.0
             Mánesův most      19.5
             Centrův most       4.0
dtype: float64

Now we can see for every bridge for every flood event the expected damage to every bridge. We can now reduce this map to get the total damage of every bridge during all flood events:

damage.groupby("bridge").sum()
bridge
Centrův most       29.6
Jiraskův most      25.9
Karlův most       236.8
Most Legií         99.9
Mánesův most      144.3
Palackého most     85.1
dtype: float64

Now let’s assume that we have for each bridge some kind of protection measure that reduces the damage.

protection = pd.Series(
    [10.0, 15.0, 12.0, 25.0, 13.0, 17.0],
    name="dwell_time",
    index=pd.Index(
        [
            "Palackého most",
            "Jiraskův most",
            "Most Legií",
            "Karlův most",
            "Mánesův most",
            "Centrův most"
        ],
        name="bridge"
    )
)
protection
bridge
Palackého most    10.0
Jiraskův most     15.0
Most Legií        12.0
Karlův most       25.0
Mánesův most      13.0
Centrův most      17.0
Name: dwell_time, dtype: float64

We also need divide the damage value by the protection value. Therefore we need to broadcast the protection values to the damage values

protection_mapped, _ = Broadcaster(damage).broadcast(protection)
protection_mapped
flood_event  bridge        
spring       Palackého most    10.0
             Jiraskův most     15.0
             Most Legií        12.0
             Karlův most       25.0
             Mánesův most      13.0
             Centrův most      17.0
summer       Palackého most    10.0
             Jiraskův most     15.0
             Most Legií        12.0
             Karlův most       25.0
             Mánesův most      13.0
             Centrův most      17.0
autumn       Palackého most    10.0
             Jiraskův most     15.0
             Most Legií        12.0
             Karlův most       25.0
             Mánesův most      13.0
             Centrův most      17.0
winter       Palackého most    10.0
             Jiraskův most     15.0
             Most Legií        12.0
             Karlův most       25.0
             Mánesův most      13.0
             Centrův most      17.0
Name: dwell_time, dtype: float64

As you can see, the broadcaster recognized the common index name “bridge” and did not spread it again.

Now we can easily multiply the mapped protection values to the damage.

damage_with_protection = damage / protection

And we can again easily calculate the damage for a certain bridge

damage_with_protection.groupby("bridge").sum()
bridge
Centrův most       1.741176
Jiraskův most      1.726667
Karlův most        9.472000
Most Legií         8.325000
Mánesův most      11.100000
Palackého most     8.510000
dtype: float64