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