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
128bool FiffEvokedSet::compensate_to(FiffEvokedSet& p_FiffEvokedSet,
129 fiff_int_t to) const
130{
131 qint32 now = p_FiffEvokedSet.info.get_current_comp();
133
134 if (now == to) {
135 qInfo("Data is already compensated as desired.\n");
136 return false;
137 }
138
139 //Make the compensator and apply it to all data sets
140 p_FiffEvokedSet.info.make_compensator(now, to, ctf_comp);
141
142 for (qsizetype i = 0; i < p_FiffEvokedSet.evoked.size(); ++i) {
143 p_FiffEvokedSet.evoked[i].data = ctf_comp.data->data * p_FiffEvokedSet.evoked[i].data;
144 }
145
146 //Update the compensation info in the channel descriptors
147 p_FiffEvokedSet.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);
238 if (!pStream) {
239 qWarning() << "[FiffEvokedSet::save] Cannot open" << fileName;
240 return false;
241 }
242
243 pStream->write_evoked_set(*this);
244 pStream->end_file();
245
246 qInfo() << "[FiffEvokedSet::save] Saved" << evoked.size()
247 << "average(s) to" << fileName;
248 return true;
249}
250
251//=============================================================================================================
252
253FiffEvokedSet FiffEvokedSet::computeGrandAverage(const QList<FiffEvokedSet>& evokedSets)
254{
255 FiffEvokedSet grandAvg;
256
257 if (evokedSets.isEmpty()) {
258 qWarning() << "[FiffEvokedSet::computeGrandAverage] No evoked sets provided.";
259 return grandAvg;
260 }
261
262 grandAvg = evokedSets[0];
263
264 for (int f = 1; f < evokedSets.size(); ++f) {
265 const FiffEvokedSet& eset = evokedSets[f];
266 int nCat = qMin(grandAvg.evoked.size(), eset.evoked.size());
267 for (int j = 0; j < nCat; ++j) {
268 if (grandAvg.evoked[j].data.cols() == eset.evoked[j].data.cols() &&
269 grandAvg.evoked[j].data.rows() == eset.evoked[j].data.rows()) {
270 grandAvg.evoked[j].data += eset.evoked[j].data;
271 grandAvg.evoked[j].nave += eset.evoked[j].nave;
272 }
273 }
274 }
275
276 for (int j = 0; j < grandAvg.evoked.size(); ++j) {
277 grandAvg.evoked[j].data /= static_cast<double>(evokedSets.size());
278 }
279
280 return grandAvg;
281}
282
283//=============================================================================================================
284
285void FiffEvokedSet::subtractBaseline(Eigen::MatrixXd& epoch, int bminSamp, int bmaxSamp)
286{
287 if (bminSamp < 0)
288 bminSamp = 0;
289 if (bmaxSamp >= epoch.cols())
290 bmaxSamp = static_cast<int>(epoch.cols()) - 1;
291 if (bminSamp >= bmaxSamp)
292 return;
293
294 int nBase = bmaxSamp - bminSamp + 1;
295 for (int c = 0; c < epoch.rows(); ++c) {
296 double baseVal = epoch.row(c).segment(bminSamp, nBase).mean();
297 epoch.row(c).array() -= baseVal;
298 }
299}
300
301//=============================================================================================================
302
304 const AverageDescription& desc,
305 const MatrixXi& events,
306 QString& log)
307{
308 FiffEvokedSet evokedSet;
309 evokedSet.info = raw.info;
310
311 float sfreq = raw.info.sfreq;
312 int nchan = raw.info.nchan;
313
314 log.clear();
315 log += QString("Averaging: %1\n").arg(desc.comment);
316
317 // Process each category
318 for (int j = 0; j < desc.categories.size(); ++j) {
319 const AverageCategory& cat = desc.categories[j];
320
321 // Compute sample indices
322 int minSamp = static_cast<int>(std::round(cat.tmin * sfreq));
323 int maxSamp = static_cast<int>(std::round(cat.tmax * sfreq));
324 int ns = maxSamp - minSamp + 1;
325 int delaySamp = static_cast<int>(std::round(cat.delay * sfreq));
326
327 // Baseline sample range (relative to epoch start)
328 int bminSamp = 0, bmaxSamp = 0;
329 if (cat.doBaseline) {
330 bminSamp = static_cast<int>(std::round(cat.bmin * sfreq)) - minSamp;
331 bmaxSamp = static_cast<int>(std::round(cat.bmax * sfreq)) - minSamp;
332 }
333
334 // Accumulator
335 MatrixXd sumData = MatrixXd::Zero(nchan, ns);
336 MatrixXd sumSqData = MatrixXd::Zero(nchan, ns);
337 int nave = 0;
338
339 log += QString("\n Category: %1\n").arg(cat.comment);
340 log += QString(" t = %1 ... %2 ms\n").arg(1000.0 * cat.tmin, 0, 'f', 1).arg(1000.0 * cat.tmax, 0, 'f', 1);
341
342 // Iterate over events
343 for (int k = 0; k < events.rows(); ++k) {
344 if (!FiffEvents::matchEvent(cat, events, k))
345 continue;
346
347 int evSample = events(k, 0);
348 int epochStart = evSample + delaySamp + minSamp;
349 int epochEnd = evSample + delaySamp + maxSamp;
350
351 // Check bounds
352 if (epochStart < raw.first_samp || epochEnd > raw.last_samp)
353 continue;
354
355 // Read epoch
356 MatrixXd epochData;
357 MatrixXd epochTimes;
358 if (!raw.read_raw_segment(epochData, epochTimes, epochStart, epochEnd)) {
359 log += QString(" Error reading epoch at sample %1\n").arg(evSample);
360 continue;
361 }
362
363 // Artifact rejection
364 QString rejReason;
365 if (!checkArtifacts(epochData, raw.info, raw.info.bads, desc.rej, rejReason)) {
366 log += QString(" %1 %2 %3 %4 [%5] %6 [omit]\n")
367 .arg(evSample, 7)
368 .arg(static_cast<float>(evSample) / sfreq, -10, 'f', 3)
369 .arg(events(k, 1), 3)
370 .arg(events(k, 2), 3)
371 .arg(cat.comment)
372 .arg(rejReason);
373 continue;
374 }
375
376 // Baseline correction
377 if (cat.doBaseline) {
378 subtractBaseline(epochData, bminSamp, bmaxSamp);
379 }
380
381 // Absolute value
382 if (cat.doAbs) {
383 epochData = epochData.cwiseAbs();
384 }
385
386 // Accumulate
387 sumData += epochData;
388 if (cat.doStdErr) {
389 sumSqData += epochData.cwiseProduct(epochData);
390 }
391 nave++;
392
393 log += QString(" %1 %2 %3 %4 [%5]\n")
394 .arg(evSample, 7)
395 .arg(static_cast<float>(evSample) / sfreq, -10, 'f', 3)
396 .arg(events(k, 1), 3)
397 .arg(events(k, 2), 3)
398 .arg(cat.comment);
399 }
400
401 // Compute average
402 FiffEvoked evoked;
403 evoked.comment = cat.comment;
404 evoked.first = minSamp;
405 evoked.last = maxSamp;
406 evoked.nave = nave;
407
408 // Build times vector
409 RowVectorXf times(ns);
410 for (int s = 0; s < ns; ++s)
411 times(s) = static_cast<float>(minSamp + s) / sfreq;
412 evoked.times = times;
413
414 if (nave > 0) {
415 evoked.data = sumData / static_cast<double>(nave);
416 } else {
417 evoked.data = MatrixXd::Zero(nchan, ns);
418 }
419
420 evoked.info = raw.info;
421
422 evokedSet.evoked.append(evoked);
423 log += QString(" nave = %1\n").arg(nave);
424 }
425
426 return evokedSet;
427}
428
429//=============================================================================================================
430
431bool FiffEvokedSet::checkArtifacts(const MatrixXd& epoch,
432 const FiffInfo& info,
433 const QStringList& bads,
434 const RejectionParams& rej,
435 QString& reason)
436{
437 for (int c = 0; c < epoch.rows(); ++c) {
438 // Skip bad channels
439 if (bads.contains(info.ch_names[c]))
440 continue;
441
442 double minVal = epoch.row(c).minCoeff();
443 double maxVal = epoch.row(c).maxCoeff();
444 double pp = maxVal - minVal;
445
446 int chKind = info.chs[c].kind;
447 int chUnit = info.chs[c].unit;
448
449 if (chKind == FIFFV_MEG_CH) {
450 if (chUnit == FIFF_UNIT_T) {
451 // Magnetometer
452 if (rej.megMagReject > 0 && pp > rej.megMagReject) {
453 reason = QString("%1 : %2 fT > %3 fT")
454 .arg(info.ch_names[c])
455 .arg(pp * 1e15, 0, 'f', 1)
456 .arg(rej.megMagReject * 1e15, 0, 'f', 1);
457 return false;
458 }
459 if (rej.megMagFlat > 0 && pp < rej.megMagFlat) {
460 reason = QString("%1 : %2 fT < %3 fT (flat)")
461 .arg(info.ch_names[c])
462 .arg(pp * 1e15, 0, 'f', 1)
463 .arg(rej.megMagFlat * 1e15, 0, 'f', 1);
464 return false;
465 }
466 } else {
467 // Gradiometer
468 if (rej.megGradReject > 0 && pp > rej.megGradReject) {
469 reason = QString("%1 : %2 fT/cm > %3 fT/cm")
470 .arg(info.ch_names[c])
471 .arg(pp * 1e13, 0, 'f', 1)
472 .arg(rej.megGradReject * 1e13, 0, 'f', 1);
473 return false;
474 }
475 if (rej.megGradFlat > 0 && pp < rej.megGradFlat) {
476 reason = QString("%1 : %2 fT/cm < %3 fT/cm (flat)")
477 .arg(info.ch_names[c])
478 .arg(pp * 1e13, 0, 'f', 1)
479 .arg(rej.megGradFlat * 1e13, 0, 'f', 1);
480 return false;
481 }
482 }
483 } else if (chKind == FIFFV_EEG_CH) {
484 if (rej.eegReject > 0 && pp > rej.eegReject) {
485 reason = QString("%1 : %2 uV > %3 uV")
486 .arg(info.ch_names[c])
487 .arg(pp * 1e6, 0, 'f', 1)
488 .arg(rej.eegReject * 1e6, 0, 'f', 1);
489 return false;
490 }
491 if (rej.eegFlat > 0 && pp < rej.eegFlat) {
492 reason = QString("%1 : %2 uV < %3 uV (flat)")
493 .arg(info.ch_names[c])
494 .arg(pp * 1e6, 0, 'f', 1)
495 .arg(rej.eegFlat * 1e6, 0, 'f', 1);
496 return false;
497 }
498 } else if (chKind == FIFFV_EOG_CH) {
499 if (rej.eogReject > 0 && pp > rej.eogReject) {
500 reason = QString("%1 : %2 uV > %3 uV (EOG)")
501 .arg(info.ch_names[c])
502 .arg(pp * 1e6, 0, 'f', 1)
503 .arg(rej.eogReject * 1e6, 0, 'f', 1);
504 return false;
505 }
506 if (rej.eogFlat > 0 && pp < rej.eogFlat) {
507 reason = QString("%1 : EOG flat").arg(info.ch_names[c]);
508 return false;
509 }
510 } else if (chKind == FIFFV_ECG_CH) {
511 if (rej.ecgReject > 0 && pp > rej.ecgReject) {
512 reason = QString("%1 : %2 mV > %3 mV (ECG)")
513 .arg(info.ch_names[c])
514 .arg(pp * 1e3, 0, 'f', 2)
515 .arg(rej.ecgReject * 1e3, 0, 'f', 2);
516 return false;
517 }
518 if (rej.ecgFlat > 0 && pp < rej.ecgFlat) {
519 reason = QString("%1 : ECG flat").arg(info.ch_names[c]);
520 return false;
521 }
522 }
523 }
524 return true;
525}
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
#define FIFFV_EOG_CH
#define FIFFV_EEG_CH
#define FIFFV_MEG_CH
#define FIFF_UNIT_T
#define FIFFV_ECG_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.
#define FIFFB_PROCESSED_DATA
Definition fiff_file.h:358
#define FIFFB_EVOKED
Definition fiff_file.h:359
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
Eigen::MatrixXd data
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.
bool compensate_to(FiffEvokedSet &p_FiffEvokedSet, fiff_int_t to) const
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 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
void set_current_comp(fiff_int_t value)
Definition fiff_info.h:317
qint32 get_current_comp()
bool make_compensator(fiff_int_t from, fiff_int_t to, FiffCtfComp &ctf_comp, bool exclude_comp_chs=false) 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)