v2.0.0
Loading...
Searching...
No Matches
bids_raw_data.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "bids_raw_data.h"
18#include "bids_channel.h"
20#include "bids_const.h"
23
24#include <fiff/fiff_constants.h>
25
26//=============================================================================================================
27// QT INCLUDES
28//=============================================================================================================
29
30#include <QDebug>
31#include <QFileInfo>
32#include <QDir>
33#include <QFile>
34#include <QJsonDocument>
35#include <QJsonObject>
36#include <cmath>
37
38//=============================================================================================================
39// USED NAMESPACES
40//=============================================================================================================
41
42using namespace BIDSLIB;
43using namespace FIFFLIB;
44using namespace Eigen;
45
46//=============================================================================================================
47// ANONYMOUS HELPERS — read side
48//=============================================================================================================
49
50namespace
51{
52
53//=========================================================================================================
55struct BidsUnit
56{
57 const char16_t* bids;
58 int unit;
59 int mul;
60};
61const BidsUnit bidsUnits[] = {
63 {u"\u00B5V", FIFF_UNIT_V, FIFF_UNITM_MU},
64 {u"uV", FIFF_UNIT_V, FIFF_UNITM_MU},
65 {u"mV", FIFF_UNIT_V, FIFF_UNITM_M},
66 {u"nV", FIFF_UNIT_V, FIFF_UNITM_N},
68 {u"fT", FIFF_UNIT_T, FIFF_UNITM_F},
69 {u"pT", FIFF_UNIT_T, FIFF_UNITM_P},
75 {u"arbitrary", FIFF_UNIT_NONE, FIFF_UNITM_NONE},
76};
77
78//=========================================================================================================
80void bidsUnitToFiff(const QString& sUnit, int& unit, int& unitMul)
81{
82 const QString u = sUnit.trimmed();
83 for (const BidsUnit& e : bidsUnits) {
84 if (u == QStringView(e.bids)) {
85 unit = e.unit;
86 unitMul = e.mul;
87 return;
88 }
89 }
90}
91
92//=========================================================================================================
93void applyChannelsTsv(FiffInfo& info, const QList<BidsChannel>& channels)
94{
95 if (channels.isEmpty())
96 return;
97
98 QMap<QString, int> bidsToFiff = bidsTypeToFiffKind();
99
100 QMap<QString, const BidsChannel*> channelMap;
101 for (const auto& ch : channels)
102 channelMap[ch.name] = &ch;
103
104 info.bads.clear();
105
106 for (int i = 0; i < info.chs.size(); ++i) {
107 FiffChInfo& fiffCh = info.chs[i];
108 auto it = channelMap.find(fiffCh.ch_name);
109 if (it == channelMap.end())
110 continue;
111 const BidsChannel* rec = it.value();
112
113 QString typeUpper = rec->type.toUpper();
114 if (bidsToFiff.contains(typeUpper))
115 fiffCh.kind = bidsToFiff[typeUpper];
116
117 bidsUnitToFiff(rec->units, fiffCh.unit, fiffCh.unit_mul);
118
119 if (rec->status.toLower() == "bad")
120 info.bads.append(fiffCh.ch_name);
121 }
122}
123
124//=========================================================================================================
125void applyElectrodePositions(FiffInfo& info,
126 const QList<BidsElectrode>& electrodes,
127 const QString& coordSystemName,
128 const QString& coordUnits)
129{
130 if (electrodes.isEmpty())
131 return;
132
133 QMap<QString, int> coordMap = bidsCoordToFiffFrame();
134 int coordFrame = FIFFV_COORD_UNKNOWN;
135 if (coordMap.contains(coordSystemName))
136 coordFrame = coordMap[coordSystemName];
137
138 float scaleFactor = 1.0f;
139 QString units = coordUnits.toLower();
140 if (units == "mm")
141 scaleFactor = 0.001f;
142 else if (units == "cm")
143 scaleFactor = 0.01f;
144
145 // Ident = 1-based ordinal among electrode channels, the order buildElectrodeRecords() reads them back in
146 QMap<QString, int> chOrdinal;
147 for (const auto& ch : info.chs) {
148 if (ch.kind == FIFFV_EEG_CH || ch.kind == FIFFV_ECOG_CH || ch.kind == FIFFV_SEEG_CH || ch.kind == FIFFV_DBS_CH)
149 chOrdinal.insert(ch.ch_name, static_cast<int>(chOrdinal.size()) + 1);
150 }
151
152 info.dig.clear();
153 int extraIdent = static_cast<int>(chOrdinal.size()) + 1;
154 for (const auto& elec : electrodes) {
155 if (elec.x == "n/a" || elec.y == "n/a" || elec.z == "n/a")
156 continue;
157
158 FiffDigPoint dp;
159 dp.kind = chOrdinal.contains(elec.name) ? FIFFV_POINT_EEG : FIFFV_POINT_EXTRA;
160 dp.ident = chOrdinal.contains(elec.name) ? chOrdinal.value(elec.name) : extraIdent++;
161 dp.r[0] = elec.x.toFloat() * scaleFactor;
162 dp.r[1] = elec.y.toFloat() * scaleFactor;
163 dp.r[2] = elec.z.toFloat() * scaleFactor;
164 dp.coord_frame = coordFrame;
165
166 info.dig.append(dp);
167 }
168}
169
170//=========================================================================================================
171QJsonObject readJsonFile(const QString& sFilePath)
172{
173 QFile file(sFilePath);
174 if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
175 return {};
176 QJsonParseError error;
177 QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
178 file.close();
179 if (error.error != QJsonParseError::NoError)
180 return {};
181 return doc.object();
182}
183
184//=========================================================================================================
185bool writeJsonFile(const QString& sFilePath, const QJsonObject& json)
186{
187 QFile file(sFilePath);
188 if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
189 return false;
190 file.write(QJsonDocument(json).toJson(QJsonDocument::Indented));
191 file.close();
192 return true;
193}
194
195//=========================================================================================================
196void readSidecarJson(const QString& sFilePath,
197 FiffInfo& info,
198 BidsRawData& data)
199{
200 QJsonObject json = readJsonFile(sFilePath);
201 if (json.isEmpty())
202 return;
203
204 // Apply to FiffInfo
205 double plf = json.value(QStringLiteral("PowerLineFrequency")).toDouble();
206 if (plf > 0.0)
207 info.linefreq = static_cast<float>(plf);
208
209 double sf = json.value(QStringLiteral("SamplingFrequency")).toDouble();
210 if (sf > 0.0 && std::abs(info.sfreq - static_cast<float>(sf)) > 0.5f)
211 qWarning() << "[BidsRawData::read] Sampling frequency mismatch: raw ="
212 << info.sfreq << "sidecar =" << sf;
213
214 // Store independent metadata fields on BidsRawData
215 data.ieegReference = json.value(QStringLiteral("iEEGReference")).toString();
216 data.taskDescription = json.value(QStringLiteral("TaskDescription")).toString();
217 data.manufacturer = json.value(QStringLiteral("Manufacturer")).toString();
218 data.manufacturerModelName = json.value(QStringLiteral("ManufacturerModelName")).toString();
219 data.softwareVersions = json.value(QStringLiteral("SoftwareVersions")).toString();
220 data.recordingType = json.value(QStringLiteral("RecordingType")).toString();
221}
222
223//=============================================================================================================
224// ANONYMOUS HELPERS — write side
225//=============================================================================================================
226
227//=========================================================================================================
228QString fiffUnitToBidsString(int unit, int unitMul)
229{
230 for (const BidsUnit& e : bidsUnits) {
231 if (e.unit == unit && e.mul == unitMul && e.unit != FIFF_UNIT_NONE)
232 return QString::fromUtf16(e.bids);
233 }
234 // A multiplier BIDS has no name for: the SI unit
235 for (const BidsUnit& e : bidsUnits) {
236 if (e.unit == unit && e.mul == FIFF_UNITM_NONE && e.unit != FIFF_UNIT_NONE)
237 return QString::fromUtf16(e.bids);
238 }
239 return QStringLiteral("n/a");
240}
241
242//=========================================================================================================
243QString channelTypeDescription(int kind)
244{
245 switch (kind) {
246 case FIFFV_EEG_CH:
247 return QStringLiteral("ElectroEncephaloGram");
248 case FIFFV_ECOG_CH:
249 return QStringLiteral("Electrocorticography");
250 case FIFFV_SEEG_CH:
251 return QStringLiteral("StereoElectroEncephaloGram");
252 case FIFFV_DBS_CH:
253 return QStringLiteral("DeepBrainStimulation");
254 case FIFFV_MEG_CH:
255 return QStringLiteral("MagnetoEncephaloGram");
256 case FIFFV_STIM_CH:
257 return QStringLiteral("Trigger");
258 case FIFFV_EOG_CH:
259 return QStringLiteral("ElectroOculoGram");
260 case FIFFV_ECG_CH:
261 return QStringLiteral("ElectroCardioGram");
262 case FIFFV_EMG_CH:
263 return QStringLiteral("ElectroMyoGram");
264 case FIFFV_MISC_CH:
265 return QStringLiteral("Miscellaneous");
266 case FIFFV_RESP_CH:
267 return QStringLiteral("Respiration");
268 default:
269 return QStringLiteral("n/a");
270 }
271}
272
273//=========================================================================================================
274QList<BidsChannel> buildChannelRecords(const FiffInfo& info)
275{
276 QList<BidsChannel> records;
277 QMap<int, QString> kindMap = fiffKindToBidsType();
278 QSet<QString> badsSet(info.bads.begin(), info.bads.end());
279
280 for (int i = 0; i < info.chs.size(); ++i) {
281 const FiffChInfo& ch = info.chs[i];
282
283 BidsChannel rec;
284 rec.name = ch.ch_name;
285 rec.type = kindMap.contains(ch.kind) ? kindMap[ch.kind] : QStringLiteral("MISC");
286 rec.units = fiffUnitToBidsString(ch.unit, ch.unit_mul);
287 rec.samplingFreq = QString::number(static_cast<double>(info.sfreq), 'g', 10);
288 rec.lowCutoff = (info.highpass > 0.0f)
289 ? QString::number(static_cast<double>(info.highpass), 'g', 10)
290 : QStringLiteral("n/a");
291 rec.highCutoff = (info.lowpass > 0.0f)
292 ? QString::number(static_cast<double>(info.lowpass), 'g', 10)
293 : QStringLiteral("n/a");
294 rec.notch = QStringLiteral("n/a");
295 rec.status = badsSet.contains(ch.ch_name) ? QStringLiteral("bad") : QStringLiteral("good");
296 rec.description = channelTypeDescription(ch.kind);
297
298 records.append(rec);
299 }
300 return records;
301}
302
303//=========================================================================================================
304QList<BidsElectrode> buildElectrodeRecords(const FiffInfo& info)
305{
306 QList<BidsElectrode> records;
307
308 QMap<int, const FiffDigPoint*> digByIdent;
309 for (const auto& dp : info.dig) {
310 if (dp.kind == FIFFV_POINT_EEG || dp.kind == FIFFV_POINT_EXTRA)
311 digByIdent[dp.ident] = &dp;
312 }
313
314 int digIdx = 0;
315
316 for (int i = 0; i < info.chs.size(); ++i) {
317 const FiffChInfo& ch = info.chs[i];
318
319 if (ch.kind == FIFFV_STIM_CH)
320 continue;
321 if (ch.kind != FIFFV_EEG_CH && ch.kind != FIFFV_ECOG_CH &&
322 ch.kind != FIFFV_SEEG_CH && ch.kind != FIFFV_DBS_CH)
323 continue;
324
325 BidsElectrode rec;
326 rec.name = ch.ch_name;
327
328 ++digIdx;
329 bool hasPosition = false;
330
331 if (digByIdent.contains(digIdx)) {
332 const FiffDigPoint* dp = digByIdent[digIdx];
333 if (std::isfinite(dp->r[0]) && std::isfinite(dp->r[1]) && std::isfinite(dp->r[2])) {
334 rec.x = QString::number(static_cast<double>(dp->r[0]), 'g', 8);
335 rec.y = QString::number(static_cast<double>(dp->r[1]), 'g', 8);
336 rec.z = QString::number(static_cast<double>(dp->r[2]), 'g', 8);
337 hasPosition = true;
338 }
339 }
340
341 if (!hasPosition) {
342 const Eigen::Vector3f& r0 = ch.chpos.r0;
343 if (r0.squaredNorm() > 0.0f && std::isfinite(r0[0])) {
344 rec.x = QString::number(static_cast<double>(r0[0]), 'g', 8);
345 rec.y = QString::number(static_cast<double>(r0[1]), 'g', 8);
346 rec.z = QString::number(static_cast<double>(r0[2]), 'g', 8);
347 } else {
348 rec.x = QStringLiteral("n/a");
349 rec.y = QStringLiteral("n/a");
350 rec.z = QStringLiteral("n/a");
351 }
352 }
353
354 rec.size = QStringLiteral("n/a");
355 rec.type = QStringLiteral("n/a");
356 rec.material = QStringLiteral("n/a");
357 rec.impedance = QStringLiteral("n/a");
358
359 records.append(rec);
360 }
361 return records;
362}
363
364//=========================================================================================================
365QJsonObject buildIeegSidecarJson(const BidsRawData& data,
366 const BIDSPath& bidsPath)
367{
368 QJsonObject json;
369 const FiffInfo& info = data.raw.info;
370
371 // Required
372 json[QStringLiteral("TaskName")] = bidsPath.task();
373 json[QStringLiteral("SamplingFrequency")] = static_cast<double>(info.sfreq);
374 json[QStringLiteral("PowerLineFrequency")] = static_cast<double>(info.linefreq);
375
376 // Reference
377 if (!data.ieegReference.isEmpty())
378 json[QStringLiteral("iEEGReference")] = data.ieegReference;
379 else
380 json[QStringLiteral("iEEGReference")] = QStringLiteral("n/a");
381
382 // Channel counts — computed from FiffInfo
383 int ecog = 0, seeg = 0, dbs = 0, eeg = 0, eog = 0, ecg = 0, emg = 0, misc = 0, trig = 0;
384 for (const auto& ch : info.chs) {
385 switch (ch.kind) {
386 case FIFFV_ECOG_CH:
387 ++ecog;
388 break;
389 case FIFFV_SEEG_CH:
390 ++seeg;
391 break;
392 case FIFFV_DBS_CH:
393 ++dbs;
394 break;
395 case FIFFV_EEG_CH:
396 ++eeg;
397 break;
398 case FIFFV_EOG_CH:
399 ++eog;
400 break;
401 case FIFFV_ECG_CH:
402 ++ecg;
403 break;
404 case FIFFV_EMG_CH:
405 ++emg;
406 break;
407 case FIFFV_MISC_CH:
408 ++misc;
409 break;
410 case FIFFV_STIM_CH:
411 ++trig;
412 break;
413 default:
414 break;
415 }
416 }
417 json[QStringLiteral("ECOGChannelCount")] = ecog;
418 json[QStringLiteral("SEEGChannelCount")] = seeg;
419 if (dbs > 0)
420 json[QStringLiteral("DBSChannelCount")] = dbs;
421 if (eeg > 0)
422 json[QStringLiteral("EEGChannelCount")] = eeg;
423 if (eog > 0)
424 json[QStringLiteral("EOGChannelCount")] = eog;
425 if (ecg > 0)
426 json[QStringLiteral("ECGChannelCount")] = ecg;
427 if (emg > 0)
428 json[QStringLiteral("EMGChannelCount")] = emg;
429 if (misc > 0)
430 json[QStringLiteral("MiscChannelCount")] = misc;
431 if (trig > 0)
432 json[QStringLiteral("TriggerChannelCount")] = trig;
433
434 // Recording metadata
435 if (!data.recordingType.isEmpty())
436 json[QStringLiteral("RecordingType")] = data.recordingType;
437 else
438 json[QStringLiteral("RecordingType")] = QStringLiteral("continuous");
439
440 // mne-bids writes raw.times[-1]
441 if (info.sfreq > 0.0f && data.raw.last_samp >= data.raw.first_samp) {
442 double dur = static_cast<double>(data.raw.last_samp - data.raw.first_samp) / static_cast<double>(info.sfreq);
443 json[QStringLiteral("RecordingDuration")] = dur;
444 }
445
446 // Optional strings from BidsRawData
447 if (!data.taskDescription.isEmpty())
448 json[QStringLiteral("TaskDescription")] = data.taskDescription;
449 if (!data.manufacturer.isEmpty())
450 json[QStringLiteral("Manufacturer")] = data.manufacturer;
451 if (!data.manufacturerModelName.isEmpty())
452 json[QStringLiteral("ManufacturerModelName")] = data.manufacturerModelName;
453 if (!data.softwareVersions.isEmpty())
454 json[QStringLiteral("SoftwareVersions")] = data.softwareVersions;
455
456 return json;
457}
458
459//=========================================================================================================
460bool copyFile(const QString& src, const QString& dst, bool overwrite)
461{
462 if (!QFileInfo::exists(src)) {
463 qWarning() << "[BidsRawData::write] Source file does not exist:" << src;
464 return false;
465 }
466 if (QFileInfo::exists(dst)) {
467 if (!overwrite) {
468 qWarning() << "[BidsRawData::write] Target file already exists:" << dst;
469 return false;
470 }
471 QFile::remove(dst);
472 }
473 return QFile::copy(src, dst);
474}
475
476//=========================================================================================================
477bool copyBrainVisionFiles(const QString& srcVhdr, const BIDSPath& bidsPath, bool overwrite)
478{
479 QFileInfo srcInfo(srcVhdr);
480 QString srcDir = srcInfo.absolutePath();
481
482 QFile vhdrFile(srcVhdr);
483 if (!vhdrFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
484 qWarning() << "[BidsRawData::write] Cannot open .vhdr file:" << srcVhdr;
485 return false;
486 }
487
488 QString dataFileName;
489 QString markerFileName;
490 QByteArray vhdrContent = vhdrFile.readAll();
491 vhdrFile.close();
492
493 for (const auto& line : vhdrContent.split('\n')) {
494 QString sLine = QString::fromUtf8(line).trimmed();
495 if (sLine.startsWith("DataFile=", Qt::CaseInsensitive))
496 dataFileName = sLine.mid(9).trimmed();
497 else if (sLine.startsWith("MarkerFile=", Qt::CaseInsensitive))
498 markerFileName = sLine.mid(11).trimmed();
499 }
500
501 QString dstDir = bidsPath.directory();
502 QString dstBase = bidsPath.basename();
503 dstBase = dstBase.left(dstBase.lastIndexOf('.'));
504
505 QString dstVhdr = bidsPath.filePath();
506 QString newDataFile = dstBase + QStringLiteral(".eeg");
507 QString newMarkerFile = dstBase + QStringLiteral(".vmrk");
508
509 QString vhdrStr = QString::fromUtf8(vhdrContent);
510 if (!dataFileName.isEmpty())
511 vhdrStr.replace("DataFile=" + dataFileName,
512 "DataFile=" + QFileInfo(newDataFile).fileName());
513 if (!markerFileName.isEmpty())
514 vhdrStr.replace("MarkerFile=" + markerFileName,
515 "MarkerFile=" + QFileInfo(newMarkerFile).fileName());
516
517 if (QFileInfo::exists(dstVhdr) && !overwrite) {
518 qWarning() << "[BidsRawData::write] Target file already exists:" << dstVhdr;
519 return false;
520 }
521 if (QFileInfo::exists(dstVhdr))
522 QFile::remove(dstVhdr);
523
524 QFile dstVhdrFile(dstVhdr);
525 if (!dstVhdrFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
526 qWarning() << "[BidsRawData::write] Cannot write .vhdr file:" << dstVhdr;
527 return false;
528 }
529 dstVhdrFile.write(vhdrStr.toUtf8());
530 dstVhdrFile.close();
531
532 if (!dataFileName.isEmpty()) {
533 QString srcData = QDir(srcDir).absoluteFilePath(dataFileName);
534 QString dstData = dstDir + QFileInfo(newDataFile).fileName();
535 if (!copyFile(srcData, dstData, overwrite)) {
536 qWarning() << "[BidsRawData::write] Failed to copy data file:" << srcData;
537 return false;
538 }
539 }
540
541 if (!markerFileName.isEmpty()) {
542 QString srcMarker = QDir(srcDir).absoluteFilePath(markerFileName);
543 QString dstMarker = dstDir + QFileInfo(newMarkerFile).fileName();
544
545 if (QFileInfo::exists(srcMarker)) {
546 QFile markerFile(srcMarker);
547 if (markerFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
548 QString markerContent = QString::fromUtf8(markerFile.readAll());
549 markerFile.close();
550
551 markerContent.replace("DataFile=" + dataFileName,
552 "DataFile=" + QFileInfo(newDataFile).fileName());
553
554 if (QFileInfo::exists(dstMarker)) {
555 if (!overwrite) {
556 qWarning() << "[BidsRawData::write] Target marker file already exists:" << dstMarker;
557 return false;
558 }
559 QFile::remove(dstMarker);
560 }
561
562 QFile dstMarkerFile(dstMarker);
563 if (dstMarkerFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
564 dstMarkerFile.write(markerContent.toUtf8());
565 dstMarkerFile.close();
566 }
567 }
568 }
569 }
570
571 return true;
572}
573
574//=========================================================================================================
575bool copyRawDataFile(const QString& sourcePath, const BIDSPath& bidsPath, bool overwrite)
576{
577 if (sourcePath.isEmpty())
578 return true;
579
580 QString ext = bidsPath.extension().toLower();
581 if (ext == ".vhdr" || ext == ".ahdr")
582 return copyBrainVisionFiles(sourcePath, bidsPath, overwrite);
583
584 return copyFile(sourcePath, bidsPath.filePath(), overwrite);
585}
586
587} // anonymous namespace
588
589//=============================================================================================================
590// MEMBER METHODS
591//=============================================================================================================
592
594: raw(other.raw) // FiffRawData has only copy ctor
595, events(std::move(other.events))
596, eventIdMap(std::move(other.eventIdMap))
597, electrodes(std::move(other.electrodes))
598, coordinateSystem(std::move(other.coordinateSystem))
599, reader(std::move(other.reader))
600, ieegReference(std::move(other.ieegReference))
601, taskDescription(std::move(other.taskDescription))
602, manufacturer(std::move(other.manufacturer))
603, manufacturerModelName(std::move(other.manufacturerModelName))
604, softwareVersions(std::move(other.softwareVersions))
605, recordingType(std::move(other.recordingType))
606, m_bIsValid(other.m_bIsValid)
607{
608 other.m_bIsValid = false;
609}
610
611//=============================================================================================================
612
614{
615 if (this != &other) {
616 raw = other.raw; // FiffRawData has only copy ctor
617 events = std::move(other.events);
618 eventIdMap = std::move(other.eventIdMap);
619 electrodes = std::move(other.electrodes);
620 coordinateSystem = std::move(other.coordinateSystem);
621 reader = std::move(other.reader);
622 ieegReference = std::move(other.ieegReference);
623 taskDescription = std::move(other.taskDescription);
624 manufacturer = std::move(other.manufacturer);
625 manufacturerModelName = std::move(other.manufacturerModelName);
626 softwareVersions = std::move(other.softwareVersions);
627 recordingType = std::move(other.recordingType);
628 m_bIsValid = other.m_bIsValid;
629 other.m_bIsValid = false;
630 }
631 return *this;
632}
633
634//=============================================================================================================
635
637{
638 raw = FiffRawData();
639 events.clear();
640 eventIdMap.clear();
641 electrodes.clear();
642 reader.reset();
643
644 ieegReference.clear();
645 taskDescription.clear();
646 manufacturer.clear();
647 manufacturerModelName.clear();
648 softwareVersions.clear();
649 recordingType.clear();
650
652
653 m_bIsValid = false;
654}
655
656//=============================================================================================================
657
659{
660 QString ext = sExtension.toLower();
661 if (ext == ".vhdr" || ext == ".ahdr")
662 return std::make_unique<BrainVisionReader>();
663 if (ext == ".edf" || ext == ".bdf")
664 return std::make_unique<EDFReader>();
665 return nullptr;
666}
667
668//=============================================================================================================
669// BidsRawData::read()
670//=============================================================================================================
671
673{
674 BidsRawData result;
675
676 //=========================================================================================================
677 // Step 1 — Validate BIDSPath
678 //=========================================================================================================
679 if (bidsPath.root().isEmpty()) {
680 qWarning() << "[BidsRawData::read] BIDSPath root is not set";
681 return result;
682 }
683 if (bidsPath.subject().isEmpty()) {
684 qWarning() << "[BidsRawData::read] BIDSPath subject is not set";
685 return result;
686 }
687 if (bidsPath.extension().isEmpty()) {
688 qWarning() << "[BidsRawData::read] BIDSPath extension is not set";
689 return result;
690 }
691
692 //=========================================================================================================
693 // Step 2 — Resolve raw file path
694 //=========================================================================================================
695 QString rawFilePath = bidsPath.filePath();
696
697 if (!QFileInfo::exists(rawFilePath)) {
698 QString ext = bidsPath.extension();
699 if (ext.toLower() == ".edf") {
700 BIDSPath altPath(bidsPath);
701 altPath.setExtension(".EDF");
702 if (QFileInfo::exists(altPath.filePath()))
703 rawFilePath = altPath.filePath();
704 }
705 }
706
707 if (!QFileInfo::exists(rawFilePath)) {
708 qWarning() << "[BidsRawData::read] Raw data file not found:" << rawFilePath;
709 return result;
710 }
711
712 //=========================================================================================================
713 // Step 3 — Create and open the format reader
714 //=========================================================================================================
715 result.reader = createReader(bidsPath.extension());
716 if (!result.reader) {
717 qWarning() << "[BidsRawData::read] Unsupported file extension:" << bidsPath.extension();
718 return result;
719 }
720
721 if (!result.reader->open(rawFilePath)) {
722 qWarning() << "[BidsRawData::read] Failed to open raw file:" << rawFilePath;
723 return result;
724 }
725
726 //=========================================================================================================
727 // Step 4 — Build FiffRawData from the reader
728 //=========================================================================================================
729 result.raw = result.reader->toFiffRawData();
730
731 //=========================================================================================================
732 // Step 5 — Read and apply *_channels.tsv
733 //=========================================================================================================
734 BIDSPath channelsPath = bidsPath.channelsTsvPath();
735 if (QFileInfo::exists(channelsPath.filePath())) {
736 QList<BidsChannel> channels = BidsChannel::readTsv(channelsPath.filePath());
737 applyChannelsTsv(result.raw.info, channels);
738 }
739
740 //=========================================================================================================
741 // Step 6 — Read *_coordsystem.json (before electrodes, for scale/frame info)
742 //=========================================================================================================
743 BIDSPath coordsysPath = bidsPath.coordsystemJsonPath();
744 if (QFileInfo::exists(coordsysPath.filePath()))
746
747 //=========================================================================================================
748 // Step 7 — Read and apply *_electrodes.tsv
749 //=========================================================================================================
750 BIDSPath electrodesPath = bidsPath.electrodesTsvPath();
751 if (QFileInfo::exists(electrodesPath.filePath())) {
752 result.electrodes = BidsElectrode::readTsv(electrodesPath.filePath());
753 applyElectrodePositions(result.raw.info, result.electrodes,
755 }
756
757 //=========================================================================================================
758 // Step 8 — Read *_events.tsv
759 //=========================================================================================================
760 BIDSPath eventsPath = bidsPath.eventsTsvPath();
761 if (QFileInfo::exists(eventsPath.filePath())) {
762 result.events = BidsEvent::readTsv(eventsPath.filePath());
763
764 // Compute sample from onset*sfreq if sample column was absent
765 float sfreq = result.raw.info.sfreq;
766 for (auto& ev : result.events) {
767 if (ev.sample == 0 && ev.onset > 0.0f && sfreq > 0)
768 ev.sample = static_cast<int>(ev.onset * sfreq);
769 }
770
771 // Build eventIdMap from trial_type → value
772 for (const auto& ev : result.events) {
773 if (!ev.trialType.isEmpty() && ev.trialType != "n/a")
774 result.eventIdMap.insert(ev.trialType, ev.value);
775 }
776 }
777
778 //=========================================================================================================
779 // Step 9 — Read sidecar *_{datatype}.json
780 //=========================================================================================================
781 BIDSPath sidecarPath = bidsPath.sidecarJsonPath();
782 if (QFileInfo::exists(sidecarPath.filePath()))
783 readSidecarJson(sidecarPath.filePath(), result.raw.info, result);
784
785 //=========================================================================================================
786 // Done
787 //=========================================================================================================
788 result.m_bIsValid = true;
789 return result;
790}
791
792//=============================================================================================================
793// BidsRawData::write()
794//=============================================================================================================
795
797 const QString& sourcePath,
798 const WriteOptions& options) const
799{
800 BIDSPath result;
801
802 //=========================================================================================================
803 // Step 1 — Validate
804 //=========================================================================================================
805 if (bidsPath.root().isEmpty()) {
806 qWarning() << "[BidsRawData::write] BIDSPath root is not set";
807 return result;
808 }
809 if (bidsPath.subject().isEmpty()) {
810 qWarning() << "[BidsRawData::write] BIDSPath subject is not set";
811 return result;
812 }
813 if (bidsPath.task().isEmpty()) {
814 qWarning() << "[BidsRawData::write] BIDSPath task is not set";
815 return result;
816 }
817 if (bidsPath.datatype().isEmpty()) {
818 qWarning() << "[BidsRawData::write] BIDSPath datatype is not set";
819 return result;
820 }
821 if (raw.info.isEmpty()) {
822 qWarning() << "[BidsRawData::write] FiffRawData info is empty";
823 return result;
824 }
825
826 //=========================================================================================================
827 // Step 2 — Create directory structure
828 //=========================================================================================================
829 if (!bidsPath.mkdirs()) {
830 qWarning() << "[BidsRawData::write] Failed to create directory:" << bidsPath.directory();
831 return result;
832 }
833
834 //=========================================================================================================
835 // Step 3 — Copy raw data file
836 //=========================================================================================================
837 if (options.copyData && !sourcePath.isEmpty()) {
838 if (!copyRawDataFile(sourcePath, bidsPath, options.overwrite)) {
839 qWarning() << "[BidsRawData::write] Failed to copy raw data file";
840 return result;
841 }
842 }
843
844 //=========================================================================================================
845 // Step 4 — Write *_channels.tsv
846 //=========================================================================================================
847 {
848 BIDSPath channelsPath = bidsPath.channelsTsvPath();
849 if (!options.overwrite && QFileInfo::exists(channelsPath.filePath())) {
850 qWarning() << "[BidsRawData::write] channels.tsv already exists:" << channelsPath.filePath();
851 return result;
852 }
853
854 QList<BidsChannel> channelRecords = buildChannelRecords(raw.info);
855 if (!BidsChannel::writeTsv(channelsPath.filePath(), channelRecords)) {
856 qWarning() << "[BidsRawData::write] Failed to write channels.tsv";
857 return result;
858 }
859 }
860
861 //=========================================================================================================
862 // Step 5 — Write *_electrodes.tsv + *_coordsystem.json
863 //=========================================================================================================
864 {
865 QList<BidsElectrode> electrodeRecords = buildElectrodeRecords(raw.info);
866
867 if (!electrodeRecords.isEmpty()) {
868 BIDSPath electrodesPath = bidsPath.electrodesTsvPath();
869 if (options.overwrite || !QFileInfo::exists(electrodesPath.filePath())) {
870 if (!BidsElectrode::writeTsv(electrodesPath.filePath(), electrodeRecords))
871 qWarning() << "[BidsRawData::write] Failed to write electrodes.tsv";
872 }
873
874 BIDSPath coordsysPath = bidsPath.coordsystemJsonPath();
875 if (options.overwrite || !QFileInfo::exists(coordsysPath.filePath())) {
876 // Build coordinate system, deriving defaults from FiffInfo if needed
878 if (cs.system.isEmpty()) {
879 if (raw.info.dig.isEmpty()) {
880 cs.system = QStringLiteral("Other");
881 cs.units = QStringLiteral("n/a");
882 } else {
883 int coordFrame = raw.info.dig.first().coord_frame;
884 QMap<int, QString> frameMap = fiffFrameToBidsCoord();
885 cs.system = frameMap.contains(coordFrame)
886 ? frameMap[coordFrame]
887 : QStringLiteral("Other");
888 cs.units = QStringLiteral("m");
889 }
890 }
891 if (cs.description.isEmpty() && !raw.info.dig.isEmpty())
892 cs.description = QStringLiteral("Coordinate system derived from recording data");
893
894 if (!BidsCoordinateSystem::writeJson(coordsysPath.filePath(), cs))
895 qWarning() << "[BidsRawData::write] Failed to write coordsystem.json";
896 }
897 }
898 }
899
900 //=========================================================================================================
901 // Step 6 — Write *_events.tsv
902 //=========================================================================================================
903 if (!events.isEmpty()) {
904 BIDSPath eventsPath = bidsPath.eventsTsvPath();
905 if (!options.overwrite && QFileInfo::exists(eventsPath.filePath())) {
906 qWarning() << "[BidsRawData::write] events.tsv already exists:" << eventsPath.filePath();
907 return result;
908 }
909
910 QList<BidsEvent> eventsToWrite = events;
911
912 // Apply trial_type fallback from eventIdMap
913 QMap<int, QString> valueToType;
914 for (auto it = eventIdMap.constBegin(); it != eventIdMap.constEnd(); ++it)
915 valueToType[it.value()] = it.key();
916 for (auto& ev : eventsToWrite) {
917 if (ev.trialType.isEmpty() || ev.trialType == "n/a")
918 ev.trialType = valueToType.value(ev.value, QStringLiteral("n/a"));
919 }
920
921 if (!BidsEvent::writeTsv(eventsPath.filePath(), eventsToWrite))
922 qWarning() << "[BidsRawData::write] Failed to write events.tsv";
923 }
924
925 //=========================================================================================================
926 // Step 7 — Write *_{datatype}.json sidecar
927 //=========================================================================================================
928 {
929 BIDSPath sidecarPath = bidsPath.sidecarJsonPath();
930 if (!options.overwrite && QFileInfo::exists(sidecarPath.filePath())) {
931 qWarning() << "[BidsRawData::write] Sidecar JSON already exists:" << sidecarPath.filePath();
932 return result;
933 }
934
935 QJsonObject sidecarJson = buildIeegSidecarJson(*this, bidsPath);
936 if (!writeJsonFile(sidecarPath.filePath(), sidecarJson)) {
937 qWarning() << "[BidsRawData::write] Failed to write sidecar JSON";
938 return result;
939 }
940 }
941
942 //=========================================================================================================
943 // Step 8 — Write dataset_description.json (never overwrite)
944 //=========================================================================================================
945 {
946 QString descPath = bidsPath.root() + QDir::separator() + QStringLiteral("dataset_description.json");
947
948 if (!QFileInfo::exists(descPath)) {
950 desc.name = options.datasetName.isEmpty()
951 ? QStringLiteral("[Unspecified]")
952 : options.datasetName;
953 desc.bidsVersion = QStringLiteral("1.9.0");
954 desc.datasetType = QStringLiteral("raw");
955
956 if (!BidsDatasetDescription::write(descPath, desc))
957 qWarning() << "[BidsRawData::write] Failed to write dataset_description.json";
958 }
959 }
960
961 //=========================================================================================================
962 // Done
963 //=========================================================================================================
964 result = bidsPath;
965 return result;
966}
Central container for a BIDS raw recording — the BIDS-side analogue of FIFFLIB::FiffRawData,...
Reader/writer for the BIDS _channels.tsv sidecar — one record per recorded channel.
Centralised BIDS vocabulary: datatype / suffix / extension whitelists, FIFF↔BIDS channel-type and coo...
Reader/writer for dataset_description.json — the REQUIRED root sidecar of every BIDS dataset.
BIDSLIB::AbstractFormatReader implementation for European Data Format (EDF / EDF+) and BioSemi BDF fi...
BIDSLIB::AbstractFormatReader implementation for the BrainVision .vhdr / .vmrk / ....
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
constexpr auto FIFF_UNIT_T_M
constexpr auto FIFFV_POINT_EXTRA
constexpr auto FIFFV_ECG_CH
#define FIFFV_EEG_CH
constexpr auto FIFF_UNIT_CEL
constexpr auto FIFFV_DBS_CH
constexpr auto FIFF_UNITM_F
constexpr auto FIFFV_MISC_CH
constexpr auto FIFF_UNITM_NONE
constexpr auto FIFFV_SEEG_CH
constexpr auto FIFFV_POINT_EEG
#define FIFFV_MEG_CH
constexpr auto FIFFV_ECOG_CH
constexpr auto FIFFV_STIM_CH
constexpr auto FIFF_UNIT_RAD
constexpr auto FIFF_UNIT_T
constexpr auto FIFFV_COORD_UNKNOWN
constexpr auto FIFF_UNIT_MHO
constexpr auto FIFF_UNITM_MU
constexpr auto FIFFV_EMG_CH
constexpr auto FIFF_UNIT_MOL
constexpr auto FIFF_UNIT_V
constexpr auto FIFF_UNITM_P
constexpr auto FIFF_UNITM_M
constexpr auto FIFF_UNIT_NONE
constexpr auto FIFFV_EOG_CH
constexpr auto FIFFV_RESP_CH
constexpr auto FIFF_UNITM_N
BIDS dataset reading, writing, path construction, and sidecar metadata handling for iEEG/EEG/MEG.
QMap< int, QString > fiffKindToBidsType()
Definition bids_const.h:126
QMap< QString, int > bidsCoordToFiffFrame()
Definition bids_const.h:179
QMap< QString, int > bidsTypeToFiffKind()
Definition bids_const.h:150
QMap< int, QString > fiffFrameToBidsCoord()
Definition bids_const.h:198
FIFF file I/O, in-memory data structures and high-level readers/writers.
Channel metadata record corresponding to one row in *_channels.tsv.
static QList< BidsChannel > readTsv(const QString &sFilePath)
Read a BIDS *_channels.tsv file.
static bool writeTsv(const QString &sFilePath, const QList< BidsChannel > &channels)
Write a BIDS *_channels.tsv file.
Coordinate system metadata from *_coordsystem.json.
static bool writeJson(const QString &sFilePath, const BidsCoordinateSystem &cs)
Write a BIDS *_coordsystem.json file.
static BidsCoordinateSystem readJson(const QString &sFilePath)
Read a BIDS *_coordsystem.json file.
Dataset-level metadata from dataset_description.json.
static bool write(const QString &sFilePath, const BidsDatasetDescription &desc)
Write a dataset_description.json file.
Electrode position record corresponding to one row in *_electrodes.tsv.
static bool writeTsv(const QString &sFilePath, const QList< BidsElectrode > &electrodes)
Write a BIDS *_electrodes.tsv file.
static QList< BidsElectrode > readTsv(const QString &sFilePath)
Read a BIDS *_electrodes.tsv file.
static QList< BidsEvent > readTsv(const QString &sFilePath)
Read a BIDS *_events.tsv file.
static bool writeTsv(const QString &sFilePath, const QList< BidsEvent > &events)
Write a BIDS *_events.tsv file.
BIDS-compliant path and filename construction.
Definition bids_path.h:74
QString subject() const
QString filePath() const
BIDSPath electrodesTsvPath() const
QString root() const
BIDSPath channelsTsvPath() const
QString directory() const
QString extension() const
QString basename() const
BIDSPath coordsystemJsonPath() const
QString task() const
BIDSPath eventsTsvPath() const
void setExtension(const QString &sExtension)
bool mkdirs() const
QString datatype() const
BIDSPath sidecarJsonPath() const
Central container for a BIDS raw dataset, bundling electrophysiological data with all associated side...
void clear()
Clears all data members and resets to invalid state.
static BidsRawData read(const BIDSPath &bidsPath)
Read a BIDS dataset from disk.
static AbstractFormatReader::UPtr createReader(const QString &sExtension)
Create the appropriate format reader for a given file extension.
FIFFLIB::FiffRawData raw
AbstractFormatReader::UPtr reader
QMap< QString, int > eventIdMap
QList< BidsElectrode > electrodes
BidsRawData & operator=(BidsRawData &&other) noexcept
QList< BidsEvent > events
BidsCoordinateSystem coordinateSystem
BIDSPath write(const BIDSPath &bidsPath, const QString &sourcePath, const WriteOptions &options) const
Write this dataset to a BIDS-compliant directory.
Options controlling how write() operates.
std::unique_ptr< AbstractFormatReader > UPtr
Per-channel FIFF descriptor: identifiers, kind, calibration, coil type, channel-frame coil position a...
Eigen::Vector3f r0
One digitizer point: kind (cardinal/HPI/EEG/extra), ident, 3D position in FIFFV_COORD_HEAD.
Full FIFF measurement info: per-channel descriptors, sampling and filter setup, projectors,...
Definition fiff_info.h:90
QList< FiffDigPoint > dig
Definition fiff_info.h:290
QList< FiffChInfo > chs
Continuous FIFF raw recording: FiffInfo plus a random-access directory of FIFF_DATA_BUFFER tags.