The pyLife Signal API
The signal api is the higher level API of pyLife. It is the API that you probably should be using. Some of the domain specific functions are also available as pure numpy functions. However, we highly recommend you to take a closer look at pandas and consider to adapt your application to the pandas way of doing things.
Motivation
In pyLife’s domain, we often deal with data structures that consist multiple
numerical values. For example a Wöhler curve (see
WoehlerCurve
) consists at least of the parameters
k_1
, ND
and SD
. Optionally it can have the additional parameters
k_2
, TN
and TS
. As experience teaches us, it is advisable to put
these kinds of values into one variable to avoid confusion. For example a
function with more than five positional arguments often leads to hard to
debugable bugs due to wrong positioning.
Moreover some of these data structures can come in different dimensionalities. For example data structures describing material behavior can come as one dataset, describing one material. They can also come as a map to a FEM mesh, for example when you are dealing with case hardened components. Then every element of your FEM mesh can have a different associated Wöhler curve dataset. In pyLife we want to deal with these kinds of mappings easily without much coding overhead for the programmer.
The pyLife signal API provides the class pylife.PylifeSignal
to
facilitate handling these kinds of data structures and broadcasting them to
another signal instance.
This page describes the basic concept of the pyLife signal API. The next page
describes the broadcasting mechanism of a pylife.PylifeSignal
.
The basic concept
The basic idea is to have all the data in a signal like data structure, that can be piped through the individual calculation process steps. Each calculation process step results in a new signal, that then can be handed over to the next process step.
Signals can be for example
stress tensors like from an FEM-solver
load collectives like time signals or a rainflow matrix
material data like Wöhler curve parameters
…
From a programmer’s point of view, signals are objects of either
pandas.Series
or pandas.DataFrame
, depending if they are one
or two dimensional (see here about dimensionality).
Functions that operate on a signal are usually written as methods of an
instance of as class derived from PylifeSignal
. These classes are
usually decorated as Series or DataFrame accessor using
pandas.api.extensions.register_series_accessor()
resp.
pandas.api.extensions.register_dataframe_accessor()
.
Due to the decorators, signal accessor classes can be instantiated also as an
attribute of a pandas.Series
or pandas.DataFrame
. The
following two lines are equivalent.
Usual class instantiation:
PlainMesh(df).coordinates
Or more convenient using the accessor decorator attribute:
df.plain_mesh.coordinates
There is also the convenience function
from_parameters()
to instantiate the signal
class from individual parameters. So a
pylife.materialdata.WoehlerCurve
can be instantiated in three ways.
directly with the class constructor
data = pd.Series({ 'k_1': 7.0, 'ND': 2e6, 'SD': 320. }) wc = WoehlerCurve(data)
using the pandas accessor
data = pd.Series({ 'k_1': 7.0, 'ND': 2e6, 'SD': 320. }) wc = data.woehler
from individual parameters
wc = WoehlerCurve.from_parameters(k_1=7.0, ND=2e6, SD= 320.)
How to use predefined signal accessors
There are too reasons to use a signal accessor:
let it validate the accessed DataFrame
use a method or access a property that the accessor defines
Example for validation
In the following example we are validating a DataFrame that if it is a valid plain mesh, i.e. if it has the columns x and y.
Import the modules. Note that the module with the signal accessors (here
mesh
) needs to be imported explicitly.
import pandas as pd
import pylife.mesh
Create a DataFrame and have it validated if it is a valid plain mesh, i.e. has the columns x and y.
df = pd.DataFrame({'x': [1.0], 'y': [1.0]})
df.plain_mesh
<pylife.mesh.meshsignal.PlainMesh at 0x7fdca07c1a90>
Now create a DataFrame which is not a valid plain mesh and try to have it validated:
df = pd.DataFrame({'x': [1.0], 'a': [1.0]})
df.plain_mesh
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[3], line 2
1 df = pd.DataFrame({'x': [1.0], 'a': [1.0]})
----> 2 df.plain_mesh
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pandas/core/generic.py:6204, in NDFrame.__getattr__(self, name)
6197 if (
6198 name not in self._internal_names_set
6199 and name not in self._metadata
6200 and name not in self._accessors
6201 and self._info_axis._can_hold_identifiers_and_holds_name(name)
6202 ):
6203 return self[name]
-> 6204 return object.__getattribute__(self, name)
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pandas/core/accessor.py:224, in CachedAccessor.__get__(self, obj, cls)
221 if obj is None:
222 # we're accessing the attribute of the class, i.e., Dataset.geo
223 return self._accessor
--> 224 accessor_obj = self._accessor(obj)
225 # Replace the property with the accessor object. Inspired by:
226 # https://www.pydanny.com/cached-property.html
227 # We need to use object.__setattr__ because we overwrite __setattr__ on
228 # NDFrame
229 object.__setattr__(obj, self._name, accessor_obj)
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pylife/core/pylifesignal.py:62, in PylifeSignal.__init__(self, pandas_obj)
54 """Instantiate a :class:`signal.PyLifeSignal`.
55
56 Parameters
(...)
59
60 """
61 self._obj = pandas_obj
---> 62 self._validate()
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pylife/mesh/meshsignal.py:102, in PlainMesh._validate(self)
100 def _validate(self):
101 self._coord_keys = ['x', 'y']
--> 102 self.fail_if_key_missing(self._coord_keys)
103 if 'z' in self._obj.columns:
104 self._coord_keys.append('z')
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pylife/core/pylifesignal.py:184, in PylifeSignal.fail_if_key_missing(self, keys_to_check, msg)
153 def fail_if_key_missing(self, keys_to_check, msg=None):
154 """Raise an exception if any key is missing in a self._obj object.
155
156 Parameters
(...)
182 :class:`stresssignal.StressTensorVoigt`
183 """
--> 184 DataValidator().fail_if_key_missing(self._obj, keys_to_check)
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pylife/core/data_validator.py:131, in DataValidator.fail_if_key_missing(self, signal, keys_to_check, msg)
129 the_class = stack[2][0].f_locals['self'].__class__
130 msg = the_class.__name__ + ' must have the items %s. Missing %s.'
--> 131 raise AttributeError(msg % (', '.join(keys_to_check), ', '.join(missing_keys)))
AttributeError: PlainMesh must have the items x, y. Missing y.
Example for accessing a property
Get the coordinates of a 2D plain mesh
df = pd.DataFrame({'x': [1.0, 2.0, 3.0], 'y': [1.0, 2.0, 3.0]})
df.plain_mesh.coordinates
x | y | |
---|---|---|
0 | 1.0 | 1.0 |
1 | 2.0 | 2.0 |
2 | 3.0 | 3.0 |
Now a 3D mesh
df = pd.DataFrame({'x': [1.0], 'y': [1.0], 'z': [1.0], 'foo': [42.0], 'bar': [23.0]})
df.plain_mesh.coordinates
x | y | z | |
---|---|---|---|
0 | 1.0 | 1.0 | 1.0 |
Defining your own signal accessors
If you want to write a processor for signals you need to put the processing
functionality in an accessor class that is derived from the signal accessor
base class like for example Mesh
. This class you
register as a pandas DataFrame accessor using a decorator
import pandas as pd
import pylife.mesh
@pd.api.extensions.register_dataframe_accessor('my_mesh_processor')
class MyMesh(meshsignal.Mesh):
def do_something(self):
# ... your code here
# the DataFrame is accessible by self._obj
# usually you would calculate a DataFrame df to return it.
df = ...
# you might want copy the index of self._obj to the returned
# DataFrame.
return df.set_index(self._obj.index)
As MyMesh is derived from Mesh
the
validation of Mesh is performed. So in the method do_something()
you can rely on that self._obj is a valid mesh DataFrame.
You then can use the class in the following way when the module is imported.
Performing additional validation
Sometimes your signal accessor needs to perform an additional validation on the accessed signal. For example you might need a mesh that needs to be 3D. Therefore you can reimplement _validate() to perform the additional validation. Make sure to call _validate() of the accessor class you are deriving from like in the following example.
import pandas as pd
import pylife.mesh
@pd.api.extensions.register_dataframe_accessor('my_only_for_3D_mesh_processor')
class MyOnlyFor3DMesh(pylife.mesh.PlainMesh):
def _validate(self):
super()._validate() # call PlainMesh._validate()
self.fail_if_key_missing(['z'])
df = pd.DataFrame({'x': [1.0], 'y': [1.0]})
df.my_only_for_3D_mesh_processor
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[6], line 11
8 self.fail_if_key_missing(['z'])
10 df = pd.DataFrame({'x': [1.0], 'y': [1.0]})
---> 11 df.my_only_for_3D_mesh_processor
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pandas/core/generic.py:6204, in NDFrame.__getattr__(self, name)
6197 if (
6198 name not in self._internal_names_set
6199 and name not in self._metadata
6200 and name not in self._accessors
6201 and self._info_axis._can_hold_identifiers_and_holds_name(name)
6202 ):
6203 return self[name]
-> 6204 return object.__getattribute__(self, name)
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pandas/core/accessor.py:224, in CachedAccessor.__get__(self, obj, cls)
221 if obj is None:
222 # we're accessing the attribute of the class, i.e., Dataset.geo
223 return self._accessor
--> 224 accessor_obj = self._accessor(obj)
225 # Replace the property with the accessor object. Inspired by:
226 # https://www.pydanny.com/cached-property.html
227 # We need to use object.__setattr__ because we overwrite __setattr__ on
228 # NDFrame
229 object.__setattr__(obj, self._name, accessor_obj)
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pylife/core/pylifesignal.py:62, in PylifeSignal.__init__(self, pandas_obj)
54 """Instantiate a :class:`signal.PyLifeSignal`.
55
56 Parameters
(...)
59
60 """
61 self._obj = pandas_obj
---> 62 self._validate()
Cell In[6], line 8, in MyOnlyFor3DMesh._validate(self)
6 def _validate(self):
7 super()._validate() # call PlainMesh._validate()
----> 8 self.fail_if_key_missing(['z'])
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pylife/core/pylifesignal.py:184, in PylifeSignal.fail_if_key_missing(self, keys_to_check, msg)
153 def fail_if_key_missing(self, keys_to_check, msg=None):
154 """Raise an exception if any key is missing in a self._obj object.
155
156 Parameters
(...)
182 :class:`stresssignal.StressTensorVoigt`
183 """
--> 184 DataValidator().fail_if_key_missing(self._obj, keys_to_check)
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pylife/core/data_validator.py:131, in DataValidator.fail_if_key_missing(self, signal, keys_to_check, msg)
129 the_class = stack[2][0].f_locals['self'].__class__
130 msg = the_class.__name__ + ' must have the items %s. Missing %s.'
--> 131 raise AttributeError(msg % (', '.join(keys_to_check), ', '.join(missing_keys)))
AttributeError: MyOnlyFor3DMesh must have the items z. Missing z.
Defining your own signals
The same way the predefined pyLife signals are defined you can define your own signals. Let’s say, for example, that in your signal there needs to be the columns alpha, beta, gamma all of which need to be positive.
You would put the signal class into a module file my_signal_mod.py
import pandas as pd
from pylife import PylifeSignal
@pd.api.extensions.register_dataframe_accessor('my_signal')
class MySignal(PylifeSignal):
def _validate(self):
self.fail_if_key_missing(['alpha', 'beta', 'gamma'])
for k in ['alpha', 'beta', 'gamma']:
if (self._obj[k] < 0).any():
raise ValueError("All values of %s need to be positive. "
"At least one is less than 0" % k)
def some_method(self):
return self._obj[['alpha', 'beta', 'gamma']] * -3.0
You can then validate signals and/or call some_method()
.
Validation success.
df = pd.DataFrame({'alpha': [1.0, 2.0], 'beta': [1.0, 0.0], 'gamma': [1.0, 2.0]})
df.my_signal.some_method()
alpha | beta | gamma | |
---|---|---|---|
0 | -3.0 | -3.0 | -3.0 |
1 | -6.0 | -0.0 | -6.0 |
Validation fails because of missing gamma column.
df = pd.DataFrame({'alpha': [1.0, 2.0], 'beta': [1.0, -1.0]})
df.my_signal.some_method()
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[9], line 2
1 df = pd.DataFrame({'alpha': [1.0, 2.0], 'beta': [1.0, -1.0]})
----> 2 df.my_signal.some_method()
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pandas/core/generic.py:6204, in NDFrame.__getattr__(self, name)
6197 if (
6198 name not in self._internal_names_set
6199 and name not in self._metadata
6200 and name not in self._accessors
6201 and self._info_axis._can_hold_identifiers_and_holds_name(name)
6202 ):
6203 return self[name]
-> 6204 return object.__getattribute__(self, name)
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pandas/core/accessor.py:224, in CachedAccessor.__get__(self, obj, cls)
221 if obj is None:
222 # we're accessing the attribute of the class, i.e., Dataset.geo
223 return self._accessor
--> 224 accessor_obj = self._accessor(obj)
225 # Replace the property with the accessor object. Inspired by:
226 # https://www.pydanny.com/cached-property.html
227 # We need to use object.__setattr__ because we overwrite __setattr__ on
228 # NDFrame
229 object.__setattr__(obj, self._name, accessor_obj)
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pylife/core/pylifesignal.py:62, in PylifeSignal.__init__(self, pandas_obj)
54 """Instantiate a :class:`signal.PyLifeSignal`.
55
56 Parameters
(...)
59
60 """
61 self._obj = pandas_obj
---> 62 self._validate()
Cell In[7], line 7, in MySignal._validate(self)
6 def _validate(self):
----> 7 self.fail_if_key_missing(['alpha', 'beta', 'gamma'])
8 for k in ['alpha', 'beta', 'gamma']:
9 if (self._obj[k] < 0).any():
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pylife/core/pylifesignal.py:184, in PylifeSignal.fail_if_key_missing(self, keys_to_check, msg)
153 def fail_if_key_missing(self, keys_to_check, msg=None):
154 """Raise an exception if any key is missing in a self._obj object.
155
156 Parameters
(...)
182 :class:`stresssignal.StressTensorVoigt`
183 """
--> 184 DataValidator().fail_if_key_missing(self._obj, keys_to_check)
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pylife/core/data_validator.py:131, in DataValidator.fail_if_key_missing(self, signal, keys_to_check, msg)
129 the_class = stack[2][0].f_locals['self'].__class__
130 msg = the_class.__name__ + ' must have the items %s. Missing %s.'
--> 131 raise AttributeError(msg % (', '.join(keys_to_check), ', '.join(missing_keys)))
AttributeError: MySignal must have the items alpha, beta, gamma. Missing gamma.
Validation fail because one beta is negative.
df = pd.DataFrame({'alpha': [1.0, 2.0], 'beta': [1.0, -1.0], 'gamma': [1.0, 2.0]})
df.my_signal.some_method()
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[10], line 2
1 df = pd.DataFrame({'alpha': [1.0, 2.0], 'beta': [1.0, -1.0], 'gamma': [1.0, 2.0]})
----> 2 df.my_signal.some_method()
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pandas/core/accessor.py:224, in CachedAccessor.__get__(self, obj, cls)
221 if obj is None:
222 # we're accessing the attribute of the class, i.e., Dataset.geo
223 return self._accessor
--> 224 accessor_obj = self._accessor(obj)
225 # Replace the property with the accessor object. Inspired by:
226 # https://www.pydanny.com/cached-property.html
227 # We need to use object.__setattr__ because we overwrite __setattr__ on
228 # NDFrame
229 object.__setattr__(obj, self._name, accessor_obj)
File ~/checkouts/readthedocs.org/user_builds/pylife/envs/latest/lib/python3.11/site-packages/pylife/core/pylifesignal.py:62, in PylifeSignal.__init__(self, pandas_obj)
54 """Instantiate a :class:`signal.PyLifeSignal`.
55
56 Parameters
(...)
59
60 """
61 self._obj = pandas_obj
---> 62 self._validate()
Cell In[7], line 10, in MySignal._validate(self)
8 for k in ['alpha', 'beta', 'gamma']:
9 if (self._obj[k] < 0).any():
---> 10 raise ValueError("All values of %s need to be positive. "
11 "At least one is less than 0" % k)
ValueError: All values of beta need to be positive. At least one is less than 0
Additional attributes in your own signals
If your accessor class needs to have attributes other than the accessed object itself you can define default values in the __init__() of your accessor and set these attributes with setter methods.
import pandas as pd
from pylife import PylifeSignal
@pd.api.extensions.register_dataframe_accessor('my_signal')
class MySignal(PylifeSignal):
def __init__(self, pandas_obj):
super(MySignal, self).__init__(pandas_obj)
self._my_attribute = 'the default value'
def set_my_attribute(self, my_attribute):
self._my_attribute = my_attribute
return self
def do_something(self, some_parameter):
# ... use some_parameter, self._my_attribute and self._obj
>>> df.my_signal.set_my_attribute('foo').do_something(2342)
Registering a method to an existing accessor class
Note
This functionality might be dropped on the way to pyLife-2.0 as it turns out that it is not that much used.
One drawback of the accessor class API is that you cannot extend accessors by
deriving from them. For example if you need a custom equivalent stress function
you cannot add it by deriving from StressTensorEquistress
,
and register it by the same accessor equistress.
The solution for that is register_method()
that lets you monkey patch a
new method to any class deriving from PylifeSignal
.
from pylife import equistress
@pl.signal_register_method(equistress.StressTensorEquistress, 'my_equistress')
def my_equistress_method(df)
# your code here
return ...
Then you can call the method on any DataFrame that is accessed by equistress:
>>> df.equistress.my_equistress()
You can also have additional arguments in the registered method:
from pylife import equistress
@pl.signal_register_method(equistress.StressTensorEquistress, 'my_equistress_with_arg')
def my_equistress_method_with_arg(df, additional_arg)
# your code here
return ...
>>> df.equistress.my_equistress_with_arg(my_additional_arg)