diff --git a/CMakeLists.txt b/CMakeLists.txt index bf84abc2..e46e90f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,7 @@ add_library( src/Channel.cpp src/hdf5/HDF5IO.cpp src/nwb/NWBFile.cpp - src/nwb/NWBRecording.cpp + src/nwb/RecordingContainers.cpp src/nwb/base/TimeSeries.cpp src/nwb/device/Device.cpp src/nwb/ecephys/ElectricalSeries.cpp diff --git a/docs/pages/1_userdocs.dox b/docs/pages/1_userdocs.dox index 20ebced1..ec3784fb 100644 --- a/docs/pages/1_userdocs.dox +++ b/docs/pages/1_userdocs.dox @@ -5,5 +5,6 @@ * with a particular data acquisition software via AqNWB. * * - \subpage user_install_page + * - \subpage workflow * - \subpage hdf5io */ diff --git a/docs/pages/userdocs/workflow.dox b/docs/pages/userdocs/workflow.dox new file mode 100644 index 00000000..106f46ff --- /dev/null +++ b/docs/pages/userdocs/workflow.dox @@ -0,0 +1,102 @@ +/** + * \page workflow AqNWB Workflow + * + * \tableofcontents + * + * \section recording_workflow Overview of a recording workflow + * + * For users wanting to integrate NWB with a particular data acquisition software, here + * we outline the steps for a single recording from file creation to saving. + * + * 1. Create the I/O object (e.g,. \ref AQNWB::HDF5::HDF5IO "HDF5IO") used for + * writing data to the file on disk. + * 2. Create the \ref AQNWB::NWB::RecordingContainers "RecordingContainers" object + * used for managing \ref AQNWB::NWB::Container "Container" objects for storing recordings. + * 3. Create the \ref AQNWB::NWB::NWBFile "NWBFile" object used for managing and creating NWB + * file contents. + * 4. Create the \ref AQNWB::NWB::Container "Container" objects (e.g., + * \ref AQNWB::NWB::ElectricalSeries "ElectricalSeries") used for recording and add them + * to the \ref AQNWB::NWB::RecordingContainers "RecordingContainers". + * 5. Start the recording. + * 6. Write data. + * 7. Stop the recording and close the \ref AQNWB::NWB::NWBFile "NWBFile". + * + * Below, we walk through these steps in more detail. + * + * + * \subsection create_io 1. Create the I/O object. + * + * First, create an I/O object (e.g., \ref AQNWB::HDF5::HDF5IO "HDF5IO") used for writing + * data to the file. AqNWB provides the convenience method, \ref AQNWB::createIO "createIO" + * to create this object using one of the supported backends. For more fine-grained + * control of different backend parameters, you can create your own `std::shared_ptr` + * using any of the derived \ref AQNWB::BaseIO "BaseIO" classes. + * + * \snippet tests/examples/testWorkflowExamples.cpp example_workflow_io_snippet + * + * + * \subsection create_recording_container 2. Create the RecordingContainer object. + * + * Next, create a \ref AQNWB::NWB::RecordingContainers "RecordingContainers" object to manage the + * different \ref AQNWB::NWB::Container "Container" objects with the datasets that you would + * like to write data to. + * + * \snippet tests/examples/testWorkflowExamples.cpp example_workflow_recording_containers_snippet + * + * + * \subsection create_nwbfile 3. Create the NWBFile + * + * Next, constructs the \ref AQNWB::NWB::NWBFile "NWBFile" object, using the I/O object as an input. + * Then, initialize the object to create the basic file structure of the NWBFile. + * + * \snippet tests/examples/testWorkflowExamples.cpp example_workflow_nwbfile_snippet + * + * + * \subsection create_datasets 4. Create datasets and add to RecordingContainers. + * + * Next, create the different data types (e.g. \ref AQNWB::NWB::ElectricalSeries "ElectricalSeries" + * or other AQNWB::NWB::TimeSeries "TimeSeries") that you would like to write data into. After + * creation, these objects are added to the \ref AQNWB::NWB::RecordingContainers "RecordingContainers" + * object so that it can mana ge access and data writing during the recording process. + * When adding containers, ownership of the \ref AQNWB::NWB::Container "Container" is transferred to the + * \ref AQNWB::NWB::RecordingContainers "RecordingContainers" object, so that we can access it again via + * its index. New containers will always be appended to the end of the + * \ref AQNWB::NWB::RecordingContainers::containers "containers" object and their index can be tracked + * using the size of the input `recordingArrays`. + * + * \snippet tests/examples/testWorkflowExamples.cpp example_workflow_datasets_snippet + * + * + * \subsection start_recording 5. Start the recording. + * + * Then, start the recording process with a call to the ``startRecording`` function of the I/O object. + * + * \note + * When using \ref AQNWB::HDF5::HDF5IO "HDF5IO" for writing to HDF5, calling + * \ref AQNWB::HDF5::HDF5IO::startRecording "startRecording" will by default enable + * \ref hdf5io_swmr "SWMR mode" to ensure file integrity and support concurrent read. + * As a result, no additional datasets or groups can be added to the file once a recording + * has been started unless the file is is closed and reopened. + * + * \snippet tests/examples/testWorkflowExamples.cpp example_workflow_start_snippet + * + * + * \subsection write_data 6. Write data. + * + * During the recording process, use the \ref AQNWB::NWB::RecordingContainers "RecordingContainers" + * as an interface to access the various \ref AQNWB::NWB::Container "Container" object and corresponding + * datasets and write blocks of data to the file. Calling `flush()` on the I/O object at any time will + * ensure the data is moved to disk. + * + * \snippet tests/examples/testWorkflowExamples.cpp example_workflow_write_snippet + * + * + * \subsection stop_recording 7. Stop the recording and finalize the file. + * + * When the recording process is finished, call `stopRecording` from the I/O object + * to flush any data and close the file. + * + * \snippet tests/examples/testWorkflowExamples.cpp example_workflow_stop_snippet + * + * + */ diff --git a/src/Utils.hpp b/src/Utils.hpp index b1a85813..f98d5b30 100644 --- a/src/Utils.hpp +++ b/src/Utils.hpp @@ -58,11 +58,11 @@ inline std::string getCurrentTime() * @brief Factory method to create an IO object. * @return A pointer to a BaseIO object */ -inline std::unique_ptr createIO(const std::string& type, +inline std::shared_ptr createIO(const std::string& type, const std::string& filename) { if (type == "HDF5") { - return std::make_unique(filename); + return std::make_shared(filename); } else { throw std::invalid_argument("Invalid IO type"); } diff --git a/src/nwb/NWBFile.cpp b/src/nwb/NWBFile.cpp index 132840ca..007bee3c 100644 --- a/src/nwb/NWBFile.cpp +++ b/src/nwb/NWBFile.cpp @@ -23,6 +23,8 @@ using namespace AQNWB::NWB; constexpr SizeType CHUNK_XSIZE = 2048; +std::vector NWBFile::emptyContainerIndexes = {}; + // NWBFile NWBFile::NWBFile(const std::string& idText, std::shared_ptr io) @@ -45,7 +47,6 @@ Status NWBFile::initialize() Status NWBFile::finalize() { - recordingContainers.reset(); return io->close(); } @@ -93,7 +94,9 @@ Status NWBFile::createFileStructure() Status NWBFile::createElectricalSeries( std::vector recordingArrays, - const BaseDataType& dataType) + const BaseDataType& dataType, + RecordingContainers* recordingContainers, + std::vector& containerIndexes) { if (!io->canModifyObjects()) { return Status::Failure; @@ -132,7 +135,8 @@ Status NWBFile::createElectricalSeries( SizeArray {0, channelVector.size()}, SizeArray {CHUNK_XSIZE, 0}); electricalSeries->initialize(); - recordingContainers->addData(std::move(electricalSeries)); + recordingContainers->addContainer(std::move(electricalSeries)); + containerIndexes.push_back(recordingContainers->containers.size() - 1); // Add electrode information to electrode table (does not write to datasets // yet) @@ -145,16 +149,6 @@ Status NWBFile::createElectricalSeries( return Status::Success; } -Status NWBFile::startRecording() -{ - return io->startRecording(); -} - -void NWBFile::stopRecording() -{ - io->stopRecording(); -} - template void NWBFile::cacheSpecifications( const std::string& specPath, @@ -182,26 +176,3 @@ std::unique_ptr NWBFile::createRecordingData( return std::unique_ptr( io->createArrayDataSet(type, size, chunking, path)); } - -TimeSeries* NWBFile::getTimeSeries(const SizeType& timeseriesInd) -{ - if (timeseriesInd >= this->recordingContainers->containers.size()) { - return nullptr; - } else { - return this->recordingContainers->containers[timeseriesInd].get(); - } -} - -// Recording Container - -RecordingContainers::RecordingContainers(const std::string& name) - : name(name) -{ -} - -RecordingContainers::~RecordingContainers() {} - -void RecordingContainers::addData(std::unique_ptr data) -{ - this->containers.push_back(std::move(data)); -} diff --git a/src/nwb/NWBFile.hpp b/src/nwb/NWBFile.hpp index 08a22d41..277f45ab 100644 --- a/src/nwb/NWBFile.hpp +++ b/src/nwb/NWBFile.hpp @@ -9,6 +9,7 @@ #include "BaseIO.hpp" #include "Types.hpp" +#include "nwb/RecordingContainers.hpp" #include "nwb/base/TimeSeries.hpp" /*! @@ -18,8 +19,6 @@ namespace AQNWB::NWB { -class RecordingContainers; // declare here because gets used in NWBFile class - /** * @brief The NWBFile class provides an interface for setting up and managing * the NWB file. @@ -69,28 +68,17 @@ class NWBFile * @param recordingArrays vector of ChannelVector indicating the electrodes to * record from. A separate ElectricalSeries will be * created for each ChannelVector. + * @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, - const BaseDataType& dataType = BaseDataType::I16); - - /** - * @brief Starts the recording. - */ - Status startRecording(); - - /** - * @brief Stops the recording. - */ - void stopRecording(); - - /** - * @brief Gets the TimeSeries object from the recordingContainers - * @param timeseriesInd The index of the timeseries dataset within the group. - */ - TimeSeries* getTimeSeries(const SizeType& timeseriesInd); + const BaseDataType& dataType = BaseDataType::I16, + RecordingContainers* recordingContainers = nullptr, + std::vector& containerIndexes = emptyContainerIndexes); protected: /** @@ -133,53 +121,9 @@ class NWBFile const std::array, N>& specVariables); - /** - * @brief Holds the Container (usually TimeSeries) objects that have been - * created in the nwb file for recording. - */ - std::unique_ptr recordingContainers = - std::make_unique("RecordingContainers"); - const std::string identifierText; std::shared_ptr io; + static std::vector emptyContainerIndexes; }; -/** - * @brief The RecordingContainers class provides an interface for managing - * groups of TimeSeries acquired during a recording. - */ -class RecordingContainers -{ -public: - /** - * @brief Constructor for RecordingContainer class. - * @param name The name of the group of time series - */ - RecordingContainers(const std::string& name); - - /** - * @brief Deleted copy constructor to prevent construction-copying. - */ - RecordingContainers(const RecordingContainers&) = delete; - - /** - * @brief Deleted copy assignment operator to prevent copying. - */ - RecordingContainers& operator=(const RecordingContainers&) = delete; - - /** - * @brief Destructor for RecordingContainer class. - */ - ~RecordingContainers(); - - /** - * @brief Adds a TimeSeries object to the container. - * @param data The TimeSeries object to add. - */ - void addData(std::unique_ptr data); - - std::vector> containers; - std::string name; -}; - -} // namespace AQNWB::NWB +} // namespace AQNWB::NWB \ No newline at end of file diff --git a/src/nwb/NWBRecording.cpp b/src/nwb/NWBRecording.cpp deleted file mode 100644 index 1747471b..00000000 --- a/src/nwb/NWBRecording.cpp +++ /dev/null @@ -1,69 +0,0 @@ - -#include "nwb/NWBRecording.hpp" - -#include "Channel.hpp" -#include "Utils.hpp" -#include "hdf5/HDF5IO.hpp" - -using namespace AQNWB::NWB; - -// NWBRecordingEngine -NWBRecording::NWBRecording() {} - -NWBRecording::~NWBRecording() -{ - if (nwbfile != nullptr) { - nwbfile->finalize(); - } -} - -Status NWBRecording::openFile(const std::string& filename, - std::vector recordingArrays, - const std::string& IOType) -{ - // close any existing files - if (nwbfile != nullptr) { - this->closeFile(); - } - - // initialize nwbfile object and create base structure - nwbfile = std::make_unique(generateUuid(), - createIO(IOType, filename)); - nwbfile->initialize(); - - // create the datasets - nwbfile->createElectricalSeries(recordingArrays); - - // start the new recording - return nwbfile->startRecording(); -} - -void NWBRecording::closeFile() -{ - nwbfile->stopRecording(); - nwbfile->finalize(); -} - -Status NWBRecording::writeTimeseriesData( - const std::string& containerName, - const SizeType& timeseriesInd, - const Channel& channel, - const std::vector& dataShape, - const std::vector& positionOffset, - const void* data, - const void* timestamps) -{ - TimeSeries* ts = nwbfile->getTimeSeries(timeseriesInd); - - if (ts == nullptr) - return Status::Failure; - - // write data and timestamps to datasets - if (channel.localIndex == 0) { - // write with timestamps if it's the first channel - return ts->writeData(dataShape, positionOffset, data, timestamps); - } else { - // write without timestamps if its another channel in the same timeseries - return ts->writeData(dataShape, positionOffset, data); - } -} diff --git a/src/nwb/NWBRecording.hpp b/src/nwb/NWBRecording.hpp deleted file mode 100644 index c24edd3b..00000000 --- a/src/nwb/NWBRecording.hpp +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#include "Types.hpp" -#include "nwb/NWBFile.hpp" - -namespace AQNWB::NWB -{ -/** - * @brief The NWBRecording class manages the recording process - */ - -class NWBRecording -{ -public: - /** - * @brief Default constructor for NWBRecording. - */ - NWBRecording(); - - /** - * @brief Deleted copy constructor to prevent construction-copying. - */ - NWBRecording(const NWBRecording&) = delete; - - /** - * @brief Deleted copy assignment operator to prevent copying. - */ - NWBRecording& operator=(const NWBRecording&) = delete; - - /** - * @brief Destructor for NWBRecordingEngine. - */ - ~NWBRecording(); - - /** - * @brief Opens the file for recording. - * @param filename The name of the file to open. - * @param recordingArrays ChannelVector objects indicating the electrodes to - * use for ElectricalSeries recordings - * @param IOType Type of backend IO to use - */ - Status openFile(const std::string& filename, - std::vector recordingArrays, - const std::string& IOType = "HDF5"); - - /** - * @brief Closes the file and performs necessary cleanup when recording - * stops. - */ - void closeFile(); - - /** - * @brief Write timeseries to an NWB file. - * @param containerName The name of the timeseries group to write to. - * @param timeseriesInd The index of the timeseries dataset within the - * timeseries group. - * @param channel The channel index to use for writing timestamps. - * @param dataShape The size of the data block. - * @param positionOffset The position of the data block to write to. - * @param data A pointer to the data block. - * @param timestamps A pointer to the timestamps block. May be null if - * multidimensional TimeSeries and only need to write the timestamps once but - * write data multiple times. - * @return The status of the write operation. - */ - Status writeTimeseriesData(const std::string& containerName, - const SizeType& timeseriesInd, - const Channel& channel, - const std::vector& dataShape, - const std::vector& positionOffset, - const void* data, - const void* timestamps); - -private: - /** - * @brief Pointer to the current NWB file. - */ - std::unique_ptr nwbfile; -}; -} // namespace AQNWB::NWB diff --git a/src/nwb/RecordingContainers.cpp b/src/nwb/RecordingContainers.cpp new file mode 100644 index 00000000..d7464bca --- /dev/null +++ b/src/nwb/RecordingContainers.cpp @@ -0,0 +1,65 @@ + +#include "nwb/RecordingContainers.hpp" + +#include "nwb/ecephys/ElectricalSeries.hpp" +#include "nwb/hdmf/base/Container.hpp" + +using namespace AQNWB::NWB; +// Recording Container + +RecordingContainers::RecordingContainers() {} + +RecordingContainers::~RecordingContainers() {} + +void RecordingContainers::addContainer(std::unique_ptr container) +{ + this->containers.push_back(std::move(container)); +} + +Container* RecordingContainers::getContainer(const SizeType& containerInd) +{ + if (containerInd >= this->containers.size()) { + return nullptr; + } else { + return this->containers[containerInd].get(); + } +} + +Status RecordingContainers::writeTimeseriesData( + const SizeType& containerInd, + const Channel& channel, + const std::vector& dataShape, + const std::vector& positionOffset, + const void* data, + const void* timestamps) +{ + TimeSeries* ts = dynamic_cast(getContainer(containerInd)); + + if (ts == nullptr) + return Status::Failure; + + // write data and timestamps to datasets + if (channel.localIndex == 0) { + // write with timestamps if it's the first channel + return ts->writeData(dataShape, positionOffset, data, timestamps); + } else { + // write without timestamps if its another channel in the same timeseries + return ts->writeData(dataShape, positionOffset, data); + } +} + +Status RecordingContainers::writeElectricalSeriesData( + const SizeType& containerInd, + const Channel& channel, + const SizeType& numSamples, + const void* data, + const void* timestamps) +{ + ElectricalSeries* es = + dynamic_cast(getContainer(containerInd)); + + if (es == nullptr) + return Status::Failure; + + es->writeChannel(channel.localIndex, numSamples, data, timestamps); +} diff --git a/src/nwb/RecordingContainers.hpp b/src/nwb/RecordingContainers.hpp new file mode 100644 index 00000000..aa003086 --- /dev/null +++ b/src/nwb/RecordingContainers.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include "Channel.hpp" +#include "Types.hpp" +#include "nwb/base/TimeSeries.hpp" + +namespace AQNWB::NWB +{ + +/** + * @brief The RecordingContainers class provides an interface for managing + * and holding groups of Containers acquired during a recording. + */ + +class RecordingContainers +{ +public: + /** + * @brief Constructor for RecordingContainer class. + */ + RecordingContainers(); + + /** + * @brief Deleted copy constructor to prevent construction-copying. + */ + RecordingContainers(const RecordingContainers&) = delete; + + /** + * @brief Deleted copy assignment operator to prevent copying. + */ + RecordingContainers& operator=(const RecordingContainers&) = delete; + + /** + * @brief Destructor for RecordingContainer class. + */ + ~RecordingContainers(); + + /** + * @brief Adds a Container object to the container. Note that this function + * transfers ownership of the Container object to the RecordingContainers + * object, and should be called with the pattern + * recordingContainers.addContainer(std::move(container)). + * @param container The Container object to add. + */ + void addContainer(std::unique_ptr container); + + /** + * @brief Gets the Container object from the recordingContainers + * @param containerInd The index of the container dataset within the group. + */ + Container* getContainer(const SizeType& containerInd); + + /** + * @brief Write timeseries data to a recordingContainer dataset. + * @param containerInd The index of the timeseries dataset within the + * timeseries group. + * @param channel The channel index to use for writing timestamps. + * @param dataShape The size of the data block. + * @param positionOffset The position of the data block to write to. + * @param data A pointer to the data block. + * @param timestamps A pointer to the timestamps block. May be null if + * multidimensional TimeSeries and only need to write the timestamps once but + * write data multiple times. + * @return The status of the write operation. + */ + Status writeTimeseriesData(const SizeType& containerInd, + const Channel& channel, + const std::vector& dataShape, + const std::vector& positionOffset, + const void* data, + const void* timestamps); + + /** + * @brief Write ElectricalSereis 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. + * @param numSamples Number of samples in the time, i.e., the size of the + * first dimension of the data parameter + * @param data A pointer to the data block. + * @param timestamps A pointer to the timestamps block. May be null if + * multidimensional TimeSeries and only need to write the timestamps once but + * write data multiple times. + * @return The status of the write operation. + */ + Status writeElectricalSeriesData(const SizeType& containerInd, + const Channel& channel, + const SizeType& numSamples, + const void* data, + const void* timestamps); + + std::vector> containers; + std::string name; +}; + +} // namespace AQNWB::NWB diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b830774f..1add110d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,9 +17,10 @@ add_executable(aqnwb_test testFile.cpp testHDF5IO.cpp testNWBFile.cpp - testNWBRecording.cpp + testRecordingWorkflow.cpp examples/test_HDF5IO_examples.cpp examples/test_example.cpp + examples/testWorkflowExamples.cpp ) # Ensure the aqnwb_test target can include headers from the current directory diff --git a/tests/examples/testWorkflowExamples.cpp b/tests/examples/testWorkflowExamples.cpp new file mode 100644 index 00000000..b04fd73f --- /dev/null +++ b/tests/examples/testWorkflowExamples.cpp @@ -0,0 +1,108 @@ + +#include + +#include "BaseIO.hpp" +#include "Channel.hpp" +#include "Types.hpp" +#include "Utils.hpp" +#include "hdf5/HDF5IO.hpp" +#include "nwb/NWBFile.hpp" +#include "nwb/RecordingContainers.hpp" +#include "nwb/file/ElectrodeTable.hpp" +#include "testUtils.hpp" + +using namespace AQNWB; + +TEST_CASE("workflowExamples") +{ + SECTION("write workflow") + { + // 0. setup mock data + SizeType numChannels = 4; + SizeType numSamples = 300; + SizeType samplesRecorded = 0; + SizeType bufferSize = numSamples / 10; + std::vector dataBuffer(bufferSize); + std::vector timestampsBuffer(bufferSize); + + std::vector mockRecordingArrays = + getMockChannelArrays(); + std::vector> mockData = + getMockData2D(numSamples, numChannels); + std::vector mockTimestamps = getMockTimestamps(numSamples); + + std::string path = getTestFilePath("exampleRecording.nwb"); + // [example_workflow_io_snippet] + std::shared_ptr io = createIO("HDF5", path); + // [example_workflow_io_snippet] + + // [example_workflow_recording_containers_snippet] + std::unique_ptr recordingContainers = + std::make_unique(); + // [example_workflow_recording_containers_snippet] + + // [example_workflow_nwbfile_snippet] + std::unique_ptr nwbfile = + std::make_unique(generateUuid(), io); + nwbfile->initialize(); + // [example_workflow_nwbfile_snippet] + + // [example_workflow_datasets_snippet] + std::vector containerIndexes; + nwbfile->createElectricalSeries(mockRecordingArrays, + BaseDataType::I16, + recordingContainers.get(), + containerIndexes); + // [example_workflow_datasets_snippet] + + // [example_workflow_start_snippet] + io->startRecording(); + // [example_workflow_start_snippet] + + // write data during the recording + bool isRecording = true; + while (isRecording) { + // write data to the file for each channel + for (SizeType i = 0; i < containerIndexes.size(); ++i) { + const auto& channelVector = mockRecordingArrays[i]; + for (const auto& channel : channelVector) { + // copy data into buffer + std::copy(mockData[channel.globalIndex].begin() + samplesRecorded, + mockData[channel.globalIndex].begin() + samplesRecorded + + bufferSize, + dataBuffer.begin()); + std::copy(mockTimestamps.begin() + samplesRecorded, + mockTimestamps.begin() + samplesRecorded + bufferSize, + timestampsBuffer.begin()); + + // write timeseries data + std::vector positionOffset = {samplesRecorded, + channel.localIndex}; + std::vector dataShape = {dataBuffer.size(), 1}; + std::unique_ptr intBuffer = transformToInt16( + dataBuffer.size(), channel.getBitVolts(), dataBuffer.data()); + + // [example_workflow_write_snippet] + recordingContainers->writeTimeseriesData(containerIndexes[i], + channel, + dataShape, + positionOffset, + intBuffer.get(), + timestampsBuffer.data()); + io->flush(); + // [example_workflow_write_snippet] + } + } + // check if recording is done + samplesRecorded += dataBuffer.size(); + if (samplesRecorded >= numSamples) { + isRecording = false; + } + } + + // [example_workflow_stop_snippet] + io->stopRecording(); + nwbfile->finalize(); + // [example_workflow_stop_snippet] + } +} diff --git a/tests/testNWBFile.cpp b/tests/testNWBFile.cpp index bae35f42..4a6f0d45 100644 --- a/tests/testNWBFile.cpp +++ b/tests/testNWBFile.cpp @@ -4,6 +4,7 @@ #include "Utils.hpp" #include "hdf5/HDF5IO.hpp" #include "nwb/NWBFile.hpp" +#include "nwb/RecordingContainers.hpp" #include "nwb/base/TimeSeries.hpp" #include "testUtils.hpp" @@ -25,18 +26,20 @@ TEST_CASE("createElectricalSeries", "[nwb]") std::string filename = getTestFilePath("createElectricalSeries.nwb"); // initialize nwbfile object and create base structure - NWB::NWBFile nwbfile(generateUuid(), - std::make_unique(filename)); + std::shared_ptr io = std::make_shared(filename); + NWB::NWBFile nwbfile(generateUuid(), io); nwbfile.initialize(); // create Electrical Series std::vector mockArrays = getMockChannelArrays(1, 2); - Status resultCreate = - nwbfile.createElectricalSeries(mockArrays, BaseDataType::F32); + std::unique_ptr recordingContainers = + std::make_unique(); + Status resultCreate = nwbfile.createElectricalSeries( + mockArrays, BaseDataType::F32, recordingContainers.get()); REQUIRE(resultCreate == Status::Success); // start recording - Status resultStart = nwbfile.startRecording(); + Status resultStart = io->startRecording(); REQUIRE(resultStart == Status::Success); // write timeseries data @@ -45,10 +48,12 @@ TEST_CASE("createElectricalSeries", "[nwb]") std::vector positionOffset = {0, 0}; std::vector dataShape = {mockData.size(), 0}; - NWB::TimeSeries* ts0 = nwbfile.getTimeSeries(0); + NWB::TimeSeries* ts0 = + static_cast(recordingContainers->getContainer(0)); ts0->writeData( dataShape, positionOffset, mockData.data(), mockTimestamps.data()); - NWB::TimeSeries* ts1 = nwbfile.getTimeSeries(1); + NWB::TimeSeries* ts1 = + static_cast(recordingContainers->getContainer(1)); ts1->writeData( dataShape, positionOffset, mockData.data(), mockTimestamps.data()); @@ -60,12 +65,12 @@ TEST_CASE("setCanModifyObjectsMode", "[nwb]") std::string filename = getTestFilePath("testCanModifyObjectsMode.nwb"); // initialize nwbfile object and create base structure with HDF5IO object - NWB::NWBFile nwbfile(generateUuid(), - std::make_unique(filename)); + std::shared_ptr io = std::make_shared(filename); + NWB::NWBFile nwbfile(generateUuid(), io); nwbfile.initialize(); // start recording - Status resultStart = nwbfile.startRecording(); + Status resultStart = io->startRecording(); REQUIRE(resultStart == Status::Success); // test that modifying the file structure after starting the recording fails diff --git a/tests/testNWBRecording.cpp b/tests/testRecordingWorkflow.cpp similarity index 75% rename from tests/testNWBRecording.cpp rename to tests/testRecordingWorkflow.cpp index 9b43d034..cdcd9a9b 100644 --- a/tests/testNWBRecording.cpp +++ b/tests/testRecordingWorkflow.cpp @@ -8,7 +8,8 @@ #include "Types.hpp" #include "Utils.hpp" #include "hdf5/HDF5IO.hpp" -#include "nwb/NWBRecording.hpp" +#include "nwb/NWBFile.hpp" +#include "nwb/RecordingContainers.hpp" #include "nwb/file/ElectrodeTable.hpp" #include "testUtils.hpp" @@ -18,10 +19,7 @@ TEST_CASE("writeContinuousData", "[recording]") { SECTION("test data and timestamps stream") { - // get file path and remove if exists - std::string path = getTestFilePath("testContinuousRecording1.nwb"); - - // setup mock data + // 0. setup mock data SizeType numChannels = 4; SizeType numSamples = 300; SizeType samplesRecorded = 0; @@ -35,11 +33,27 @@ TEST_CASE("writeContinuousData", "[recording]") getMockData2D(numSamples, numChannels); std::vector mockTimestamps = getMockTimestamps(numSamples); - // open files - NWB::NWBRecording nwbRecording; - nwbRecording.openFile(path, mockRecordingArrays); + // 1. create IO object + std::string path = getTestFilePath("testContinuousRecording1.nwb"); + std::shared_ptr io = createIO("HDF5", path); + + // 2. create RecordingContainers object + std::unique_ptr recordingContainers = + std::make_unique(); + + // 3. create NWBFile object + std::unique_ptr nwbfile = + std::make_unique(generateUuid(), io); + nwbfile->initialize(); + + // 4. create datasets and add to recording containers + nwbfile->createElectricalSeries( + mockRecordingArrays, BaseDataType::I16, recordingContainers.get()); - // run recording + // 5. start the recording + io->startRecording(); + + // 6. write data during the recording bool isRecording = true; while (isRecording) { // write data to the file for each channel @@ -55,20 +69,19 @@ TEST_CASE("writeContinuousData", "[recording]") mockTimestamps.begin() + samplesRecorded + bufferSize, timestampsBuffer.begin()); - // write timseries data + // write timeseries data std::vector positionOffset = {samplesRecorded, channel.localIndex}; std::vector dataShape = {dataBuffer.size(), 1}; std::unique_ptr intBuffer = transformToInt16( dataBuffer.size(), channel.getBitVolts(), dataBuffer.data()); - nwbRecording.writeTimeseriesData("ElectricalSeries", - i, - channel, - dataShape, - positionOffset, - intBuffer.get(), - timestampsBuffer.data()); + recordingContainers->writeTimeseriesData(i, + channel, + dataShape, + positionOffset, + intBuffer.get(), + timestampsBuffer.data()); } } // check if recording is done @@ -77,7 +90,10 @@ TEST_CASE("writeContinuousData", "[recording]") isRecording = false; } } - nwbRecording.closeFile(); + + // 7. stop the recording and finalize the file + io->stopRecording(); + nwbfile->finalize(); // check contents of data std::string dataPath = "/acquisition/array0/data"; @@ -121,9 +137,4 @@ TEST_CASE("writeContinuousData", "[recording]") REQUIRE_THAT(timestampsOut, Catch::Matchers::Approx(mockTimestamps).margin(tolerance)); } - - SECTION("add a new recording number to the same file", "[recording]") - { - // TODO - } }