ybouteiller commited on
Commit
15952ff
·
1 Parent(s): 9adbe4f

optimized and tested online filtering

Browse files
portiloop/capture.py CHANGED
@@ -141,9 +141,11 @@ def mod_config(config, datarate, channel_modes):
141
  def filter_24(value):
142
  return (value * 4.5) / (2**23 - 1) # 23 because 1 bit is lost for sign
143
 
 
144
  def filter_2scomplement_np(value):
145
  return np.where((value & (1 << 23)) != 0, value - (1 << 24), value)
146
 
 
147
  def filter_np(value):
148
  return filter_24(filter_2scomplement_np(value))
149
 
@@ -162,21 +164,22 @@ def shift_numpy(arr, num, fill_value=np.nan):
162
 
163
 
164
  class FIR:
165
- def __init__(self, coefficients, buffer=None):
166
- self.coefficients = coefficients
 
167
  self.taps = len(self.coefficients)
168
- if buffer is not None:
169
- self.buffer = np.array(z)
170
- else:
171
- self.buffer = np.zeros(self.taps)
172
 
173
  def filter(self, x):
174
  self.buffer = shift_numpy(self.buffer, 1, x)
175
- return np.sum(self.buffer * self.coefficients)
176
-
177
 
 
178
  class FilterPipeline:
179
- def __init__(self, power_line_fq=60):
 
180
  assert power_line_fq in [50, 60], f"The only supported power line frequencies are 50Hz and 60Hz"
181
  if power_line_fq == 60:
182
  self.notch_coeff1 = -0.12478308884588535
@@ -190,10 +193,10 @@ class FilterPipeline:
190
  self.notch_coeff3 = 0.99364593398236511
191
  self.notch_coeff4 = -0.61410695998423581
192
  self.notch_coeff5 = 0.99364593398236511
193
- self.dfs = [0, 0]
194
 
195
  self.moving_average = None
196
- self.moving_variance = 0
197
  self.ALPHA_AVG = 0.1
198
  self.ALPHA_STD = 0.001
199
  self.EPSILON = 0.000001
@@ -220,12 +223,13 @@ class FilterPipeline:
220
  0.021287595318265635502275046064823982306,
221
  0.014988684599373741992978104065059596905,
222
  0.001623780150148094927192721215192250384]
223
- self.fir = FIR(self.fir_30_coef)
224
 
225
  def filter(self, value):
226
-
227
- result = np.zeros(value.size)
228
- for i, x in enumerate(value):
 
229
  # FIR:
230
  x = self.fir.filter(x)
231
  # notch:
@@ -242,9 +246,9 @@ class FilterPipeline:
242
  x = (x - self.moving_average) / (moving_std + self.EPSILON)
243
  else:
244
  self.moving_average = x
245
- result[i] = x
246
- return result
247
-
248
 
249
  class LiveDisplay():
250
  def __init__(self, channel_names, window_len=100):
@@ -716,7 +720,7 @@ class Capture:
716
  p_msg_io, p_msg_io_2 = mp.Pipe()
717
  p_data_i, p_data_o = mp.Pipe(duplex=False)
718
  SAMPLE_TIME = 1 / self.frequency
