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