v2.0.0
Loading...
Searching...
No Matches
surface_laplacian.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "surface_laplacian.h"
18
19//=============================================================================================================
20// EIGEN INCLUDES
21//=============================================================================================================
22
23#include <Eigen/Core>
24#include <Eigen/Dense>
25
26//=============================================================================================================
27// STD INCLUDES
28//=============================================================================================================
29
30#include <cmath>
31
32//=============================================================================================================
33// QT INCLUDES
34//=============================================================================================================
35
36#include <QDebug>
37
38//=============================================================================================================
39// USED NAMESPACES
40//=============================================================================================================
41
42using namespace UTILSLIB;
43using namespace Eigen;
44
45//=============================================================================================================
46
47namespace
48{
49constexpr double CSD_PI = 3.14159265358979323846;
50}
51
52//=============================================================================================================
53// DEFINE MEMBER METHODS
54//=============================================================================================================
55
56MatrixXd SurfaceLaplacian::evaluateLegendre(const MatrixXd& matX, int iMaxOrder)
57{
58 // Returns matrix of shape (iMaxOrder+1, matX.size()) where row n = P_n(x)
59 // using flattened matX
60 const Index nElements = matX.size();
61 MatrixXd result(iMaxOrder + 1, nElements);
62
63 // P_0(x) = 1
64 result.row(0).setOnes();
65
66 if (iMaxOrder >= 1) {
67 // P_1(x) = x
68 for (Index i = 0; i < nElements; ++i)
69 result(1, i) = matX.data()[i];
70 }
71
72 // Bonnet's recurrence: (n+1) P_{n+1}(x) = (2n+1) x P_n(x) - n P_{n-1}(x)
73 for (int n = 1; n < iMaxOrder; ++n) {
74 const double a = static_cast<double>(2 * n + 1) / static_cast<double>(n + 1);
75 const double b = static_cast<double>(n) / static_cast<double>(n + 1);
76 for (Index i = 0; i < nElements; ++i)
77 result(n + 1, i) = a * matX.data()[i] * result(n, i) - b * result(n - 1, i);
78 }
79
80 return result;
81}
82
83//=============================================================================================================
84
85MatrixXd SurfaceLaplacian::computeG(const MatrixXd& matCosAng,
86 int iStiffness,
87 int iNLegendreTerms)
88{
89 // G(cos θ) = Σ_{n=1}^{N} (2n+1) / (n^m (n+1)^m 4π) · P_n(cos θ)
90 const Index nRows = matCosAng.rows();
91 const Index nCols = matCosAng.cols();
92
93 // Evaluate Legendre polynomials
94 MatrixXd legP = evaluateLegendre(matCosAng, iNLegendreTerms);
95
96 // Accumulate weighted sum
97 MatrixXd G = MatrixXd::Zero(nRows, nCols);
98
99 for (int n = 1; n <= iNLegendreTerms; ++n) {
100 const double factor = static_cast<double>(2 * n + 1)
101 / (std::pow(static_cast<double>(n), iStiffness)
102 * std::pow(static_cast<double>(n + 1), iStiffness)
103 * 4.0 * CSD_PI);
104
105 for (Index i = 0; i < nRows * nCols; ++i)
106 G.data()[i] += factor * legP(n, i);
107 }
108
109 return G;
110}
111
112//=============================================================================================================
113
114MatrixXd SurfaceLaplacian::computeH(const MatrixXd& matCosAng,
115 int iStiffness,
116 int iNLegendreTerms)
117{
118 // H(cos θ) = Σ_{n=1}^{N} (2n+1) / (n^(m-1) (n+1)^(m-1) 4π) · P_n(cos θ)
119 const Index nRows = matCosAng.rows();
120 const Index nCols = matCosAng.cols();
121
122 MatrixXd legP = evaluateLegendre(matCosAng, iNLegendreTerms);
123
124 MatrixXd H = MatrixXd::Zero(nRows, nCols);
125
126 for (int n = 1; n <= iNLegendreTerms; ++n) {
127 const double factor = static_cast<double>(2 * n + 1)
128 / (std::pow(static_cast<double>(n), iStiffness - 1)
129 * std::pow(static_cast<double>(n + 1), iStiffness - 1)
130 * 4.0 * CSD_PI);
131
132 for (Index i = 0; i < nRows * nCols; ++i)
133 H.data()[i] += factor * legP(n, i);
134 }
135
136 return H;
137}
138
139//=============================================================================================================
140
141MatrixXd SurfaceLaplacian::computeTransform(const MatrixX3d& matPositions,
142 double dLambda2,
143 int iStiffness,
144 int iNLegendreTerms,
145 double dSphereRadius)
146{
147 const int nCh = static_cast<int>(matPositions.rows());
148 if (nCh < 2) {
149 qWarning() << "[SurfaceLaplacian::computeTransform] Need at least 2 channels.";
150 return MatrixXd();
151 }
152
153 // 1. Centre positions and normalise to unit sphere
154 Vector3d centre = matPositions.colwise().mean();
155 MatrixX3d posCentered = matPositions.rowwise() - centre.transpose();
156
157 // Fit sphere radius if not provided
158 if (dSphereRadius <= 0.0) {
159 dSphereRadius = 0.0;
160 for (int i = 0; i < nCh; ++i)
161 dSphereRadius += posCentered.row(i).norm();
162 dSphereRadius /= static_cast<double>(nCh);
163 }
164
165 // Normalise to unit sphere
166 MatrixX3d posNorm(nCh, 3);
167 for (int i = 0; i < nCh; ++i) {
168 double norm = posCentered.row(i).norm();
169 if (norm > 0.0)
170 posNorm.row(i) = posCentered.row(i) / norm;
171 else
172 posNorm.row(i).setZero();
173 }
174
175 // 2. Cosine angle matrix: cos(θ_ij) = pos_i · pos_j (on unit sphere)
176 MatrixXd cosAng = posNorm * posNorm.transpose();
177
178 // Clamp to [-1, 1]
179 cosAng = cosAng.cwiseMax(-1.0).cwiseMin(1.0);
180
181 // 3. Compute G and H matrices
182 MatrixXd G = computeG(cosAng, iStiffness, iNLegendreTerms);
183 MatrixXd H = computeH(cosAng, iStiffness, iNLegendreTerms);
184
185 // 4. Regularise G: G(i,i) += lambda²
186 for (int i = 0; i < nCh; ++i)
187 G(i, i) += dLambda2;
188
189 // 5. Invert G
190 MatrixXd Gi = G.inverse();
191
192 // Column sums of Gi
193 VectorXd TC = Gi.colwise().sum();
194 double sgi = TC.sum();
195
196 // 6. Build CSD transform
197 // Z = I - (1/n) * ones: average-reference the identity
198 MatrixXd Z = MatrixXd::Identity(nCh, nCh);
199 Z.array() -= 1.0 / static_cast<double>(nCh);
200
201 // Cp2 = Gi * Z
202 MatrixXd Cp2 = Gi * Z;
203
204 // c02 = (colwise sum of Cp2) / sgi
205 RowVectorXd c02 = Cp2.colwise().sum() / sgi;
206
207 // C2 = Cp2 - TC * c02
208 MatrixXd C2 = Cp2 - TC * c02;
209
210 // Transform = (C2^T * H)^T / R^2 = H^T * C2 / R^2
211 MatrixXd X = (H.transpose() * C2) / (dSphereRadius * dSphereRadius);
212
213 return X;
214}
215
216//=============================================================================================================
217
219 const MatrixX3d& matPositions,
220 double dLambda2,
221 int iStiffness,
222 int iNLegendreTerms,
223 double dSphereRadius)
224{
226
227 if (matData.rows() != matPositions.rows()) {
228 qWarning() << "[SurfaceLaplacian::compute] Data rows" << matData.rows()
229 << "!= position rows" << matPositions.rows();
230 return result;
231 }
232
233 result.matTransform = computeTransform(matPositions, dLambda2, iStiffness,
234 iNLegendreTerms, dSphereRadius);
235
236 if (result.matTransform.size() == 0)
237 return result;
238
239 // Apply transform: CSD_data = X * data
240 result.matData = result.matTransform * matData;
241
242 return result;
243}
constexpr int Z
constexpr int X
Spherical-spline surface Laplacian (Current Source Density) for EEG.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
Result of a surface Laplacian (CSD) computation.
Eigen::MatrixXd matData
Transformed data (n_eeg_channels × n_times).
Eigen::MatrixXd matTransform
CSD transformation matrix (n_eeg × n_eeg).
static SurfaceLaplacianResult compute(const Eigen::MatrixXd &matData, const Eigen::MatrixX3d &matPositions, double dLambda2=1e-5, int iStiffness=4, int iNLegendreTerms=50, double dSphereRadius=-1.0)
static Eigen::MatrixXd computeTransform(const Eigen::MatrixX3d &matPositions, double dLambda2=1e-5, int iStiffness=4, int iNLegendreTerms=50, double dSphereRadius=-1.0)