v2.0.0
Loading...
Searching...
No Matches
decoding_csp.cpp
Go to the documentation of this file.
1//=============================================================================================================
30
31//=============================================================================================================
32// INCLUDES
33//=============================================================================================================
34
35#include "decoding_csp.h"
36
37//=============================================================================================================
38// EIGEN INCLUDES
39//=============================================================================================================
40
41#include <Eigen/Eigenvalues>
42#include <Eigen/SVD>
43
44//=============================================================================================================
45// STL INCLUDES
46//=============================================================================================================
47
48#include <algorithm>
49#include <cmath>
50#include <set>
51#include <stdexcept>
52
53//=============================================================================================================
54// USED NAMESPACES
55//=============================================================================================================
56
57using namespace DECODINGLIB;
58using namespace Eigen;
59
60//=============================================================================================================
61
63 TransformMode transformInto,
64 bool useLog)
65 : m_nComponents(nComponents)
66 , m_transformInto(transformInto)
67 , m_useLog(useLog)
68{
69}
70
71//=============================================================================================================
72
73void DecodingCsp::fit(const std::vector<MatrixXd>& epochs,
74 const VectorXi& y)
75{
76 if (epochs.empty()) {
77 throw std::invalid_argument("DecodingCsp::fit: epochs must be non-empty");
78 }
79 if (static_cast<int>(epochs.size()) != y.size()) {
80 throw std::invalid_argument(
81 "DecodingCsp::fit: epochs and y must have the same length");
82 }
83
84 // Identify unique classes
85 std::set<int> classSet(y.data(), y.data() + y.size());
86 if (classSet.size() != 2) {
87 throw std::invalid_argument(
88 "DecodingCsp::fit: y must contain exactly 2 unique class labels");
89 }
90
91 auto it = classSet.begin();
92 int label1 = *it++;
93 int label2 = *it;
94
95 // Split epochs by class
96 std::vector<MatrixXd> epochs1, epochs2;
97 for (int i = 0; i < y.size(); ++i) {
98 if (y(i) == label1) {
99 epochs1.push_back(epochs[static_cast<size_t>(i)]);
100 } else {
101 epochs2.push_back(epochs[static_cast<size_t>(i)]);
102 }
103 }
104
105 const auto n_ch = epochs1[0].rows();
106
107 // Average covariance for each class
108 MatrixXd cov1 = MatrixXd::Zero(n_ch, n_ch);
109 for (const auto& epoch : epochs1) {
110 MatrixXd centered = epoch.colwise() - epoch.rowwise().mean();
111 cov1 += centered * centered.transpose()
112 / static_cast<double>(epoch.cols() - 1);
113 }
114 cov1 /= static_cast<double>(epochs1.size());
115
116 MatrixXd cov2 = MatrixXd::Zero(n_ch, n_ch);
117 for (const auto& epoch : epochs2) {
118 MatrixXd centered = epoch.colwise() - epoch.rowwise().mean();
119 cov2 += centered * centered.transpose()
120 / static_cast<double>(epoch.cols() - 1);
121 }
122 cov2 /= static_cast<double>(epochs2.size());
123
124 // Composite covariance
125 MatrixXd cov_comp = cov1 + cov2;
126
127 // Whitening: W = D^{-1/2} U^T
128 SelfAdjointEigenSolver<MatrixXd> eig_comp(cov_comp);
129 VectorXd d = eig_comp.eigenvalues();
130 MatrixXd U = eig_comp.eigenvectors();
131
132 const double d_min = d.maxCoeff() * 1e-10;
133 for (Index i = 0; i < d.size(); ++i) {
134 if (d(i) < d_min) d(i) = d_min;
135 }
136
137 MatrixXd W = d.array().sqrt().inverse().matrix().asDiagonal()
138 * U.transpose();
139
140 // Whiten class-1 covariance and eigendecompose
141 MatrixXd S1 = W * cov1 * W.transpose();
142 SelfAdjointEigenSolver<MatrixXd> eig_s1(S1);
143 VectorXd lambdas = eig_s1.eigenvalues();
144 MatrixXd B = eig_s1.eigenvectors();
145
146 // All filters in eigenvalue order (ascending)
147 MatrixXd all_filters = B.transpose() * W;
148
149 // Select first and last components
150 int n_per_class = m_nComponents / 2;
151 int n_total = std::min(m_nComponents, static_cast<int>(n_ch));
152 n_per_class = std::min(n_per_class, static_cast<int>(n_ch) / 2);
153
154 m_filters = MatrixXd(n_total, n_ch);
155 VectorXd eigenvalues(n_total);
156
157 // Bottom n_per_class (maximize class-2 variance)
158 for (int i = 0; i < n_per_class; ++i) {
159 m_filters.row(i) = all_filters.row(i);
160 eigenvalues(i) = lambdas(i);
161 }
162 // Top (n_total - n_per_class) (maximize class-1 variance)
163 for (int i = 0; i < n_total - n_per_class; ++i) {
164 m_filters.row(n_per_class + i) =
165 all_filters.row(static_cast<int>(n_ch) - 1 - i);
166 eigenvalues(n_per_class + i) =
167 lambdas(static_cast<int>(n_ch) - 1 - i);
168 }
169
170 // Patterns = pinv(filters) — shape (n_ch × n_comp)
171 auto svd = m_filters.bdcSvd<ComputeThinU | ComputeThinV>();
172 m_patterns = svd.solve(MatrixXd::Identity(n_total, n_total));
173
174 // Compute mean band power for z-score normalisation
175 MatrixXd powerFeatures = computePowerFeatures(epochs);
176 m_mean = powerFeatures.colwise().mean();
177
178 VectorXd centered = VectorXd(powerFeatures.rows());
179 m_std = VectorXd(powerFeatures.cols());
180 for (int c = 0; c < powerFeatures.cols(); ++c) {
181 centered = powerFeatures.col(c).array() - m_mean(c);
182 m_std(c) = std::sqrt(centered.squaredNorm()
183 / static_cast<double>(centered.size()));
184 }
185
186 m_fitted = true;
187}
188
189//=============================================================================================================
190
191MatrixXd DecodingCsp::transform(const std::vector<MatrixXd>& epochs) const
192{
193 if (!m_fitted) {
194 throw std::runtime_error("DecodingCsp::transform: not fitted");
195 }
196
197 if (m_transformInto == TransformMode::CspSpace) {
198 // Return data in CSP space: (n_epochs * n_components, n_times)
199 const int nEpochs = static_cast<int>(epochs.size());
200 const int nComp = static_cast<int>(m_filters.rows());
201 const int nTimes = static_cast<int>(epochs[0].cols());
202
203 MatrixXd result(nEpochs * nComp, nTimes);
204 for (int e = 0; e < nEpochs; ++e) {
205 result.middleRows(static_cast<Eigen::Index>(e) * nComp, nComp) = m_filters * epochs[static_cast<size_t>(e)];
206 }
207 return result;
208 }
209
210 // AveragePower mode
211 MatrixXd X = computePowerFeatures(epochs);
212
213 if (m_useLog) {
214 X = X.array().max(1e-30).log().matrix();
215 } else {
216 // z-score
217 for (int c = 0; c < X.cols(); ++c) {
218 double s = m_std(c);
219 if (s < 1e-15) s = 1.0;
220 X.col(c) = (X.col(c).array() - m_mean(c)) / s;
221 }
222 }
223
224 return X;
225}
226
227//=============================================================================================================
228
229MatrixXd DecodingCsp::fitTransform(const std::vector<MatrixXd>& epochs,
230 const VectorXi& y)
231{
232 fit(epochs, y);
233 return transform(epochs);
234}
235
236//=============================================================================================================
237
238MatrixXd DecodingCsp::inverseTransform(const MatrixXd& X) const
239{
240 if (!m_fitted) {
241 throw std::runtime_error("DecodingCsp::inverseTransform: not fitted");
242 }
243 if (m_transformInto != TransformMode::AveragePower) {
244 throw std::runtime_error(
245 "DecodingCsp::inverseTransform: only valid for AveragePower mode");
246 }
247
248 const int nComp = static_cast<int>(m_filters.rows());
249 if (X.cols() != nComp) {
250 throw std::invalid_argument(
251 "DecodingCsp::inverseTransform: X must have n_components columns");
252 }
253
254 // patterns_ is (n_channels × n_components), X is (n_epochs × n_components)
255 // Result: (n_epochs × n_channels)
256 return X * m_patterns.transpose();
257}
258
259//=============================================================================================================
260
261const MatrixXd& DecodingCsp::filters() const
262{
263 if (!m_fitted) {
264 throw std::runtime_error("DecodingCsp::filters: not fitted");
265 }
266 return m_filters;
267}
268
269//=============================================================================================================
270
271const MatrixXd& DecodingCsp::patterns() const
272{
273 if (!m_fitted) {
274 throw std::runtime_error("DecodingCsp::patterns: not fitted");
275 }
276 return m_patterns;
277}
278
279//=============================================================================================================
280
281const VectorXd& DecodingCsp::mean() const
282{
283 if (!m_fitted) {
284 throw std::runtime_error("DecodingCsp::mean: not fitted");
285 }
286 return m_mean;
287}
288
289//=============================================================================================================
290
291const VectorXd& DecodingCsp::stddev() const
292{
293 if (!m_fitted) {
294 throw std::runtime_error("DecodingCsp::stddev: not fitted");
295 }
296 return m_std;
297}
298
299//=============================================================================================================
300
302{
303 return m_fitted;
304}
305
306//=============================================================================================================
307
308MatrixXd DecodingCsp::computePowerFeatures(
309 const std::vector<MatrixXd>& epochs) const
310{
311 const int nEpochs = static_cast<int>(epochs.size());
312 const int nComp = static_cast<int>(m_filters.rows());
313
314 MatrixXd features(nEpochs, nComp);
315 for (int e = 0; e < nEpochs; ++e) {
316 MatrixXd filtered = m_filters * epochs[static_cast<size_t>(e)];
317 for (int c = 0; c < nComp; ++c) {
318 features(e, c) = filtered.row(c).squaredNorm()
319 / static_cast<double>(filtered.cols());
320 }
321 }
322
323 return features;
324}
constexpr int X
Common Spatial Patterns (CSP) for two-class discriminative spatial filtering of band-passed M/EEG.
Eigen::JacobiSVD< Eigen::Matrix3f > svd(S, Eigen::ComputeFullU|Eigen::ComputeFullV)
Supervised and unsupervised spatial-filter decompositions for M/EEG decoding.
const Eigen::MatrixXd & filters() const
const Eigen::VectorXd & mean() const
const Eigen::VectorXd & stddev() const
Eigen::MatrixXd inverseTransform(const Eigen::MatrixXd &X) const
Eigen::MatrixXd fitTransform(const std::vector< Eigen::MatrixXd > &epochs, const Eigen::VectorXi &y)
void fit(const std::vector< Eigen::MatrixXd > &epochs, const Eigen::VectorXi &y)
DecodingCsp(int nComponents=4, TransformMode transformInto=TransformMode::AveragePower, bool useLog=true)
const Eigen::MatrixXd & patterns() const
Eigen::MatrixXd transform(const std::vector< Eigen::MatrixXd > &epochs) const