Skip to content

Commit a9c0593

Browse files
ENH: Add the gufunc backlash_sum (Prandtl-Ishlinskii model)
1 parent 6604018 commit a9c0593

9 files changed

+196
-5
lines changed

README.md

+38
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ processed with the script in `ufunclab/tools/conv_template.py`.
121121
| -------- | ----------- |
122122
| [`fillnan1d`](#fillnan1d) | Replace `nan` using linear interpolation |
123123
| [`backlash`](#backlash) | Backlash operator |
124+
| [`backlash_sum`](#backlash_sum) | Sum backlash operators (Prandtl-Ishlinkskii) |
124125
| [`hysteresis_relay`](#hysteresis_relay) | Relay with hysteresis (Schmitt trigger) |
125126
| [`sosfilter`](#sosfilter) | SOS (second order sections) linear filter |
126127
| [`sosfilter_ic`](#sosfilter_ic) | SOS linear filter with initial condition |
@@ -1727,6 +1728,43 @@ the plot
17271728

17281729
![Backlash plot](https://github.com/WarrenWeckesser/ufunclab/blob/main/examples/backlash_demo.png)
17291730

1731+
#### `backlash_sum`
1732+
1733+
`backlash_sum` computes the linear combination of `m` `backlash` operations.
1734+
This is known as the Prandtl-Ishlinskii hysteresis model. The function is a gufunc
1735+
with signature `(n),(m),(m),(m)->(n),(m)`. The second return value is the final value of
1736+
each of the backlash processes.
1737+
1738+
For example,
1739+
1740+
```
1741+
>>> import numpy as np
1742+
>>> from ufunclab import backlash_sum
1743+
1744+
>>> x = np.array([0.0, 0.2, 0.5, 1.1, 1.25, 1.0, 0.2, -1])
1745+
1746+
Here the weights are all the same and happen to sum to 1, but that
1747+
is not required in general.
1748+
1749+
>>> w = np.array([0.25, 0.25, 0.25, 0.25])
1750+
>>> deadband = np.array([0.2, 0.4, 0.6, 0.8])
1751+
>>> initial = np.zeros(4)
1752+
1753+
>>> y, final = backlash_sum(x, w, deadband, initial)
1754+
>>> y
1755+
array([ 0. , 0.025 , 0.25 , 0.85 , 1. , 0.9875, 0.45 , -0.75 ])
1756+
```
1757+
1758+
-----
1759+
1760+
Another example is in the script `backlash_sum_demo.py` in the `examples` directory.
1761+
It passes two cycles of a sine wave through a Prandtl-Ishlinskii model with three
1762+
backlash operations. The weights are `w = [0.25, 1.0, 0.5]`, the deadband values
1763+
are `[0.5, 1.0, 2.0]` and the initial values are all zero.
1764+
1765+
![Backlash_sum plot](https://github.com/WarrenWeckesser/ufunclab/blob/main/examples/backlash_sum_demo_x_vs_t.png)
1766+
1767+
![Backlash_sum plot](https://github.com/WarrenWeckesser/ufunclab/blob/main/examples/backlash_sum_demo_y_vs_x.png)
17301768

17311769
#### `hysteresis_relay`
17321770

examples/backlash_sum_demo.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import numpy as np
2+
import matplotlib.pyplot as plt
3+
4+
from ufunclab import backlash_sum
5+
6+
7+
t = np.linspace(0, 4*np.pi, 1000)
8+
x = np.sin(t)
9+
10+
w = np.array([0.25, 1.0, 1.25])
11+
deadband = np.array([0.5, 1.0, 2.0])
12+
initial = np.zeros(3)
13+
14+
y, final = backlash_sum(x, w, deadband, initial)
15+
16+
17+
plt.figure(figsize=(6, 4))
18+
plt.plot(t, x, linewidth=2, label='x(t)')
19+
plt.plot(t, y, '--', linewidth=2, label='backlash_sum(x(t))')
20+
plt.xlabel('t')
21+
plt.title('backlash_sum')
22+
plt.legend(shadow=True)
23+
plt.grid(alpha=0.6)
24+
plt.savefig('backlash_sum_demo_x_vs_t.png')
25+
26+
plt.figure(figsize=(6, 4))
27+
plt.plot(x, y, linewidth=2)
28+
plt.xlabel('x')
29+
plt.ylabel('y')
30+
plt.title('backlash_sum')
31+
plt.grid(alpha=0.6)
32+
plt.savefig('backlash_sum_demo_y_vs_x.png')
33+
34+
# plt.show()

examples/backlash_sum_demo_x_vs_t.png

40.8 KB
Loading

examples/backlash_sum_demo_y_vs_x.png

24.7 KB
Loading

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ requires = [
88

99
[project]
1010
name = 'ufunclab'
11-
version = '0.0.8.dev14'
11+
version = '0.0.8.dev15'
1212
description = 'NumPy ufuncs and utilities.'
1313
readme = 'README.md'
1414
requires-python = '>=3.9'

src/backlash/backlash_gufunc.h

+47
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,51 @@ backlash_core(
4242
}
4343
}
4444

45+
46+
template<typename T>
47+
static void
48+
backlash_sum_core(
49+
npy_intp n, // core dimension n
50+
npy_intp m, // core dimension m
51+
T *p_x, // pointer to first element of x, a strided 1-d array with shape (n,)
52+
const npy_intp x_stride, // stride (in bytes) of x
53+
T *p_w, // pointer to first element of w, a strided 1-d array with shape (m,)
54+
const npy_intp w_stride, // stride (in bytes) of w
55+
T *p_deadband, // pointer to first element of deadband, a strided 1-d array with shape (m,)
56+
const npy_intp deadband_stride, // stride (in bytes) of deadband
57+
T *p_initial, // pointer to initial
58+
const npy_intp initial_stride, // stride (in bytes) of initial, a strided 1-d array with shape (m,)
59+
T *p_out, // pointer to out, a strided 1-d array with shape (n,)
60+
const npy_intp out_stride, // stride (in bytes) of out
61+
T *p_final, // point to final, a strided 1-d array with shape (m,)
62+
const npy_intp final_stride // stride (in bytes) of final
63+
)
64+
{
65+
for (npy_intp j = 0; j < m; ++j) {
66+
set(p_final, final_stride, j, get(p_initial, initial_stride, j));
67+
}
68+
for (npy_intp k = 0; k < n; ++k) {
69+
T current_x = get(p_x, x_stride, k);
70+
T y_total = 0.0;
71+
for (npy_intp j = 0; j < m; ++j) {
72+
T current_y = get(p_final, final_stride, j);
73+
T halfband = get(p_deadband, deadband_stride, j)/2;
74+
T xminus = current_x - halfband;
75+
if (xminus > current_y) {
76+
current_y = xminus;
77+
}
78+
else {
79+
T xplus = current_x + halfband;
80+
if (xplus < current_y) {
81+
current_y = xplus;
82+
}
83+
}
84+
set(p_final, final_stride, j, current_y);
85+
T w = get(p_w, w_stride, j);
86+
y_total += w*current_y;
87+
}
88+
set(p_out, out_stride, k, y_total);
89+
}
90+
}
91+
4592
#endif

src/backlash/define_cxx_gufunc_extmod.py

+58-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,49 @@
2929
array([0. , 0.8, 0.9, 0.9, 1.3, 1.3, 1.3, 0.7])
3030
"""
3131

32+
BACKLASH_SUM_DOCSTRING = """\
33+
backlash_sum(x, w, deadband, initial, /, ...)
34+
35+
Compute the linear combination of several backlash processes. This
36+
operation is also known as the Prandtl-Ishlinskii hysteresis model.
37+
38+
Parameters
39+
----------
40+
x : array_like, length n
41+
The input signal
42+
w : array_like, length m
43+
The weights of the backlash operators.
44+
deadband : array_like, length m
45+
The deadband values of the backlash operators.
46+
initial : array_like, length m
47+
The initial values of the backlash operators.
48+
49+
Returns
50+
-------
51+
out : ndarray, same shape as `x`
52+
Output of the Prandtl-Ishlinskii hysteresis model.
53+
final : ndarray, same shape as `initial`
54+
State of the individual backlash processes.
55+
56+
Examples
57+
--------
58+
>>> import numpy as np
59+
>>> from ufunclab import backlash_sum
60+
61+
>>> x = np.array([0.0, 0.2, 0.5, 1.1, 1.25, 1.0, 0.2, -1])
62+
63+
Here the weights are all the same and happen to sum to 1, but that
64+
is not required in general.
65+
66+
>>> w = np.array([0.25, 0.25, 0.25, 0.25])
67+
>>> deadband = np.array([0.2, 0.4, 0.6, 0.8])
68+
>>> initial = np.zeros(4)
69+
70+
>>> y, final = backlash_sum(x, w, deadband, initial)
71+
>>> y
72+
array([ 0. , 0.025 , 0.25 , 0.85 , 1. , 0.9875, 0.45 , -0.75 ])
73+
74+
"""
3275

3376
backlash_core_source = UFuncSource(
3477
funcname='backlash_core',
@@ -44,12 +87,25 @@
4487
)
4588

4689

90+
backlash_sum_core_source = UFuncSource(
91+
funcname='backlash_sum_core',
92+
typesignatures=['ffff->ff', 'dddd->dd', 'gggg->gg'],
93+
)
94+
95+
backlash_sum_gufunc = UFunc(
96+
name='backlash_sum',
97+
docstring=BACKLASH_SUM_DOCSTRING,
98+
header='backlash_gufunc.h',
99+
signature='(n),(m),(m),(m) -> (n),(m)',
100+
sources=[backlash_sum_core_source],
101+
)
102+
47103
MODULE_DOCSTRING = """\
48-
This module defines the backlash function.
104+
This module defines the backlash and backlash_sum functions.
49105
"""
50106

51107
extmod = UFuncExtMod(
52108
module='_backlash',
53109
docstring=MODULE_DOCSTRING,
54-
ufuncs=[backlash_gufunc],
110+
ufuncs=[backlash_gufunc, backlash_sum_gufunc],
55111
)

ufunclab/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
'tri_area': '._tri_area',
6363
'tri_area_indexed': '._tri_area',
6464
'backlash': '._backlash',
65+
'backlash_sum': '._backlash',
6566
'fillnan1d': '._fillnan1d',
6667
'linear_interp1d': '._linear_interp1d',
6768
'deadzone': '._deadzone',

ufunclab/tests/test_backlash.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import numpy as np
2-
from numpy.testing import assert_array_equal
3-
from ufunclab import backlash
2+
from numpy.testing import assert_array_equal, assert_allclose
3+
from ufunclab import backlash, backlash_sum
44

55

66
def test_all_zeros():
@@ -50,3 +50,18 @@ def test_out_nonstandard_strides():
5050
y1 = backlash(x, 1, 1)
5151
y2 = backlash(x, 1, 1, out=out)
5252
assert_array_equal(y1, y2)
53+
54+
55+
def test_backlash_sum():
56+
x = np.array([0.25, 0.5, 10.0, 0.125, 0.2, 0.2, 0.2, 0.2])
57+
w = np.array([0.5, 3.0, 2.0])
58+
deadband = np.array([0.5, 1.0, 0.75])
59+
initial = np.array([1.5, 5.0, -2.0])
60+
yb = []
61+
for k in range(3):
62+
y = backlash(x, deadband[k], initial[k])
63+
yb.append(y)
64+
yb = np.array(yb)
65+
yy, final = backlash_sum(x, w, deadband, initial)
66+
assert_allclose(yy, w@yb, rtol=1e-14)
67+
assert_allclose(final, yb[:, -1], rtol=1e-14)

0 commit comments

Comments
 (0)