v2.0.0
Loading...
Searching...
No Matches
mne_description_parser.cpp
Go to the documentation of this file.
1//=============================================================================================================
16
17//=============================================================================================================
18// INCLUDES
19//=============================================================================================================
20
22
23#include <QFile>
24#include <QDebug>
25#include <cmath>
26
27//=============================================================================================================
28// USED NAMESPACES
29//=============================================================================================================
30
31using namespace MNELIB;
32
33//=============================================================================================================
34// STATIC HELPERS
35//=============================================================================================================
36
37void MNEDescriptionParser::skipComments(QTextStream &in)
38{
39 while (!in.atEnd()) {
40 QChar c;
41 in >> c;
42 if (c == '#') {
43 in.readLine(); // consume rest of comment line
44 } else {
45 // push back by seeking
46 in.seek(in.pos() - 1);
47 return;
48 }
49 }
50}
51
52//=============================================================================================================
53
54QString MNEDescriptionParser::nextWord(QTextStream &in)
55{
56 skipComments(in);
57
58 // Skip whitespace
59 while (!in.atEnd()) {
60 QChar c;
61 in >> c;
62 if (!c.isSpace()) {
63 // Check if quoted string
64 if (c == '"') {
65 QString word;
66 while (!in.atEnd()) {
67 in >> c;
68 if (c == '"') break;
69 word.append(c);
70 }
71 return word;
72 }
73 // Regular word
74 QString word;
75 word.append(c);
76 while (!in.atEnd()) {
77 in >> c;
78 if (c.isSpace()) break;
79 if (c == '#') {
80 in.readLine();
81 break;
82 }
83 word.append(c);
84 }
85 return word;
86 }
87 }
88 return QString(); // EOF
89}
90
91//=============================================================================================================
92
93bool MNEDescriptionParser::getInt(QTextStream &in, int &val)
94{
95 QString word = nextWord(in);
96 if (word.isEmpty()) {
97 qWarning() << "MNEDescriptionParser: expected integer, got EOF";
98 return false;
99 }
100 bool ok;
101 val = word.toInt(&ok);
102 if (!ok) {
103 qWarning() << "MNEDescriptionParser: bad integer:" << word;
104 return false;
105 }
106 return true;
107}
108
109//=============================================================================================================
110
111bool MNEDescriptionParser::getFloat(QTextStream &in, float &val)
112{
113 QString word = nextWord(in);
114 if (word.isEmpty()) {
115 qWarning() << "MNEDescriptionParser: expected float, got EOF";
116 return false;
117 }
118 bool ok;
119 val = word.toFloat(&ok);
120 if (!ok) {
121 qWarning() << "MNEDescriptionParser: bad float:" << word;
122 return false;
123 }
124 return true;
125}
126
127//=============================================================================================================
128
129bool MNEDescriptionParser::parseRejectionParam(const QString &keyword, QTextStream &in,
130 RejectionParams &rej, bool &ok)
131{
132 ok = true;
133 float fval;
134
135 if (keyword.compare("gradReject", Qt::CaseInsensitive) == 0) {
136 ok = getFloat(in, fval); if (ok) rej.megGradReject = fval;
137 } else if (keyword.compare("magReject", Qt::CaseInsensitive) == 0) {
138 ok = getFloat(in, fval); if (ok) rej.megMagReject = fval;
139 } else if (keyword.compare("eegReject", Qt::CaseInsensitive) == 0) {
140 ok = getFloat(in, fval); if (ok) rej.eegReject = fval;
141 } else if (keyword.compare("eogReject", Qt::CaseInsensitive) == 0) {
142 ok = getFloat(in, fval); if (ok) rej.eogReject = fval;
143 } else if (keyword.compare("ecgReject", Qt::CaseInsensitive) == 0) {
144 ok = getFloat(in, fval); if (ok) rej.ecgReject = fval;
145 } else if (keyword.compare("gradFlat", Qt::CaseInsensitive) == 0) {
146 ok = getFloat(in, fval); if (ok) rej.megGradFlat = fval;
147 } else if (keyword.compare("magFlat", Qt::CaseInsensitive) == 0) {
148 ok = getFloat(in, fval); if (ok) rej.megMagFlat = fval;
149 } else if (keyword.compare("eegFlat", Qt::CaseInsensitive) == 0) {
150 ok = getFloat(in, fval); if (ok) rej.eegFlat = fval;
151 } else if (keyword.compare("eogFlat", Qt::CaseInsensitive) == 0) {
152 ok = getFloat(in, fval); if (ok) rej.eogFlat = fval;
153 } else if (keyword.compare("ecgFlat", Qt::CaseInsensitive) == 0) {
154 ok = getFloat(in, fval); if (ok) rej.ecgFlat = fval;
155 } else if (keyword.compare("stimIgnore", Qt::CaseInsensitive) == 0) {
156 ok = getFloat(in, fval); if (ok) rej.stimIgnore = std::fabs(fval);
157 } else {
158 return false; // not a rejection keyword
159 }
160 return true; // was a rejection keyword (ok indicates parse success)
161}
162
163//=============================================================================================================
164// AVERAGE DESCRIPTION PARSER
165//=============================================================================================================
166
168{
169 QFile file(fileName);
170 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
171 qWarning() << "MNEDescriptionParser: cannot open" << fileName;
172 return false;
173 }
174
175 QTextStream in(&file);
176 desc = AverageDescription();
177
178 bool expectBrace = false;
179 bool bminSet = false, bmaxSet = false;
180 bool inAverage = false;
181 bool inCategory = false;
182 AverageCategory currentCat;
183
184 QString word;
185 while (!(word = nextWord(in)).isEmpty()) {
186 if (expectBrace) {
187 if (word == "{") {
188 expectBrace = false;
189 } else {
190 qWarning() << "MNEDescriptionParser: expected '{', got" << word;
191 return false;
192 }
193 continue;
194 }
195
196 if (word == "{") {
197 qWarning() << "MNEDescriptionParser: unexpected '{'";
198 return false;
199 }
200
201 if (word == "}") {
202 if (inCategory) {
203 currentCat.doBaseline = bminSet && bmaxSet;
204 // Validate category
205 if (currentCat.comment.isEmpty()) {
206 qWarning() << "MNEDescriptionParser: category name missing";
207 return false;
208 }
209 if (currentCat.tmin >= currentCat.tmax) {
210 qWarning() << "MNEDescriptionParser: illegal time range for" << currentCat.comment;
211 return false;
212 }
213 if (currentCat.events.isEmpty()) {
214 qWarning() << "MNEDescriptionParser: no events for" << currentCat.comment;
215 return false;
216 }
217 if (!currentCat.prevIgnore) currentCat.prevIgnore = currentCat.ignore;
218 if (!currentCat.nextIgnore) currentCat.nextIgnore = currentCat.ignore;
219 desc.categories.append(currentCat);
220 currentCat = AverageCategory();
221 inCategory = false;
222 bminSet = bmaxSet = false;
223 } else if (inAverage) {
224 if (desc.comment.isEmpty()) desc.comment = "Average";
225 inAverage = false;
226 break; // done
227 }
228 continue;
229 }
230
231 // Top-level: "average" keyword
232 if (word.compare("average", Qt::CaseInsensitive) == 0) {
233 if (inAverage || inCategory) {
234 qWarning() << "MNEDescriptionParser: nested average";
235 return false;
236 }
237 inAverage = true;
238 expectBrace = true;
239 continue;
240 }
241
242 // Category start
243 if (word.compare("category", Qt::CaseInsensitive) == 0 ||
244 word.compare("condition", Qt::CaseInsensitive) == 0) {
245 if (!inAverage || inCategory) {
246 qWarning() << "MNEDescriptionParser: misplaced category";
247 return false;
248 }
249 inCategory = true;
250 expectBrace = true;
251 bminSet = bmaxSet = false;
252 continue;
253 }
254
255 // "name" keyword
256 if (word.compare("name", Qt::CaseInsensitive) == 0) {
257 QString val = nextWord(in);
258 if (val.isEmpty()) { qWarning() << "MNEDescriptionParser: name requires a value"; return false; }
259 if (inCategory) currentCat.comment = val;
260 else if (inAverage) desc.comment = val;
261 else { qWarning() << "MNEDescriptionParser: misplaced name"; return false; }
262 continue;
263 }
264
265 // "outfile" keyword
266 if (word.compare("outfile", Qt::CaseInsensitive) == 0) {
267 QString val = nextWord(in);
268 if (val.isEmpty()) { qWarning() << "MNEDescriptionParser: outfile requires a value"; return false; }
269 if (inAverage && !inCategory) desc.filename = val;
270 else { qWarning() << "MNEDescriptionParser: misplaced outfile"; return false; }
271 continue;
272 }
273
274 // "eventfile" keyword
275 if (word.compare("eventfile", Qt::CaseInsensitive) == 0) {
276 QString val = nextWord(in);
277 if (val.isEmpty()) { qWarning() << "MNEDescriptionParser: eventfile requires a value"; return false; }
278 if (inAverage && !inCategory) desc.eventFile = val;
279 else { qWarning() << "MNEDescriptionParser: misplaced eventfile"; return false; }
280 continue;
281 }
282
283 // "logfile" keyword
284 if (word.compare("logfile", Qt::CaseInsensitive) == 0) {
285 QString val = nextWord(in);
286 if (val.isEmpty()) { qWarning() << "MNEDescriptionParser: logfile requires a value"; return false; }
287 if (inAverage && !inCategory) desc.logFile = val;
288 else { qWarning() << "MNEDescriptionParser: misplaced logfile"; return false; }
289 continue;
290 }
291
292 // "fixskew" keyword
293 if (word.compare("fixskew", Qt::CaseInsensitive) == 0) {
294 if (inAverage && !inCategory) desc.fixSkew = true;
295 else { qWarning() << "MNEDescriptionParser: misplaced fixskew"; return false; }
296 continue;
297 }
298
299 // --- Category-level keywords ---
300 if (word.compare("tmin", Qt::CaseInsensitive) == 0) {
301 float fval; if (!getFloat(in, fval)) return false;
302 if (inCategory) currentCat.tmin = fval;
303 else { qWarning() << "MNEDescriptionParser: misplaced tmin"; return false; }
304 continue;
305 }
306 if (word.compare("tmax", Qt::CaseInsensitive) == 0) {
307 float fval; if (!getFloat(in, fval)) return false;
308 if (inCategory) currentCat.tmax = fval;
309 else { qWarning() << "MNEDescriptionParser: misplaced tmax"; return false; }
310 continue;
311 }
312 if (word.compare("basemin", Qt::CaseInsensitive) == 0 ||
313 word.compare("bmin", Qt::CaseInsensitive) == 0) {
314 float fval; if (!getFloat(in, fval)) return false;
315 if (inCategory) { currentCat.bmin = fval; bminSet = true; }
316 else { qWarning() << "MNEDescriptionParser: misplaced basemin"; return false; }
317 continue;
318 }
319 if (word.compare("basemax", Qt::CaseInsensitive) == 0 ||
320 word.compare("bmax", Qt::CaseInsensitive) == 0) {
321 float fval; if (!getFloat(in, fval)) return false;
322 if (inCategory) { currentCat.bmax = fval; bmaxSet = true; }
323 else { qWarning() << "MNEDescriptionParser: misplaced basemax"; return false; }
324 continue;
325 }
326 if (word.compare("event", Qt::CaseInsensitive) == 0) {
327 int ival; if (!getInt(in, ival)) return false;
328 if (ival <= 0) { qWarning() << "MNEDescriptionParser: event numbers must be positive"; return false; }
329 if (inCategory) currentCat.events.append(static_cast<unsigned int>(ival));
330 else { qWarning() << "MNEDescriptionParser: misplaced event"; return false; }
331 continue;
332 }
333 if (word.compare("nextevent", Qt::CaseInsensitive) == 0) {
334 int ival; if (!getInt(in, ival)) return false;
335 if (ival <= 0) { qWarning() << "MNEDescriptionParser: nextevent must be positive"; return false; }
336 if (inCategory) currentCat.nextEvent = static_cast<unsigned int>(ival);
337 else { qWarning() << "MNEDescriptionParser: misplaced nextevent"; return false; }
338 continue;
339 }
340 if (word.compare("prevevent", Qt::CaseInsensitive) == 0) {
341 int ival; if (!getInt(in, ival)) return false;
342 if (ival <= 0) { qWarning() << "MNEDescriptionParser: prevevent must be positive"; return false; }
343 if (inCategory) currentCat.prevEvent = static_cast<unsigned int>(ival);
344 else { qWarning() << "MNEDescriptionParser: misplaced prevevent"; return false; }
345 continue;
346 }
347 if (word.compare("ignore", Qt::CaseInsensitive) == 0) {
348 int ival; if (!getInt(in, ival)) return false;
349 if (inCategory) currentCat.ignore = static_cast<unsigned int>(ival);
350 else { qWarning() << "MNEDescriptionParser: misplaced ignore"; return false; }
351 continue;
352 }
353 if (word.compare("prevignore", Qt::CaseInsensitive) == 0) {
354 int ival; if (!getInt(in, ival)) return false;
355 if (inCategory) currentCat.prevIgnore = static_cast<unsigned int>(ival);
356 else { qWarning() << "MNEDescriptionParser: misplaced prevignore"; return false; }
357 continue;
358 }
359 if (word.compare("nextignore", Qt::CaseInsensitive) == 0) {
360 int ival; if (!getInt(in, ival)) return false;
361 if (inCategory) currentCat.nextIgnore = static_cast<unsigned int>(ival);
362 else { qWarning() << "MNEDescriptionParser: misplaced nextignore"; return false; }
363 continue;
364 }
365 if (word.compare("mask", Qt::CaseInsensitive) == 0) {
366 int ival; if (!getInt(in, ival)) return false;
367 if (ival <= 0) { qWarning() << "MNEDescriptionParser: mask must be positive"; return false; }
368 if (inCategory) { currentCat.ignore = static_cast<unsigned int>(ival); currentCat.ignore = ~currentCat.ignore; }
369 else { qWarning() << "MNEDescriptionParser: misplaced mask"; return false; }
370 continue;
371 }
372 if (word.compare("prevmask", Qt::CaseInsensitive) == 0) {
373 int ival; if (!getInt(in, ival)) return false;
374 if (inCategory) { currentCat.prevIgnore = static_cast<unsigned int>(ival); currentCat.prevIgnore = ~currentCat.prevIgnore; }
375 else { qWarning() << "MNEDescriptionParser: misplaced prevmask"; return false; }
376 continue;
377 }
378 if (word.compare("nextmask", Qt::CaseInsensitive) == 0) {
379 int ival; if (!getInt(in, ival)) return false;
380 if (inCategory) { currentCat.nextIgnore = static_cast<unsigned int>(ival); currentCat.nextIgnore = ~currentCat.nextIgnore; }
381 else { qWarning() << "MNEDescriptionParser: misplaced nextmask"; return false; }
382 continue;
383 }
384 if (word.compare("delay", Qt::CaseInsensitive) == 0) {
385 float fval; if (!getFloat(in, fval)) return false;
386 if (inCategory) currentCat.delay = fval;
387 else { qWarning() << "MNEDescriptionParser: misplaced delay"; return false; }
388 continue;
389 }
390 if (word.compare("stderr", Qt::CaseInsensitive) == 0) {
391 if (inCategory) currentCat.doStdErr = true;
392 else { qWarning() << "MNEDescriptionParser: misplaced stderr"; return false; }
393 continue;
394 }
395 if (word.compare("abs", Qt::CaseInsensitive) == 0) {
396 if (inCategory) currentCat.doAbs = true;
397 else { qWarning() << "MNEDescriptionParser: misplaced abs"; return false; }
398 continue;
399 }
400 if (word.compare("color", Qt::CaseInsensitive) == 0) {
401 float r, g, b;
402 if (!getFloat(in, r) || !getFloat(in, g) || !getFloat(in, b)) return false;
403 if (inCategory) { currentCat.color[0] = r; currentCat.color[1] = g; currentCat.color[2] = b; }
404 else { qWarning() << "MNEDescriptionParser: misplaced color"; return false; }
405 continue;
406 }
407
408 // Try rejection parameters (average-level only)
409 if (inAverage && !inCategory) {
410 bool parseOk;
411 if (parseRejectionParam(word, in, desc.rej, parseOk)) {
412 if (!parseOk) return false;
413 continue;
414 }
415 }
416
417 qWarning() << "MNEDescriptionParser: unknown keyword" << word << "in" << fileName;
418 }
419
420 if (desc.categories.isEmpty()) {
421 qWarning() << "MNEDescriptionParser: no categories found in" << fileName;
422 return false;
423 }
424
425 return true;
426}
427
428//=============================================================================================================
429// COVARIANCE DESCRIPTION PARSER
430//=============================================================================================================
431
433{
434 QFile file(fileName);
435 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
436 qWarning() << "MNEDescriptionParser: cannot open" << fileName;
437 return false;
438 }
439
440 QTextStream in(&file);
441 desc = CovDescription();
442
443 bool expectBrace = false;
444 bool bminSet = false, bmaxSet = false;
445 bool inCov = false;
446 bool inDef = false;
447 CovDefinition currentDef;
448
449 QString word;
450 while (!(word = nextWord(in)).isEmpty()) {
451 if (expectBrace) {
452 if (word == "{") {
453 expectBrace = false;
454 } else {
455 qWarning() << "MNEDescriptionParser: expected '{', got" << word;
456 return false;
457 }
458 continue;
459 }
460
461 if (word == "{") {
462 qWarning() << "MNEDescriptionParser: unexpected '{'";
463 return false;
464 }
465
466 if (word == "}") {
467 if (inDef) {
468 currentDef.doBaseline = bminSet && bmaxSet;
469 if (currentDef.tmin >= currentDef.tmax) {
470 qWarning() << "MNEDescriptionParser: illegal time range in def";
471 return false;
472 }
473 desc.defs.append(currentDef);
474 currentDef = CovDefinition();
475 inDef = false;
476 bminSet = bmaxSet = false;
477 } else if (inCov) {
478 inCov = false;
479 break;
480 }
481 continue;
482 }
483
484 if (word.compare("cov", Qt::CaseInsensitive) == 0) {
485 if (inCov || inDef) {
486 qWarning() << "MNEDescriptionParser: nested cov";
487 return false;
488 }
489 inCov = true;
490 expectBrace = true;
491 continue;
492 }
493
494 if (word.compare("def", Qt::CaseInsensitive) == 0) {
495 if (!inCov || inDef) {
496 qWarning() << "MNEDescriptionParser: misplaced def";
497 return false;
498 }
499 inDef = true;
500 expectBrace = true;
501 bminSet = bmaxSet = false;
502 continue;
503 }
504
505 // Cov-level keywords
506 if (word.compare("outfile", Qt::CaseInsensitive) == 0) {
507 QString val = nextWord(in);
508 if (inCov && !inDef) desc.filename = val;
509 else { qWarning() << "MNEDescriptionParser: misplaced outfile"; return false; }
510 continue;
511 }
512 if (word.compare("eventfile", Qt::CaseInsensitive) == 0) {
513 QString val = nextWord(in);
514 if (inCov && !inDef) desc.eventFile = val;
515 else { qWarning() << "MNEDescriptionParser: misplaced eventfile"; return false; }
516 continue;
517 }
518 if (word.compare("logfile", Qt::CaseInsensitive) == 0) {
519 QString val = nextWord(in);
520 if (inCov && !inDef) desc.logFile = val;
521 else { qWarning() << "MNEDescriptionParser: misplaced logfile"; return false; }
522 continue;
523 }
524 if (word.compare("keepsamplemean", Qt::CaseInsensitive) == 0) {
525 if (inCov && !inDef) desc.removeSampleMean = false;
526 else { qWarning() << "MNEDescriptionParser: misplaced keepsamplemean"; return false; }
527 continue;
528 }
529 if (word.compare("fixskew", Qt::CaseInsensitive) == 0) {
530 if (inCov && !inDef) desc.fixSkew = true;
531 else { qWarning() << "MNEDescriptionParser: misplaced fixskew"; return false; }
532 continue;
533 }
534
535 // Def-level keywords
536 if (word.compare("tmin", Qt::CaseInsensitive) == 0) {
537 float fval; if (!getFloat(in, fval)) return false;
538 if (inDef) currentDef.tmin = fval;
539 else { qWarning() << "MNEDescriptionParser: misplaced tmin"; return false; }
540 continue;
541 }
542 if (word.compare("tmax", Qt::CaseInsensitive) == 0) {
543 float fval; if (!getFloat(in, fval)) return false;
544 if (inDef) currentDef.tmax = fval;
545 else { qWarning() << "MNEDescriptionParser: misplaced tmax"; return false; }
546 continue;
547 }
548 if (word.compare("basemin", Qt::CaseInsensitive) == 0 ||
549 word.compare("bmin", Qt::CaseInsensitive) == 0) {
550 float fval; if (!getFloat(in, fval)) return false;
551 if (inDef) { currentDef.bmin = fval; bminSet = true; }
552 else { qWarning() << "MNEDescriptionParser: misplaced basemin"; return false; }
553 continue;
554 }
555 if (word.compare("basemax", Qt::CaseInsensitive) == 0 ||
556 word.compare("bmax", Qt::CaseInsensitive) == 0) {
557 float fval; if (!getFloat(in, fval)) return false;
558 if (inDef) { currentDef.bmax = fval; bmaxSet = true; }
559 else { qWarning() << "MNEDescriptionParser: misplaced basemax"; return false; }
560 continue;
561 }
562 if (word.compare("event", Qt::CaseInsensitive) == 0) {
563 int ival; if (!getInt(in, ival)) return false;
564 if (ival <= 0) { qWarning() << "MNEDescriptionParser: event must be positive"; return false; }
565 if (inDef) currentDef.events.append(static_cast<unsigned int>(ival));
566 else { qWarning() << "MNEDescriptionParser: misplaced event"; return false; }
567 continue;
568 }
569 if (word.compare("ignore", Qt::CaseInsensitive) == 0) {
570 int ival; if (!getInt(in, ival)) return false;
571 if (inDef) currentDef.ignore = static_cast<unsigned int>(ival);
572 else { qWarning() << "MNEDescriptionParser: misplaced ignore"; return false; }
573 continue;
574 }
575 if (word.compare("mask", Qt::CaseInsensitive) == 0) {
576 int ival; if (!getInt(in, ival)) return false;
577 if (inDef) { currentDef.ignore = static_cast<unsigned int>(ival); currentDef.ignore = ~currentDef.ignore; }
578 else { qWarning() << "MNEDescriptionParser: misplaced mask"; return false; }
579 continue;
580 }
581 if (word.compare("delay", Qt::CaseInsensitive) == 0) {
582 float fval; if (!getFloat(in, fval)) return false;
583 if (inDef) currentDef.delay = fval;
584 else { qWarning() << "MNEDescriptionParser: misplaced delay"; return false; }
585 continue;
586 }
587
588 // Rejection parameters (cov-level only)
589 if (inCov && !inDef) {
590 bool parseOk;
591 if (parseRejectionParam(word, in, desc.rej, parseOk)) {
592 if (!parseOk) return false;
593 continue;
594 }
595 }
596
597 qWarning() << "MNEDescriptionParser: unknown keyword" << word << "in" << fileName;
598 }
599
600 return true;
601}
Parser for the legacy MNE-C .ave / .cov plain-text description files.
Core MNE data structures (source spaces, source estimates, hemispheres).
One averaging category in an MNE-C ave-description file: trigger logic, timing window,...
QVector< unsigned int > events
unsigned int nextIgnore
unsigned int nextEvent
unsigned int prevIgnore
unsigned int prevEvent
unsigned int ignore
Top-level MNE-C ave-description record: comment, category list, shared rejection limits and output fi...
RejectionParams rej
QList< AverageCategory > categories
static bool parseAverageFile(const QString &fileName, AverageDescription &desc)
static bool parseCovarianceFile(const QString &fileName, CovDescription &desc)
QVector< unsigned int > events
QList< CovDefinition > defs