v2.0.0
Loading...
Searching...
No Matches
fiff_evoked.cpp
Go to the documentation of this file.
1//=============================================================================================================
18
19//=============================================================================================================
20// INCLUDES
21//=============================================================================================================
22
23#include "fiff_evoked.h"
24#include "fiff_stream.h"
25#include "fiff_tag.h"
26#include "fiff_dir_node.h"
27
28#include <math/numerics.h>
29#include <QDebug>
30
31#include <cmath>
32#include <stdexcept>
33//=============================================================================================================
34// USED NAMESPACES
35//=============================================================================================================
36
37using namespace FIFFLIB;
38using namespace UTILSLIB;
39using namespace Eigen;
40
41//=============================================================================================================
42// DEFINE MEMBER METHODS
43//=============================================================================================================
44
46: nave(-1)
47, aspect_kind(-1)
48, first(-1)
49, last(-1)
50, baseline(qMakePair(-1.0f, -1.0f))
51{
52}
53
54//=============================================================================================================
55
56FiffEvoked::FiffEvoked(QIODevice& p_IODevice,
57 QVariant setno,
58 QPair<float, float> t_baseline,
59 bool proj,
60 fiff_int_t p_aspect_kind)
61{
62 if (!FiffEvoked::read(p_IODevice, *this, setno, t_baseline, proj, p_aspect_kind)) {
63 baseline = t_baseline;
64
65 throw std::runtime_error("Fiff evoked data not found");
66 }
67}
68
69//=============================================================================================================
70
72: info(p_FiffEvoked.info)
73, nave(p_FiffEvoked.nave)
74, aspect_kind(p_FiffEvoked.aspect_kind)
75, first(p_FiffEvoked.first)
76, last(p_FiffEvoked.last)
77, comment(p_FiffEvoked.comment)
78, times(p_FiffEvoked.times)
79, data(p_FiffEvoked.data)
80, proj(p_FiffEvoked.proj)
81, baseline(p_FiffEvoked.baseline)
82{
83}
84
85//=============================================================================================================
86
90
91//=============================================================================================================
92
94{
95 info.clear();
96 nave = -1;
97 aspect_kind = -1;
98 first = -1;
99 last = -1;
100 comment = QString("");
101 times = RowVectorXf();
102 data = MatrixXd();
103 proj = MatrixXd();
104}
105
106//=============================================================================================================
107
108FiffEvoked FiffEvoked::pick_channels(const QStringList& include,
109 const QStringList& exclude) const
110{
111 if (include.size() == 0 && exclude.size() == 0)
112 return FiffEvoked(*this);
113
114 RowVectorXi sel = FiffInfo::pick_channels(this->info.ch_names, include, exclude);
115 if (sel.cols() == 0) {
116 qWarning("Warning : No channels match the selection.\n");
117 return FiffEvoked(*this);
118 }
119
120 FiffEvoked res(*this);
121 //
122 // Modify the measurement info
123 //
124 res.info = FiffInfo(res.info.pick_info(sel));
125 //
126 // Create the reduced data set
127 //
128 MatrixXd selBlock = MatrixXd::Zero(sel.cols(), res.data.cols());
129 for (qint32 l = 0; l < sel.cols(); ++l) {
130 if (sel(0, l) < res.data.rows()) {
131 selBlock.block(l, 0, 1, selBlock.cols()) = res.data.block(sel(0, l), 0, 1, selBlock.cols());
132 } else {
133 qWarning("FiffEvoked::pick_channels - Warning : Selected channel index out of bound.\n");
134 }
135 }
136 res.data.resize(sel.cols(), res.data.cols());
137 res.data = selBlock;
138
139 return res;
140}
141
142//=============================================================================================================
143
144bool FiffEvoked::read(QIODevice& p_IODevice,
145 FiffEvoked& p_FiffEvoked,
146 QVariant setno,
147 QPair<float, float> t_baseline,
148 bool proj,
149 fiff_int_t p_aspect_kind)
150{
151 p_FiffEvoked.clear();
152
153 //
154 // Open the file
155 //
156 FiffStream::SPtr t_pStream(new FiffStream(&p_IODevice));
157 QString t_sFileName = t_pStream->streamName();
158
159 qInfo("Reading %s ...\n", t_sFileName.toUtf8().constData());
160
161 if (!t_pStream->open())
162 return false;
163 //
164 // Read the measurement info
165 //
168 if (!t_pStream->read_meas_info(t_pStream->dirtree(), info, meas))
169 return false;
170 info.filename = t_sFileName; //move fname storage to read_meas_info member function
171 //
172 // Locate the data of interest
173 //
174 QList<FiffDirNode::SPtr> processed = meas->dir_tree_find(FIFFB_PROCESSED_DATA);
175 if (processed.size() == 0) {
176 qWarning("Could not find processed data");
177 return false;
178 }
179 //
180 QList<FiffDirNode::SPtr> evoked_node = meas->dir_tree_find(FIFFB_EVOKED);
181 if (evoked_node.size() == 0) {
182 qWarning("Could not find evoked data");
183 return false;
184 }
185
186 // convert setno to an integer
187 if (!setno.isValid()) {
188 if (evoked_node.size() > 1) {
189 QStringList comments;
190 QList<fiff_int_t> aspect_kinds;
191 QString t;
192 if (!t_pStream->get_evoked_entries(evoked_node, comments, aspect_kinds, t))
193 t = QString("None found, must use integer");
194 qWarning("%lld datasets present, setno parameter must be set. Candidate setno names:\n%s", static_cast<long long>(evoked_node.size()), t.toUtf8().constData());
195 return false;
196 } else
197 setno = 0;
198 } else {
199 // find string-based entry
200 bool t_bIsInteger = true;
201 setno.toInt(&t_bIsInteger);
202 if (!t_bIsInteger) {
203 if (p_aspect_kind != FIFFV_ASPECT_AVERAGE && p_aspect_kind != FIFFV_ASPECT_STD_ERR) {
204 qWarning("kindStat must be \"FIFFV_ASPECT_AVERAGE\" or \"FIFFV_ASPECT_STD_ERR\"");
205 return false;
206 }
207
208 QStringList comments;
209 QList<fiff_int_t> aspect_kinds;
210 QString t;
211 t_pStream->get_evoked_entries(evoked_node, comments, aspect_kinds, t);
212
213 bool found = false;
214 for (qint32 i = 0; i < comments.size(); ++i) {
215 if (comments[i].compare(setno.toString()) == 0 && p_aspect_kind == aspect_kinds[i]) {
216 setno = i;
217 found = true;
218 break;
219 }
220 }
221 if (!found) {
222 qWarning() << "setno " << setno << " (" << p_aspect_kind << ") not found, out of found datasets:\n " << t;
223 return false;
224 }
225 }
226 }
227
228 if (setno.toInt() >= evoked_node.size() || setno.toInt() < 0) {
229 qWarning("Data set selector out of range");
230 return false;
231 }
232
233 FiffDirNode::SPtr my_evoked = evoked_node[setno.toInt()];
234
235 //
236 // Identify the aspects
237 //
238 QList<FiffDirNode::SPtr> aspects = my_evoked->dir_tree_find(FIFFB_ASPECT);
239
240 if (aspects.size() > 1)
241 qInfo("\tMultiple (%lld) aspects found. Taking first one.\n", static_cast<long long>(aspects.size()));
242
243 FiffDirNode::SPtr my_aspect = aspects[0];
244
245 //
246 // Now find the data in the evoked block
247 //
248 fiff_int_t nchan = 0;
249 float sfreq = -1.0f;
250 QList<FiffChInfo> chs;
251 fiff_int_t kind, pos, first = 0, last = 0;
252 FiffTag::UPtr t_pTag;
253 QString comment("");
254 qint32 k;
255 for (k = 0; k < my_evoked->nent(); ++k) {
256 kind = my_evoked->dir[k]->kind;
257 pos = my_evoked->dir[k]->pos;
258 switch (kind) {
259 case FIFF_COMMENT:
260 t_pStream->read_tag(t_pTag, pos);
261 comment = t_pTag->toString();
262 break;
264 t_pStream->read_tag(t_pTag, pos);
265 first = *t_pTag->toInt();
266 break;
267 case FIFF_LAST_SAMPLE:
268 t_pStream->read_tag(t_pTag, pos);
269 last = *t_pTag->toInt();
270 break;
271 case FIFF_NCHAN:
272 t_pStream->read_tag(t_pTag, pos);
273 nchan = *t_pTag->toInt();
274 break;
275 case FIFF_SFREQ:
276 t_pStream->read_tag(t_pTag, pos);
277 sfreq = *t_pTag->toFloat();
278 break;
279 case FIFF_CH_INFO:
280 t_pStream->read_tag(t_pTag, pos);
281 chs.append(t_pTag->toChInfo());
282 break;
283 }
284 }
285 if (comment.isEmpty())
286 comment = QString("No comment");
287
288 //
289 // Local channel information?
290 //
291 if (nchan > 0) {
292 if (chs.size() == 0) {
293 qWarning("Local channel information was not found when it was expected.");
294 return false;
295 }
296 if (chs.size() != nchan) {
297 qWarning("Number of channels and number of channel definitions are different.");
298 return false;
299 }
300 info.chs = chs;
301 info.nchan = nchan;
302 qInfo("\tFound channel information in evoked data. nchan = %d\n", nchan);
303 if (sfreq > 0.0f)
304 info.sfreq = sfreq;
305 }
306 qint32 nsamp = last - first + 1;
307 qInfo("\tFound the data of interest:\n");
308 qInfo("\t\tt = %10.2f ... %10.2f ms (%s)\n", 1000 * static_cast<float>(first) / info.sfreq, 1000 * static_cast<float>(last) / info.sfreq, comment.toUtf8().constData());
309 if (info.comps.size() > 0)
310 qInfo("\t\t%lld CTF compensation matrices available\n", static_cast<long long>(info.comps.size()));
311
312 //
313 // Read the data in the aspect block
314 //
316 fiff_int_t nave = -1;
317 QList<FiffTag> epoch;
318 for (k = 0; k < my_aspect->nent(); ++k) {
319 kind = my_aspect->dir[k]->kind;
320 pos = my_aspect->dir[k]->pos;
321
322 switch (kind) {
323 case FIFF_COMMENT:
324 t_pStream->read_tag(t_pTag, pos);
325 comment = t_pTag->toString();
326 break;
327 case FIFF_ASPECT_KIND:
328 t_pStream->read_tag(t_pTag, pos);
329 aspect_kind = *t_pTag->toInt();
330 break;
331 case FIFF_NAVE:
332 t_pStream->read_tag(t_pTag, pos);
333 nave = *t_pTag->toInt();
334 break;
335 case FIFF_EPOCH:
336 t_pStream->read_tag(t_pTag, pos);
337 epoch.append(FiffTag(*t_pTag));
338 break;
339 }
340 }
341 if (nave == -1)
342 nave = 1;
343 qInfo("\t\tnave = %d - aspect type = %d\n", nave, aspect_kind);
344
345 qint32 nepoch = epoch.size();
346 if (nepoch != 1 && nepoch != info.nchan) {
347 qWarning("Number of epoch tags is unreasonable (nepoch = %d nchan = %d)", nepoch, info.nchan);
348 return false;
349 }
350 MatrixXd all_data;
351 if (nepoch == 1) {
352 //
353 // Only one epoch
354 //
355 all_data = epoch[0].toFloatMatrix().cast<double>();
356 all_data.transposeInPlace();
357 //
358 // May need a transpose if the number of channels is one
359 //
360 if (all_data.cols() == 1 && info.nchan == 1)
361 all_data.transposeInPlace();
362 } else {
363 //
364 // Put the old style epochs together: one channel per tag
365 //
366 all_data.resize(nepoch, epoch[0].size() / static_cast<int>(sizeof(float)));
367 for (k = 0; k < nepoch; ++k) {
368 const int n = epoch[k].size() / static_cast<int>(sizeof(float));
369 if (n != all_data.cols()) {
370 qWarning("Old style epochs have different lengths (%d and %d)", n, static_cast<int>(all_data.cols()));
371 return false;
372 }
373 all_data.row(k) = Map<const RowVectorXf>(epoch[k].toFloat(), n).cast<double>();
374 }
375 }
376 if (all_data.cols() != nsamp) {
377 qWarning("Incorrect number of samples (%d instead of %d)", static_cast<int>(all_data.cols()), nsamp);
378 return false;
379 }
380
381 //
382 // Calibrate
383 //
384 qInfo("\n\tPreprocessing...\n");
385 qInfo("\t%d channels remain after picking\n", info.nchan);
386
387 using T = Eigen::Triplet<double>;
388 std::vector<T> tripletList;
389 tripletList.reserve(info.nchan);
390 for (k = 0; k < info.nchan; ++k)
391 tripletList.push_back(T(k, k, info.chs[k].cal));
392 SparseMatrix<double> cals(info.nchan, info.nchan);
393 cals.setFromTriplets(tripletList.begin(), tripletList.end());
394
395 all_data = cals * all_data;
396
397 RowVectorXf times = RowVectorXf(last - first + 1);
398 for (k = 0; k < times.size(); ++k)
399 times[k] = static_cast<float>(first + k) / info.sfreq;
400
401 //
402 // Set up projection
403 //
404 if (info.projs.size() == 0 || !proj) {
405 qInfo("\tNo projector specified for these data.\n");
406 p_FiffEvoked.proj = MatrixXd();
407 } else {
408 // Create the projector
409 MatrixXd projection;
410 qint32 nproj = info.make_projector(projection);
411 if (nproj == 0) {
412 qWarning("\tThe projection vectors do not apply to these channels\n");
413 p_FiffEvoked.proj = MatrixXd();
414 } else {
415 qInfo("\tCreated an SSP operator (subspace dimension = %d)\n", nproj);
416 p_FiffEvoked.proj = projection;
417 }
418
419 // The projection items have been activated
421 }
422
423 if (p_FiffEvoked.proj.rows() > 0) {
424 all_data = p_FiffEvoked.proj * all_data;
425 qInfo("\tSSP projectors applied to the evoked data\n");
426 }
427
428 // Put it all together
429 p_FiffEvoked.info = info;
430 p_FiffEvoked.nave = nave;
431 p_FiffEvoked.aspect_kind = aspect_kind;
432 p_FiffEvoked.first = first;
433 p_FiffEvoked.last = last;
434 p_FiffEvoked.comment = comment;
435 p_FiffEvoked.times = times;
436 p_FiffEvoked.data = all_data;
437
438 // Run baseline correction only if explicitly requested
439 // A baseline of (-1, -1) or (first == second) means "no baseline correction"
440 // This matches mne-python (baseline=None) and SVN-MNE (no --bmin/--bmax) behavior
441 if (t_baseline.first != t_baseline.second) {
442 p_FiffEvoked.applyBaselineCorrection(t_baseline);
443 } else {
444 p_FiffEvoked.baseline = t_baseline;
445 qInfo("\tNo baseline correction applied\n");
446 }
447
448 return true;
449}
450
451//=============================================================================================================
452
453void FiffEvoked::setInfo(const FiffInfo& p_info,
454 bool applyProj)
455{
456 info = p_info;
457 //
458 // Set up projection
459 //
460 if (info.projs.size() == 0 || !applyProj) {
461 qInfo("\tNo projector specified for these data.\n");
462 this->proj = MatrixXd();
463 } else {
464 // Create the projector
465 MatrixXd projection;
466 qint32 nproj = info.make_projector(projection);
467 if (nproj == 0) {
468 qWarning("\tThe projection vectors do not apply to these channels\n");
469 this->proj = MatrixXd();
470 } else {
471 qInfo("\tCreated an SSP operator (subspace dimension = %d)\n", nproj);
472 this->proj = projection;
473 }
474
475 // The projection items have been activated
477 }
478}
479
480//=============================================================================================================
481
482FiffEvoked& FiffEvoked::operator+=(const MatrixXd& newData)
483{
484 //Init matrix if necessary
485 if (nave == -1 || nave == 0) {
486 data = MatrixXd::Zero(newData.rows(), newData.cols());
487 }
488
489 if (data.cols() == newData.cols() && data.rows() == newData.rows()) {
490 //Revert old averaging
491 data = data * nave;
492
493 //Do new averaging
494 data += newData;
495 if (nave <= 0) {
496 nave = 1;
497 } else {
498 nave++;
499 }
500
501 data /= nave;
502 }
503
504 return *this;
505}
506
507//=============================================================================================================
508
509FiffEvoked FiffEvoked::combine(const QList<FiffEvoked>& evokeds, CombineWeights weights)
510{
511 VectorXd w(evokeds.size());
512 double naveSum = 0.0;
513 for (const FiffEvoked& e : evokeds) {
514 naveSum += e.nave;
515 }
516 for (int i = 0; i < evokeds.size(); ++i) {
517 w(i) = weights == CombineWeights::Nave ? evokeds[i].nave / naveSum
518 : 1.0 / static_cast<double>(evokeds.size());
519 }
520 return combine(evokeds, w);
521}
522
523//=============================================================================================================
524
525FiffEvoked FiffEvoked::combine(const QList<FiffEvoked>& evokeds, const VectorXd& weights)
526{
527 if (evokeds.isEmpty() || weights.size() != evokeds.size()) {
528 qWarning() << "[FiffEvoked::combine] weights must be the same size as the evoked list.";
529 return FiffEvoked();
530 }
531 const FiffEvoked& ref = evokeds.first();
532 for (const FiffEvoked& e : evokeds) {
533 if (e.info.ch_names != ref.info.ch_names || e.times.size() != ref.times.size() || (e.times - ref.times).cwiseAbs().maxCoeff() > 1e-6f) {
534 qWarning() << "[FiffEvoked::combine] All evoked data must have the same channels and times.";
535 return FiffEvoked();
536 }
537 }
538
539 FiffEvoked res(ref);
540 res.data = MatrixXd::Zero(ref.data.rows(), ref.data.cols());
541 double invNave = 0.0;
542 QString comment;
543 for (int i = 0; i < evokeds.size(); ++i) {
544 const FiffEvoked& e = evokeds[i];
545 const double w = weights(i);
546 res.data += w * e.data;
547 invNave += w * w / e.nave;
548 for (const QString& bad : e.info.bads) {
549 if (!res.info.bads.contains(bad)) {
550 res.info.bads.append(bad);
551 }
552 }
553
554 const QString sign = w >= 0.0 ? QString() : QString("-");
555 const QString weight = qAbs(qAbs(w) - 1.0) <= 1e-8 + 1e-5 ? QString()
556 : QString::number(qAbs(w), 'f', 3);
557 const QString multiplier = weight.isEmpty() ? QString() : QString::fromUtf8(" × ");
558 const QString thisComment = e.comment.contains(" + ") ? QString("(%1)").arg(e.comment)
559 : e.comment.isEmpty() ? QString("unknown")
560 : e.comment;
561 if (i == 0) {
562 comment += sign + weight + multiplier + thisComment;
563 } else {
564 comment += QString(" %1 ").arg(sign.isEmpty() ? QString("+") : sign) + weight + multiplier + thisComment;
565 }
566 }
567 res.comment = comment.replace(" - - ", " + ");
568 res.nave = static_cast<fiff_int_t>(std::lround(1.0 / invNave));
569 return res;
570}
571
572//=============================================================================================================
573
574void FiffEvoked::applyBaselineCorrection(QPair<float, float>& p_baseline)
575{
576 // Skip baseline correction if sentinel value (-1, -1) or equal bounds are passed
577 if (p_baseline.first == p_baseline.second) {
578 qInfo("\tNo baseline correction applied\n");
579 return;
580 }
581
582 // Run baseline correction
583 qInfo("Applying baseline correction ... (mode: mean)\n");
584 this->data = Numerics::rescale(this->data, this->times, p_baseline, QString("mean"));
585 this->baseline = p_baseline;
586}
FIFF tag: the 16-byte tag header (kind, type, size, next) plus its decoded payload.
Single averaged evoked response: time axis, samples, baseline, channel info and processing history.
FIFF binary tag-stream layer: wraps a QIODevice to read and write FIFF tags, directories,...
constexpr auto FIFFB_PROCESSED_DATA
Definition fiff_file.h:356
constexpr auto FIFF_EPOCH
Definition fiff_file.h:549
constexpr auto FIFF_COMMENT
Definition fiff_file.h:450
constexpr auto FIFFB_ASPECT
Definition fiff_file.h:359
constexpr auto FIFF_LAST_SAMPLE
Definition fiff_file.h:453
constexpr auto FIFF_NAVE
Definition fiff_file.h:451
constexpr auto FIFF_SFREQ
Definition fiff_file.h:445
constexpr auto FIFF_NCHAN
Definition fiff_file.h:444
constexpr auto FIFF_FIRST_SAMPLE
Definition fiff_file.h:452
constexpr auto FIFFV_ASPECT_AVERAGE
Definition fiff_file.h:429
constexpr auto FIFFB_EVOKED
Definition fiff_file.h:357
constexpr auto FIFF_ASPECT_KIND
Definition fiff_file.h:454
constexpr auto FIFFV_ASPECT_STD_ERR
Definition fiff_file.h:430
constexpr auto FIFF_CH_INFO
Definition fiff_file.h:447
if(w.size() > 0)
Recursive node of the parsed FIFF block tree (FIFFB_* hierarchy with directory entries and children).
General numerical helpers: GCD, log2, histogram binning, baseline rescaling, sparsity tests.
FIFF file I/O, in-memory data structures and high-level readers/writers.
qint32 fiff_int_t
Definition fiff_types.h:86
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
QSharedPointer< FiffDirNode > SPtr
Single averaged evoked response: time axis, data, baseline, channel info and averaging metadata.
Definition fiff_evoked.h:77
Eigen::MatrixXd proj
Eigen::RowVectorXf times
static FiffEvoked combine(const QList< FiffEvoked > &evokeds, const Eigen::VectorXd &weights)
Eigen::MatrixXd data
FiffEvoked pick_channels(const QStringList &include=defaultQStringList, const QStringList &exclude=defaultQStringList) const
fiff_int_t aspect_kind
QPair< float, float > baseline
void setInfo(const FiffInfo &p_info, bool applyProj=true)
FiffEvoked & operator+=(const Eigen::MatrixXd &newData)
static bool read(QIODevice &p_IODevice, FiffEvoked &p_FiffEvoked, QVariant setno=0, QPair< float, float > t_baseline=defaultFloatPair, bool proj=true, fiff_int_t p_aspect_kind=FIFFV_ASPECT_AVERAGE)
void applyBaselineCorrection(QPair< float, float > &p_baseline)
Full FIFF measurement info: per-channel descriptors, sampling and filter setup, projectors,...
Definition fiff_info.h:90
FiffInfo pick_info(const Eigen::RowVectorXi &sel=defaultVectorXi) const
static Eigen::RowVectorXi pick_channels(const QStringList &ch_names, const QStringList &include=defaultQStringList, const QStringList &exclude=defaultQStringList)
static void activate_projs(QList< FiffProj > &p_qListFiffProj)
FIFF tag-stream reader/writer: wraps a QIODevice and exposes typed read_* / write_* methods for every...
QSharedPointer< FiffStream > SPtr
FIFF tag: 16-byte header (kind, type, size, next) plus payload, with typed decoders for every FIFFT_*...
Definition fiff_tag.h:161
std::unique_ptr< FiffTag > UPtr
Definition fiff_tag.h:165
static Eigen::MatrixXd rescale(const Eigen::MatrixXd &data, const Eigen::RowVectorXf &times, const QPair< float, float > &baseline, QString mode)