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
56//=============================================================================================================
60void gramSchmidt(VectorXd& w, const MatrixXd& W, int iComp)
61{
62 for (int j = 0; j < iComp; ++j) {
63 w -= w.dot(W.row(j)) * W.row(j).transpose();
64 }
65}
66
67} // anonymous namespace
68
69//=============================================================================================================
70// MEMBER DEFINITIONS
71//=============================================================================================================
72
73IcaResult ICA::run(const MatrixXd& matData,
74 int nComponents,
75 int maxIter,
76 double tol,
77 int randomSeed)
78{
79 const int nCh = static_cast<int>(matData.rows());
80 const int nSamples = static_cast<int>(matData.cols());
81
82 if (nComponents <= 0 || nComponents > nCh) {
83 nComponents = nCh;
84 }
85
86 //----------------------------------------------------------------------------------------------------------
87 // 1. Center: subtract per-channel mean
88 //----------------------------------------------------------------------------------------------------------
89 VectorXd vecMean = matData.rowwise().mean();
90 MatrixXd matCentered = matData.colwise() - vecMean;
91
92 //----------------------------------------------------------------------------------------------------------
93 // 2. Whiten
94 //----------------------------------------------------------------------------------------------------------
95 MatrixXd matWhitening, matDewhitening;
96 MatrixXd matWhite = whiten(matCentered, nComponents, matWhitening, matDewhitening);
97
98 //----------------------------------------------------------------------------------------------------------
99 // 3. FastICA — deflationary approach with logcosh (tanh) nonlinearity
100 //
101 // For each component i, iterate:
102 // g = tanh(W[i]^T * X_white) [1 x nSamples]
103 // g' = 1 - g^2 [1 x nSamples]
104 // w_new = (1/n) * X_white * g^T - mean(g') * w_i
105 // Gram-Schmidt orthogonalise against already-found rows
106 // Normalise
107 // Convergence: |w_new · w_old| ≥ 1 - tol
108 //----------------------------------------------------------------------------------------------------------
109 std::mt19937 rng(static_cast<unsigned>(randomSeed));
110 std::normal_distribution<double> dist(0.0, 1.0);
111
112 MatrixXd W_ica(nComponents, nComponents); // unmixing in whitened space
113 bool bConverged = true;
114
115 for (int i = 0; i < nComponents; ++i) {
116 // Random unit-vector initialisation
117 VectorXd w(nComponents);
118 for (int k = 0; k < nComponents; ++k) {
119 w(k) = dist(rng);
120 }
121 w.normalize();
122
123 bool compConverged = false;
124 for (int iter = 0; iter < maxIter; ++iter) {
125 // g = tanh(w^T * X_white) → row vector (1 x nSamples)
126 RowVectorXd u = w.transpose() * matWhite;
127 RowVectorXd g = u.array().tanh();
128 RowVectorXd gPrime = 1.0 - g.array().square(); // derivative of tanh
129
130 // Newton update
131 VectorXd wNew = (matWhite * g.transpose()) / nSamples - 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 // Take the nComponents largest eigenvalues/vectors (rightmost columns)
257 VectorXd eigenvalues = eig.eigenvalues().tail(nComponents).cwiseMax(1e-12);
258 MatrixXd eigenvectors = eig.eigenvectors().rightCols(nComponents); // n_ch x n_comp
259
260 // Whitening : W_w = D^{-1/2} * V^T (n_comp x n_ch)
261 // Dewhitening: W_d = V * D^{1/2} (n_ch x n_comp)
262 VectorXd sqrtEig = eigenvalues.cwiseSqrt();
263 VectorXd invSqrtEig = sqrtEig.cwiseInverse();
264
265 matWhitening = invSqrtEig.asDiagonal() * eigenvectors.transpose();
266 matDewhitening = eigenvectors * sqrtEig.asDiagonal();
267
268 return matWhitening * matCentered;
269}
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:69
Eigen::MatrixXd matUnmixing
Definition ica.h:71
Eigen::MatrixXd matSources
Definition ica.h:72
Eigen::MatrixXd matMixing
Definition ica.h:70
Eigen::VectorXd vecMean
Definition ica.h:73
bool bConverged
Definition ica.h:74
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:73
static Eigen::MatrixXd applyUnmixing(const Eigen::MatrixXd &matData, const IcaResult &result)
Definition ica.cpp:193