v2.0.0
Loading...
Searching...
No Matches
decoding_spoc.cpp
Go to the documentation of this file.
1//=============================================================================================================
32
33//=============================================================================================================
34// INCLUDES
35//=============================================================================================================
36
37#include "decoding_spoc.h"
38
39//=============================================================================================================
40// EIGEN INCLUDES
41//=============================================================================================================
42
43#include <Eigen/Eigenvalues>
44
45//=============================================================================================================
46// STL INCLUDES
47//=============================================================================================================
48
49#include <algorithm>
50#include <cmath>
51#include <numeric>
52#include <stdexcept>
53
54//=============================================================================================================
55// USED NAMESPACES
56//=============================================================================================================
57
58using namespace DECODINGLIB;
59using namespace Eigen;
60
61//=============================================================================================================
62
64 TransformMode transformInto,
65 bool useLog)
66: m_nComponents(nComponents)
67, m_transformInto(transformInto)
68, m_useLog(useLog)
69{
70}
71
72//=============================================================================================================
73
74void DecodingSpoc::fit(const std::vector<MatrixXd>& epochs,
75 const VectorXd& y)
76{
77 if (epochs.empty()) {
78 throw std::invalid_argument("DecodingSpoc::fit: epochs must be non-empty");
79 }
80 if (static_cast<int>(epochs.size()) != y.size()) {
81 throw std::invalid_argument(
82 "DecodingSpoc::fit: epochs and y must have the same length");
83 }
84
85 // Delegate core GED (SPoC algorithm)
86 const auto n_epochs = static_cast<Index>(epochs.size());
87 const auto n_ch = epochs[0].rows();
88
89 // 1. Normalize target
90 double z_mean = y.mean();
91 double z_std = std::sqrt(
92 (y.array() - z_mean).square().sum() / static_cast<double>(n_epochs - 1));
93 VectorXd z = (y.array() - z_mean).matrix();
94 if (z_std > 1e-15)
95 z /= z_std;
96
97 // 2. Per-epoch covariance → C and Cz
98 MatrixXd C = MatrixXd::Zero(n_ch, n_ch);
99 MatrixXd Cz = MatrixXd::Zero(n_ch, n_ch);
100
101 for (Index e = 0; e < n_epochs; ++e) {
102 const auto& X = epochs[static_cast<size_t>(e)];
103 MatrixXd Xc = X.colwise() - X.rowwise().mean();
104 MatrixXd cov_e = (Xc * Xc.transpose()) / static_cast<double>(Xc.cols());
105 C += cov_e;
106 Cz += z(e) * cov_e;
107 }
108 C /= static_cast<double>(n_epochs);
109 Cz /= static_cast<double>(n_epochs);
110
111 // 3. Generalized eigenvalue problem: Cz w = λ C w
112 GeneralizedSelfAdjointEigenSolver<MatrixXd> solver(Cz, C);
113 if (solver.info() != Eigen::Success) {
114 throw std::runtime_error(
115 "DecodingSpoc::fit: eigenvalue decomposition failed");
116 }
117
118 const VectorXd& all_evals = solver.eigenvalues();
119 const MatrixXd& all_evecs = solver.eigenvectors();
120
121 // 4. Sort by |eigenvalue| descending
122 std::vector<int> idx(static_cast<size_t>(n_ch));
123 std::iota(idx.begin(), idx.end(), 0);
124 std::sort(idx.begin(), idx.end(), [&](int a, int b) {
125 return std::abs(all_evals(a)) > std::abs(all_evals(b));
126 });
127
128 int n_comp = std::min(m_nComponents, static_cast<int>(n_ch));
129 m_filters.resize(n_comp, n_ch);
130
131 for (int i = 0; i < n_comp; ++i) {
132 int j = idx[static_cast<size_t>(i)];
133 VectorXd w = all_evecs.col(j);
134 double norm = w.norm();
135 if (norm > 0.0)
136 w /= norm;
137 m_filters.row(i) = w.transpose();
138 }
139
140 // 5. Patterns: A = C W inv(W^T C W)
141 MatrixXd Wt = m_filters.transpose();
142 MatrixXd CW = C * Wt;
143 MatrixXd WtCW = Wt.transpose() * CW;
144 m_patterns = (CW * WtCW.inverse()); // (n_ch × n_comp) — already correct shape
145
146 // Compute mean band power for z-score normalisation
147 MatrixXd powerFeatures = computePowerFeatures(epochs);
148 m_mean = powerFeatures.colwise().mean();
149
150 m_std = VectorXd(powerFeatures.cols());
151 for (int c = 0; c < powerFeatures.cols(); ++c) {
152 VectorXd centered = powerFeatures.col(c).array() - m_mean(c);
153 m_std(c) = std::sqrt(centered.squaredNorm() / static_cast<double>(centered.size()));
154 }
155
156 m_fitted = true;
157}
158
159//=============================================================================================================
160
161MatrixXd DecodingSpoc::transform(const std::vector<MatrixXd>& epochs) const
162{
163 if (!m_fitted) {
164 throw std::runtime_error("DecodingSpoc::transform: not fitted");
165 }
166
167 if (m_transformInto == TransformMode::CspSpace) {
168 const int nEpochs = static_cast<int>(epochs.size());
169 const int nComp = static_cast<int>(m_filters.rows());
170 const int nTimes = static_cast<int>(epochs[0].cols());
171
172 MatrixXd result(nEpochs * nComp, nTimes);
173 for (int e = 0; e < nEpochs; ++e) {
174 result.middleRows(static_cast<Eigen::Index>(e) * nComp, nComp) = m_filters * epochs[static_cast<size_t>(e)];
175 }
176 return result;
177 }
178
179 // AveragePower mode
180 MatrixXd X = computePowerFeatures(epochs);
181
182 if (m_useLog) {
183 X = X.array().max(1e-30).log().matrix();
184 } else {
185 for (int c = 0; c < X.cols(); ++c) {
186 double s = m_std(c);
187 if (s < 1e-15)
188 s = 1.0;
189 X.col(c) = (X.col(c).array() - m_mean(c)) / s;
190 }
191 }
192
193 return X;
194}
195
196//=============================================================================================================
197
198MatrixXd DecodingSpoc::fitTransform(const std::vector<MatrixXd>& epochs,
199 const VectorXd& y)
200{
201 fit(epochs, y);
202 return transform(epochs);
203}
204
205//=============================================================================================================
206
207const MatrixXd& DecodingSpoc::filters() const
208{
209 if (!m_fitted) {
210 throw std::runtime_error("DecodingSpoc::filters: not fitted");
211 }
212 return m_filters;
213}
214
215//=============================================================================================================
216
217const MatrixXd& DecodingSpoc::patterns() const
218{
219 if (!m_fitted) {
220 throw std::runtime_error("DecodingSpoc::patterns: not fitted");
221 }
222 return m_patterns;
223}
224
225//=============================================================================================================
226
227const VectorXd& DecodingSpoc::mean() const
228{
229 if (!m_fitted) {
230 throw std::runtime_error("DecodingSpoc::mean: not fitted");
231 }
232 return m_mean;
233}
234
235//=============================================================================================================
236
237const VectorXd& DecodingSpoc::stddev() const
238{
239 if (!m_fitted) {
240 throw std::runtime_error("DecodingSpoc::stddev: not fitted");
241 }
242 return m_std;
243}
244
245//=============================================================================================================
246
248{
249 return m_fitted;
250}
251
252//=============================================================================================================
253
254MatrixXd DecodingSpoc::computePowerFeatures(
255 const std::vector<MatrixXd>& epochs) const
256{
257 const int nEpochs = static_cast<int>(epochs.size());
258 const int nComp = static_cast<int>(m_filters.rows());
259
260 MatrixXd features(nEpochs, nComp);
261 for (int e = 0; e < nEpochs; ++e) {
262 MatrixXd filtered = m_filters * epochs[static_cast<size_t>(e)];
263 for (int c = 0; c < nComp; ++c) {
264 features(e, c) = filtered.row(c).squaredNorm() / static_cast<double>(filtered.cols());
265 }
266 }
267
268 return features;
269}
Source Power Comodulation (SPoC) for regressing continuous targets onto narrowband M/EEG power.
constexpr int X
Supervised and unsupervised spatial-filter decompositions for M/EEG decoding.
Eigen::MatrixXd fitTransform(const std::vector< Eigen::MatrixXd > &epochs, const Eigen::VectorXd &y)
void fit(const std::vector< Eigen::MatrixXd > &epochs, const Eigen::VectorXd &y)
Eigen::MatrixXd transform(const std::vector< Eigen::MatrixXd > &epochs) const
DecodingSpoc(int nComponents=4, TransformMode transformInto=TransformMode::AveragePower, bool useLog=true)
const Eigen::VectorXd & mean() const
const Eigen::MatrixXd & patterns() const
const Eigen::MatrixXd & filters() const
const Eigen::VectorXd & stddev() const