v2.0.0
Loading...
Searching...
No Matches
inv_tf_mxne.cpp
Go to the documentation of this file.
1//=============================================================================================================
20
21//=============================================================================================================
22// INCLUDES
23//=============================================================================================================
24
25#include "inv_tf_mxne.h"
26
27#include <Eigen/SVD>
28
29#include <cmath>
30
31//=============================================================================================================
32// QT INCLUDES
33//=============================================================================================================
34
35#include <QDebug>
36#include <QtMath>
37
38//=============================================================================================================
39// USED NAMESPACES
40//=============================================================================================================
41
42using namespace INVLIB;
43using namespace Eigen;
44
45//=============================================================================================================
46// DEFINE MEMBER METHODS
47//=============================================================================================================
48
49MatrixXd InvTfMxne::buildGaborDictionary(int iNSamples, int iNFreqs,
50 double dFMin, double dFMax,
51 double dSFreq)
52{
53 // Build a set of Gabor atoms: windowed complex exponentials at different frequencies
54 // Returns real-valued matrix: for each frequency, we store cos and sin rows
55 const int nAtoms = 2 * iNFreqs; // cos + sin per frequency
56 MatrixXd dict = MatrixXd::Zero(nAtoms, iNSamples);
57
58 VectorXd timeVec(iNSamples);
59 for (int t = 0; t < iNSamples; ++t) {
60 timeVec(t) = static_cast<double>(t) / dSFreq;
61 }
62
63 // Log-spaced frequencies between fMin and fMax
64 for (int f = 0; f < iNFreqs; ++f) {
65 double freq;
66 if (iNFreqs > 1) {
67 double logMin = std::log(dFMin);
68 double logMax = std::log(dFMax);
69 freq = std::exp(logMin + (logMax - logMin) * f / (iNFreqs - 1));
70 } else {
71 freq = (dFMin + dFMax) / 2.0;
72 }
73
74 // Gaussian window width: ~3 cycles at this frequency
75 double sigma = 3.0 / (2.0 * M_PI * freq);
76
77 double tCenter = timeVec(iNSamples / 2);
78
79 for (int t = 0; t < iNSamples; ++t) {
80 double dt = timeVec(t) - tCenter;
81 double envelope = std::exp(-0.5 * dt * dt / (sigma * sigma));
82 dict(2 * f, t) = envelope * std::cos(2.0 * M_PI * freq * timeVec(t));
83 dict(2 * f + 1, t) = envelope * std::sin(2.0 * M_PI * freq * timeVec(t));
84 }
85
86 // Normalize each atom to unit norm
87 double normCos = dict.row(2 * f).norm();
88 if (normCos > 1e-12)
89 dict.row(2 * f) /= normCos;
90
91 double normSin = dict.row(2 * f + 1).norm();
92 if (normSin > 1e-12)
93 dict.row(2 * f + 1) /= normSin;
94 }
95
96 return dict;
97}
98
99//=============================================================================================================
100
101InvTfMxneResult InvTfMxne::compute(const MatrixXd& matGain,
102 const MatrixXd& matData,
103 const InvTfMxneParams& params)
104{
105 InvTfMxneResult result;
106
107 const int nChannels = matGain.rows();
108 const int nSources = matGain.cols();
109 const int nTimes = matData.cols();
110
111 if (nChannels == 0 || nSources == 0 || nTimes == 0) {
112 return result;
113 }
114 const int nAtoms = 2 * params.iNFreqs;
115
116 if (matData.rows() != nChannels) {
117 qWarning() << "[InvTfMxne::compute] Dimension mismatch: data rows" << matData.rows()
118 << "!= gain rows" << nChannels;
119 return result;
120 }
121
122 // Build Gabor dictionary: (nAtoms × nTimes)
123 MatrixXd Phi = buildGaborDictionary(nTimes, params.iNFreqs,
124 params.dFMin, params.dFMax,
125 params.dSFreq);
126
127 // TF coefficients: Z (nSources × nAtoms)
128 // The model is: M = G * X, where X = Z * Phi (each source has TF representation)
129 // Equivalently in expanded form: M = G_expanded * z_vec
130 // where G_expanded = G ⊗ Phi^T, z_vec = vec(Z)
131 // But we solve iteratively using Block Coordinate Descent.
132
133 // FISTA on 0.5 ||M - G Z Phi||^2 + alpha_space ||Z||_21 + alpha_time ||Z||_1. The step is 1/L with the global
134 // Lipschitz constant L = ||G||_2^2 ||Phi||_2^2; per-source steps on a joint gradient overshoot and stall.
135 const double gNorm = JacobiSVD<MatrixXd>(matGain).singularValues()(0);
136 const double phiNorm = JacobiSVD<MatrixXd>(Phi).singularValues()(0);
137 const double lipschitz = gNorm * gNorm * phiNorm * phiNorm;
138
139 // Sparse-group prox: soft-threshold each coefficient, then shrink the source's group norm.
140 const auto prox = [&](MatrixXd& V) {
141 const double threshL1 = params.dAlphaTime / lipschitz;
142 const double threshL21 = params.dAlphaSpace / lipschitz;
143 V = V.array().sign() * (V.array().abs() - threshL1).max(0.0);
144 for (int j = 0; j < nSources; ++j) {
145 const double groupNorm = V.row(j).norm();
146 if (groupNorm > threshL21)
147 V.row(j) *= 1.0 - threshL21 / groupNorm;
148 else
149 V.row(j).setZero();
150 }
151 };
152 const auto objectiveOf = [&](const MatrixXd& V) {
153 return 0.5 * (matData - matGain * V * Phi).squaredNorm() + params.dAlphaSpace * V.rowwise().norm().sum() + params.dAlphaTime * V.cwiseAbs().sum();
154 };
155
156 MatrixXd Z = MatrixXd::Zero(nSources, nAtoms);
157 MatrixXd Y = Z;
158 double tk = 1.0;
159 double prevObj = objectiveOf(Z);
160 for (int iter = 0; iter < params.iMaxIterations; ++iter) {
161 MatrixXd zNew = Y + matGain.transpose() * (matData - matGain * Y * Phi) * Phi.transpose() / lipschitz;
162 prox(zNew);
163 const double tNew = 0.5 * (1.0 + std::sqrt(1.0 + 4.0 * tk * tk));
164 Y = zNew + ((tk - 1.0) / tNew) * (zNew - Z);
165 Z = zNew;
166 tk = tNew;
167 result.nIterations = iter + 1;
168 const double objective = objectiveOf(Z);
169 if (std::abs(prevObj - objective) <= params.dTolerance * std::abs(objective))
170 break;
171 prevObj = objective;
172 }
173 MatrixXd residual = matData - matGain * Z * Phi;
174
175 // Reconstruct time-domain source estimate from TF coefficients
176 MatrixXd X = Z * Phi; // (nSources × nTimes)
177
178 // Find active sources
179 QVector<int> activeVertices;
180 for (int j = 0; j < nSources; ++j) {
181 if (Z.row(j).norm() > 1e-12) {
182 activeVertices.append(j);
183 }
184 }
185
186 // Optional debiasing: re-estimate amplitudes on active set
187 MatrixXd finalX;
188 if (params.bDebias && !activeVertices.isEmpty()) {
189 MatrixXd Gactive(nChannels, activeVertices.size());
190 for (int i = 0; i < activeVertices.size(); ++i) {
191 Gactive.col(i) = matGain.col(activeVertices[i]);
192 }
193 // Least-squares on active set: X_active = pinv(G_active) * M
194 finalX = Gactive.bdcSvd<ComputeThinU | ComputeThinV>().solve(matData);
195 } else if (!activeVertices.isEmpty()) {
196 finalX = MatrixXd(activeVertices.size(), nTimes);
197 for (int i = 0; i < activeVertices.size(); ++i) {
198 finalX.row(i) = X.row(activeVertices[i]);
199 }
200 } else {
201 finalX = MatrixXd::Zero(0, nTimes);
202 }
203
204 // Build result
205 VectorXi vertices(activeVertices.size());
206 for (int i = 0; i < activeVertices.size(); ++i) {
207 vertices(i) = activeVertices[i];
208 }
209
210 result.stc = InvSourceEstimate(finalX, vertices, 0.0f,
211 static_cast<float>(1.0 / params.dSFreq));
213 result.activeVertices = activeVertices;
214 result.residualNorm = residual.norm();
215
216 // Store TF coefficients for active sources
217 if (!activeVertices.isEmpty()) {
218 result.tfCoefficients = MatrixXd(activeVertices.size(), nAtoms);
219 for (int i = 0; i < activeVertices.size(); ++i) {
220 result.tfCoefficients.row(i) = Z.row(activeVertices[i]);
221 }
222 }
223
224 return result;
225}
#define M_PI
constexpr int Y
constexpr int Z
constexpr int X
Time-Frequency Mixed-Norm Estimate (TF-MxNE) sparse inverse solver — joint L21 + L1 sparsity in a Gab...
Inverse source estimation (MNE, dSPM, sLORETA, dipole fitting).
Source-space inverse-solution container with dense grid plus optional focal-dipole,...
Result structure for the TF-MxNE solver.
Definition inv_tf_mxne.h:65
InvSourceEstimate stc
Definition inv_tf_mxne.h:66
Eigen::MatrixXd tfCoefficients
Definition inv_tf_mxne.h:70
QVector< int > activeVertices
Definition inv_tf_mxne.h:67
Parameters for the TF-MxNE solver.
Definition inv_tf_mxne.h:80
static Eigen::MatrixXd buildGaborDictionary(int iNSamples, int iNFreqs, double dFMin, double dFMax, double dSFreq)
Build a Gabor dictionary (tight frame) for time-frequency decomposition.
static InvTfMxneResult compute(const Eigen::MatrixXd &matGain, const Eigen::MatrixXd &matData, const InvTfMxneParams &params=InvTfMxneParams())
Compute the TF-MxNE inverse solution.