v2.0.0
Loading...
Searching...
No Matches
fiff_evoked_set.cpp
Go to the documentation of this file.
1//=============================================================================================================
21
22//=============================================================================================================
23// INCLUDES
24//=============================================================================================================
25
26#include "fiff_evoked_set.h"
27#include "fiff_events.h"
28#include "fiff_raw_data.h"
29#include "fiff_tag.h"
30#include "fiff_dir_node.h"
31#include "fiff_stream.h"
32#include "fiff_file.h"
33#include "fiff_constants.h"
34
35//=============================================================================================================
36// EIGEN INCLUDES
37//=============================================================================================================
38
39#include <Eigen/SparseCore>
40
41#include <cmath>
42
43//=============================================================================================================
44// QT INCLUDES
45//=============================================================================================================
46
47#include <QFile>
48#include <QDebug>
49
50#include <stdexcept>
51//=============================================================================================================
52// USED NAMESPACES
53//=============================================================================================================
54
55using namespace FIFFLIB;
56using namespace Eigen;
57
58//=============================================================================================================
59// DEFINE MEMBER METHODS
60//=============================================================================================================
61
63{
64 qRegisterMetaType<FIFFLIB::FiffEvokedSet>("FIFFLIB::FiffEvokedSet");
65 qRegisterMetaType<FIFFLIB::FiffEvokedSet::SPtr>("FIFFLIB::FiffEvokedSet::SPtr");
66}
67
68//=============================================================================================================
69
70FiffEvokedSet::FiffEvokedSet(QIODevice& p_IODevice)
71{
72 qRegisterMetaType<FIFFLIB::FiffEvokedSet>("FIFFLIB::FiffEvokedSet");
73 qRegisterMetaType<FIFFLIB::FiffEvokedSet::SPtr>("FIFFLIB::FiffEvokedSet::SPtr");
74
75 if (!FiffEvokedSet::read(p_IODevice, *this)) {
76 throw std::runtime_error("Fiff evoked data set not found");
77 }
78}
79
80//=============================================================================================================
81
82FiffEvokedSet::FiffEvokedSet(const FiffEvokedSet& p_FiffEvokedSet)
83: info(p_FiffEvokedSet.info)
84, evoked(p_FiffEvokedSet.evoked)
85{
86}
87
88//=============================================================================================================
89
91{
92}
93
94//=============================================================================================================
95
97{
98 info.clear();
99 evoked.clear();
100}
101
102//=============================================================================================================
103
104FiffEvokedSet FiffEvokedSet::pick_channels(const QStringList& include,
105 const QStringList& exclude) const
106{
107 FiffEvokedSet res;
108
109 //
110 // Update info to match the channel selection
111 //
112 RowVectorXi sel = FiffInfo::pick_channels(this->info.ch_names, include, exclude);
113 if (sel.cols() > 0) {
114 res.info = this->info.pick_info(sel);
115 } else {
116 res.info = this->info;
117 }
118
119 QList<FiffEvoked>::ConstIterator ev;
120 for (ev = evoked.begin(); ev != evoked.end(); ++ev)
121 res.evoked.push_back(ev->pick_channels(include, exclude));
122
123 return res;
124}
125
126//=============================================================================================================
127
129{
130 const qint32 now = info.get_current_comp();
131
132 if (now == to) {
133 qInfo("Data is already compensated as desired.\n");
134 return false;
135 }
136
138 if (!info.make_compensator(now, to, ctf_comp)) {
139 qWarning("[FiffEvokedSet::compensate_to] Cannot change compensation from grade %d to %d.", now, to);
140 return false;
141 }
142
143 for (FiffEvoked& response : evoked) {
144 response.data = ctf_comp.data->data * response.data;
145 response.info.set_current_comp(to);
146 }
147 info.set_current_comp(to);
148
149 return true;
150}
151
152//=============================================================================================================
153
154bool FiffEvokedSet::find_evoked(const FiffEvokedSet& p_FiffEvokedSet) const
155{
156 if (!p_FiffEvokedSet.evoked.size()) {
157 qWarning("No evoked response data sets in %s\n", p_FiffEvokedSet.info.filename.toUtf8().constData());
158 return false;
159 } else
160 qInfo("\nFound %lld evoked response data sets in %s :\n", static_cast<long long>(p_FiffEvokedSet.evoked.size()), p_FiffEvokedSet.info.filename.toUtf8().constData());
161
162 for (qint32 i = 0; i < p_FiffEvokedSet.evoked.size(); ++i) {
163 qInfo("%s (%s)\n", p_FiffEvokedSet.evoked.at(i).comment.toUtf8().constData(), p_FiffEvokedSet.evoked.at(i).aspectKindToString().toUtf8().constBegin());
164 }
165
166 return true;
167}
168
169//=============================================================================================================
170
171bool FiffEvokedSet::read(QIODevice& p_IODevice,
172 FiffEvokedSet& p_FiffEvokedSet, QPair<float, float> baseline,
173 bool proj)
174{
175 p_FiffEvokedSet.clear();
176
177 //
178 // Open the file
179 //
180 FiffStream::SPtr t_pStream(new FiffStream(&p_IODevice));
181 QString t_sFileName = t_pStream->streamName();
182
183 qInfo("Exploring %s ...\n", t_sFileName.toUtf8().constData());
184
185 if (!t_pStream->open())
186 return false;
187 //
188 // Read the measurement info
189 //
191 if (!t_pStream->read_meas_info(t_pStream->dirtree(), p_FiffEvokedSet.info, meas))
192 return false;
193 p_FiffEvokedSet.info.filename = t_sFileName; //move fname storage to read_meas_info member function
194 //
195 // Locate the data of interest
196 //
197 QList<FiffDirNode::SPtr> processed = meas->dir_tree_find(FIFFB_PROCESSED_DATA);
198 if (processed.size() == 0) {
199 qWarning("Could not find processed data");
200 return false;
201 }
202 //
203 QList<FiffDirNode::SPtr> evoked_node = meas->dir_tree_find(FIFFB_EVOKED);
204 if (evoked_node.size() == 0) {
205 qWarning("Could not find evoked data");
206 return false;
207 }
208
209 QStringList comments;
210 QList<fiff_int_t> aspect_kinds;
211 QString t;
212 if (!t_pStream->get_evoked_entries(evoked_node, comments, aspect_kinds, t))
213 t = QString("None found, must use integer");
214 qInfo("\tFound %lld datasets\n", static_cast<long long>(evoked_node.size()));
215
216 for (qint32 i = 0; i < comments.size(); ++i) {
217 QFile t_file(p_FiffEvokedSet.info.filename);
218 qInfo(">> Processing %s <<\n", comments[i].toUtf8().constData());
219 FiffEvoked t_FiffEvoked;
220 if (FiffEvoked::read(t_file, t_FiffEvoked, i, baseline, proj))
221 p_FiffEvokedSet.evoked.push_back(t_FiffEvoked);
222 }
223
224 return true;
225}
226
227//=============================================================================================================
228
229bool FiffEvokedSet::save(const QString& fileName) const
230{
231 if (fileName.isEmpty()) {
232 qWarning() << "[FiffEvokedSet::save] Output file not specified.";
233 return false;
234 }
235
236 QFile file(fileName);
237 if (!save(file)) {
238 qWarning() << "[FiffEvokedSet::save] Cannot open" << fileName;
239 return false;
240 }
241
242 qInfo() << "[FiffEvokedSet::save] Saved" << evoked.size()
243 << "average(s) to" << fileName;
244 return true;
245}
246
247//=============================================================================================================
248
249bool FiffEvokedSet::save(QIODevice& device) const
250{
252 if (!pStream) {
253 return false;
254 }
255
256 pStream->write_evoked_set(*this);
257 pStream->end_file();
258 device.close();
259 return true;
260}
261
262//=============================================================================================================
263
264FiffEvokedSet FiffEvokedSet::computeGrandAverage(const QList<FiffEvokedSet>& evokedSets)
265{
266 FiffEvokedSet grandAvg;
267
268 if (evokedSets.isEmpty()) {
269 qWarning() << "[FiffEvokedSet::computeGrandAverage] No evoked sets provided.";
270 return grandAvg;
271 }
272
273 int nCat = evokedSets.first().evoked.size();
274 for (const FiffEvokedSet& eset : evokedSets) {
275 nCat = qMin(nCat, static_cast<int>(eset.evoked.size()));
276 }
277
278 for (int j = 0; j < nCat; ++j) {
279 // Drop every channel marked bad in any data set, then keep the common channels.
280 QStringList bads;
281 for (const FiffEvokedSet& eset : evokedSets) {
282 for (const QString& bad : eset.evoked[j].info.bads) {
283 if (!bads.contains(bad)) {
284 bads.append(bad);
285 }
286 }
287 }
288 QStringList common;
289 for (const QString& name : evokedSets.first().evoked[j].info.ch_names) {
290 bool inAll = !bads.contains(name);
291 for (const FiffEvokedSet& eset : evokedSets) {
292 inAll = inAll && eset.evoked[j].info.ch_names.contains(name);
293 }
294 if (inAll) {
295 common.append(name);
296 }
297 }
298
299 QList<FiffEvoked> picked;
300 for (const FiffEvokedSet& eset : evokedSets) {
301 picked.append(eset.evoked[j].pick_channels(common));
302 }
304 if (avg.isEmpty()) {
305 continue;
306 }
307 avg.nave = static_cast<fiff_int_t>(evokedSets.size());
308 avg.comment = QString("Grand average (n = %1)").arg(avg.nave);
309 grandAvg.info = avg.info;
310 grandAvg.evoked.append(avg);
311 }
312
313 return grandAvg;
314}
315
316//=============================================================================================================
317
318void FiffEvokedSet::subtractBaseline(Eigen::MatrixXd& epoch, int bminSamp, int bmaxSamp)
319{
320 if (bminSamp < 0)
321 bminSamp = 0;
322 if (bmaxSamp >= epoch.cols())
323 bmaxSamp = static_cast<int>(epoch.cols()) - 1;
324 if (bminSamp >= bmaxSamp)
325 return;
326
327 int nBase = bmaxSamp - bminSamp + 1;
328 for (int c = 0; c < epoch.rows(); ++c) {
329 double baseVal = epoch.row(c).segment(bminSamp, nBase).mean();
330 epoch.row(c).array() -= baseVal;
331 }
332}
333
334//=============================================================================================================
335
337 const AverageDescription& desc,
338 const MatrixXi& events,
339 QString& log)
340{
341 FiffEvokedSet evokedSet;
342 evokedSet.info = raw.info;
343
344 float sfreq = raw.info.sfreq;
345 int nchan = raw.info.nchan;
346
347 log.clear();
348 log += QString("Averaging: %1\n").arg(desc.comment);
349
350 // Process each category
351 for (int j = 0; j < desc.categories.size(); ++j) {
352 const AverageCategory& cat = desc.categories[j];
353
354 // Compute sample indices
355 int minSamp = static_cast<int>(std::round(cat.tmin * sfreq));
356 int maxSamp = static_cast<int>(std::round(cat.tmax * sfreq));
357 int ns = maxSamp - minSamp + 1;
358 int delaySamp = static_cast<int>(std::round(cat.delay * sfreq));
359
360 // Baseline sample range (relative to epoch start)
361 int bminSamp = 0, bmaxSamp = 0;
362 if (cat.doBaseline) {
363 bminSamp = static_cast<int>(std::round(cat.bmin * sfreq)) - minSamp;
364 bmaxSamp = static_cast<int>(std::round(cat.bmax * sfreq)) - minSamp;
365 }
366
367 // Accumulator
368 MatrixXd sumData = MatrixXd::Zero(nchan, ns);
369 MatrixXd sumSqData = MatrixXd::Zero(nchan, ns);
370 int nave = 0;
371
372 log += QString("\n Category: %1\n").arg(cat.comment);
373 log += QString(" t = %1 ... %2 ms\n").arg(1000.0 * cat.tmin, 0, 'f', 1).arg(1000.0 * cat.tmax, 0, 'f', 1);
374
375 // Iterate over events
376 for (int k = 0; k < events.rows(); ++k) {
377 if (!FiffEvents::matchEvent(cat, events, k))
378 continue;
379
380 int evSample = events(k, 0);
381 int epochStart = evSample + delaySamp + minSamp;
382 int epochEnd = evSample + delaySamp + maxSamp;
383
384 // Check bounds
385 if (epochStart < raw.first_samp || epochEnd > raw.last_samp)
386 continue;
387
388 // Read epoch
389 MatrixXd epochData;
390 MatrixXd epochTimes;
391 if (!raw.read_raw_segment(epochData, epochTimes, epochStart, epochEnd)) {
392 log += QString(" Error reading epoch at sample %1\n").arg(evSample);
393 continue;
394 }
395
396 // Artifact rejection
397 QString rejReason;
398 if (!checkArtifacts(epochData, raw.info, raw.info.bads, desc.rej, rejReason)) {
399 log += QString(" %1 %2 %3 %4 [%5] %6 [omit]\n")
400 .arg(evSample, 7)
401 .arg(static_cast<float>(evSample) / sfreq, -10, 'f', 3)
402 .arg(events(k, 1), 3)
403 .arg(events(k, 2), 3)
404 .arg(cat.comment)
405 .arg(rejReason);
406 continue;
407 }
408
409 // Baseline correction
410 if (cat.doBaseline) {
411 subtractBaseline(epochData, bminSamp, bmaxSamp);
412 }
413
414 // Absolute value
415 if (cat.doAbs) {
416 epochData = epochData.cwiseAbs();
417 }
418
419 // Accumulate
420 sumData += epochData;
421 if (cat.doStdErr) {
422 sumSqData += epochData.cwiseProduct(epochData);
423 }
424 nave++;
425
426 log += QString(" %1 %2 %3 %4 [%5]\n")
427 .arg(evSample, 7)
428 .arg(static_cast<float>(evSample) / sfreq, -10, 'f', 3)
429 .arg(events(k, 1), 3)
430 .arg(events(k, 2), 3)
431 .arg(cat.comment);
432 }
433
434 // Compute average
435 FiffEvoked evoked;
436 evoked.comment = cat.comment;
437 evoked.first = minSamp;
438 evoked.last = maxSamp;
439 evoked.nave = nave;
440
441 // Build times vector
442 RowVectorXf times(ns);
443 for (int s = 0; s < ns; ++s)
444 times(s) = static_cast<float>(minSamp + s) / sfreq;
445 evoked.times = times;
446
447 if (nave > 0) {
448 evoked.data = sumData / static_cast<double>(nave);
449 } else {
450 evoked.data = MatrixXd::Zero(nchan, ns);
451 }
452
453 evoked.info = raw.info;
454
455 evokedSet.evoked.append(evoked);
456 log += QString(" nave = %1\n").arg(nave);
457 }
458
459 return evokedSet;
460}
461
462//=============================================================================================================
463
464bool FiffEvokedSet::checkArtifacts(const MatrixXd& epoch,
465 const FiffInfo& info,
466 const QStringList& bads,
467 const RejectionParams& rej,
468 QString& reason)
469{
470 for (int c = 0; c < epoch.rows(); ++c) {
471 // Skip bad channels
472 if (bads.contains(info.ch_names[c]))
473 continue;
474
475 double minVal = epoch.row(c).minCoeff();
476 double maxVal = epoch.row(c).maxCoeff();
477 double pp = maxVal - minVal;
478
479 int chKind = info.chs[c].kind;
480 int chUnit = info.chs[c].unit;
481
482 if (chKind == FIFFV_MEG_CH) {
483 if (chUnit == FIFF_UNIT_T) {
484 // Magnetometer
485 if (rej.megMagReject > 0 && pp > rej.megMagReject) {
486 reason = QString("%1 : %2 fT > %3 fT")
487 .arg(info.ch_names[c])
488 .arg(pp * 1e15, 0, 'f', 1)
489 .arg(rej.megMagReject * 1e15, 0, 'f', 1);
490 return false;
491 }
492 if (rej.megMagFlat > 0 && pp < rej.megMagFlat) {
493 reason = QString("%1 : %2 fT < %3 fT (flat)")
494 .arg(info.ch_names[c])
495 .arg(pp * 1e15, 0, 'f', 1)
496 .arg(rej.megMagFlat * 1e15, 0, 'f', 1);
497 return false;
498 }
499 } else {
500 // Gradiometer
501 if (rej.megGradReject > 0 && pp > rej.megGradReject) {
502 reason = QString("%1 : %2 fT/cm > %3 fT/cm")
503 .arg(info.ch_names[c])
504 .arg(pp * 1e13, 0, 'f', 1)
505 .arg(rej.megGradReject * 1e13, 0, 'f', 1);
506 return false;
507 }
508 if (rej.megGradFlat > 0 && pp < rej.megGradFlat) {
509 reason = QString("%1 : %2 fT/cm < %3 fT/cm (flat)")
510 .arg(info.ch_names[c])
511 .arg(pp * 1e13, 0, 'f', 1)
512 .arg(rej.megGradFlat * 1e13, 0, 'f', 1);
513 return false;
514 }
515 }
516 } else if (chKind == FIFFV_EEG_CH) {
517 if (rej.eegReject > 0 && pp > rej.eegReject) {
518 reason = QString("%1 : %2 uV > %3 uV")
519 .arg(info.ch_names[c])
520 .arg(pp * 1e6, 0, 'f', 1)
521 .arg(rej.eegReject * 1e6, 0, 'f', 1);
522 return false;
523 }
524 if (rej.eegFlat > 0 && pp < rej.eegFlat) {
525 reason = QString("%1 : %2 uV < %3 uV (flat)")
526 .arg(info.ch_names[c])
527 .arg(pp * 1e6, 0, 'f', 1)
528 .arg(rej.eegFlat * 1e6, 0, 'f', 1);
529 return false;
530 }
531 } else if (chKind == FIFFV_EOG_CH) {
532 if (rej.eogReject > 0 && pp > rej.eogReject) {
533 reason = QString("%1 : %2 uV > %3 uV (EOG)")
534 .arg(info.ch_names[c])
535 .arg(pp * 1e6, 0, 'f', 1)
536 .arg(rej.eogReject * 1e6, 0, 'f', 1);
537 return false;
538 }
539 if (rej.eogFlat > 0 && pp < rej.eogFlat) {
540 reason = QString("%1 : EOG flat").arg(info.ch_names[c]);
541 return false;
542 }
543 } else if (chKind == FIFFV_ECG_CH) {
544 if (rej.ecgReject > 0 && pp > rej.ecgReject) {
545 reason = QString("%1 : %2 mV > %3 mV (ECG)")
546 .arg(info.ch_names[c])
547 .arg(pp * 1e3, 0, 'f', 2)
548 .arg(rej.ecgReject * 1e3, 0, 'f', 2);
549 return false;
550 }
551 if (rej.ecgFlat > 0 && pp < rej.ecgFlat) {
552 reason = QString("%1 : ECG flat").arg(info.ch_names[c]);
553 return false;
554 }
555 }
556 }
557 return true;
558}
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
constexpr auto FIFFV_ECG_CH
#define FIFFV_EEG_CH
#define FIFFV_MEG_CH
constexpr auto FIFF_UNIT_T
constexpr auto FIFFV_EOG_CH
FIFF tag: the 16-byte tag header (kind, type, size, next) plus its decoded payload.
Set of averaged evoked responses sharing a FiffInfo, plus the ave-style category / rejection descript...
FIFF binary tag-stream layer: wraps a QIODevice to read and write FIFF tags, directories,...
FIFF tag-kind, block-kind and type-code numerical definitions, authoritative for FIFFLIB.
constexpr auto FIFFB_PROCESSED_DATA
Definition fiff_file.h:356
constexpr auto FIFFB_EVOKED
Definition fiff_file.h:357
Recursive node of the parsed FIFF block tree (FIFFB_* hierarchy with directory entries and children).
Stim-channel event list (sample, previous value, new value triples) with FIFF read/write helpers.
FIFF continuous raw recording: FiffInfo plus a directory of FIFF_DATA_BUFFER tags for random-access s...
FIFF file I/O, in-memory data structures and high-level readers/writers.
qint32 fiff_int_t
Definition fiff_types.h:86
One CTF software-gradient compensation matrix: grade kind, calibration flag and the gradiometer × ref...
QSharedPointer< FiffDirNode > SPtr
static bool matchEvent(const AverageCategory &cat, const Eigen::MatrixXi &events, int eventIdx)
Single averaged evoked response: time axis, data, baseline, channel info and averaging metadata.
Definition fiff_evoked.h:77
Eigen::RowVectorXf times
static FiffEvoked combine(const QList< FiffEvoked > &evokeds, const Eigen::VectorXd &weights)
Eigen::MatrixXd data
bool isEmpty() const
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)
Artifact-rejection thresholds for the MNE-C batch averaging pipeline (gradiometer / magnetometer / EE...
One averaging category in an MNE-C ave-description file: trigger logic, timing window,...
Top-level MNE-C ave-description record: comment, category list, shared rejection limits and output fi...
QList< AverageCategory > categories
Set of FiffEvoked instances sharing one FiffInfo, plus channel-picking and compensation helpers.
static FiffEvokedSet computeAverages(const FiffRawData &raw, const AverageDescription &desc, const Eigen::MatrixXi &events, QString &log)
bool find_evoked(const FiffEvokedSet &p_FiffEvokedSet) const
static FiffEvokedSet computeGrandAverage(const QList< FiffEvokedSet > &evokedSets)
static bool checkArtifacts(const Eigen::MatrixXd &epoch, const FiffInfo &info, const QStringList &bads, const RejectionParams &rej, QString &reason)
static void subtractBaseline(Eigen::MatrixXd &epoch, int bminSamp, int bmaxSamp)
Subtract baseline from each channel of an epoch.
FiffEvokedSet pick_channels(const QStringList &include=defaultQStringList, const QStringList &exclude=defaultQStringList) const
static bool read(QIODevice &p_IODevice, FiffEvokedSet &p_FiffEvokedSet, QPair< float, float > baseline=defaultFloatPair, bool proj=true)
QList< FiffEvoked > evoked
bool compensate_to(fiff_int_t to)
bool save(const QString &fileName) const
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)
QList< FiffChInfo > chs
Continuous FIFF raw recording: FiffInfo plus a random-access directory of FIFF_DATA_BUFFER tags.
bool read_raw_segment(Eigen::MatrixXd &data, Eigen::MatrixXd &times, fiff_int_t from=-1, fiff_int_t to=-1, const Eigen::RowVectorXi &sel=defaultRowVectorXi, bool do_debug=false) const
FIFF tag-stream reader/writer: wraps a QIODevice and exposes typed read_* / write_* methods for every...
QSharedPointer< FiffStream > SPtr
static FiffStream::SPtr start_file(QIODevice &p_IODevice)