v2.0.0
Loading...
Searching...
No Matches
decoding_ssd.cpp
Go to the documentation of this file.
1//=============================================================================================================
34
35//=============================================================================================================
36// INCLUDES
37//=============================================================================================================
38
39#include "decoding_ssd.h"
40
41//=============================================================================================================
42// EIGEN INCLUDES
43//=============================================================================================================
44
45#include <Eigen/Eigenvalues>
46
47//=============================================================================================================
48// STL INCLUDES
49//=============================================================================================================
50
51#include <algorithm>
52#include <cmath>
53#include <stdexcept>
54
55//=============================================================================================================
56// USED NAMESPACES
57//=============================================================================================================
58
59using namespace DECODINGLIB;
60using namespace Eigen;
61
62//=============================================================================================================
63
64DecodingSsd::DecodingSsd(int nComponents, double regParam)
65 : m_nComponents(nComponents)
66 , m_regParam(regParam)
67{
68}
69
70//=============================================================================================================
71
72void DecodingSsd::fit(const Ref<const MatrixXd>& data,
73 double sfreq,
74 double signalLow, double signalHigh,
75 double noiseLow, double noiseHigh)
76{
77 if (noiseLow > signalLow || signalHigh > noiseHigh) {
78 throw std::invalid_argument(
79 "DecodingSsd::fit: signal band must be within noise band");
80 }
81
82 const auto n_ch = data.rows();
83 const auto n_times = data.cols();
84
85 if (n_ch < 2 || n_times < 2) {
86 throw std::invalid_argument(
87 "DecodingSsd::fit: data must have >= 2 channels and >= 2 time points");
88 }
89
90 int n_comp = std::min(m_nComponents, static_cast<int>(n_ch));
91
92 // Mean-center
93 MatrixXd centered = data.colwise() - data.rowwise().mean();
94
95 // Bandpass filter for signal and noise bands
96 MatrixXd data_signal = bandpassFilter(centered, sfreq, signalLow, signalHigh);
97 MatrixXd data_noise = bandpassFilter(centered, sfreq, noiseLow, noiseHigh);
98
99 // Covariance matrices
100 MatrixXd cov_signal = (data_signal * data_signal.transpose())
101 / static_cast<double>(n_times - 1);
102 MatrixXd cov_noise = (data_noise * data_noise.transpose())
103 / static_cast<double>(n_times - 1);
104
105 // Regularize noise covariance
106 cov_noise += m_regParam * cov_noise.trace()
107 / static_cast<double>(n_ch)
108 * MatrixXd::Identity(n_ch, n_ch);
109
110 // Generalized eigenvalue problem: C_signal w = λ C_noise w
111 GeneralizedSelfAdjointEigenSolver<MatrixXd> ges(cov_signal, cov_noise);
112 if (ges.info() != Eigen::Success) {
113 throw std::runtime_error(
114 "DecodingSsd::fit: eigenvalue decomposition failed");
115 }
116
117 // Eigenvalues ascending → reverse for descending
118 VectorXd evals = ges.eigenvalues().reverse();
119 MatrixXd evecs = ges.eigenvectors().rowwise().reverse();
120
121 m_eigenvalues = evals.head(n_comp);
122 m_filters = evecs.leftCols(n_comp).transpose(); // (n_comp × n_ch)
123
124 // Patterns: A = C W inv(W^T C W)
125 MatrixXd cov_full = (centered * centered.transpose())
126 / static_cast<double>(n_times - 1);
127 MatrixXd WtCW = m_filters * cov_full * m_filters.transpose();
128 m_patterns = (cov_full * m_filters.transpose()
129 * WtCW.inverse()); // (n_ch × n_comp)
130
131 m_fitted = true;
132}
133
134//=============================================================================================================
135
136MatrixXd DecodingSsd::transform(const Ref<const MatrixXd>& data) const
137{
138 if (!m_fitted) {
139 throw std::runtime_error("DecodingSsd::transform: not fitted");
140 }
141 return m_filters * data;
142}
143
144//=============================================================================================================
145
146MatrixXd DecodingSsd::fitTransform(const Ref<const MatrixXd>& data,
147 double sfreq,
148 double signalLow, double signalHigh,
149 double noiseLow, double noiseHigh)
150{
151 fit(data, sfreq, signalLow, signalHigh, noiseLow, noiseHigh);
152 return transform(data);
153}
154
155//=============================================================================================================
156
157MatrixXd DecodingSsd::apply(const Ref<const MatrixXd>& data) const
158{
159 if (!m_fitted) {
160 throw std::runtime_error("DecodingSsd::apply: not fitted");
161 }
162
163 // Denoise: project to SSD space, then reconstruct using patterns
164 // patterns_ is (n_ch × n_comp), ssdSources is (n_comp × n_times)
165 MatrixXd ssdSources = transform(data);
166 return m_patterns * ssdSources;
167}
168
169//=============================================================================================================
170
171const MatrixXd& DecodingSsd::filters() const
172{
173 if (!m_fitted) {
174 throw std::runtime_error("DecodingSsd::filters: not fitted");
175 }
176 return m_filters;
177}
178
179//=============================================================================================================
180
181const MatrixXd& DecodingSsd::patterns() const
182{
183 if (!m_fitted) {
184 throw std::runtime_error("DecodingSsd::patterns: not fitted");
185 }
186 return m_patterns;
187}
188
189//=============================================================================================================
190
191const VectorXd& DecodingSsd::eigenvalues() const
192{
193 if (!m_fitted) {
194 throw std::runtime_error("DecodingSsd::eigenvalues: not fitted");
195 }
196 return m_eigenvalues;
197}
198
199//=============================================================================================================
200
202{
203 return m_fitted;
204}
205
206//=============================================================================================================
207
208MatrixXd DecodingSsd::bandpassFilter(const MatrixXd& data,
209 double sfreq,
210 double lowFreq,
211 double highFreq)
212{
213 const auto n_ch = data.rows();
214 const auto n_times = data.cols();
215
216 int filter_order = std::min(
217 static_cast<int>(std::round(3.0 * sfreq / lowFreq)),
218 static_cast<int>(n_times) - 1);
219 if (filter_order % 2 != 0) ++filter_order;
220 filter_order = std::min(filter_order, 128);
221
222 const int half = filter_order / 2;
223 const double pi = M_PI;
224
225 // Windowed-sinc coefficients
226 VectorXd h(filter_order + 1);
227 double w_low = 2.0 * pi * lowFreq / sfreq;
228 double w_high = 2.0 * pi * highFreq / sfreq;
229
230 for (int i = 0; i <= filter_order; ++i) {
231 int n = i - half;
232 if (n == 0) {
233 h(i) = (w_high - w_low) / pi;
234 } else {
235 h(i) = (std::sin(w_high * static_cast<double>(n))
236 - std::sin(w_low * static_cast<double>(n)))
237 / (pi * static_cast<double>(n));
238 }
239 // Hamming window
240 h(i) *= 0.54
241 - 0.46 * std::cos(
242 2.0 * pi * static_cast<double>(i)
243 / static_cast<double>(filter_order));
244 }
245
246 // Normalize to unit gain at center frequency
247 double center = (lowFreq + highFreq) / 2.0;
248 double w_center = 2.0 * pi * center / sfreq;
249 double gain = 0.0;
250 for (int i = 0; i <= filter_order; ++i) {
251 gain += h(i) * std::cos(
252 w_center * static_cast<double>(i - half));
253 }
254 if (std::abs(gain) > 1e-12) h /= std::abs(gain);
255
256 // Forward + reverse convolution (zero-phase)
257 MatrixXd result(n_ch, n_times);
258
259 for (Index ch = 0; ch < n_ch; ++ch) {
260 VectorXd forward(n_times);
261 for (Index t = 0; t < n_times; ++t) {
262 double sum = 0.0;
263 for (int k = 0; k <= filter_order; ++k) {
264 auto idx = t - k;
265 if (idx >= 0 && idx < n_times)
266 sum += h(k) * data(ch, idx);
267 }
268 forward(t) = sum;
269 }
270
271 VectorXd backward(n_times);
272 for (Index t = n_times - 1; t >= 0; --t) {
273 double sum = 0.0;
274 for (int k = 0; k <= filter_order; ++k) {
275 auto idx = t + k;
276 if (idx >= 0 && idx < n_times)
277 sum += h(k) * forward(idx);
278 }
279 backward(t) = sum;
280 }
281
282 result.row(ch) = backward.transpose();
283 }
284
285 return result;
286}
Spatio-Spectral Decomposition (SSD) for noise-aware narrowband enhancement of continuous M/EEG.
#define M_PI
Supervised and unsupervised spatial-filter decompositions for M/EEG decoding.
DecodingSsd(int nComponents=6, double regParam=0.05)
Eigen::MatrixXd apply(const Eigen::Ref< const Eigen::MatrixXd > &data) const
const Eigen::MatrixXd & filters() const
const Eigen::VectorXd & eigenvalues() const
Eigen::MatrixXd transform(const Eigen::Ref< const Eigen::MatrixXd > &data) const
const Eigen::MatrixXd & patterns() const
void fit(const Eigen::Ref< const Eigen::MatrixXd > &data, double sfreq, double signalLow, double signalHigh, double noiseLow, double noiseHigh)
Eigen::MatrixXd fitTransform(const Eigen::Ref< const Eigen::MatrixXd > &data, double sfreq, double signalLow, double signalHigh, double noiseLow, double noiseHigh)