v2.0.0
Loading...
Searching...
No Matches
fiff_events.cpp
Go to the documentation of this file.
1//=============================================================================================================
16
17//=============================================================================================================
18// INCLUDES
19//=============================================================================================================
20
21#include "fiff_events.h"
22#include "fiff_evoked_set.h"
23#include "fiff_raw_data.h"
24#include "fiff_stream.h"
25#include "fiff_dir_node.h"
26#include "fiff_tag.h"
27#include "fiff_constants.h"
28#include "fiff_file.h"
29
30//=============================================================================================================
31// QT INCLUDES
32//=============================================================================================================
33
34#include <QFile>
35#include <QDebug>
36#include <QRegularExpression>
37#include <QTextStream>
38
39#include <algorithm>
40#include <cmath>
41#include <numeric>
42#include <vector>
43
44//=============================================================================================================
45// USED NAMESPACES
46//=============================================================================================================
47
48using namespace FIFFLIB;
49using namespace Eigen;
50
51//=============================================================================================================
52// DEFINE MEMBER METHODS
53//=============================================================================================================
54
56{
57}
58
59//=============================================================================================================
60
61FiffEvents::FiffEvents(QIODevice& p_IODevice)
62{
63 // Try FIFF first, then ASCII
64 if (!read_from_fif(p_IODevice, *this)) {
65 // The FIFF attempt leaves the device open in binary mode.
66 p_IODevice.close();
67 read_from_ascii(p_IODevice, *this);
68 }
69}
70
71//=============================================================================================================
72
73bool FiffEvents::read(const QString& t_sEventName,
74 const QString& t_fileRawName,
75 FiffEvents& p_Events)
76{
77 QString eventName = t_sEventName;
78 QFile t_EventFile;
79 qint32 p;
80
81 if (eventName.isEmpty()) {
82 eventName = t_fileRawName;
83 p = eventName.indexOf(".fif");
84 if (p > 0) {
85 eventName.replace(p, 4, "-eve.fif");
86 } else {
87 qWarning("Raw file name does not end properly\n");
88 return false;
89 }
90
91 t_EventFile.setFileName(eventName);
92 if (!read_from_fif(t_EventFile, p_Events)) {
93 qWarning("Error while read events.\n");
94 return false;
95 }
96 qInfo("Events read from %s\n", eventName.toUtf8().constData());
97 } else {
98 // Binary file
99 if (eventName.contains(".fif")) {
100 t_EventFile.setFileName(eventName);
101 if (!read_from_fif(t_EventFile, p_Events)) {
102 qWarning("Error while read events.\n");
103 return false;
104 }
105 qInfo("Binary event file %s read\n", eventName.toUtf8().constData());
106 } else if (eventName.contains(".eve")) {
107 } else {
108 // Text file
109 qWarning("Text file %s is not supported jet.\n", eventName.toUtf8().constData());
110 }
111 }
112
113 return true;
114}
115
116//=============================================================================================================
117
118bool FiffEvents::read_from_fif(QIODevice& p_IODevice,
119 FiffEvents& p_Events)
120{
121 //
122 // Open file
123 //
124 FiffStream::SPtr t_pStream(new FiffStream(&p_IODevice));
125
126 if (!t_pStream->open()) {
127 return false;
128 }
129
130 //
131 // Find the desired block
132 //
133 QList<FiffDirNode::SPtr> eventsBlocks = t_pStream->dirtree()->dir_tree_find(FIFFB_MNE_EVENTS);
134
135 if (eventsBlocks.size() == 0) {
136 qWarning("Could not find event data\n");
137 return false;
138 }
139
140 // nelem is only assigned when a matching tag is found; the guard below
141 // returns before it is read in that case, but it must not start out
142 // indeterminate.
143 qint32 k, nelem = 0;
144 fiff_int_t kind, pos;
145 FiffTag::UPtr t_pTag;
146 quint32* serial_eventlist_uint = nullptr;
147 qint32* serial_eventlist_int = nullptr;
148
149 for (k = 0; k < eventsBlocks[0]->nent(); ++k) {
150 kind = eventsBlocks[0]->dir[k]->kind;
151 pos = eventsBlocks[0]->dir[k]->pos;
152 if (kind == FIFF_MNE_EVENT_LIST) {
153 t_pStream->read_tag(t_pTag, pos);
154 if (t_pTag->type == FIFFT_UINT) {
155 serial_eventlist_uint = t_pTag->toUnsignedInt();
156 nelem = t_pTag->size() / 4;
157 }
158
159 if (t_pTag->type == FIFFT_INT) {
160 serial_eventlist_int = t_pTag->toInt();
161 nelem = t_pTag->size() / 4;
162 }
163
164 break;
165 }
166 }
167
168 if (serial_eventlist_uint == nullptr && serial_eventlist_int == nullptr) {
169 qWarning("Could not find any events\n");
170 return false;
171 }
172
173 p_Events.events.resize(nelem / 3, 3);
174 if (serial_eventlist_uint != nullptr) {
175 for (k = 0; k < nelem / 3; ++k) {
176 p_Events.events(k, 0) = serial_eventlist_uint[k * 3];
177 p_Events.events(k, 1) = serial_eventlist_uint[k * 3 + 1];
178 p_Events.events(k, 2) = serial_eventlist_uint[k * 3 + 2];
179 }
180 }
181
182 if (serial_eventlist_int != nullptr) {
183 for (k = 0; k < nelem / 3; ++k) {
184 p_Events.events(k, 0) = serial_eventlist_int[k * 3];
185 p_Events.events(k, 1) = serial_eventlist_int[k * 3 + 1];
186 p_Events.events(k, 2) = serial_eventlist_int[k * 3 + 2];
187 }
188 }
189
190 return true;
191}
192
193//=============================================================================================================
194
195bool FiffEvents::read_from_ascii(QIODevice& p_IODevice,
196 FiffEvents& p_Events)
197{
198 if (!p_IODevice.open(QIODevice::ReadOnly | QIODevice::Text)) {
199 return false;
200 }
201 QTextStream textStream(&p_IODevice);
202
203 QList<int> sampleList;
204 QList<int> beforeList;
205 QList<int> afterList;
206
207 while (!textStream.atEnd()) {
208 QString line = textStream.readLine().trimmed();
209 if (line.isEmpty())
210 continue;
211
212 // A standard .eve line has four fields: sample, onset in seconds,
213 // value before and value after the transition. The onset is redundant
214 // with the sample and is skipped, which is what write_to_ascii emits.
215 // Reading it as an integer used to shift every later field along by
216 // one, so the trigger code was lost and the event looked like it had
217 // a code of zero. Files carrying only the three integer fields are
218 // still accepted.
219 const QStringList fields = line.split(QRegularExpression("\\s+"),
220 Qt::SkipEmptyParts);
221 if (fields.isEmpty())
222 continue;
223
224 const int iOffset = (fields.size() >= 4) ? 1 : 0;
225
226 int iSample = fields.at(0).toInt();
227 int iBefore = (fields.size() > 1 + iOffset) ? fields.at(1 + iOffset).toInt() : 0;
228 int iAfter = (fields.size() > 2 + iOffset) ? fields.at(2 + iOffset).toInt() : 0;
229
230 sampleList.append(iSample);
231 beforeList.append(iBefore);
232 afterList.append(iAfter);
233 }
234
235 p_Events.events.resize(sampleList.size(), 3);
236
237 for (int i = 0; i < sampleList.size(); i++) {
238 p_Events.events(i, 0) = sampleList[i];
239 p_Events.events(i, 1) = beforeList[i];
240 p_Events.events(i, 2) = afterList[i];
241 }
242 return true;
243}
244
245//=============================================================================================================
246
247bool FiffEvents::write_to_fif(QIODevice& p_IODevice) const
248{
249 if (events.rows() == 0 || events.cols() < 3)
250 return false;
251
252 FiffStream::SPtr pStream = FiffStream::start_file(p_IODevice);
253 if (!pStream)
254 return false;
255
256 // FIFF stores the list row by row (sample, before, after).
257 const Eigen::Matrix<int, Eigen::Dynamic, 3, Eigen::RowMajor> rows = events.leftCols(3);
258 pStream->start_block(FIFFB_MNE_EVENTS);
259 pStream->write_int(FIFF_MNE_EVENT_LIST, rows.data(), rows.rows() * 3);
260 pStream->end_block(FIFFB_MNE_EVENTS);
261 pStream->end_file();
262
263 return true;
264}
265
266//=============================================================================================================
267
268bool FiffEvents::write_to_ascii(QIODevice& p_IODevice,
269 float sfreq) const
270{
271 if (!p_IODevice.open(QIODevice::WriteOnly | QIODevice::Text))
272 return false;
273
274 for (int k = 0; k < events.rows(); ++k) {
275 int sample = events(k, 0);
276 int before = (events.cols() > 1) ? events(k, 1) : 0;
277 int after = (events.cols() > 2) ? events(k, 2) : 0;
278 float time = (sfreq > 0.0f) ? static_cast<float>(sample) / sfreq : 0.0f;
279 QTextStream out(&p_IODevice);
280 out << QString("%1 %2 %3 %4\n")
281 .arg(sample, 6)
282 .arg(time, -10, 'f', 3)
283 .arg(before, 3)
284 .arg(after, 3);
285 }
286
287 p_IODevice.close();
288 return true;
289}
290
291//=============================================================================================================
292
294 FiffEvents& p_Events,
295 const QString& triggerCh,
296 unsigned int triggerMask,
297 bool leadingEdge)
298{
299 QString stimCh = triggerCh.isEmpty() ? QString("STI 014") : triggerCh;
300
301 // Channel names are read without spaces ("STI014"), so compare without them.
302 const QString wanted = QString(stimCh).remove(' ');
303 int triggerChIdx = -1;
304 for (int k = 0; k < raw.info.ch_names.size(); ++k) {
305 if (QString(raw.info.ch_names[k]).remove(' ') == wanted) {
306 triggerChIdx = k;
307 break;
308 }
309 }
310 if (triggerChIdx < 0) {
311 qWarning() << "[FiffEvents::detect_from_raw] Trigger channel" << stimCh << "not found.";
312 return false;
313 }
314
315 // Read trigger channel data
316 MatrixXd data;
317 MatrixXd times;
318 if (!raw.read_raw_segment(data, times, raw.first_samp, raw.last_samp,
319 RowVectorXi::LinSpaced(1, triggerChIdx, triggerChIdx))) {
320 qWarning() << "[FiffEvents::detect_from_raw] Could not read trigger channel data.";
321 return false;
322 }
323
324 RowVectorXd trigData = data.row(0);
325 int nSamples = static_cast<int>(trigData.cols());
326
327 // Detect flanks
328 QList<int> eventSamples;
329 QList<int> eventBefore;
330 QList<int> eventAfter;
331
332 int prevVal = static_cast<int>(trigData(0)) & triggerMask;
333 for (int s = 1; s < nSamples; ++s) {
334 int curVal = static_cast<int>(trigData(s)) & triggerMask;
335 if (curVal != prevVal) {
336 if (!leadingEdge || (leadingEdge && prevVal == 0 && curVal != 0)) {
337 eventSamples.append(static_cast<int>(raw.first_samp) + s);
338 eventBefore.append(prevVal);
339 eventAfter.append(curVal);
340 }
341 }
342 prevVal = curVal;
343 }
344
345 int nEvents = eventSamples.size();
346 p_Events.events.resize(nEvents, 3);
347 for (int k = 0; k < nEvents; ++k) {
348 p_Events.events(k, 0) = eventSamples[k];
349 p_Events.events(k, 1) = eventBefore[k];
350 p_Events.events(k, 2) = eventAfter[k];
351 }
352
353 return nEvents > 0;
354}
355
356//=============================================================================================================
357
359 const MatrixXi& events,
360 int eventIdx)
361{
362 if (eventIdx < 0 || eventIdx >= events.rows())
363 return false;
364
365 int evFrom = events(eventIdx, 1);
366 int evTo = events(eventIdx, 2);
367
368 // Check if any of the category's event codes match
369 bool match = false;
370 for (int k = 0; k < cat.events.size(); ++k) {
371 if ((evFrom & ~cat.ignore) == 0 &&
372 (evTo & ~cat.ignore) == cat.events[k]) {
373 match = true;
374 break;
375 }
376 }
377 if (!match)
378 return false;
379
380 // Check previous event constraint
381 if (cat.prevEvent != 0) {
382 bool found = false;
383 for (int j = eventIdx - 1; j >= 0; --j) {
384 if ((events(j, 1) & ~cat.prevIgnore) == 0) {
385 found = true;
386 match = match && ((events(j, 2) & ~cat.prevIgnore) == cat.prevEvent);
387 break;
388 }
389 }
390 if (!found)
391 match = false;
392 }
393
394 // Check next event constraint
395 if (cat.nextEvent != 0) {
396 bool found = false;
397 for (int j = eventIdx + 1; j < events.rows(); ++j) {
398 if ((events(j, 1) & ~cat.nextIgnore) == 0) {
399 found = true;
400 match = match && ((events(j, 2) & ~cat.nextIgnore) == cat.nextEvent);
401 break;
402 }
403 }
404 if (!found)
405 match = false;
406 }
407
408 return match;
409}
410
411//=============================================================================================================
412
413namespace
414{
415
417MatrixXi selectRows(const MatrixXi& events, const std::vector<bool>& keep)
418{
419 MatrixXi out(std::count(keep.cbegin(), keep.cend(), true), events.cols());
420 Index r = 0;
421 for (Index i = 0; i < events.rows(); ++i) {
422 if (keep[i])
423 out.row(r++) = events.row(i);
424 }
425 return out;
426}
427
428} // namespace
429
430//=============================================================================================================
431
432FiffEvents FiffEvents::pick(const QList<int>& include, const QList<int>& exclude, bool step) const
433{
434 std::vector<bool> keep(events.rows());
435 for (Index i = 0; i < events.rows(); ++i) {
436 const bool matches = !include.isEmpty()
437 ? include.contains(events(i, 2)) || (step && include.contains(events(i, 1)))
438 : !(exclude.contains(events(i, 2)) || (step && exclude.contains(events(i, 1))));
439 keep[i] = matches;
440 }
441 FiffEvents out;
442 out.events = selectRows(events, keep);
443 return out;
444}
445
446//=============================================================================================================
447
448FiffEvents FiffEvents::merge(const QList<int>& ids, int newId, bool replaceEvents) const
449{
450 FiffEvents out;
451 out.events = events;
452 std::vector<bool> touched(events.rows(), false);
453 for (Index i = 0; i < events.rows(); ++i) {
454 for (int col = 1; col <= 2; ++col) {
455 if (ids.contains(events(i, col))) {
456 out.events(i, col) = newId;
457 touched[i] = true;
458 }
459 }
460 }
461 if (!replaceEvents) {
462 const MatrixXi originals = selectRows(events, touched);
463 MatrixXi all(out.events.rows() + originals.rows(), 3);
464 all << out.events, originals;
465 std::vector<Index> order(all.rows());
466 std::iota(order.begin(), order.end(), 0);
467 std::stable_sort(order.begin(), order.end(), [&all](Index a, Index b) {
468 return std::lexicographical_compare(all.row(a).begin(), all.row(a).end(), all.row(b).begin(), all.row(b).end());
469 });
470 out.events.resize(all.rows(), 3);
471 for (Index i = 0; i < all.rows(); ++i)
472 out.events.row(i) = all.row(order[i]);
473 }
474 return out;
475}
476
477//=============================================================================================================
478
479QMap<int, int> FiffEvents::count(const QList<int>& ids) const
480{
481 QMap<int, int> counts;
482 for (const int id : ids)
483 counts.insert(id, 0);
484 for (Index i = 0; i < events.rows(); ++i) {
485 if (ids.isEmpty() || ids.contains(events(i, 2)))
486 ++counts[events(i, 2)];
487 }
488 return counts;
489}
490
491//=============================================================================================================
492
493FiffEvents FiffEvents::concatenate(const QList<FiffEvents>& events, const QList<int>& firstSamps, const QList<int>& lastSamps)
494{
495 FiffEvents out;
496 if (events.isEmpty() || events.size() != firstSamps.size() || events.size() != lastSamps.size())
497 return out;
498 Index rows = 0;
499 for (const FiffEvents& e : events)
500 rows += e.events.rows();
501 out.events.resize(rows, 3);
502 Index r = 0;
503 int offset = 0;
504 for (qsizetype k = 0; k < events.size(); ++k) {
505 MatrixXi shifted = events[k].events;
506 if (k > 0)
507 shifted.col(0).array() += offset + firstSamps[0] - firstSamps[k];
508 out.events.middleRows(r, shifted.rows()) = shifted;
509 r += shifted.rows();
510 offset += lastSamps[k] - firstSamps[k] + 1;
511 }
512 return out;
513}
514
515//=============================================================================================================
516
517FiffEvents FiffEvents::find_stim_steps(const MatrixXi& stimData, int firstSamp, std::optional<int> padStart, std::optional<int> padStop, int merge)
518{
519 std::vector<RowVector3i> steps;
520 for (Index t = 0; t + 1 < stimData.cols(); ++t) {
521 if ((stimData.col(t + 1).array() != stimData.col(t).array()).all())
522 steps.emplace_back(static_cast<int>(t + 1) + firstSamp, stimData(0, t), stimData(0, t + 1));
523 }
524 FiffEvents out;
525 if (steps.empty())
526 return out;
527 if (padStart && steps.front()(1) != *padStart)
528 steps.insert(steps.begin(), RowVector3i(0, *padStart, steps.front()(1)));
529 if (padStop && steps.back()(2) != *padStop)
530 steps.emplace_back(static_cast<int>(stimData.cols()) + firstSamp, steps.back()(2), *padStop);
531 if (merge != 0) {
532 const size_t n = steps.size();
533 std::vector<bool> close(n > 0 ? n - 1 : 0);
534 for (size_t i = 0; i + 1 < n; ++i)
535 close[i] = steps[i + 1](0) - steps[i](0) <= std::abs(merge);
536 if (std::find(close.cbegin(), close.cend(), true) != close.cend()) {
537 // Merged steps take the outer values; the step that is merged away is dropped
538 std::vector<bool> keep(n, true);
539 const std::vector<RowVector3i> original = steps;
540 for (size_t i = 0; i + 1 < n; ++i) {
541 if (!close[i])
542 continue;
543 if (merge > 0) {
544 steps[i + 1](1) = original[i](1);
545 keep[i] = false;
546 } else {
547 steps[i](2) = original[i + 1](2);
548 keep[i + 1] = false;
549 }
550 }
551 std::vector<RowVector3i> kept;
552 for (size_t i = 0; i < n; ++i) {
553 if (keep[i] && steps[i](1) != steps[i](2))
554 kept.push_back(steps[i]);
555 }
556 steps.swap(kept);
557 }
558 }
559 out.events.resize(static_cast<Index>(steps.size()), 3);
560 for (size_t i = 0; i < steps.size(); ++i)
561 out.events.row(static_cast<Index>(i)) = steps[i];
562 return out;
563}
564
565//=============================================================================================================
566
567FiffEvents FiffEvents::make_fixed_length(const FiffRawData& raw, int id, double start, double stop, double duration, bool firstSamp, double overlap)
568{
569 FiffEvents out;
570 const double sfreq = raw.info.sfreq;
571 if (overlap < 0.0 || overlap >= duration || sfreq <= 0.0)
572 return out;
573 double first = std::nearbyint(start * sfreq);
574 double last = stop >= 0.0 ? std::nearbyint(stop * sfreq) : raw.last_samp + 1.0;
575 if (firstSamp) {
576 first += raw.first_samp;
577 last = std::min(last + (stop >= 0.0 ? raw.first_samp : 0), raw.last_samp + 1.0);
578 } else {
579 last = std::min(last - (stop >= 0.0 ? 0 : raw.first_samp), static_cast<double>(raw.last_samp - raw.first_samp + 1));
580 }
581 last -= std::nearbyint(sfreq * duration);
582 const double stepSamples = sfreq * (duration - overlap);
583 const auto count = static_cast<Index>(std::max(0.0, std::ceil((last + 1.0 - first) / stepSamples)));
584 out.events = MatrixXi::Zero(count, 3);
585 for (Index i = 0; i < count; ++i) {
586 out.events(i, 0) = static_cast<int>(first + static_cast<double>(i) * stepSamples);
587 out.events(i, 2) = id;
588 }
589 return out;
590}
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
#define FIFF_MNE_EVENT_LIST
#define FIFFB_MNE_EVENTS
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,...
MatrixXi selectRows(const MatrixXi &events, const std::vector< bool > &keep)
FIFF tag-kind, block-kind and type-code numerical definitions, authoritative for FIFFLIB.
#define FIFFT_UINT
Definition fiff_file.h:229
#define FIFFT_INT
Definition fiff_file.h:224
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
FIFF event list: (sample, prev, new) integer triples with read / write / detect / filter helpers.
Definition fiff_events.h:76
static FiffEvents concatenate(const QList< FiffEvents > &events, const QList< int > &firstSamps, const QList< int > &lastSamps)
Eigen::MatrixXi events
QMap< int, int > count(const QList< int > &ids={}) const
bool write_to_ascii(QIODevice &p_IODevice, float sfreq=0.0f) const
static bool matchEvent(const AverageCategory &cat, const Eigen::MatrixXi &events, int eventIdx)
static bool read(const QString &t_sEventName, const QString &t_fileRawName, FiffEvents &p_Events)
FiffEvents merge(const QList< int > &ids, int newId, bool replaceEvents=true) const
static bool read_from_fif(QIODevice &p_IODevice, FiffEvents &p_Events)
static FiffEvents make_fixed_length(const FiffRawData &raw, int id=1, double start=0.0, double stop=-1.0, double duration=1.0, bool firstSamp=true, double overlap=0.0)
bool write_to_fif(QIODevice &p_IODevice) const
FiffEvents pick(const QList< int > &include, const QList< int > &exclude={}, bool step=false) const
static bool detect_from_raw(const FiffRawData &raw, FiffEvents &p_Events, const QString &triggerCh=QString("STI 014"), unsigned int triggerMask=0xFFFFFFFF, bool leadingEdge=true)
static FiffEvents find_stim_steps(const Eigen::MatrixXi &stimData, int firstSamp, std::optional< int > padStart=std::nullopt, std::optional< int > padStop=std::nullopt, int merge=0)
static bool read_from_ascii(QIODevice &p_IODevice, FiffEvents &p_Events)
One averaging category in an MNE-C ave-description file: trigger logic, timing window,...
QVector< unsigned int > events
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)
std::unique_ptr< FiffTag > UPtr
Definition fiff_tag.h:165