v2.0.0
Loading...
Searching...
No Matches
multitaper_tfr.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "multitaper_tfr.h"
18#include "dpss.h"
19
20//=============================================================================================================
21// EIGEN INCLUDES
22//=============================================================================================================
23
24#include <Eigen/Core>
25//#ifndef EIGEN_FFTW_DEFAULT
26//#define EIGEN_FFTW_DEFAULT
27//#endif
28#include <unsupported/Eigen/FFT>
29
30//=============================================================================================================
31// STD INCLUDES
32//=============================================================================================================
33
34#include <cmath>
35#include <complex>
36#include <numbers>
37
38//=============================================================================================================
39// USED NAMESPACES
40//=============================================================================================================
41
42using namespace UTILSLIB;
43using namespace Eigen;
44
45//=============================================================================================================
46// DEFINE MEMBER METHODS
47//=============================================================================================================
48
50 double sfreq,
51 int windowSize,
52 int stepSize,
53 double halfBandwidth,
54 int nTapers)
55{
56 const int nChannels = static_cast<int>(matData.rows());
57 const int nTimes = static_cast<int>(matData.cols());
58
59 if (stepSize < 0)
60 stepSize = windowSize / 2;
61
62 const int nFreqs = windowSize / 2 + 1;
63
64 // 1. Compute DPSS tapers for the window size
65 DpssResult dpss = Dpss::compute(windowSize, halfBandwidth, nTapers);
66 const int nTap = static_cast<int>(dpss.matTapers.rows());
67
68 // Compute eigenvalue weight sum
69 double weightSum = 0.0;
70 for (int t = 0; t < nTap; ++t)
71 weightSum += dpss.vecEigenvalues[t];
72
73 // 2. Determine number of time steps and build time/freq axes
74 int nSteps = 0;
75 for (int start = 0; start + windowSize <= nTimes; start += stepSize)
76 ++nSteps;
77
79
80 // Frequency axis
81 result.vecFreqs.resize(nFreqs);
82 for (int k = 0; k < nFreqs; ++k)
83 result.vecFreqs[k] = static_cast<double>(k) * sfreq / static_cast<double>(windowSize);
84
85 // Time axis: centre of each window in seconds
86 result.vecTimes.resize(nSteps);
87 for (int s = 0; s < nSteps; ++s) {
88 const int start = s * stepSize;
89 const double centre = static_cast<double>(start) + static_cast<double>(windowSize - 1) / 2.0;
90 result.vecTimes[s] = centre / sfreq;
91 }
92
93 // 3. Compute TFR for each channel
94 Eigen::FFT<double> fft;
95
96 result.tfrData.reserve(nChannels);
97 for (int ch = 0; ch < nChannels; ++ch) {
98 MatrixXd tfr = MatrixXd::Zero(nFreqs, nSteps);
99
100 int stepIdx = 0;
101 for (int start = 0; start + windowSize <= nTimes; start += stepSize) {
102 // Extract window segment
103 VectorXd segment = matData.row(ch).segment(start, windowSize).transpose();
104
105 // Apply each taper, FFT, accumulate weighted |X|^2
106 RowVectorXd psd = RowVectorXd::Zero(nFreqs);
107
108 for (int t = 0; t < nTap; ++t) {
109 VectorXd tapered = segment.array() * dpss.matTapers.row(t).transpose().array();
110
111 VectorXcd spec;
112 fft.fwd(spec, tapered);
113
114 const double w = dpss.vecEigenvalues[t];
115 for (int k = 0; k < nFreqs; ++k)
116 psd[k] += w * std::norm(spec[k]);
117 }
118
119 // Normalise: divide by (weightSum × sfreq)
120 const double dNorm = 1.0 / (weightSum * sfreq);
121 psd *= dNorm;
122
123 // One-sided: double all non-DC, non-Nyquist bins
124 for (int k = 1; k < nFreqs - 1; ++k)
125 psd[k] *= 2.0;
126
127 tfr.col(stepIdx) = psd.transpose();
128 ++stepIdx;
129 }
130
131 result.tfrData.append(tfr);
132 }
133
134 return result;
135}
136
137//=============================================================================================================
138
140 double sfreq,
141 const RowVectorXd& vecFreqs,
142 double nCycles,
143 double timeBandwidth,
144 bool zeroMean)
145{
146 const int nChannels = static_cast<int>(matData.rows());
147 const int nTimes = static_cast<int>(matData.cols());
148 const int nFreqs = static_cast<int>(vecFreqs.size());
149 const int nTap = static_cast<int>(std::floor(timeBandwidth - 1.0));
150
151 MultitaperTfrResult result;
152 result.vecFreqs = vecFreqs;
153 result.vecTimes = RowVectorXd::LinSpaced(nTimes, 0.0, (nTimes - 1) / sfreq);
154 for (int ch = 0; ch < nChannels; ++ch)
155 result.tfrData.append(MatrixXd::Zero(nFreqs, nTimes));
156
157 for (int fi = 0; fi < nFreqs; ++fi) {
158 const double f = vecFreqs[fi];
159 const double tWin = nCycles / f;
160 // Length of np.arange(0.0, t_win, 1.0 / sfreq)
161 const int nWav = static_cast<int>(std::ceil(tWin / (1.0 / sfreq)));
162 const DpssResult dpss = Dpss::compute(nWav, timeBandwidth / 2.0, nTap, false);
163
164 double weightSum = 0.0;
165 for (int m = 0; m < nTap; ++m) {
166 // Oscillation centred on the window before tapering
167 VectorXcd wavelet(nWav);
168 for (int k = 0; k < nWav; ++k) {
169 const double phase = 2.0 * std::numbers::pi * f * (k * (1.0 / sfreq) - tWin / 2.0);
170 wavelet[k] = dpss.matTapers(m, k) * std::complex<double>(std::cos(phase), std::sin(phase));
171 }
172 if (zeroMean)
173 wavelet.array() -= wavelet.mean();
174 wavelet /= std::sqrt(0.5) * wavelet.norm();
175
176 const double weight = dpss.vecEigenvalues[m]; // squared sqrt(concentration)
177 weightSum += weight;
178
179 // 'same' convolution: centre of the full convolution
180 const int offset = (nWav - 1) / 2;
181 for (int ch = 0; ch < nChannels; ++ch) {
182 for (int t = 0; t < nTimes; ++t) {
183 std::complex<double> sum(0.0, 0.0);
184 const int n = t + offset;
185 for (int k = std::max(0, n - nTimes + 1); k <= std::min(nWav - 1, n); ++k)
186 sum += matData(ch, n - k) * wavelet[k];
187 result.tfrData[ch](fi, t) += weight * std::norm(sum);
188 }
189 }
190 }
191 if (nTap > 1)
192 for (int ch = 0; ch < nChannels; ++ch)
193 result.tfrData[ch].row(fi) *= 2.0 / weightSum;
194 }
195
196 return result;
197}
Sliding-window multitaper time-frequency representation.
Discrete Prolate Spheroidal Sequences (Slepian tapers) for multitaper spectral estimation.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
Result of a DPSS taper computation.
Definition dpss.h:56
Eigen::MatrixXd matTapers
nTapers × N, each row is a unit-norm taper
Definition dpss.h:57
Eigen::VectorXd vecEigenvalues
Concentration ratios, length nTapers.
Definition dpss.h:58
static DpssResult compute(int nSamples, double halfBandwidth, int nTapers=-1, bool sym=true, bool lowBias=false)
Definition dpss.cpp:48
Result of a multitaper TFR computation.
QVector< Eigen::MatrixXd > tfrData
One matrix per channel, each: n_freqs × n_times.
Eigen::RowVectorXd vecTimes
Time axis in seconds (window centres).
Eigen::RowVectorXd vecFreqs
Frequency axis in Hz.
static MultitaperTfrResult compute(const Eigen::MatrixXd &matData, double sfreq, int windowSize=256, int stepSize=-1, double halfBandwidth=4.0, int nTapers=-1)
static MultitaperTfrResult computeArray(const Eigen::MatrixXd &matData, double sfreq, const Eigen::RowVectorXd &vecFreqs, double nCycles=7.0, double timeBandwidth=4.0, bool zeroMean=true)