v2.0.0
Loading...
Searching...
No Matches
ica.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "ica.h"
18
19//=============================================================================================================
20// EIGEN INCLUDES
21//=============================================================================================================
22
23#include <Eigen/Dense>
24
25//=============================================================================================================
26// QT INCLUDES
27//=============================================================================================================
28
29#include <QDebug>
30
31//=============================================================================================================
32// C++ INCLUDES
33//=============================================================================================================
34
35#include <cmath>
36#include <random>
37
38//=============================================================================================================
39// USED NAMESPACES
40//=============================================================================================================
41
42using namespace UTILSLIB;
43using namespace Eigen;
44
45//=============================================================================================================
46// STATIC MEMBER DEFINITIONS
47//=============================================================================================================
48
49//=============================================================================================================
50// PRIVATE HELPERS
51//=============================================================================================================
52
53namespace {
54
55//=============================================================================================================
59void gramSchmidt(VectorXd& w, const MatrixXd& W, int iComp)
60{
61 for (int j = 0; j < iComp; ++j) {
62 w -= w.dot(W.row(j)) * W.row(j).transpose();
63 }
64}
65
66} // anonymous namespace
67
68//=============================================================================================================
69// MEMBER DEFINITIONS
70//=============================================================================================================
71
72IcaResult ICA::run(const MatrixXd& matData,
73 int nComponents,
74 int maxIter,
75 double tol,
76 int randomSeed)
77{
78 const int nCh = static_cast<int>(matData.rows());
79 const int nSamples = static_cast<int>(matData.cols());
80
81 if (nComponents <= 0 || nComponents > nCh) {
82 nComponents = nCh;
83 }
84
85 //----------------------------------------------------------------------------------------------------------
86 // 1. Center: subtract per-channel mean
87 //----------------------------------------------------------------------------------------------------------
88 VectorXd vecMean = matData.rowwise().mean();
89 MatrixXd matCentered = matData.colwise() - vecMean;
90
91 //----------------------------------------------------------------------------------------------------------
92 // 2. Whiten
93 //----------------------------------------------------------------------------------------------------------
94 MatrixXd matWhitening, matDewhitening;
95 MatrixXd matWhite = whiten(matCentered, nComponents, matWhitening, matDewhitening);
96
97 //----------------------------------------------------------------------------------------------------------
98 // 3. FastICA — deflationary approach with logcosh (tanh) nonlinearity
99 //
100 // For each component i, iterate:
101 // g = tanh(W[i]^T * X_white) [1 x nSamples]
102 // g' = 1 - g^2 [1 x nSamples]
103 // w_new = (1/n) * X_white * g^T - mean(g') * w_i
104 // Gram-Schmidt orthogonalise against already-found rows
105 // Normalise
106 // Convergence: |w_new · w_old| ≥ 1 - tol
107 //----------------------------------------------------------------------------------------------------------
108 std::mt19937 rng(static_cast<unsigned>(randomSeed));
109 std::normal_distribution<double> dist(0.0, 1.0);
110
111 MatrixXd W_ica(nComponents, nComponents); // unmixing in whitened space
112 bool bConverged = true;
113
114 for (int i = 0; i < nComponents; ++i) {
115 // Random unit-vector initialisation
116 VectorXd w(nComponents);
117 for (int k = 0; k < nComponents; ++k) {
118 w(k) = dist(rng);
119 }
120 w.normalize();
121
122 bool compConverged = false;
123 for (int iter = 0; iter < maxIter; ++iter) {
124 // g = tanh(w^T * X_white) → row vector (1 x nSamples)
125 RowVectorXd u = w.transpose() * matWhite;
126 RowVectorXd g = u.array().tanh();
127 RowVectorXd gPrime = 1.0 - g.array().square(); // derivative of tanh
128
129 // Newton update
130 VectorXd wNew = (matWhite * g.transpose()) / nSamples
131 - gPrime.mean() * w;
132
133 // Deflation: orthogonalise against already-converged components
134 gramSchmidt(wNew, W_ica, i);
135
136 // Normalise
137 double norm = wNew.norm();
138 if (norm < 1e-12) {
139 // Degenerate — reinitialise randomly and restart this component
140 for (int k = 0; k < nComponents; ++k) {
141 w(k) = dist(rng);
142 }
143 w.normalize();
144 continue;
145 }
146 wNew /= norm;
147
148 // Convergence check: |w_new · w_old| should approach 1
149 double delta = std::abs(wNew.dot(w)) - 1.0;
150 w = wNew;
151
152 if (std::abs(delta) < tol) {
153 compConverged = true;
154 break;
155 }
156 }
157
158 if (!compConverged) {
159 bConverged = false;
160 qWarning() << "ICA::run: component" << i << "did not converge within" << maxIter << "iterations.";
161 }
162
163 W_ica.row(i) = w.transpose();
164 }
165
166 //----------------------------------------------------------------------------------------------------------
167 // 4. Compose full (sensor-space) unmixing and mixing matrices
168 //
169 // W_full = W_ica * W_whitening (n_comp x n_ch)
170 // A_full = W_dewhitening * W_ica^T (n_ch x n_comp) — exact inverse when n_comp == n_ch,
171 // pseudo-inverse otherwise
172 //----------------------------------------------------------------------------------------------------------
173 MatrixXd matUnmixing = W_ica * matWhitening; // n_comp x n_ch
174 MatrixXd matMixing = matDewhitening * W_ica.transpose(); // n_ch x n_comp
175
176 //----------------------------------------------------------------------------------------------------------
177 // 5. Compute source time series
178 //----------------------------------------------------------------------------------------------------------
179 MatrixXd matSources = matUnmixing * matCentered; // n_comp x n_samples
180
181 IcaResult result;
182 result.matMixing = std::move(matMixing);
183 result.matUnmixing = std::move(matUnmixing);
184 result.matSources = std::move(matSources);
185 result.vecMean = std::move(vecMean);
186 result.bConverged = bConverged;
187
188 return result;
189}
190
191//=============================================================================================================
192
193MatrixXd ICA::applyUnmixing(const MatrixXd& matData, const IcaResult& result)
194{
195 // Centre using the training mean, then project
196 MatrixXd matCentered = matData.colwise() - result.vecMean;
197 return result.matUnmixing * matCentered;
198}
199
200//=============================================================================================================
201
202MatrixXd ICA::excludeComponents(const MatrixXd& matData,
203 const IcaResult& result,
204 const QVector<int>& excludedComponents)
205{
206 if (excludedComponents.isEmpty()) {
207 return matData;
208 }
209
210 // Subtract only the contribution of excluded components from the original data:
211 // artifact = A[:, excluded] * W[excluded, :] * (data - mean)
212 // cleaned = data - artifact
213 //
214 // This preserves the full-rank original signal and removes only the estimated
215 // artifact, matching the approach used by MNE-Python's ica.apply().
216
217 MatrixXd matCentered = matData.colwise() - result.vecMean;
218
219 // Build partial mixing and partial sources for only the excluded components
220 const int nExcl = excludedComponents.size();
221 const int nCh = static_cast<int>(result.matMixing.rows());
222 const int nSamps = static_cast<int>(matCentered.cols());
223
224 MatrixXd partialMixing(nCh, nExcl);
225 MatrixXd partialSources(nExcl, nSamps);
226
227 for (int i = 0; i < nExcl; ++i) {
228 int idx = excludedComponents[i];
229 if (idx >= 0 && idx < static_cast<int>(result.matUnmixing.rows())) {
230 partialMixing.col(i) = result.matMixing.col(idx);
231 partialSources.row(i) = result.matUnmixing.row(idx) * matCentered;
232 } else {
233 partialMixing.col(i).setZero();
234 partialSources.row(i).setZero();
235 }
236 }
237
238 return matData - partialMixing * partialSources;
239}
240
241//=============================================================================================================
242
243MatrixXd ICA::whiten(const MatrixXd& matCentered,
244 int nComponents,
245 MatrixXd& matWhitening,
246 MatrixXd& matDewhitening)
247{
248 const int nSamples = static_cast<int>(matCentered.cols());
249
250 // Sample covariance (unscaled)
251 MatrixXd cov = matCentered * matCentered.transpose() / static_cast<double>(nSamples);
252
253 // Eigendecomposition — SelfAdjointEigenSolver returns eigenvalues in ascending order
254 SelfAdjointEigenSolver<MatrixXd> eig(cov);
255
256 const int nCh = static_cast<int>(cov.rows());
257 // Take the nComponents largest eigenvalues/vectors (rightmost columns)
258 VectorXd eigenvalues = eig.eigenvalues().tail(nComponents).cwiseMax(1e-12);
259 MatrixXd eigenvectors = eig.eigenvectors().rightCols(nComponents); // n_ch x n_comp
260
261 // Whitening : W_w = D^{-1/2} * V^T (n_comp x n_ch)
262 // Dewhitening: W_d = V * D^{1/2} (n_ch x n_comp)
263 VectorXd sqrtEig = eigenvalues.cwiseSqrt();
264 VectorXd invSqrtEig = sqrtEig.cwiseInverse();
265
266 matWhitening = invSqrtEig.asDiagonal() * eigenvectors.transpose();
267 matDewhitening = eigenvectors * sqrtEig.asDiagonal();
268
269 return matWhitening * matCentered;
270}
FastICA-based independent component analysis for MEG / EEG artifact removal.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
Result of an ICA decomposition.
Definition ica.h:67
Eigen::MatrixXd matUnmixing
Definition ica.h:69
Eigen::MatrixXd matSources
Definition ica.h:70
Eigen::MatrixXd matMixing
Definition ica.h:68
Eigen::VectorXd vecMean
Definition ica.h:71
bool bConverged
Definition ica.h:72
static Eigen::MatrixXd excludeComponents(const Eigen::MatrixXd &matData, const IcaResult &result, const QVector< int > &excludedComponents)
Definition ica.cpp:202
static IcaResult run(const Eigen::MatrixXd &matData, int nComponents=-1, int maxIter=200, double tol=1e-4, int randomSeed=42)
Definition ica.cpp:72
static Eigen::MatrixXd applyUnmixing(const Eigen::MatrixXd &matData, const IcaResult &result)
Definition ica.cpp:193