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