719
- fp_vec = [FilterPipeline() for _ in range(8)]
720
  self._p_capture = mp.Process(target=_capture_process,
721
  args=(p_data_o,
722
  p_msg_io_2,
@@ -760,9 +764,10 @@ class Capture:
760
  n_array = filter_np(n_array)
761
 
762
  if filter:
763
- n_array = np.swapaxes(n_array, 0, 1)
764
- n_array = np.array([fp_vec[i].filter(a) if self.channel_states[i] != 'disabled' else [0] for i, a in enumerate(n_array)])
765
- n_array = np.swapaxes(n_array, 0, 1)
 
766
 
767
  buffer += n_array.tolist()
768
  if len(buffer) >= 50:
 
141
  def filter_24(value):
142
  return (value * 4.5) / (2**23 - 1) # 23 because 1 bit is lost for sign
143
 
144
+
145
  def filter_2scomplement_np(value):
146
  return np.where((value & (1 << 23)) != 0, value - (1 << 24), value)
147
 
148
+
149
  def filter_np(value):
150
  return filter_24(filter_2scomplement_np(value))
151
 
 
164
 
165
 
166
  class FIR:
167
+ def __init__(self, nb_channels, coefficients, buffer=None):
168
+
169
+ self.coefficients = np.expand_dims(np.array(coefficients), axis=1)
170
  self.taps = len(self.coefficients)
171
+ self.nb_channels = nb_channels
172
+ self.buffer = np.array(z) if buffer is not None else np.zeros((self.taps, self.nb_channels))
 
 
173
 
174
  def filter(self, x):
175
  self.buffer = shift_numpy(self.buffer, 1, x)
176
+ filtered = np.sum(self.buffer * self.coefficients, axis=0)
177
+ return filtered
178
 
179
+
180
  class FilterPipeline:
181
+ def __init__(self, nb_channels, power_line_fq=60):
182
+ self.nb_channels = nb_channels
183
  assert power_line_fq in [50, 60], f"The only supported power line frequencies are 50Hz and 60Hz"
184
  if power_line_fq == 60:
185
  self.notch_coeff1 = -0.12478308884588535
 
193
  self.notch_coeff3 = 0.99364593398236511
194
  self.notch_coeff4 = -0.61410695998423581
195
  self.notch_coeff5 = 0.99364593398236511
196
+ self.dfs = [np.zeros(self.nb_channels), np.zeros(self.nb_channels)]
197
 
198
  self.moving_average = None
199
+ self.moving_variance = np.zeros(self.nb_channels)
200
  self.ALPHA_AVG = 0.1
201
  self.ALPHA_STD = 0.001
202
  self.EPSILON = 0.000001
 
223
  0.021287595318265635502275046064823982306,
224
  0.014988684599373741992978104065059596905,
225
  0.001623780150148094927192721215192250384]
226
+ self.fir = FIR(self.nb_channels, self.fir_30_coef)
227
 
228
  def filter(self, value):
229
+ """
230
+ value: a numpy array of shape (data series, channels)
231
+ """
232
+ for i, x in enumerate(value): # loop over the data series
233
  # FIR:
234
  x = self.fir.filter(x)
235
  # notch:
 
246
  x = (x - self.moving_average) / (moving_std + self.EPSILON)
247
  else:
248
  self.moving_average = x
249
+ value[i] = x
250
+ return value
251
+
252
 
253
  class LiveDisplay():
254
  def __init__(self, channel_names, window_len=100):
 
720
  p_msg_io, p_msg_io_2 = mp.Pipe()
721
  p_data_i, p_data_o = mp.Pipe(duplex=False)
722
  SAMPLE_TIME = 1 / self.frequency
723
+ fp = FilterPipeline(nb_channels=8)
724
  self._p_capture = mp.Process(target=_capture_process,
725
  args=(p_data_o,
726
  p_msg_io_2,
 
764
  n_array = filter_np(n_array)
765
 
766
  if filter:
767
+ n_array = fp.filter(n_array)
768
+ # n_array = np.swapaxes(n_array, 0, 1)
769
+ # n_array = np.array([fp_vec[i].filter(a) if self.channel_states[i] != 'disabled' else [0] for i, a in enumerate(n_array)])
770
+ # n_array = np.swapaxes(n_array, 0, 1)
771
 
772
  buffer += n_array.tolist()
773
  if len(buffer) >= 50:
portiloop/notebooks/tests.ipynb CHANGED
@@ -13,14 +13,6 @@
13
  "\n",
14
  "cap = Capture()"
15
  ]
16
- },
17
- {
18
- "cell_type": "code",
19
- "execution_count": null,
20
- "id": "b6295738",
21
- "metadata": {},
22
- "outputs": [],
23
- "source": []
24
  }
25
  ],
26
  "metadata": {
 
13
  "\n",
14
  "cap = Capture()"
15
  ]
 
 
 
 
 
 
 
 
16
  }
17
  ],
