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//=============================================================================================================
28// QT INCLUDES
29//=============================================================================================================
30
31#include <QDebug>
32#include <QtMath>
33
34//=============================================================================================================
35// USED NAMESPACES
36//=============================================================================================================
37
38using namespace INVLIB;
39using namespace Eigen;
40
41//=============================================================================================================
42// DEFINE MEMBER METHODS
43//=============================================================================================================
44
45MatrixXd InvTfMxne::buildGaborDictionary(int iNSamples, int iNFreqs,
46 double dFMin, double dFMax,
47 double dSFreq)
48{
49 // Build a set of Gabor atoms: windowed complex exponentials at different frequencies
50 // Returns real-valued matrix: for each frequency, we store cos and sin rows
51 const int nAtoms = 2 * iNFreqs; // cos + sin per frequency
52 MatrixXd dict = MatrixXd::Zero(nAtoms, iNSamples);
53
54 VectorXd timeVec(iNSamples);
55 for (int t = 0; t < iNSamples; ++t) {
56 timeVec(t) = static_cast<double>(t) / dSFreq;
57 }
58
59 // Log-spaced frequencies between fMin and fMax
60 for (int f = 0; f < iNFreqs; ++f) {
61 double freq;
62 if (iNFreqs > 1) {
63 double logMin = std::log(dFMin);
64 double logMax = std::log(dFMax);
65 freq = std::exp(logMin + (logMax - logMin) * f / (iNFreqs - 1));
66 } else {
67 freq = (dFMin + dFMax) / 2.0;
68 }
69
70 // Gaussian window width: ~3 cycles at this frequency
71 double sigma = 3.0 / (2.0 * M_PI * freq);
72
73 double tCenter = timeVec(iNSamples / 2);
74
75 for (int t = 0; t < iNSamples; ++t) {
76 double dt = timeVec(t) - tCenter;
77 double envelope = std::exp(-0.5 * dt * dt / (sigma * sigma));
78 dict(2 * f, t) = envelope * std::cos(2.0 * M_PI * freq * timeVec(t));
79 dict(2 * f + 1, t) = envelope * std::sin(2.0 * M_PI * freq * timeVec(t));
80 }
81
82 // Normalize each atom to unit norm
83 double normCos = dict.row(2 * f).norm();
84 if (normCos > 1e-12) dict.row(2 * f) /= normCos;
85
86 double normSin = dict.row(2 * f + 1).norm();
87 if (normSin > 1e-12) dict.row(2 * f + 1) /= normSin;
88 }
89
90 return dict;
91}
92
93//=============================================================================================================
94
95InvTfMxneResult InvTfMxne::compute(const MatrixXd& matGain,
96 const MatrixXd& matData,
97 const InvTfMxneParams& params)
98{
99 InvTfMxneResult result;
100
101 const int nChannels = matGain.rows();
102 const int nSources = matGain.cols();
103 const int nTimes = matData.cols();
104
105 if (nChannels == 0 || nSources == 0 || nTimes == 0) {
106 return result;
107 }
108 const int nAtoms = 2 * params.iNFreqs;
109
110 if (matData.rows() != nChannels) {
111 qWarning() << "[InvTfMxne::compute] Dimension mismatch: data rows" << matData.rows()
112 << "!= gain rows" << nChannels;
113 return result;
114 }
115
116 // Build Gabor dictionary: (nAtoms × nTimes)
117 MatrixXd Phi = buildGaborDictionary(nTimes, params.iNFreqs,
118 params.dFMin, params.dFMax,
119 params.dSFreq);
120
121 // TF coefficients: Z (nSources × nAtoms)
122 // The model is: M = G * X, where X = Z * Phi (each source has TF representation)
123 // Equivalently in expanded form: M = G_expanded * z_vec
124 // where G_expanded = G ⊗ Phi^T, z_vec = vec(Z)
125 // But we solve iteratively using Block Coordinate Descent.
126
127 // Initialize Z to zero
128 MatrixXd Z = MatrixXd::Zero(nSources, nAtoms);
129
130 // Precompute G^T * G diagonal for Lipschitz constants
131 VectorXd lipschitz(nSources);
132 for (int j = 0; j < nSources; ++j) {
133 lipschitz(j) = matGain.col(j).squaredNorm();
134 }
135 // Scale by dictionary energy
136 double phiEnergy = 0.0;
137 for (int a = 0; a < nAtoms; ++a) {
138 phiEnergy += Phi.row(a).squaredNorm();
139 }
140 lipschitz *= phiEnergy;
141
142 // Proximal gradient descent (ISTA-like)
143 MatrixXd residual = matData;
144 double prevObj = std::numeric_limits<double>::max();
145
146 for (int iter = 0; iter < params.iMaxIterations; ++iter) {
147 // Compute residual: R = M - G * (Z * Phi)
148 MatrixXd X = Z * Phi; // (nSources × nTimes)
149 residual = matData - matGain * X;
150
151 // Compute objective: ||R||^2_F + alpha_space * ||Z||_21 + alpha_time * ||Z||_1
152 double dataFit = residual.squaredNorm();
153 double l21Norm = 0.0;
154 double l1Norm = 0.0;
155 for (int j = 0; j < nSources; ++j) {
156 l21Norm += Z.row(j).norm();
157 l1Norm += Z.row(j).lpNorm<1>();
158 }
159 double objective = 0.5 * dataFit + params.dAlphaSpace * l21Norm + params.dAlphaTime * l1Norm;
160
161 // Check convergence
162 if (std::abs(prevObj - objective) / (std::abs(prevObj) + 1e-12) < params.dTolerance) {
163 result.nIterations = iter + 1;
164 break;
165 }
166 prevObj = objective;
167 result.nIterations = iter + 1;
168
169 // Gradient step + proximal operator for each source
170 // Gradient of data term w.r.t. Z_j: -G_j^T * R * Phi^T
171 MatrixXd GtR = matGain.transpose() * residual; // (nSources × nTimes)
172 MatrixXd grad = GtR * Phi.transpose(); // (nSources × nAtoms)
173
174 for (int j = 0; j < nSources; ++j) {
175 if (lipschitz(j) < 1e-12) continue;
176
177 double stepSize = 1.0 / lipschitz(j);
178
179 // Gradient step
180 RowVectorXd zNew = Z.row(j) + stepSize * grad.row(j);
181
182 // Proximal operator: soft-threshold (L1) then group-threshold (L21)
183 // L1 soft threshold
184 double threshL1 = params.dAlphaTime * stepSize;
185 for (int a = 0; a < nAtoms; ++a) {
186 double val = zNew(a);
187 double sign = (val > 0.0) ? 1.0 : -1.0;
188 zNew(a) = sign * std::max(0.0, std::abs(val) - threshL1);
189 }
190
191 // L21 group threshold
192 double groupNorm = zNew.norm();
193 double threshL21 = params.dAlphaSpace * stepSize;
194 if (groupNorm > threshL21) {
195 zNew *= (1.0 - threshL21 / groupNorm);
196 } else {
197 zNew.setZero();
198 }
199
200 Z.row(j) = zNew;
201 }
202 }
203
204 // Reconstruct time-domain source estimate from TF coefficients
205 MatrixXd X = Z * Phi; // (nSources × nTimes)
206
207 // Find active sources
208 QVector<int> activeVertices;
209 for (int j = 0; j < nSources; ++j) {
210 if (Z.row(j).norm() > 1e-12) {
211 activeVertices.append(j);
212 }
213 }
214
215 // Optional debiasing: re-estimate amplitudes on active set
216 MatrixXd finalX;
217 if (params.bDebias && !activeVertices.isEmpty()) {
218 MatrixXd Gactive(nChannels, activeVertices.size());
219 for (int i = 0; i < activeVertices.size(); ++i) {
220 Gactive.col(i) = matGain.col(activeVertices[i]);
221 }
222 // Least-squares on active set: X_active = pinv(G_active) * M
223 finalX = Gactive.bdcSvd<ComputeThinU | ComputeThinV>().solve(matData);
224 } else if (!activeVertices.isEmpty()) {
225 finalX = MatrixXd(activeVertices.size(), nTimes);
226 for (int i = 0; i < activeVertices.size(); ++i) {
227 finalX.row(i) = X.row(activeVertices[i]);
228 }
229 } else {
230 finalX = MatrixXd::Zero(0, nTimes);
231 }
232
233 // Build result
234 VectorXi vertices(activeVertices.size());
235 for (int i = 0; i < activeVertices.size(); ++i) {
236 vertices(i) = activeVertices[i];
237 }
238
239 result.stc = InvSourceEstimate(finalX, vertices, 0.0f,
240 static_cast<float>(1.0 / params.dSFreq));
242 result.activeVertices = activeVertices;
243 result.residualNorm = residual.norm();
244
245 // Store TF coefficients for active sources
246 if (!activeVertices.isEmpty()) {
247 result.tfCoefficients = MatrixXd(activeVertices.size(), nAtoms);
248 for (int i = 0; i < activeVertices.size(); ++i) {
249 result.tfCoefficients.row(i) = Z.row(activeVertices[i]);
250 }
251 }
252
253 return result;
254}
constexpr int Z
constexpr int X
#define M_PI
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:63
InvSourceEstimate stc
Definition inv_tf_mxne.h:64
Eigen::MatrixXd tfCoefficients
Definition inv_tf_mxne.h:68
QVector< int > activeVertices
Definition inv_tf_mxne.h:65
Parameters for the TF-MxNE solver.
Definition inv_tf_mxne.h:76
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.