diff --git a/CMakeLists.txt b/CMakeLists.txt index 5906c9e4..86fc42cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,7 @@ add_library( src/nwb/base/TimeSeries.cpp src/nwb/device/Device.cpp src/nwb/ecephys/ElectricalSeries.cpp + src/nwb/ecephys/SpikeEventSeries.cpp src/nwb/file/ElectrodeGroup.cpp src/nwb/file/ElectrodeTable.cpp src/nwb/hdmf/base/Container.cpp diff --git a/src/BaseIO.hpp b/src/BaseIO.hpp index 3dd375db..19d47cb1 100644 --- a/src/BaseIO.hpp +++ b/src/BaseIO.hpp @@ -292,6 +292,14 @@ class BaseIO virtual std::unique_ptr getDataSet( const std::string& path) = 0; + /** + * @brief Checks whether a Dataset, Group, or Link already exists at the + * location in the file. + * @param path The location of the object in the file. + * @return Whether the object exists. + */ + virtual bool objectExists(const std::string& path) = 0; + /** * @brief Convenience function for creating NWB related attributes. * @param path The location of the object in the file. @@ -325,6 +333,7 @@ class BaseIO * @return The status of the operation. */ Status createTimestampsAttributes(const std::string& path); + /** * @brief Returns true if the file is open. * @return True if the file is open, false otherwise. diff --git a/src/hdf5/HDF5IO.cpp b/src/hdf5/HDF5IO.cpp index bf4b69a8..58fe39e0 100644 --- a/src/hdf5/HDF5IO.cpp +++ b/src/hdf5/HDF5IO.cpp @@ -437,6 +437,16 @@ bool HDF5IO::canModifyObjects() return statusOK && !inSWMRMode; } +bool HDF5IO::objectExists(const std::string& path) +{ + htri_t exists = H5Lexists(file->getId(), path.c_str(), H5P_DEFAULT); + if (exists > 0) { + return true; + } else { + return false; + } +} + std::unique_ptr HDF5IO::getDataSet( const std::string& path) { diff --git a/src/hdf5/HDF5IO.hpp b/src/hdf5/HDF5IO.hpp index 2a94aef2..8d2480dc 100644 --- a/src/hdf5/HDF5IO.hpp +++ b/src/hdf5/HDF5IO.hpp @@ -234,6 +234,14 @@ class HDF5IO : public BaseIO std::unique_ptr getDataSet( const std::string& path) override; + /** + * @brief Checks whether a Dataset, Group, or Link already exists at the + * location in the file. + * @param path The location of the object in the file. + * @return Whether the object exists. + */ + bool objectExists(const std::string& path) override; + /** * @brief Returns the HDF5 type of object at a given path. * @param path The location in the file of the object. diff --git a/src/nwb/NWBFile.cpp b/src/nwb/NWBFile.cpp index 361f298d..0142634e 100644 --- a/src/nwb/NWBFile.cpp +++ b/src/nwb/NWBFile.cpp @@ -13,15 +13,18 @@ #include "Utils.hpp" #include "nwb/device/Device.hpp" #include "nwb/ecephys/ElectricalSeries.hpp" +#include "nwb/ecephys/SpikeEventSeries.hpp" #include "nwb/file/ElectrodeGroup.hpp" -#include "nwb/file/ElectrodeTable.hpp" #include "spec/core.hpp" #include "spec/hdmf_common.hpp" #include "spec/hdmf_experimental.hpp" using namespace AQNWB::NWB; -constexpr SizeType CHUNK_XSIZE = 2048; +constexpr SizeType CHUNK_XSIZE = + 2048; // TODO - replace these with io settings input +constexpr SizeType SPIKE_CHUNK_XSIZE = + 8; // TODO - replace with io settings input std::vector NWBFile::emptyContainerIndexes = {}; @@ -99,6 +102,7 @@ Status NWBFile::createFileStructure(std::string description, Status NWBFile::createElectricalSeries( std::vector recordingArrays, + std::vector recordingNames, const BaseDataType& dataType, RecordingContainers* recordingContainers, std::vector& containerIndexes) @@ -107,27 +111,44 @@ Status NWBFile::createElectricalSeries( return Status::Failure; } - // store all recorded data in the acquisition group - std::string rootPath = "/acquisition/"; + if (recordingNames.size() != recordingArrays.size()) { + return Status::Failure; + } + + // Setup electrode table if it was not yet created + bool electrodeTableCreated = + io->objectExists(ElectrodeTable::electrodeTablePath); + if (!electrodeTableCreated) { + elecTable = std::make_unique(io); + elecTable->initialize(); - // Setup electrode table - ElectrodeTable elecTable = ElectrodeTable(io); - elecTable.initialize(); + // Add electrode information to table (does not write to datasets yet) + for (const auto& channelVector : recordingArrays) { + elecTable->addElectrodes(channelVector); + } + } + + // Create datasets + for (size_t i = 0; i < recordingArrays.size(); ++i) { + const auto& channelVector = recordingArrays[i]; + const std::string& recordingName = recordingNames[i]; - // Create continuous datasets - for (const auto& channelVector : recordingArrays) { // Setup electrodes and devices std::string groupName = channelVector[0].groupName; std::string devicePath = "/general/devices/" + groupName; std::string electrodePath = "/general/extracellular_ephys/" + groupName; - std::string electricalSeriesPath = rootPath + groupName; + std::string electricalSeriesPath = acquisitionPath + "/" + recordingName; - Device device = Device(devicePath, io, "description", "unknown"); - device.initialize(); + // Check if device exists for groupName, create device and electrode group + // if not + if (!io->objectExists(devicePath)) { + Device device = Device(devicePath, io, "description", "unknown"); + device.initialize(); - ElectrodeGroup elecGroup = - ElectrodeGroup(electrodePath, io, "description", "unknown", device); - elecGroup.initialize(); + ElectrodeGroup elecGroup = + ElectrodeGroup(electrodePath, io, "description", "unknown", device); + elecGroup.initialize(); + } // Setup electrical series datasets auto electricalSeries = std::make_unique( @@ -142,14 +163,96 @@ Status NWBFile::createElectricalSeries( electricalSeries->initialize(); recordingContainers->addContainer(std::move(electricalSeries)); containerIndexes.push_back(recordingContainers->containers.size() - 1); + } - // Add electrode information to electrode table (does not write to datasets - // yet) - elecTable.addElectrodes(channelVector); + // write electrode information to datasets + // (requires that the ElectrodeGroup has been written) + if (!electrodeTableCreated) { + elecTable->finalize(); + } + + return Status::Success; +} + +Status NWBFile::createSpikeEventSeries( + std::vector recordingArrays, + std::vector recordingNames, + const BaseDataType& dataType, + RecordingContainers* recordingContainers, + std::vector& containerIndexes) +{ + if (!io->canModifyObjects()) { + return Status::Failure; + } + + if (recordingNames.size() != recordingArrays.size()) { + return Status::Failure; + } + + // Setup electrode table if it was not yet created + bool electrodeTableCreated = + io->objectExists(ElectrodeTable::electrodeTablePath); + if (!electrodeTableCreated) { + elecTable = std::make_unique(io); + elecTable->initialize(); + + // Add electrode information to table (does not write to datasets yet) + for (const auto& channelVector : recordingArrays) { + elecTable->addElectrodes(channelVector); + } + } + + // Create datasets + for (size_t i = 0; i < recordingArrays.size(); ++i) { + const auto& channelVector = recordingArrays[i]; + const std::string& recordingName = recordingNames[i]; + + // Setup electrodes and devices + std::string groupName = channelVector[0].groupName; + std::string devicePath = "/general/devices/" + groupName; + std::string electrodePath = "/general/extracellular_ephys/" + groupName; + std::string spikeEventSeriesPath = acquisitionPath + "/" + recordingName; + + // Check if device exists for groupName, create device and electrode group + // if not + if (!io->objectExists(devicePath)) { + Device device = Device(devicePath, io, "description", "unknown"); + device.initialize(); + + ElectrodeGroup elecGroup = + ElectrodeGroup(electrodePath, io, "description", "unknown", device); + elecGroup.initialize(); + } + + // Setup Spike Event Series datasets + SizeArray dsetSize; + SizeArray chunkSize; + if (channelVector.size() == 1) { + dsetSize = SizeArray {0, 0}; + chunkSize = SizeArray {SPIKE_CHUNK_XSIZE, 1}; + } else { + dsetSize = SizeArray {0, channelVector.size(), 0}; + chunkSize = SizeArray {SPIKE_CHUNK_XSIZE, 1, 1}; + } + + auto spikeEventSeries = std::make_unique( + spikeEventSeriesPath, + io, + dataType, + channelVector, + "Stores spike waveforms from an extracellular ephys recording", + dsetSize, + chunkSize); + spikeEventSeries->initialize(); + recordingContainers->addContainer(std::move(spikeEventSeries)); + containerIndexes.push_back(recordingContainers->containers.size() - 1); } // write electrode information to datasets - elecTable.finalize(); + // (requires that the ElectrodeGroup has been written) + if (!electrodeTableCreated) { + elecTable->finalize(); + } return Status::Success; } @@ -161,7 +264,7 @@ void NWBFile::cacheSpecifications( const std::array, N>& specVariables) { - io->createGroup("/specifications/" + specPath + "/"); + io->createGroup("/specifications/" + specPath); io->createGroup("/specifications/" + specPath + "/" + versionNumber); for (const auto& [name, content] : specVariables) { diff --git a/src/nwb/NWBFile.hpp b/src/nwb/NWBFile.hpp index 313c54e2..a5a5c212 100644 --- a/src/nwb/NWBFile.hpp +++ b/src/nwb/NWBFile.hpp @@ -11,6 +11,7 @@ #include "Types.hpp" #include "nwb/RecordingContainers.hpp" #include "nwb/base/TimeSeries.hpp" +#include "nwb/file/ElectrodeTable.hpp" /*! * \namespace AQNWB::NWB @@ -71,14 +72,38 @@ class NWBFile * @param recordingArrays vector of ChannelVector indicating the electrodes to * record from. A separate ElectricalSeries will be * created for each ChannelVector. + * @param recordingNames vector indicating the names of the ElectricalSeries + * within the acquisition group + * @param dataType The data type of the elements in the data block. * @param recordingContainers The container to store the created TimeSeries. * @param containerIndexes The indexes of the containers added to * recordingContainers - * @param dataType The data type of the elements in the data block. * @return Status The status of the object creation operation. */ Status createElectricalSeries( std::vector recordingArrays, + std::vector recordingNames, + const BaseDataType& dataType = BaseDataType::I16, + RecordingContainers* recordingContainers = nullptr, + std::vector& containerIndexes = emptyContainerIndexes); + + /** + * @brief Create SpikeEventSeries objects to record data into. + * Created objects are stored in recordingContainers. + * @param recordingArrays vector of ChannelVector indicating the electrodes to + * record from. A separate ElectricalSeries will be + * created for each ChannelVector. + * @param recordingNames vector indicating the names of the SpikeEventSeries + * within the acquisition group + * @param dataType The data type of the elements in the data block. + * @param recordingContainers The container to store the created TimeSeries. + * @param containerIndexes The indexes of the containers added to + * recordingContainers + * @return Status The status of the object creation operation. + */ + Status createSpikeEventSeries( + std::vector recordingArrays, + std::vector recordingNames, const BaseDataType& dataType = BaseDataType::I16, RecordingContainers* recordingContainers = nullptr, std::vector& containerIndexes = emptyContainerIndexes); @@ -127,9 +152,11 @@ class NWBFile const std::array, N>& specVariables); + std::unique_ptr elecTable; const std::string identifierText; std::shared_ptr io; static std::vector emptyContainerIndexes; + inline const static std::string acquisitionPath = "/acquisition"; }; } // namespace AQNWB::NWB \ No newline at end of file diff --git a/src/nwb/RecordingContainers.cpp b/src/nwb/RecordingContainers.cpp index d7464bca..4658d551 100644 --- a/src/nwb/RecordingContainers.cpp +++ b/src/nwb/RecordingContainers.cpp @@ -2,6 +2,7 @@ #include "nwb/RecordingContainers.hpp" #include "nwb/ecephys/ElectricalSeries.hpp" +#include "nwb/ecephys/SpikeEventSeries.hpp" #include "nwb/hdmf/base/Container.hpp" using namespace AQNWB::NWB; @@ -63,3 +64,18 @@ Status RecordingContainers::writeElectricalSeriesData( es->writeChannel(channel.localIndex, numSamples, data, timestamps); } + +Status RecordingContainers::writeSpikeEventData(const SizeType& containerInd, + const SizeType& numSamples, + const SizeType& numChannels, + const void* data, + const void* timestamps) +{ + SpikeEventSeries* ses = + dynamic_cast(getContainer(containerInd)); + + if (ses == nullptr) + return Status::Failure; + + ses->writeSpike(numSamples, numChannels, data, timestamps); +} diff --git a/src/nwb/RecordingContainers.hpp b/src/nwb/RecordingContainers.hpp index aa003086..f38d84c7 100644 --- a/src/nwb/RecordingContainers.hpp +++ b/src/nwb/RecordingContainers.hpp @@ -71,7 +71,7 @@ class RecordingContainers const void* timestamps); /** - * @brief Write ElectricalSereis data to a recordingContainer dataset. + * @brief Write ElectricalSeries data to a recordingContainer dataset. * @param containerInd The index of the electrical series dataset within the * electrical series group. * @param channel The channel index to use for writing timestamps. @@ -89,6 +89,22 @@ class RecordingContainers const void* data, const void* timestamps); + /** + * @brief Write SpikeEventSeries data to a recordingContainer dataset. + * @param containerInd The index of the SpikeEventSeries dataset within the + * SpikeEventSeries containers. + * @param numSamples Number of samples in the time for the single event. + * @param numChannels Number of channels in the time for the single event. + * @param data A pointer to the data block. + * @param timestamps A pointer to the timestamps block + * @return The status of the write operation. + */ + Status writeSpikeEventData(const SizeType& containerInd, + const SizeType& numSamples, + const SizeType& numChannels, + const void* data, + const void* timestamps); + std::vector> containers; std::string name; }; diff --git a/src/nwb/ecephys/SpikeEventSeries.cpp b/src/nwb/ecephys/SpikeEventSeries.cpp new file mode 100644 index 00000000..7c0c53ce --- /dev/null +++ b/src/nwb/ecephys/SpikeEventSeries.cpp @@ -0,0 +1,60 @@ +#include "nwb/ecephys/SpikeEventSeries.hpp" + +using namespace AQNWB::NWB; + +// SpikeEventSeries + +/** Constructor */ +SpikeEventSeries::SpikeEventSeries(const std::string& path, + std::shared_ptr io, + const BaseDataType& dataType, + const Types::ChannelVector& channelVector, + const std::string& description, + const SizeArray& dsetSize, + const SizeArray& chunkSize, + const float& conversion, + const float& resolution, + const float& offset) + : ElectricalSeries(path, + io, + dataType, + channelVector, + description, + dsetSize, + chunkSize, + conversion, + resolution, + offset) +{ +} + +/** Destructor */ +SpikeEventSeries::~SpikeEventSeries() {} + +void SpikeEventSeries::initialize() +{ + ElectricalSeries::initialize(); + + this->eventsRecorded = 0; +} + +Status SpikeEventSeries::writeSpike(const SizeType& numSamples, + const SizeType& numChannels, + const void* data, + const void* timestamps) +{ + // get offsets and datashape + std::vector dataShape; + std::vector positionOffset; + if (numChannels == 1) { + dataShape = {1, numSamples}; + positionOffset = {this->eventsRecorded, 0}; + } else { + dataShape = {1, numChannels, numSamples}; + positionOffset = {this->eventsRecorded, 0, 0}; + } + this->eventsRecorded += 1; + + // write channel data + return writeData(dataShape, positionOffset, data, timestamps); +} \ No newline at end of file diff --git a/src/nwb/ecephys/SpikeEventSeries.hpp b/src/nwb/ecephys/SpikeEventSeries.hpp new file mode 100644 index 00000000..ad2dd2ae --- /dev/null +++ b/src/nwb/ecephys/SpikeEventSeries.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include + +#include "BaseIO.hpp" +#include "Channel.hpp" +#include "nwb/ecephys/ElectricalSeries.hpp" + +namespace AQNWB::NWB +{ +/** + * @brief Stores snapshots/snippets of recorded spike events (i.e., threshold + * crossings). + */ +class SpikeEventSeries : public ElectricalSeries +{ +public: + /** + * @brief Constructor. + * @param path The location of the SpikeEventSeries in the file. + * @param io A shared pointer to the IO object. + * @param description The description of the SpikeEventSeries, should describe + * how events were detected. + */ + SpikeEventSeries(const std::string& path, + std::shared_ptr io, + const BaseDataType& dataType, + const Types::ChannelVector& channelVector, + const std::string& description, + const SizeArray& dsetSize, + const SizeArray& chunkSize, + const float& conversion = 1.0f, + const float& resolution = -1.0f, + const float& offset = 0.0f); + + /** + * @brief Destructor + */ + ~SpikeEventSeries(); + + /** + * @brief Initializes the Electrical Series + */ + void initialize(); + + /** + * @brief Write a single spike series event + * @param numSamples The number of samples in the event + * @param numChannels The number of channels in the event + * @param data The data of the event + * @param timestamps The timestamps of the event + * @param + */ + Status writeSpike(const SizeType& numSamples, + const SizeType& numChannels, + const void* data, + const void* timestamps); + +private: + /** + * @brief The neurodataType of the SpikeEventSeries. + */ + std::string neurodataType = "SpikeEventSeries"; + + /** + * @brief The number of events already written. + */ + SizeType eventsRecorded; +}; +} // namespace AQNWB::NWB diff --git a/tests/examples/testWorkflowExamples.cpp b/tests/examples/testWorkflowExamples.cpp index b04fd73f..64c7d309 100644 --- a/tests/examples/testWorkflowExamples.cpp +++ b/tests/examples/testWorkflowExamples.cpp @@ -27,6 +27,8 @@ TEST_CASE("workflowExamples") std::vector mockRecordingArrays = getMockChannelArrays(); + std::vector mockChannelNames = + getMockChannelArrayNames("esdata"); std::vector> mockData = getMockData2D(numSamples, numChannels); std::vector mockTimestamps = getMockTimestamps(numSamples); @@ -50,6 +52,7 @@ TEST_CASE("workflowExamples") // [example_workflow_datasets_snippet] std::vector containerIndexes; nwbfile->createElectricalSeries(mockRecordingArrays, + mockChannelNames, BaseDataType::I16, recordingContainers.get(), containerIndexes); diff --git a/tests/testEcephys.cpp b/tests/testEcephys.cpp index 48737171..97b9418f 100644 --- a/tests/testEcephys.cpp +++ b/tests/testEcephys.cpp @@ -9,6 +9,7 @@ #include "hdf5/HDF5IO.hpp" #include "nwb/device/Device.hpp" #include "nwb/ecephys/ElectricalSeries.hpp" +#include "nwb/ecephys/SpikeEventSeries.hpp" #include "nwb/file/ElectrodeGroup.hpp" #include "nwb/file/ElectrodeTable.hpp" #include "testUtils.hpp" @@ -32,16 +33,6 @@ TEST_CASE("ElectricalSeries", "[ecephys]") std::string devicePath = "/device"; std::string electrodePath = "/elecgroup/"; - SECTION("test initialization") - { - // TODO - } - - SECTION("test linking to electrode table region") - { - // TODO - } - SECTION("test writing channels") { // setup io object @@ -164,3 +155,138 @@ TEST_CASE("ElectricalSeries", "[ecephys]") REQUIRE_THAT(dataOut[1], Catch::Matchers::Approx(mockData[1]).margin(1)); } } + +TEST_CASE("SpikeEventSeries", "[ecephys]") +{ + // setup recording info + SizeType numSamples = 32; + SizeType numEvents = 10; + std::string dataPath = "/sesdata"; + BaseDataType dataType = BaseDataType::F32; + std::vector mockTimestamps = getMockTimestamps(numEvents, 1); + std::string devicePath = "/device"; + std::string electrodePath = "/elecgroup/"; + + SECTION("test writing events - events x channels x samples") + { + // setup mock data + SizeType numChannels = 4; + std::vector mockArrays = + getMockChannelArrays(numChannels); + std::vector> mockData = + getMockData2D(numSamples * numChannels, numEvents); + + // setup io object + std::string path = getTestFilePath("SpikeEventSeries3D.h5"); + std::shared_ptr io = createIO("HDF5", path); + io->open(); + io->createGroup("/general"); + io->createGroup("/general/extracellular_ephys"); + + // setup electrode table, device, and electrode group + NWB::ElectrodeTable elecTable = NWB::ElectrodeTable(io); + elecTable.initialize(); + + // setup electrical series + NWB::SpikeEventSeries ses = + NWB::SpikeEventSeries(dataPath, + io, + dataType, + mockArrays[0], + "no description", + SizeArray {0, numChannels, numSamples}, + SizeArray {8, 1, 1}); + ses.initialize(); + + // write channel data + for (SizeType e = 0; e < numEvents; ++e) { + double timestamp = mockTimestamps[e]; + ses.writeSpike(numSamples, numChannels, mockData[e].data(), ×tamp); + } + io->close(); + + // Read data back from file + std::unique_ptr file = + std::make_unique(path, H5F_ACC_RDONLY); + std::unique_ptr dataset = + std::make_unique(file->openDataSet(dataPath + "/data")); + std::vector> dataOut( + numEvents, std::vector(numSamples * numChannels)); + float* buffer = new float[numEvents * numSamples * numChannels]; + + H5::DataSpace fSpace = dataset->getSpace(); + hsize_t dims[3]; + fSpace.getSimpleExtentDims(dims, NULL); + hsize_t memdims = dims[0] * dims[1] * dims[2]; + dataset->read(buffer, H5::PredType::NATIVE_FLOAT, fSpace, fSpace); + + for (SizeType i = 0; i < numEvents; ++i) { + for (SizeType j = 0; j < (numSamples * numChannels); ++j) { + dataOut[i][j] = buffer[i * (numSamples * numChannels) + j]; + } + } + delete[] buffer; + REQUIRE_THAT(dataOut[0], Catch::Matchers::Approx(mockData[0]).margin(1)); + REQUIRE_THAT(dataOut[1], Catch::Matchers::Approx(mockData[1]).margin(1)); + } + + SECTION("test writing events - events x samples") + { + // setup mock data + std::vector mockArrays = getMockChannelArrays(1); + std::vector> mockData = + getMockData2D(numSamples, numEvents); + + // setup io object + std::string path = getTestFilePath("SpikeEventSeries2D.h5"); + std::shared_ptr io = createIO("HDF5", path); + io->open(); + io->createGroup("/general"); + io->createGroup("/general/extracellular_ephys"); + + // setup electrode table, device, and electrode group + NWB::ElectrodeTable elecTable = NWB::ElectrodeTable(io); + elecTable.initialize(); + + // setup electrical series + NWB::SpikeEventSeries ses = NWB::SpikeEventSeries(dataPath, + io, + dataType, + mockArrays[0], + "no description", + SizeArray {0, numSamples}, + SizeArray {8, 1}); + ses.initialize(); + + // write channel data + for (SizeType e = 0; e < numEvents; ++e) { + double timestamp = mockTimestamps[e]; + ses.writeSpike(numSamples, 1, mockData[e].data(), ×tamp); + } + io->close(); + + // Read data back from file + std::unique_ptr file = + std::make_unique(path, H5F_ACC_RDONLY); + std::unique_ptr dataset = + std::make_unique(file->openDataSet(dataPath + "/data")); + std::vector> dataOut(numEvents, + std::vector(numSamples)); + float* buffer = new float[numEvents * numSamples]; + + H5::DataSpace fSpace = dataset->getSpace(); + hsize_t dims[3]; + fSpace.getSimpleExtentDims(dims, NULL); + hsize_t memdims = dims[0] * dims[1] * dims[2]; + dataset->read(buffer, H5::PredType::NATIVE_FLOAT, fSpace, fSpace); + + for (SizeType i = 0; i < numEvents; ++i) { + for (SizeType j = 0; j < (numSamples); ++j) { + dataOut[i][j] = buffer[i * (numSamples) + j]; + } + } + delete[] buffer; + REQUIRE_THAT(dataOut[0], Catch::Matchers::Approx(mockData[0]).margin(1)); + REQUIRE_THAT(dataOut[1], Catch::Matchers::Approx(mockData[1]).margin(1)); + } +} \ No newline at end of file diff --git a/tests/testNWBFile.cpp b/tests/testNWBFile.cpp index 4a6f0d45..fe6b50ba 100644 --- a/tests/testNWBFile.cpp +++ b/tests/testNWBFile.cpp @@ -6,6 +6,7 @@ #include "nwb/NWBFile.hpp" #include "nwb/RecordingContainers.hpp" #include "nwb/base/TimeSeries.hpp" +#include "nwb/ecephys/SpikeEventSeries.hpp" #include "testUtils.hpp" using namespace AQNWB; @@ -32,10 +33,15 @@ TEST_CASE("createElectricalSeries", "[nwb]") // create Electrical Series std::vector mockArrays = getMockChannelArrays(1, 2); + std::vector mockChannelNames = + getMockChannelArrayNames("esdata"); std::unique_ptr recordingContainers = std::make_unique(); - Status resultCreate = nwbfile.createElectricalSeries( - mockArrays, BaseDataType::F32, recordingContainers.get()); + Status resultCreate = + nwbfile.createElectricalSeries(mockArrays, + mockChannelNames, + BaseDataType::F32, + recordingContainers.get()); REQUIRE(resultCreate == Status::Success); // start recording @@ -60,6 +66,71 @@ TEST_CASE("createElectricalSeries", "[nwb]") nwbfile.finalize(); } +TEST_CASE("createMultipleEcephysDatasets", "[nwb]") +{ + std::string filename = getTestFilePath("createESandSES.nwb"); + + // initialize nwbfile object and create base structure + std::shared_ptr io = std::make_shared(filename); + NWB::NWBFile nwbfile(generateUuid(), io); + nwbfile.initialize(); + + // create Electrical Series + std::vector mockArrays = getMockChannelArrays(1, 2); + std::vector mockChannelNames = + getMockChannelArrayNames("esdata"); + std::unique_ptr recordingContainers = + std::make_unique(); + Status resultCreateES = + nwbfile.createElectricalSeries(mockArrays, + mockChannelNames, + BaseDataType::F32, + recordingContainers.get()); + REQUIRE(resultCreateES == Status::Success); + + // create SpikeEventSeries + SizeType numSamples = 5; + std::vector mockSpikeChannelNames = + getMockChannelArrayNames("spikedata"); + Status resultCreateSES = + nwbfile.createSpikeEventSeries(mockArrays, + mockSpikeChannelNames, + BaseDataType::F32, + recordingContainers.get()); + + // start recording + Status resultStart = io->startRecording(); + REQUIRE(resultStart == Status::Success); + + // write electrical series data + std::vector mockData = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + std::vector mockTimestamps = {0.1, 0.2, 0.3, 0.4, 0.5}; + std::vector positionOffset = {0, 0}; + std::vector dataShape = {mockData.size(), 0}; + + NWB::TimeSeries* ts0 = + static_cast(recordingContainers->getContainer(0)); + ts0->writeData( + dataShape, positionOffset, mockData.data(), mockTimestamps.data()); + NWB::TimeSeries* ts1 = + static_cast(recordingContainers->getContainer(1)); + ts1->writeData( + dataShape, positionOffset, mockData.data(), mockTimestamps.data()); + + // write spike event series data + SizeType numEvents = 10; + NWB::SpikeEventSeries* ses0 = + static_cast(recordingContainers->getContainer(2)); + NWB::SpikeEventSeries* ses1 = + static_cast(recordingContainers->getContainer(3)); + for (SizeType i = 0; i < numEvents; ++i) { + ses0->writeSpike(numSamples, 1, mockData.data(), &mockTimestamps[0]); + ses1->writeSpike(numSamples, 1, mockData.data(), &mockTimestamps[0]); + } + + nwbfile.finalize(); +} + TEST_CASE("setCanModifyObjectsMode", "[nwb]") { std::string filename = getTestFilePath("testCanModifyObjectsMode.nwb"); @@ -79,8 +150,10 @@ TEST_CASE("setCanModifyObjectsMode", "[nwb]") // test that dataset creation fails after starting the recording std::vector mockArrays = getMockChannelArrays(1, 2); - Status resultCreatePostStart = - nwbfile.createElectricalSeries(mockArrays, BaseDataType::F32); + std::vector mockChannelNames = + getMockChannelArrayNames("esdata"); + Status resultCreatePostStart = nwbfile.createElectricalSeries( + mockArrays, mockChannelNames, BaseDataType::F32); REQUIRE(resultCreatePostStart == Status::Failure); // stop recording diff --git a/tests/testRecordingWorkflow.cpp b/tests/testRecordingWorkflow.cpp index 3a90c5fa..d5c8d05a 100644 --- a/tests/testRecordingWorkflow.cpp +++ b/tests/testRecordingWorkflow.cpp @@ -29,6 +29,8 @@ TEST_CASE("writeContinuousData", "[recording]") std::vector mockRecordingArrays = getMockChannelArrays(); + std::vector mockChannelNames = + getMockChannelArrayNames("esdata"); std::vector> mockData = getMockData2D(numSamples, numChannels); std::vector mockTimestamps = getMockTimestamps(numSamples); @@ -47,8 +49,10 @@ TEST_CASE("writeContinuousData", "[recording]") nwbfile->initialize(); // 4. create datasets and add to recording containers - nwbfile->createElectricalSeries( - mockRecordingArrays, BaseDataType::F32, recordingContainers.get()); + nwbfile->createElectricalSeries(mockRecordingArrays, + mockChannelNames, + BaseDataType::F32, + recordingContainers.get()); // 5. start the recording io->startRecording(); @@ -94,7 +98,7 @@ TEST_CASE("writeContinuousData", "[recording]") nwbfile->finalize(); // check contents of data - std::string dataPath = "/acquisition/array0/data"; + std::string dataPath = "/acquisition/esdata0/data"; std::unique_ptr file = std::make_unique(path, H5F_ACC_RDONLY); std::unique_ptr dataset = @@ -120,7 +124,7 @@ TEST_CASE("writeContinuousData", "[recording]") REQUIRE_THAT(dataOut[1], Catch::Matchers::Approx(mockData[1]).margin(1)); // check contents of timestamps - std::string timestampsPath = "/acquisition/array0/timestamps"; + std::string timestampsPath = "/acquisition/esdata0/timestamps"; std::unique_ptr tsDataset = std::make_unique(file->openDataSet(timestampsPath)); double* tsBuffer = new double[numSamples]; diff --git a/tests/testUtils.hpp b/tests/testUtils.hpp index e8acc658..90440919 100644 --- a/tests/testUtils.hpp +++ b/tests/testUtils.hpp @@ -33,14 +33,16 @@ inline std::string getTestFilePath(std::string filename) } inline std::vector getMockChannelArrays( - SizeType numChannels = 2, SizeType numArrays = 2) + SizeType numChannels = 2, + SizeType numArrays = 2, + std::string groupName = "array") { std::vector arrays(numArrays); for (SizeType i = 0; i < numArrays; i++) { std::vector chGroup; for (SizeType j = 0; j < numChannels; j++) { Channel ch("ch" + std::to_string(j), - "array" + std::to_string(i), + groupName + std::to_string(i), i, j, i * numArrays + j); @@ -51,6 +53,16 @@ inline std::vector getMockChannelArrays( return arrays; } +inline std::vector getMockChannelArrayNames( + std::string baseName = "esdata", SizeType numArrays = 2) +{ + std::vector arrayNames(numArrays); + for (SizeType i = 0; i < numArrays; i++) { + arrayNames[i] = baseName + std::to_string(i); + } + return arrayNames; +} + inline std::vector getMockData1D(SizeType numSamples = 1000) { std::vector mockData(numSamples);