diff --git a/src/Channel.hpp b/src/Channel.hpp index fccdf227..4166aa8b 100644 --- a/src/Channel.hpp +++ b/src/Channel.hpp @@ -25,9 +25,9 @@ class Channel const SizeType globalIndex, const float conversion = 1e6f, // uV to V const float samplingRate = 30000.f, // placeholder - const float bitVolts = 0.000002f, // least significant bit needed to - // convert 16-bit int to volts - // currently a placeholder + const float bitVolts = 0.05f, // least significant bit needed to + // convert 16-bit int to volts + // currently a placeholder const std::array<float, 3> position = {0.f, 0.f, 0.f}, const std::string comments = "no comments"); diff --git a/src/Utils.hpp b/src/Utils.hpp index b8a29231..8f2681f9 100644 --- a/src/Utils.hpp +++ b/src/Utils.hpp @@ -1,12 +1,16 @@ #pragma once +#include <algorithm> #include <chrono> +#include <cmath> +#include <cstdint> #include <ctime> #include <iomanip> #include <sstream> #include <string> #include <boost/date_time.hpp> +#include <boost/endian/conversion.hpp> #include <boost/uuid/uuid.hpp> #include <boost/uuid/uuid_generators.hpp> #include <boost/uuid/uuid_io.hpp> @@ -71,6 +75,40 @@ inline std::shared_ptr<IO::BaseIO> createIO(const std::string& type, } } +/** + * @brief Method to convert float values to uint16 values. This method + * was adapted from JUCE AudioDataConverters using a default value of + * destBytesPerSample = 2. + * @param source The source float data to convert + * @param dest The destination for the converted uint16 data + * @param numSamples The number of samples to convert + */ +inline void convertFloatToInt16LE(const float* source, + void* dest, + int numSamples) +{ + // TODO - several steps in this function may be unnecessary for our use + // case. Consider simplifying the intermediate cast to char and the + // final cast to uint16_t. + auto maxVal = static_cast<double>(0x7fff); + auto intData = static_cast<char*>(dest); + + for (int i = 0; i < numSamples; ++i) { + auto clampedValue = std::clamp(maxVal * source[i], -maxVal, maxVal); + auto intValue = + static_cast<uint16_t>(static_cast<int16_t>(std::round(clampedValue))); + intValue = boost::endian::native_to_little(intValue); + *reinterpret_cast<uint16_t*>(intData) = intValue; + intData += 2; // destBytesPerSample is always 2 + } +} + +/** + * @brief Method to scale float values and convert to int16 values + * @param numSamples The number of samples to convert + * @param conversion_factor The conversion factor to scale the data + * @param data The data to convert + */ inline std::unique_ptr<int16_t[]> transformToInt16(SizeType numSamples, float conversion_factor, const float* data) @@ -86,12 +124,7 @@ inline std::unique_ptr<int16_t[]> transformToInt16(SizeType numSamples, [multFactor](float value) { return value * multFactor; }); // convert float to int16 - std::transform( - scaledData.get(), - scaledData.get() + numSamples, - intData.get(), - [](float value) - { return static_cast<int16_t>(std::clamp(value, -32768.0f, 32767.0f)); }); + convertFloatToInt16LE(scaledData.get(), intData.get(), numSamples); return intData; } diff --git a/src/nwb/NWBFile.cpp b/src/nwb/NWBFile.cpp index 9a39d399..974fdeb5 100644 --- a/src/nwb/NWBFile.cpp +++ b/src/nwb/NWBFile.cpp @@ -34,17 +34,26 @@ NWBFile::NWBFile(std::shared_ptr<IO::BaseIO> io) NWBFile::~NWBFile() {} -Status NWBFile::initialize(const std::string& identifierText) +Status NWBFile::initialize(const std::string& identifierText, + const std::string& description, + const std::string& dataCollection) { if (std::filesystem::exists(io->getFileName())) { return io->open(false); } else { io->open(true); - return createFileStructure(identifierText); + return createFileStructure(identifierText, description, dataCollection); } } -Status NWBFile::createFileStructure(const std::string& identifierText) +Status NWBFile::finalize() +{ + return io->close(); +} + +Status NWBFile::createFileStructure(const std::string& identifierText, + const std::string& description, + const std::string& dataCollection) { if (!io->canModifyObjects()) { return Status::Failure; @@ -62,6 +71,9 @@ Status NWBFile::createFileStructure(const std::string& identifierText) io->createGroup("/general"); io->createGroup("/general/devices"); io->createGroup("/general/extracellular_ephys"); + if (dataCollection != "") { + io->createStringDataSet("/general/data_collection", dataCollection); + } io->createGroup("/specifications"); io->createReferenceAttribute("/specifications", "/", ".specloc"); @@ -78,7 +90,7 @@ Status NWBFile::createFileStructure(const std::string& identifierText) std::string time = getCurrentTime(); std::vector<std::string> timeVec = {time}; io->createStringDataSet("/file_create_date", timeVec); - io->createStringDataSet("/session_description", "a recording session"); + io->createStringDataSet("/session_description", description); io->createStringDataSet("/session_start_time", time); io->createStringDataSet("/timestamps_reference_time", time); io->createStringDataSet("/identifier", identifierText); @@ -86,11 +98,6 @@ Status NWBFile::createFileStructure(const std::string& identifierText) return Status::Success; } -Status NWBFile::finalize() -{ - return io->close(); -} - Status NWBFile::createElectricalSeries( std::vector<Types::ChannelVector> recordingArrays, const IO::BaseDataType& dataType, diff --git a/src/nwb/NWBFile.hpp b/src/nwb/NWBFile.hpp index 99f09447..0ab8c4af 100644 --- a/src/nwb/NWBFile.hpp +++ b/src/nwb/NWBFile.hpp @@ -51,9 +51,13 @@ class NWBFile : public Container * @brief Initializes the NWB file by opening and setting up the file * structure. * - * @param identifierText The identifier text for the NWBFile. + * @param identifierText The identifier text for the NWBFile. + * @param description A description of the NWBFile session. + * @param dataCollection Information about the data collection methods. */ - Status initialize(const std::string& identifierText); + Status initialize(const std::string& identifierText, + const std::string& description = "a recording session", + const std::string& dataCollection = ""); /** * @brief Finalizes the NWB file by closing it. @@ -88,11 +92,14 @@ class NWBFile : public Container * new objects cannot be added, which can be checked via * nwbfile.io->canModifyObjects() * - * @param identifierText The identifier text for the NWBFile. - * + * @param identifierText The identifier text for the NWBFile. + * @param description A description of the NWBFile session. + * @param dataCollection Information about the data collection methods. * @return Status The status of the file structure creation. */ - Status createFileStructure(const std::string& identifierText); + Status createFileStructure(const std::string& identifierText, + const std::string& description, + const std::string& dataCollection); private: /** diff --git a/src/nwb/ecephys/ElectricalSeries.cpp b/src/nwb/ecephys/ElectricalSeries.cpp index a32ae7d4..f0c690f1 100644 --- a/src/nwb/ecephys/ElectricalSeries.cpp +++ b/src/nwb/ecephys/ElectricalSeries.cpp @@ -38,9 +38,11 @@ void ElectricalSeries::initialize(const IO::BaseDataType& dataType, offset); // setup variables based on number of channels - std::vector<SizeType> electrodeInds(channelVector.size()); + std::vector<int> electrodeInds(channelVector.size()); + std::vector<float> channelConversions(channelVector.size()); for (size_t i = 0; i < channelVector.size(); ++i) { electrodeInds[i] = channelVector[i].globalIndex; + channelConversions[i] = channelVector[i].getConversion(); } samplesRecorded = SizeArray(channelVector.size(), 0); @@ -50,6 +52,10 @@ void ElectricalSeries::initialize(const IO::BaseDataType& dataType, SizeArray {1}, chunkSize, getPath() + "/channel_conversion")); + channelConversion->writeDataBlock( + std::vector<SizeType>(1, channelVector.size()), + BaseDataType::F32, + &channelConversions[0]); io->createCommonNWBAttributes(getPath() + "/channel_conversion", "hdmf-common", "", diff --git a/tests/reader.cpp b/tests/reader.cpp index 1809c21a..75b65c02 100644 --- a/tests/reader.cpp +++ b/tests/reader.cpp @@ -9,35 +9,39 @@ using namespace H5; int readerFunction(const std::string& path, const std::string& dataPath) { - std::unique_ptr<H5File> file = - std::make_unique<H5File>(path, H5F_ACC_RDONLY | H5F_ACC_SWMR_READ); - std::unique_ptr<H5::DataSet> dSet = - std::make_unique<H5::DataSet>(file->openDataSet(dataPath)); - - std::vector<hsize_t> dsetSizes; - for (int i = 0; i < 3; ++i) { - H5Drefresh(dSet->getId()); - - // Get the current size of the dataset - DataSpace fSpace = dSet->getSpace(); - hsize_t currentSize; - fSpace.getSimpleExtentDims(¤tSize, nullptr); - - // Update the size - dsetSizes.push_back(currentSize); - sleep(1); // Simulate real-time data streaming - } - - // print out dataset sizes - std::cout << "Dataset sizes: "; - for (int val : dsetSizes) { - std::cout << val << " "; - } - std::cout << std::endl; - - // check that data is being appended (last value should be greater than the - // first) - if (dsetSizes[0] >= dsetSizes[2]) { + try { + std::unique_ptr<H5File> file = + std::make_unique<H5File>(path, H5F_ACC_RDONLY | H5F_ACC_SWMR_READ); + std::unique_ptr<H5::DataSet> dSet = + std::make_unique<H5::DataSet>(file->openDataSet(dataPath)); + + std::vector<hsize_t> dsetSizes; + for (int i = 0; i < 3; ++i) { + H5Drefresh(dSet->getId()); + + // Get the current size of the dataset + DataSpace fSpace = dSet->getSpace(); + hsize_t currentSize; + fSpace.getSimpleExtentDims(¤tSize, nullptr); + + // Update the size + dsetSizes.push_back(currentSize); + sleep(1); // Simulate real-time data streaming + } + + // print out dataset sizes + std::cout << "Dataset sizes: "; + for (int val : dsetSizes) { + std::cout << val << " "; + } + std::cout << std::endl; + + // check that data is being appended (last value should be greater than the + // first) + if (dsetSizes[0] >= dsetSizes[2]) { + return -1; + } + } catch (const FileIException& error) { return -1; } diff --git a/tests/testRecordingWorkflow.cpp b/tests/testRecordingWorkflow.cpp index 4248eda0..9651dc88 100644 --- a/tests/testRecordingWorkflow.cpp +++ b/tests/testRecordingWorkflow.cpp @@ -21,7 +21,7 @@ TEST_CASE("writeContinuousData", "[recording]") { // 0. setup mock data SizeType numChannels = 4; - SizeType numSamples = 300; + SizeType numSamples = 100; SizeType samplesRecorded = 0; SizeType bufferSize = numSamples / 10; std::vector<float> dataBuffer(bufferSize); @@ -47,7 +47,7 @@ TEST_CASE("writeContinuousData", "[recording]") // 4. create datasets and add to recording containers nwbfile->createElectricalSeries( - mockRecordingArrays, BaseDataType::I16, recordingContainers.get()); + mockRecordingArrays, BaseDataType::F32, recordingContainers.get()); // 5. start the recording io->startRecording(); @@ -72,14 +72,12 @@ TEST_CASE("writeContinuousData", "[recording]") std::vector<SizeType> positionOffset = {samplesRecorded, channel.localIndex}; std::vector<SizeType> dataShape = {dataBuffer.size(), 1}; - std::unique_ptr<int16_t[]> intBuffer = transformToInt16( - dataBuffer.size(), channel.getBitVolts(), dataBuffer.data()); recordingContainers->writeTimeseriesData(i, channel, dataShape, positionOffset, - intBuffer.get(), + dataBuffer.data(), timestampsBuffer.data()); } } @@ -113,8 +111,7 @@ TEST_CASE("writeContinuousData", "[recording]") std::vector<float>(numSamples)); for (SizeType i = 0; i < numChannelsToRead; ++i) { for (SizeType j = 0; j < numSamples; ++j) { - dataOut[i][j] = - buffer[j * numChannelsToRead + i] * (32767.0f * 0.000002f); + dataOut[i][j] = buffer[j * numChannelsToRead + i]; } } delete[] buffer; diff --git a/tests/testUtils.hpp b/tests/testUtils.hpp index a29aabd9..06778827 100644 --- a/tests/testUtils.hpp +++ b/tests/testUtils.hpp @@ -81,7 +81,7 @@ inline std::vector<std::vector<float>> getMockData2D(SizeType numSamples = 1000, for (auto& channelData : mockData) { for (auto& data : channelData) { data = static_cast<float>(dis(rng)) - * 1000.f; // approximate microvolt unit range + * 100.f; // approximate microvolt unit range } }