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()
93 / static_cast<double>(n_epochs - 1));
94 VectorXd z = (y.array() - z_mean).matrix();
95 if (z_std > 1e-15) 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())
105 / static_cast<double>(Xc.cols());
106 C += cov_e;
107 Cz += z(e) * cov_e;
108 }
109 C /= static_cast<double>(n_epochs);
110 Cz /= static_cast<double>(n_epochs);
111
112 // 3. Generalized eigenvalue problem: Cz w = λ C w
113 GeneralizedSelfAdjointEigenSolver<MatrixXd> solver(Cz, C);
114 if (solver.info() != Eigen::Success) {
115 throw std::runtime_error(
116 "DecodingSpoc::fit: eigenvalue decomposition failed");
117 }
118
119 const VectorXd& all_evals = solver.eigenvalues();
120 const MatrixXd& all_evecs = solver.eigenvectors();
121
122 // 4. Sort by |eigenvalue| descending
123 std::vector<int> idx(static_cast<size_t>(n_ch));
124 std::iota(idx.begin(), idx.end(), 0);
125 std::sort(idx.begin(), idx.end(), [&](int a, int b) {
126 return std::abs(all_evals(a)) > std::abs(all_evals(b));
127 });
128
129 int n_comp = std::min(m_nComponents, static_cast<int>(n_ch));
130 m_filters.resize(n_comp, n_ch);
131
132 for (int i = 0; i < n_comp; ++i) {
133 int j = idx[static_cast<size_t>(i)];
134 VectorXd w = all_evecs.col(j);
135 double norm = w.norm();
136 if (norm > 0.0) 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()
154 / static_cast<double>(centered.size()));
155 }
156
157 m_fitted = true;
158}
159
160//=============================================================================================================
161
162MatrixXd DecodingSpoc::transform(const std::vector<MatrixXd>& epochs) const
163{
164 if (!m_fitted) {
165 throw std::runtime_error("DecodingSpoc::transform: not fitted");
166 }
167
168 if (m_transformInto == TransformMode::CspSpace) {
169 const int nEpochs = static_cast<int>(epochs.size());
170 const int nComp = static_cast<int>(m_filters.rows());
171 const int nTimes = static_cast<int>(epochs[0].cols());
172
173 MatrixXd result(nEpochs * nComp, nTimes);
174 for (int e = 0; e < nEpochs; ++e) {
175 result.middleRows(static_cast<Eigen::Index>(e) * nComp, nComp) = m_filters * epochs[static_cast<size_t>(e)];
176 }
177 return result;
178 }
179
180 // AveragePower mode
181 MatrixXd X = computePowerFeatures(epochs);
182
183 if (m_useLog) {
184 X = X.array().max(1e-30).log().matrix();
185 } else {
186 for (int c = 0; c < X.cols(); ++c) {
187 double s = m_std(c);
188 if (s < 1e-15) 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()
265 / static_cast<double>(filtered.cols());
266 }
267 }
268
269 return features;
270}
constexpr int X
Source Power Comodulation (SPoC) for regressing continuous targets onto narrowband M/EEG power.
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