v2.0.0
Loading...
Searching...
No Matches
firfilter.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "firfilter.h"
18
19#include <algorithm>
20#include <cmath>
21#include <complex>
22#include <vector>
23
24//=============================================================================================================
25// EIGEN INCLUDES
26//=============================================================================================================
27
28#include <Eigen/Core>
29#include <unsupported/Eigen/FFT>
30
31//=============================================================================================================
32// QT INCLUDES
33//=============================================================================================================
34
35#include <QString>
36
37//=============================================================================================================
38// USED NAMESPACES
39//=============================================================================================================
40
41using namespace UTILSLIB;
42using namespace Eigen;
43
44//=============================================================================================================
45// STATIC DEFINITIONS
46//=============================================================================================================
47
49 FilterType type,
50 double dCutoffLow,
51 double dCutoffHigh,
52 double dSFreq,
53 double dTransition,
54 DesignMethod method)
55{
56 // FilterKernel frequency encoding (all values normalised to Nyquist = sFreq/2):
57 //
58 // LPF : dCenterfreq = dCutoffLow / nyquist, dBandwidth = 0
59 // HPF : dCenterfreq = dCutoffLow / nyquist, dBandwidth = 0
60 // BPF : dCenterfreq = (lo+hi) / sFreq, dBandwidth = (hi-lo) / nyquist
61 // NOTCH: dCenterfreq = (lo+hi) / sFreq, dBandwidth = (hi-lo) / nyquist
62 //
63 // dParkswidth = dTransition / nyquist
64
65 const double nyquist = dSFreq / 2.0;
66
67 double dCenterfreq = 0.0;
68 double dBandwidth = 0.0;
69 const double dParkswidth = dTransition / nyquist;
70
71 int iFilterType = static_cast<int>(type); // LPF=0, HPF=1, BPF=2, NOTCH=3
72
73 switch (type) {
74 case LowPass:
75 case HighPass:
76 dCenterfreq = dCutoffLow / nyquist;
77 dBandwidth = 0.0;
78 break;
79 case BandPass:
80 case BandStop:
81 dCenterfreq = (dCutoffLow + dCutoffHigh) / dSFreq; // = centre / nyquist normalised to [0,1]
82 dBandwidth = (dCutoffHigh - dCutoffLow) / nyquist;
83 break;
84 }
85
86 QString sName;
87 switch (type) {
88 case LowPass:
89 sName = QStringLiteral("LP_%1Hz").arg(dCutoffLow);
90 break;
91 case HighPass:
92 sName = QStringLiteral("HP_%1Hz").arg(dCutoffLow);
93 break;
94 case BandPass:
95 sName = QStringLiteral("BP_%1-%2Hz").arg(dCutoffLow).arg(dCutoffHigh);
96 break;
97 case BandStop:
98 sName = QStringLiteral("BS_%1-%2Hz").arg(dCutoffLow).arg(dCutoffHigh);
99 break;
100 }
101
102 return FilterKernel(sName,
103 iFilterType,
104 iOrder,
105 dCenterfreq,
106 dBandwidth,
107 dParkswidth,
108 dSFreq,
109 static_cast<int>(method));
110}
111
112//=============================================================================================================
113
114RowVectorXd FirFilter::apply(const RowVectorXd& vecData,
115 FilterKernel& kernel)
116{
117 RowVectorXd work = vecData;
118 kernel.applyFftFilter(work, /*bKeepOverhead=*/false);
119 return work;
120}
121
122//=============================================================================================================
123
124RowVectorXd FirFilter::applyZeroPhase(const RowVectorXd& vecData,
125 FilterKernel& kernel)
126{
127 // Forward pass
128 RowVectorXd work = vecData;
129 kernel.applyFftFilter(work, false);
130
131 // Reverse pass
132 RowVectorXd rev = work.reverse();
133 kernel.applyFftFilter(rev, false);
134
135 return rev.reverse();
136}
137
138//=============================================================================================================
139
140MatrixXd FirFilter::applyZeroPhaseMatrix(const MatrixXd& matData,
141 FilterKernel& kernel,
142 const RowVectorXi& vecPicks)
143{
144 MatrixXd result = matData;
145
146 if (vecPicks.size() == 0) {
147 // All rows
148 for (int i = 0; i < result.rows(); ++i) {
149 RowVectorXd row = result.row(i);
150 result.row(i) = applyZeroPhase(row, kernel);
151 }
152 } else {
153 for (int k = 0; k < vecPicks.size(); ++k) {
154 int i = vecPicks(k);
155 if (i < 0 || i >= result.rows())
156 continue;
157 RowVectorXd row = result.row(i);
158 result.row(i) = applyZeroPhase(row, kernel);
159 }
160 }
161
162 return result;
163}
164
165//=============================================================================================================
166
167RowVectorXd FirFilter::designMne(double dSFreq, double dLFreq, double dHFreq)
168{
169 // Adapted from mne.filter create_filter / _triage_filter_params / _firwin_design
170 // (MNE-Python, BSD-3-Clause), fir_window="hamming", fir_design="firwin", phase="zero".
171 const double nyquist = dSFreq / 2.0;
172 const bool highPass = dLFreq > 0.0;
173 const bool lowPass = dHFreq > 0.0;
174 const double lTrans = highPass ? std::min(std::max(0.25 * dLFreq, 2.0), dLFreq) : 0.0;
175 const double hTrans = lowPass ? std::min(std::max(0.25 * dHFreq, 2.0), nyquist - dHFreq) : 0.0;
176 const double narrowest = std::min(highPass ? lTrans : INFINITY, lowPass ? hTrans : INFINITY);
177
178 int nTaps = std::max(static_cast<int>(std::ceil(3.3 / narrowest * dSFreq)), 1);
179 nTaps += (nTaps - 1) % 2;
180
181 // Piecewise-constant gain as (frequency, gain) points from 0 to Nyquist
182 std::vector<double> freq{0.0};
183 std::vector<double> gain{highPass ? 0.0 : 1.0};
184 if (highPass) {
185 freq.insert(freq.end(), {dLFreq - lTrans, dLFreq});
186 gain.insert(gain.end(), {0.0, 1.0});
187 }
188 if (lowPass) {
189 freq.insert(freq.end(), {dHFreq, dHFreq + hTrans});
190 gain.insert(gain.end(), {1.0, 0.0});
191 }
192 freq.push_back(nyquist);
193 gain.push_back(gain.back());
194
195 // Each gain step is one windowed-sinc low-pass of the length its transition needs, centred.
196 RowVectorXd h = RowVectorXd::Zero(nTaps);
197 if (gain.back() == 1.0) {
198 h(nTaps / 2) = 1.0;
199 }
200 for (int k = static_cast<int>(freq.size()) - 2; k >= 0; --k) {
201 if (gain[k] == gain[k + 1]) {
202 continue;
203 }
204 const double transition = (freq[k + 1] - freq[k]) / nyquist / 2.0;
205 int nStep = static_cast<int>(std::nearbyint(3.3 / transition)); // Python round(): half to even
206 nStep += 1 - nStep % 2;
207 const double cutoff = (freq[k + 1] + freq[k]) / 2.0 / nyquist;
208 RowVectorXd step(nStep);
209 for (int n = 0; n < nStep; ++n) {
210 const double m = n - (nStep - 1) / 2.0;
211 const double sinc = m == 0.0 ? 1.0 : std::sin(M_PI * cutoff * m) / (M_PI * cutoff * m);
212 const double window = nStep > 1 ? 0.54 - 0.46 * std::cos(2.0 * M_PI * n / (nStep - 1)) : 1.0;
213 step(n) = cutoff * sinc * window;
214 }
215 step /= step.sum();
216 const int offset = (nTaps - nStep) / 2;
217 h.segment(offset, nStep) += (gain[k] == 0.0 ? -1.0 : 1.0) * step;
218 }
219 return h;
220}
221
222//=============================================================================================================
223
224MatrixXd FirFilter::filterData(const MatrixXd& matData, double dSFreq, double dLFreq, double dHFreq)
225{
226 const RowVectorXd h = designMne(dSFreq, dLFreq, dHFreq);
227 return overlapAddZeroPhase(matData, h, std::min<Index>(h.size(), matData.cols()) - 1);
228}
229
230//=============================================================================================================
231
232RowVectorXd FirFilter::firwin2(int nTaps, const std::vector<double>& freq, const std::vector<double>& gain, FirWindow window)
233{
234 // Adapted from scipy.signal.firwin2 (SciPy, BSD-3-Clause) for nyq = 1, no antisymmetry.
235 const int nFreqs = 1 + (1 << static_cast<int>(std::ceil(std::log2(nTaps))));
236 RowVectorXcd fx(nFreqs);
237 std::size_t k = 0;
238 for (int i = 0; i < nFreqs; ++i) {
239 const double x = static_cast<double>(i) / (nFreqs - 1);
240 while (k + 2 < freq.size() && x > freq[k + 1])
241 ++k;
242 const double span = freq[k + 1] - freq[k];
243 const double g = span > 0.0 ? gain[k] + (gain[k + 1] - gain[k]) * (x - freq[k]) / span : gain[k + 1];
244 const double phase = -(nTaps - 1) / 2.0 * M_PI * x;
245 fx(i) = g * std::complex<double>(std::cos(phase), std::sin(phase));
246 }
247 FFT<double> fft;
248 fft.SetFlag(FFT<double>::HalfSpectrum);
249 RowVectorXd full;
250 fft.inv(full, fx, 2 * (nFreqs - 1));
251
252 const double a0 = window == FirWindow::Hamming ? 0.54 : 0.5;
253 RowVectorXd h(nTaps);
254 for (int n = 0; n < nTaps; ++n) {
255 const double w = nTaps > 1 ? a0 - (1.0 - a0) * std::cos(2.0 * M_PI * n / (nTaps - 1)) : 1.0;
256 h(n) = full(n) * w;
257 }
258 return h;
259}
260
261//=============================================================================================================
262
263MatrixXd FirFilter::filterDataZeroDouble(const MatrixXd& matData, double dSFreq, double dLFreq, double dHFreq, FirWindow window)
264{
265 // Adapted from mne.filter._triage_filter_params / _construct_fir_filter (MNE-Python, BSD-3-Clause):
266 // filter_length "10s" rounded up to a power of two for zero-double, 0.5 Hz transition bands.
267 const int nTaps = 1 << static_cast<int>(std::ceil(std::log2(std::ceil(10.0 * dSFreq))));
268 const double nyquist = dSFreq / 2.0;
269 const RowVectorXd h = firwin2(nTaps,
270 {0.0, (dLFreq - 0.5) / nyquist, dLFreq / nyquist, dHFreq / nyquist, (dHFreq + 0.5) / nyquist, 1.0},
271 {0.0, 0.0, 1.0, 1.0, 0.0, 0.0},
272 window);
273
274 // Forward and backward pass = one pass with h convolved with its reverse; padding follows the single h.
275 const Index nH = h.size();
276 FFT<double> fft;
277 RowVectorXd padded = RowVectorXd::Zero(2 * nH);
278 padded.head(nH) = h;
279 RowVectorXcd spec;
280 fft.fwd(spec, padded);
281 spec = spec.cwiseAbs2().cast<std::complex<double>>();
282 RowVectorXd autocorr;
283 fft.inv(autocorr, spec);
284 // Lags -(nH - 1) .. nH - 1
285 RowVectorXd hh(2 * nH - 1);
286 hh << autocorr.tail(nH - 1), autocorr.head(nH);
287 return overlapAddZeroPhase(matData, hh, std::min<Index>(nH, matData.cols()) - 1);
288}
289
290//=============================================================================================================
291
292MatrixXd FirFilter::overlapAddZeroPhase(const MatrixXd& matData, const RowVectorXd& h, Index nEdge)
293{
294 // Adapted from mne.filter._overlap_add_filter / mne.cuda._smart_pad (MNE-Python, BSD-3-Clause).
295 const Index nTimes = matData.cols();
296 const Index nH = h.size();
297 if (nTimes == 0 || nH == 1) {
298 return matData * (nH == 1 ? h(0) : 1.0);
299 }
300 const Index nExt = nTimes + 2 * nEdge;
301 // A power of two keeps kissfft fast; any length >= nExt + nH - 1 gives the same linear convolution
302 Index nFft = 1;
303 while (nFft < nExt + nH - 1)
304 nFft <<= 1;
305
306 FFT<double> fft;
307 RowVectorXd hPadded = RowVectorXd::Zero(nFft);
308 hPadded.head(nH) = h;
309 RowVectorXcd hSpec;
310 fft.fwd(hSpec, hPadded);
311
312 MatrixXd out(matData.rows(), nTimes);
313 RowVectorXd ext = RowVectorXd::Zero(nFft);
314 RowVectorXcd spec;
315 RowVectorXd conv;
316 for (Index r = 0; r < matData.rows(); ++r) {
317 const RowVectorXd x = matData.row(r);
318 // "reflect_limited" = odd reflection about the end samples (nEdge < nTimes, so no zero part)
319 ext.setZero();
320 for (Index i = 0; i < nEdge; ++i) {
321 ext(i) = 2.0 * x(0) - x(nEdge - i);
322 ext(nEdge + nTimes + i) = 2.0 * x(nTimes - 1) - x(nTimes - 2 - i);
323 }
324 ext.segment(nEdge, nTimes) = x;
325 fft.fwd(spec, ext);
326 spec = spec.cwiseProduct(hSpec);
327 fft.inv(conv, spec);
328 out.row(r) = conv.segment(nEdge + (nH - 1) / 2, nTimes);
329 }
330 return out;
331}
Discoverable design / apply façade over the UTILSLIB::FilterKernel FIR engine.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
The FilterKernel class provides methods to create/design a FIR filter kernel.
void applyFftFilter(Eigen::RowVectorXd &vecData, bool bKeepOverhead=false)
FilterType
FIR filter type — mirrors IirFilter::FilterType for a uniform API.
Definition firfilter.h:72
static Eigen::MatrixXd filterData(const Eigen::MatrixXd &matData, double dSFreq, double dLFreq, double dHFreq)
static Eigen::MatrixXd applyZeroPhaseMatrix(const Eigen::MatrixXd &matData, FilterKernel &kernel, const Eigen::RowVectorXi &vecPicks=Eigen::RowVectorXi())
static Eigen::MatrixXd filterDataZeroDouble(const Eigen::MatrixXd &matData, double dSFreq, double dLFreq, double dHFreq, FirWindow window=FirWindow::Hann)
static Eigen::RowVectorXd designMne(double dSFreq, double dLFreq, double dHFreq)
DesignMethod
FIR design method — wraps FilterKernel's iDesignMethod integer codes.
Definition firfilter.h:84
static Eigen::RowVectorXd applyZeroPhase(const Eigen::RowVectorXd &vecData, FilterKernel &kernel)
static Eigen::RowVectorXd firwin2(int nTaps, const std::vector< double > &freq, const std::vector< double > &gain, FirWindow window=FirWindow::Hann)
static FilterKernel design(int iOrder, FilterType type, double dCutoffLow, double dCutoffHigh, double dSFreq, double dTransition=5.0, DesignMethod method=Cosine)
Definition firfilter.cpp:48
static Eigen::RowVectorXd apply(const Eigen::RowVectorXd &vecData, FilterKernel &kernel)