v2.0.0
Loading...
Searching...
No Matches
mvar_model.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "mvar_model.h"
18
19//=============================================================================================================
20// QT INCLUDES
21//=============================================================================================================
22
23#include <QDebug>
24#include <QtMath>
25
26//=============================================================================================================
27// EIGEN INCLUDES
28//=============================================================================================================
29
30#include <Eigen/Dense>
31
32//=============================================================================================================
33// USED NAMESPACES
34//=============================================================================================================
35
36using namespace CONNECTIVITYLIB;
37using namespace Eigen;
38
39//=============================================================================================================
40// DEFINE GLOBAL METHODS
41//=============================================================================================================
42
43//=============================================================================================================
44// DEFINE MEMBER METHODS
45//=============================================================================================================
46
47void MvarModel::fit(const MatrixXd& data, int p)
48{
49 m_nChannels = static_cast<int>(data.rows());
50
51 if(p <= 0) {
52 p = selectOrderBIC(data);
53 }
54
55 fitLevinsonDurbin(data, p);
56}
57
58//=============================================================================================================
59
60QVector<MatrixXd> MvarModel::coefficients() const
61{
62 return m_coeffs;
63}
64
65//=============================================================================================================
66
67MatrixXd MvarModel::noiseCov() const
68{
69 return m_noiseCov;
70}
71
72//=============================================================================================================
73
75{
76 return m_order;
77}
78
79//=============================================================================================================
80
81QVector<MatrixXcd> MvarModel::transferFunction(const VectorXd& freqs) const
82{
83 QVector<MatrixXcd> vecH;
84 vecH.reserve(static_cast<int>(freqs.size()));
85
86 const int nCh = m_nChannels;
87 const MatrixXcd matI = MatrixXcd::Identity(nCh, nCh);
88 const std::complex<double> j(0.0, 1.0);
89
90 for(int fi = 0; fi < freqs.size(); ++fi) {
91 MatrixXcd matA = matI;
92
93 for(int k = 0; k < m_order; ++k) {
94 const double phase = -2.0 * M_PI * freqs(fi) * (k + 1);
95 const std::complex<double> expVal = std::exp(j * phase);
96 matA -= m_coeffs[k].cast<std::complex<double>>() * expVal;
97 }
98
99 vecH.append(matA.inverse());
100 }
101
102 return vecH;
103}
104
105//=============================================================================================================
106
107QVector<MatrixXcd> MvarModel::spectralMatrix(const VectorXd& freqs) const
108{
109 QVector<MatrixXcd> vecH = transferFunction(freqs);
110 QVector<MatrixXcd> vecS;
111 vecS.reserve(vecH.size());
112
113 const MatrixXcd matSigma = m_noiseCov.cast<std::complex<double>>();
114
115 for(int fi = 0; fi < vecH.size(); ++fi) {
116 vecS.append(vecH[fi] * matSigma * vecH[fi].adjoint());
117 }
118
119 return vecS;
120}
121
122//=============================================================================================================
123
124void MvarModel::fitLevinsonDurbin(const MatrixXd& data, int p)
125{
126 m_order = p;
127 m_nChannels = static_cast<int>(data.rows());
128
129 const int nCh = m_nChannels;
130 const int nSamples = static_cast<int>(data.cols());
131 const int nObs = nSamples - p;
132
133 if(nObs <= 0) {
134 qWarning() << "MvarModel::fitLevinsonDurbin - Not enough samples for model order" << p;
135 m_coeffs.clear();
136 m_noiseCov = MatrixXd::Identity(nCh, nCh);
137 return;
138 }
139
140 // Subtract mean from each channel
141 MatrixXd dataCentered = data;
142 for(int i = 0; i < nCh; ++i) {
143 dataCentered.row(i).array() -= dataCentered.row(i).mean();
144 }
145
146 // Build regression matrices for OLS solution of Yule-Walker equations
147 // Y = data(:, p:end) -> nCh x nObs
148 // Z = [data(:, p-1:end-1); -> nCh*p x nObs
149 // data(:, p-2:end-2);
150 // ...
151 // data(:, 0:end-p)]
152 MatrixXd matY = dataCentered.rightCols(nObs);
153 MatrixXd matZ(nCh * p, nObs);
154
155 for(int k = 0; k < p; ++k) {
156 matZ.middleRows(static_cast<Eigen::Index>(k) * nCh, nCh) = dataCentered.middleCols(p - 1 - k, nObs);
157 }
158
159 // Solve Y = A * Z via least squares: A = Y * Z^T * (Z * Z^T)^{-1}
160 MatrixXd matZZT = matZ * matZ.transpose();
161 MatrixXd matYZT = matY * matZ.transpose();
162 MatrixXd matA = matYZT * matZZT.ldlt().solve(MatrixXd::Identity(nCh * p, nCh * p));
163
164 // Extract coefficient matrices A_1..A_p
165 m_coeffs.clear();
166 m_coeffs.reserve(p);
167 for(int k = 0; k < p; ++k) {
168 m_coeffs.append(matA.middleCols(k * nCh, nCh));
169 }
170
171 // Compute noise covariance from residuals
172 MatrixXd matE = matY - matA * matZ;
173 m_noiseCov = (matE * matE.transpose()) / static_cast<double>(nObs);
174}
175
176//=============================================================================================================
177
178int MvarModel::selectOrderBIC(const MatrixXd& data, int maxOrder) const
179{
180 const int nCh = static_cast<int>(data.rows());
181 const int nSamples = static_cast<int>(data.cols());
182
183 // Limit max order to avoid underdetermined systems
184 maxOrder = qMin(maxOrder, nSamples / (nCh + 1));
185 if(maxOrder < 1) {
186 maxOrder = 1;
187 }
188
189 // Subtract mean
190 MatrixXd dataCentered = data;
191 for(int i = 0; i < nCh; ++i) {
192 dataCentered.row(i).array() -= dataCentered.row(i).mean();
193 }
194
195 double bestBIC = std::numeric_limits<double>::max();
196 int bestOrder = 1;
197
198 for(int p = 1; p <= maxOrder; ++p) {
199 const int nObs = nSamples - p;
200 if(nObs <= nCh * p) {
201 break;
202 }
203
204 // Build regression matrices
205 MatrixXd matY = dataCentered.rightCols(nObs);
206 MatrixXd matZ(nCh * p, nObs);
207
208 for(int k = 0; k < p; ++k) {
209 matZ.middleRows(static_cast<Eigen::Index>(k) * nCh, nCh) = dataCentered.middleCols(p - 1 - k, nObs);
210 }
211
212 // Solve via OLS
213 MatrixXd matZZT = matZ * matZ.transpose();
214 MatrixXd matYZT = matY * matZ.transpose();
215 MatrixXd matA = matYZT * matZZT.ldlt().solve(MatrixXd::Identity(nCh * p, nCh * p));
216
217 // Residual covariance
218 MatrixXd matE = matY - matA * matZ;
219 MatrixXd matSigma = (matE * matE.transpose()) / static_cast<double>(nObs);
220
221 // BIC = n * ln(det(Sigma)) + k * ln(n), where k = p * nCh^2
222 double detSigma = matSigma.determinant();
223 if(detSigma <= 0.0) {
224 continue;
225 }
226
227 const int nParams = p * nCh * nCh;
228 double bic = nObs * std::log(detSigma) + nParams * std::log(static_cast<double>(nObs));
229
230 if(bic < bestBIC) {
231 bestBIC = bic;
232 bestOrder = p;
233 }
234 }
235
236 return bestOrder;
237}
#define M_PI
Multivariate autoregressive (MVAR) model fit and its frequency-domain decomposition; backbone of the ...
Functional connectivity metrics (coherence, PLV, cross-correlation, etc.).
Eigen::MatrixXd noiseCov() const
QVector< Eigen::MatrixXcd > spectralMatrix(const Eigen::VectorXd &freqs) const
QVector< Eigen::MatrixXcd > transferFunction(const Eigen::VectorXd &freqs) const
void fit(const Eigen::MatrixXd &data, int p=0)
QVector< Eigen::MatrixXd > coefficients() const