18
  "metadata": {
portiloop/notebooks/tests_filtering.ipynb ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "id": "4b15d9c3",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "import numpy as np\n",
11
+ "import matplotlib.pyplot as plt"
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": null,
17
+ "id": "4e9129a5",
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": [
21
+ "# Do not try to mask unused channels to optimize the code: we have tried it and it was in fact COUNTER-PRODUCTIVE.\n",
22
+ "# Python is the bottleneck with 8 channels, not numpy, and it does not matter whether we use all 8 or 0 channels.\n",
23
+ "\n",
24
+ "def shift_numpy(arr, num, fill_value=np.nan):\n",
25
+ " result = np.empty_like(arr)\n",
26
+ " if num > 0:\n",
27
+ " result[:num] = fill_value\n",
28
+ " result[num:] = arr[:-num]\n",
29
+ " elif num < 0:\n",
30
+ " result[num:] = fill_value\n",
31
+ " result[:num] = arr[-num:]\n",
32
+ " else:\n",
33
+ " result[:] = arr\n",
34
+ " return result\n",
35
+ "\n",
36
+ "\n",
37
+ "class FIR:\n",
38
+ " def __init__(self, nb_channels, coefficients, buffer=None):\n",
39
+ " \n",
40
+ " self.coefficients = np.expand_dims(np.array(coefficients), axis=1)\n",
41
+ " self.taps = len(self.coefficients)\n",
42
+ " self.nb_channels = nb_channels\n",
43
+ " self.buffer = np.array(z) if buffer is not None else np.zeros((self.taps, self.nb_channels))\n",
44
+ " \n",
45
+ " def filter(self, x):\n",
46
+ " self.buffer = shift_numpy(self.buffer, 1, x)\n",
47
+ " filtered = np.sum(self.buffer * self.coefficients, axis=0)\n",
48
+ " return filtered\n",
49
+ "\n",
50
+ " \n",
51
+ "class FilterPipeline:\n",
52
+ " def __init__(self, nb_channels, power_line_fq=60):\n",
53
+ " self.nb_channels = nb_channels\n",
54
+ " assert power_line_fq in [50, 60], f\"The only supported power line frequencies are 50Hz and 60Hz\"\n",
55
+ " if power_line_fq == 60:\n",
56
+ " self.notch_coeff1 = -0.12478308884588535\n",
57
+ " self.notch_coeff2 = 0.98729186796473023\n",
58
+ " self.notch_coeff3 = 0.99364593398236511\n",
59
+ " self.notch_coeff4 = -0.12478308884588535\n",
60
+ " self.notch_coeff5 = 0.99364593398236511\n",
61
+ " else:\n",
62
+ " self.notch_coeff1 = -0.61410695998423581\n",
63
+ " self.notch_coeff2 = 0.98729186796473023\n",
64
+ " self.notch_coeff3 = 0.99364593398236511\n",
65
+ " self.notch_coeff4 = -0.61410695998423581\n",
66
+ " self.notch_coeff5 = 0.99364593398236511\n",
67
+ " self.dfs = [np.zeros(self.nb_channels), np.zeros(self.nb_channels)]\n",
68
+ " \n",
69
+ " self.moving_average = None\n",
70
+ " self.moving_variance = np.zeros(self.nb_channels)\n",
71
+ " self.ALPHA_AVG = 0.1\n",
72
+ " self.ALPHA_STD = 0.001\n",
73
+ " self.EPSILON = 0.000001\n",
74
+ " \n",
75
+ " self.fir_30_coef = [\n",
76
+ " 0.001623780150148094927192721215192250384,\n",
77
+ " 0.014988684599373741992978104065059596905,\n",
78
+ " 0.021287595318265635502275046064823982306,\n",
79
+ " 0.007349500393709578957568417933998716762,\n",
80
+ " -0.025127515717112181709014251396183681209,\n",
81
+ " -0.052210507359822452833064687638398027048,\n",
82
+ " -0.039273839505489904766477593511808663607,\n",
83
+ " 0.033021568427940004020193498490698402748,\n",
84
+ " 0.147606943281569008563636202779889572412,\n",
85
+ " 0.254000252034505602516389899392379447818,\n",
86
+ " 0.297330876398883392486283128164359368384,\n",
87
+ " 0.254000252034505602516389899392379447818,\n",
88
+ " 0.147606943281569008563636202779889572412,\n",
89
+ " 0.033021568427940004020193498490698402748,\n",
90
+ " -0.039273839505489904766477593511808663607,\n",
91
+ " -0.052210507359822452833064687638398027048,\n",
92
+ " -0.025127515717112181709014251396183681209,\n",
93
+ " 0.007349500393709578957568417933998716762,\n",
94
+ " 0.021287595318265635502275046064823982306,\n",
95
+ " 0.014988684599373741992978104065059596905,\n",
96
+ " 0.001623780150148094927192721215192250384]\n",
97
+ " self.fir = FIR(self.nb_channels, self.fir_30_coef)\n",
98
+ " \n",
99
+ " def filter(self, value):\n",
100
+ " \"\"\"\n",
101
+ " value: a numpy array of shape (data series, channels)\n",
102
+ " \"\"\"\n",
103
+ " for i, x in enumerate(value): # loop over the data series\n",
104
+ " # FIR:\n",
105
+ " x = self.fir.filter(x)\n",
106
+ " # notch:\n",
107
+ " denAccum = (x - self.notch_coeff1 * self.dfs[0]) - self.notch_coeff2 * self.dfs[1]\n",
108
+ " x = (self.notch_coeff3 * denAccum + self.notch_coeff4 * self.dfs[0]) + self.notch_coeff5 * self.dfs[1]\n",
109
+ " self.dfs[1] = self.dfs[0]\n",
110
+ " self.dfs[0] = denAccum\n",
111
+ " # standardization:\n",
112
+ " if self.moving_average is not None:\n",
113
+ " delta = x - self.moving_average\n",
114
+ " self.moving_average = self.moving_average + self.ALPHA_AVG * delta\n",
115
+ " self.moving_variance = (1 - self.ALPHA_STD) * (self.moving_variance + self.ALPHA_STD * delta**2)\n",
116
+ " moving_std = np.sqrt(self.moving_variance)\n",
117
+ " x = (x - self.moving_average) / (moving_std + self.EPSILON)\n",
118
+ " else:\n",
119
+ " self.moving_average = x\n",
120
+ " value[i] = x\n",
121
+ " return value"
122
+ ]
123
+ },
124
+ {
125
+ "cell_type": "code",
126
+ "execution_count": null,
127
+ "id": "80fc186e",
128
+ "metadata": {},
129
+ "outputs": [],
130
+ "source": [
131
+ "duration = 1\n",
132
+ "fsample = 250\n",
133
+ "f1 = 15\n",
134
+ "f2 = 50\n",
135
+ "f3 = 60\n",
136
+ "f4 = 100\n",
137
+ "f5 = 70\n",
138
+ "f6 = 80\n",
139
+ "f7 = 90\n",
140
+ "scale = 4.0e-5\n",
141
+ "\n",
142
+ "w1 = 2*np.pi*f1\n",
143
+ "w2 = 2*np.pi*f2\n",
144
+ "w3 = 2*np.pi*f3\n",
145
+ "w4 = 2*np.pi*f4\n",
146
+ "w5 = 2*np.pi*f5\n",
147
+ "w6 = 2*np.pi*f6\n",
148
+ "w7 = 2*np.pi*f7\n",
149
+ "nb_samples = int(duration*fsample)\n",
150
+ "\n",
151
+ "sig1 = np.array([np.sin(w1*i/fsample) for i in range(nb_samples)])\n",
152
+ "sig2 = np.array([np.sin(w2*i/fsample) for i in range(nb_samples)])\n",
153
+ "sig3 = np.array([np.sin(w3*i/fsample) for i in range(nb_samples)])\n",
154
+ "sig4 = np.array([np.sin(w4*i/fsample) for i in range(nb_samples)])\n",
155
+ "sig5 = np.array([np.sin(w5*i/fsample) for i in range(nb_samples)])\n",
156
+ "sig6 = np.array([np.sin(w6*i/fsample) for i in range(nb_samples)])\n",
157
+ "sig7 = np.array([np.sin(w7*i/fsample) for i in range(nb_samples)])\n",
158
+ "sig8 = sig1 + sig2 + sig3 + sig4 + sig5 + sig6 + sig7\n",
159
+ "\n",
160
+ "v = np.array([sig1, sig2, sig3, sig4, sig5, sig6, sig7, sig8]).T * scale\n",
161
+ "\n",
162
+ "mask = [0,0,0,0,0,0,0,1]\n",
163
+ "\n",
164
+ "v.shape"
165
+ ]
166
+ },
167
+ {
168
+ "cell_type": "code",
169
+ "execution_count": null,
170
+ "id": "b974a851",
171
+ "metadata": {},
172
+ "outputs": [],
173
+ "source": [
174
+ "import matplotlib.pyplot as plt\n",
175
+ "\n",
176
+ "plt.figure(figsize=(20,5))\n",
177
+ "plt.plot(v[:, 7])"
178
+ ]
179
+ },
180
+ {
181
+ "cell_type": "code",
182
+ "execution_count": null,
183
+ "id": "d2b7145c",
184
+ "metadata": {},
185
+ "outputs": [],
186
+ "source": [
187
+ "import time\n",
188
+ "print(mask)\n",
189
+ "fp = FilterPipeline(nb_channels=8, power_line_fq=60)\n",
190
+ "\n",
191
+ "ts = time.time()\n",
192
+ "v = fp.filter(v)\n",
193
+ "print(time.time() - ts)"
194
+ ]
195
+ },
196
+ {
197
+ "cell_type": "code",
198
+ "execution_count": null,
199
+ "id": "70235c0a",
200
+ "metadata": {},
201
+ "outputs": [],
202
+ "source": [
203
+ "plt.figure(figsize=(20,10))\n",
204
+ "plt.plot(v[:, 7])"
205
+ ]
206
+ }
207
+ ],
208
+ "metadata": {
209
+ "kernelspec": {
210
+ "display_name": "Python 3 (ipykernel)",
211
+ "language": "python",
212
+ "name": "python3"
213
+ },
214
+ "language_info": {
215
+ "codemirror_mode": {
216
+ "name": "ipython",
217
+ "version": 3
218
+ },
219
+ "file_extension": ".py",
220
+ "mimetype": "text/x-python",
221
+ "name": "python",
222
+ "nbconvert_exporter": "python",
223
+ "pygments_lexer": "ipython3",
224
+ "version": "3.7.3"
225
+ }
226
+ },
227
+ "nbformat": 4,
228
+ "nbformat_minor": 5
229
+ }