v2.0.0
Loading...
Searching...
No Matches
csd.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "csd.h"
18#include "dpss.h"
19#include "morlet_tfr.h"
20
21//=============================================================================================================
22// EIGEN INCLUDES
23//=============================================================================================================
24
25#include <Eigen/Core>
26#include <Eigen/Dense>
27//#ifndef EIGEN_FFTW_DEFAULT
28//#define EIGEN_FFTW_DEFAULT
29//#endif
30#include <unsupported/Eigen/FFT>
31
32//=============================================================================================================
33// STD INCLUDES
34//=============================================================================================================
35
36#include <cmath>
37#include <complex>
38#include <vector>
39
40//=============================================================================================================
41// USED NAMESPACES
42//=============================================================================================================
43
44using namespace UTILSLIB;
45using namespace Eigen;
46
47namespace
48{
49constexpr double CSD_PI = 3.14159265358979323846;
50}
51
52//=============================================================================================================
53// DEFINE MEMBER METHODS
54//=============================================================================================================
55
56CsdResult Csd::computeMultitaper(const MatrixXd& matData,
57 double sfreq,
58 double fmin,
59 double fmax,
60 double halfBandwidth,
61 int nTapers)
62{
63 const int nChannels = static_cast<int>(matData.rows());
64 const int nTimes = static_cast<int>(matData.cols());
65 const int nFreqsFull = nTimes / 2 + 1;
66
67 if (fmax < 0.0)
68 fmax = sfreq / 2.0;
69
70 // 1. Compute DPSS tapers; by default those mne.time_frequency.csd_array_multitaper uses
71 const bool lowBias = nTapers < 0;
72 DpssResult dpss = Dpss::compute(nTimes, halfBandwidth, lowBias ? static_cast<int>(2.0 * halfBandwidth) : nTapers, false, lowBias);
73 const int nTap = static_cast<int>(dpss.matTapers.rows());
74
75 // Compute eigenvalue weight sum
76 double weightSum = 0.0;
77 for (int t = 0; t < nTap; ++t)
78 weightSum += dpss.vecEigenvalues[t];
79
80 // Build full frequency axis
81 const double freqRes = sfreq / static_cast<double>(nTimes);
82
83 // Determine frequency bin range (DC excluded, as in MNE-Python)
84 const int iBinMin = std::max(1, static_cast<int>(std::ceil(fmin / freqRes)));
85 const int iBinMax = std::min(nFreqsFull - 1, static_cast<int>(std::floor(fmax / freqRes)));
86 const int nFreqsSel = std::max(0, iBinMax - iBinMin + 1);
87
88 // Initialize per-frequency CSD accumulators
89 std::vector<MatrixXcd> csdAccum(static_cast<std::size_t>(nFreqsSel),
90 MatrixXcd::Zero(nChannels, nChannels));
91
92 Eigen::FFT<double> fft;
93
94 // 2. For each taper, compute FFT for all channels, then accumulate CSD
95 for (int t = 0; t < nTap; ++t) {
96 const double w = dpss.vecEigenvalues[t];
97
98 // Compute FFT spectra for all channels with this taper
99 MatrixXcd spectra(nChannels, nFreqsFull);
100
101 for (int ch = 0; ch < nChannels; ++ch) {
102 VectorXd tapered = (matData.row(ch).array() - matData.row(ch).mean()).transpose() * dpss.matTapers.row(t).transpose().array();
103
104 VectorXcd spec;
105 fft.fwd(spec, tapered);
106
107 spectra.row(ch) = spec.head(nFreqsFull).transpose();
108 }
109 if (nTimes % 2 == 0)
110 spectra.col(nFreqsFull - 1) /= std::sqrt(2.0);
111
112 // Accumulate weighted CSD: X(:,f) * X(:,f)^H for selected frequency bins
113 for (int fi = 0; fi < nFreqsSel; ++fi) {
114 const int fBin = iBinMin + fi;
115 VectorXcd col = spectra.col(fBin);
116 csdAccum[static_cast<std::size_t>(fi)] += w * (col * col.adjoint());
117 }
118 }
119
120 // 3. Assemble result
121 CsdResult result;
122 result.vecFreqs.resize(nFreqsSel);
123 result.csdByFreq.resize(nFreqsSel);
124
125 MatrixXcd meanCsd = MatrixXcd::Zero(nChannels, nChannels);
126
127 for (int fi = 0; fi < nFreqsSel; ++fi) {
128 // One-sided and per Hz, as mne.time_frequency.csd_array_multitaper
129 csdAccum[static_cast<std::size_t>(fi)] *= 2.0 / (weightSum * sfreq);
130 result.csdByFreq[fi] = csdAccum[static_cast<std::size_t>(fi)];
131 result.vecFreqs[fi] = (iBinMin + fi) * freqRes;
132 meanCsd += result.csdByFreq[fi];
133 }
134
135 if (nFreqsSel > 0)
136 meanCsd /= static_cast<double>(nFreqsSel);
137
138 result.matCsd = meanCsd;
139 return result;
140}
141
142//=============================================================================================================
143
144CsdResult Csd::computeFourier(const MatrixXd& matData,
145 double sfreq,
146 double fmin,
147 double fmax,
148 int nFft,
149 double overlap)
150{
151 const int nChannels = static_cast<int>(matData.rows());
152 const int nTimes = static_cast<int>(matData.cols());
153
154 if (nFft > nTimes)
155 nFft = nTimes;
156
157 const int nFreqsFull = nFft / 2 + 1;
158
159 if (fmax < 0.0)
160 fmax = sfreq / 2.0;
161
162 const double freqRes = sfreq / static_cast<double>(nFft);
163 const int iBinMin = std::max(1, static_cast<int>(std::ceil(fmin / freqRes)));
164 const int iBinMax = std::min(nFreqsFull - 1, static_cast<int>(std::floor(fmax / freqRes)));
165 const int nFreqsSel = std::max(0, iBinMax - iBinMin + 1);
166
167 // Build Hann window
168 VectorXd hannWin(nFft);
169 for (int n = 0; n < nFft; ++n) {
170 const double t = static_cast<double>(n) / static_cast<double>(nFft - 1);
171 hannWin[n] = 0.5 * (1.0 - std::cos(2.0 * CSD_PI * t));
172 }
173
174 const int iStep = std::max(1, static_cast<int>(std::round(static_cast<double>(nFft) * (1.0 - overlap))));
175
176 // Initialize per-frequency CSD accumulators
177 std::vector<MatrixXcd> csdAccum(static_cast<std::size_t>(nFreqsSel),
178 MatrixXcd::Zero(nChannels, nChannels));
179
180 Eigen::FFT<double> fft;
181 int nSeg = 0;
182
183 // Process each segment
184 for (int start = 0; start + nFft <= nTimes; start += iStep) {
185 // FFT all channels for this segment
186 MatrixXcd spectra(nChannels, nFreqsFull);
187
188 for (int ch = 0; ch < nChannels; ++ch) {
189 VectorXd seg = matData.row(ch).segment(start, nFft).transpose();
190 seg = (seg.array() - seg.mean()) * hannWin.array();
191
192 VectorXcd spec;
193 fft.fwd(spec, seg);
194
195 spectra.row(ch) = spec.head(nFreqsFull).transpose();
196 }
197 // One-sided spectrum: DC and an even-length Nyquist bin have no mirror image
198 spectra.col(0) /= std::sqrt(2.0);
199 if (nFft % 2 == 0)
200 spectra.col(nFreqsFull - 1) /= std::sqrt(2.0);
201
202 // Accumulate CSD for selected frequency bins
203 for (int fi = 0; fi < nFreqsSel; ++fi) {
204 const int fBin = iBinMin + fi;
205 VectorXcd col = spectra.col(fBin);
206 csdAccum[static_cast<std::size_t>(fi)] += col * col.adjoint();
207 }
208
209 ++nSeg;
210 }
211
212 // 3. Assemble result
213 CsdResult result;
214 result.vecFreqs.resize(nFreqsSel);
215 result.csdByFreq.resize(nFreqsSel);
216
217 MatrixXcd meanCsd = MatrixXcd::Zero(nChannels, nChannels);
218
219 // As mne.time_frequency.csd_array_fourier: 2 for one-sided, 8/3 for the Hann power loss, per Hz
220 const double dNorm = (nSeg > 0) ? 2.0 * 8.0 / 3.0 / (static_cast<double>(nSeg) * nFft * sfreq) : 0.0;
221
222 for (int fi = 0; fi < nFreqsSel; ++fi) {
223 csdAccum[static_cast<std::size_t>(fi)] *= dNorm;
224 result.csdByFreq[fi] = csdAccum[static_cast<std::size_t>(fi)];
225 result.vecFreqs[fi] = (iBinMin + fi) * freqRes;
226 meanCsd += result.csdByFreq[fi];
227 }
228
229 if (nFreqsSel > 0)
230 meanCsd /= static_cast<double>(nFreqsSel);
231
232 result.matCsd = meanCsd;
233 return result;
234}
235
236//=============================================================================================================
237
238CsdResult Csd::computeMorlet(const MatrixXd& matData,
239 double sfreq,
240 const RowVectorXd& frequencies,
241 int nCycles)
242{
243 const int nChannels = static_cast<int>(matData.rows());
244 const int nTimes = static_cast<int>(matData.cols());
245 const int nFreqs = static_cast<int>(frequencies.size());
246
247 CsdResult result;
248 result.vecFreqs = frequencies;
249 result.csdByFreq.resize(nFreqs);
250
251 MatrixXcd meanCsd = MatrixXcd::Zero(nChannels, nChannels);
252
253 for (int fi = 0; fi < nFreqs; ++fi) {
254 int halfLen = 0;
255 const VectorXcd wavelet = MorletTfr::buildWavelet(frequencies[fi], sfreq, nCycles, halfLen);
256 const int wavLen = static_cast<int>(wavelet.size());
257
258 // Same-length convolution; conj(w[k]) = w[wavLen - 1 - k] for a Morlet wavelet without offset
259 MatrixXcd analytic(nChannels, nTimes);
260
261 for (int ch = 0; ch < nChannels; ++ch) {
262 for (int t = 0; t < nTimes; ++t) {
263 std::complex<double> sum(0.0, 0.0);
264 for (int k = 0; k < wavLen; ++k) {
265 const int idx = t - halfLen + k;
266 if (idx >= 0 && idx < nTimes) {
267 sum += matData(ch, idx) * std::conj(wavelet[k]);
268 }
269 }
270 analytic(ch, t) = sum;
271 }
272 }
273
274 // CSD at this frequency = mean over time of C(:,t) * C(:,t)^H
275 MatrixXcd csdAtFreq = MatrixXcd::Zero(nChannels, nChannels);
276 for (int t = 0; t < nTimes; ++t) {
277 VectorXcd col = analytic.col(t);
278 csdAtFreq += col * col.adjoint();
279 }
280 // Scaled by the sampling frequency like mne.time_frequency.csd_array_morlet
281 csdAtFreq /= static_cast<double>(nTimes) * sfreq;
282
283 result.csdByFreq[fi] = csdAtFreq;
284 meanCsd += csdAtFreq;
285 }
286
287 if (nFreqs > 0)
288 meanCsd /= static_cast<double>(nFreqs);
289
290 result.matCsd = meanCsd;
291 return result;
292}
Complex Morlet wavelet time-frequency representation (TFR).
Discrete Prolate Spheroidal Sequences (Slepian tapers) for multitaper spectral estimation.
Cross-spectral density (CSD) estimation via averaged windowed FFTs.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
Result of a Cross-Spectral Density computation.
Definition csd.h:63
Eigen::MatrixXcd matCsd
n_channels × n_channels mean CSD across selected frequencies
Definition csd.h:64
Eigen::RowVectorXd vecFreqs
Frequency axis (Hz) for csdByFreq entries.
Definition csd.h:66
QVector< Eigen::MatrixXcd > csdByFreq
One n_ch × n_ch CSD matrix per selected frequency bin.
Definition csd.h:65
static CsdResult computeMultitaper(const Eigen::MatrixXd &matData, double sfreq, double fmin=0.0, double fmax=-1.0, double halfBandwidth=4.0, int nTapers=-1)
Definition csd.cpp:56
static CsdResult computeMorlet(const Eigen::MatrixXd &matData, double sfreq, const Eigen::RowVectorXd &frequencies, int nCycles=7)
Definition csd.cpp:238
static CsdResult computeFourier(const Eigen::MatrixXd &matData, double sfreq, double fmin=0.0, double fmax=-1.0, int nFft=256, double overlap=0.5)
Definition csd.cpp:144
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
static Eigen::VectorXcd buildWavelet(double dFreq, double dSFreq, double dNCycles, int &halfLen, bool bZeroMean=false)