Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync add_read with main branch #101

Merged
merged 6 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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
Expand Down Expand Up @@ -77,6 +78,7 @@ target_include_directories(
)

target_compile_features(aqnwb_aqnwb PUBLIC cxx_std_17)
target_compile_definitions(aqnwb_aqnwb PUBLIC BOOST_NO_CXX98_FUNCTION_BASE)

# ---- Additional libraries needed ----
find_package(HDF5 REQUIRED COMPONENTS CXX)
Expand Down
9 changes: 9 additions & 0 deletions src/io/BaseIO.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,14 @@ class BaseIO
virtual std::unique_ptr<BaseRecordingData> 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.
Expand Down Expand Up @@ -376,6 +384,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.
Expand Down
10 changes: 10 additions & 0 deletions src/io/hdf5/HDF5IO.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,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<AQNWB::IO::BaseRecordingData> HDF5IO::getDataSet(
const std::string& path)
{
Expand Down
8 changes: 8 additions & 0 deletions src/io/hdf5/HDF5IO.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,14 @@ class HDF5IO : public BaseIO
std::unique_ptr<IO::BaseRecordingData> 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.
Expand Down
142 changes: 120 additions & 22 deletions src/nwb/NWBFile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@
#include "io/BaseIO.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<SizeType> NWBFile::emptyContainerIndexes = {};

Expand Down Expand Up @@ -58,10 +61,8 @@ Status NWBFile::createFileStructure(const std::string& identifierText,
if (!io->canModifyObjects()) {
return Status::Failure;
}

io->createCommonNWBAttributes("/", "core", "NWBFile", "");
io->createAttribute(AQNWB::SPEC::CORE::version, "/", "nwb_version");

io->createGroup("/acquisition");
io->createGroup("/analysis");
io->createGroup("/processing");
Expand Down Expand Up @@ -94,12 +95,12 @@ Status NWBFile::createFileStructure(const std::string& identifierText,
io->createStringDataSet("/session_start_time", time);
io->createStringDataSet("/timestamps_reference_time", time);
io->createStringDataSet("/identifier", identifierText);

return Status::Success;
}

Status NWBFile::createElectricalSeries(
std::vector<Types::ChannelVector> recordingArrays,
std::vector<std::string> recordingNames,
const IO::BaseDataType& dataType,
RecordingContainers* recordingContainers,
std::vector<SizeType>& containerIndexes)
Expand All @@ -108,26 +109,43 @@ 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
ElectrodeTable elecTable = ElectrodeTable(io);
elecTable.initialize();
// Setup electrode table if it was not yet created
bool electrodeTableCreated =
io->objectExists(ElectrodeTable::electrodeTablePath);
if (!electrodeTableCreated) {
elecTable = std::make_unique<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);
device.initialize("description", "unknown");
// Check if device exists for groupName, create device and electrode group
// if not
if (!io->objectExists(devicePath)) {
Device device = Device(devicePath, io);
device.initialize("description", "unknown");

ElectrodeGroup elecGroup = ElectrodeGroup(electrodePath, io);
elecGroup.initialize("description", "unknown", device);
ElectrodeGroup elecGroup = ElectrodeGroup(electrodePath, io);
elecGroup.initialize("description", "unknown", device);
}

// Setup electrical series datasets
auto electricalSeries =
Expand All @@ -141,14 +159,94 @@ Status NWBFile::createElectricalSeries(
SizeArray {CHUNK_XSIZE, 0});
recordingContainers->addContainer(std::move(electricalSeries));
containerIndexes.push_back(recordingContainers->containers.size() - 1);
}

// write electrode information to datasets
// (requires that the ElectrodeGroup has been written)
if (!electrodeTableCreated) {
elecTable->finalize();
}

return Status::Success;
}

// Add electrode information to electrode table (does not write to datasets
// yet)
elecTable.addElectrodes(channelVector);
Status NWBFile::createSpikeEventSeries(
std::vector<Types::ChannelVector> recordingArrays,
std::vector<std::string> recordingNames,
const IO::BaseDataType& dataType,
RecordingContainers* recordingContainers,
std::vector<SizeType>& 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<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];

// 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);
device.initialize("description", "unknown");

ElectrodeGroup elecGroup = ElectrodeGroup(electrodePath, io);
elecGroup.initialize("description", "unknown", device);
}

// 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<SpikeEventSeries>(spikeEventSeriesPath, io);
spikeEventSeries->initialize(
dataType,
channelVector,
"Stores spike waveforms from an extracellular ephys recording",
dsetSize,
chunkSize);
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;
}
Expand All @@ -160,7 +258,7 @@ void NWBFile::cacheSpecifications(
const std::array<std::pair<std::string_view, std::string_view>, N>&
specVariables)
{
io->createGroup("/specifications/" + specPath + "/");
io->createGroup("/specifications/" + specPath);
io->createGroup("/specifications/" + specPath + "/" + versionNumber);

for (const auto& [name, content] : specVariables) {
Expand Down
30 changes: 29 additions & 1 deletion src/nwb/NWBFile.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "io/BaseIO.hpp"
#include "nwb/RecordingContainers.hpp"
#include "nwb/base/TimeSeries.hpp"
#include "nwb/file/ElectrodeTable.hpp"

/*!
* \namespace AQNWB::NWB
Expand Down Expand Up @@ -73,14 +74,38 @@ class NWBFile : public Container
* @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<Types::ChannelVector> recordingArrays,
std::vector<std::string> recordingNames,
const IO::BaseDataType& dataType = IO::BaseDataType::I16,
RecordingContainers* recordingContainers = nullptr,
std::vector<SizeType>& 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<Types::ChannelVector> recordingArrays,
std::vector<std::string> recordingNames,
const IO::BaseDataType& dataType = IO::BaseDataType::I16,
RecordingContainers* recordingContainers = nullptr,
std::vector<SizeType>& containerIndexes = emptyContainerIndexes);
Expand Down Expand Up @@ -132,7 +157,10 @@ class NWBFile : public Container
const std::array<std::pair<std::string_view, std::string_view>, N>&
specVariables);

std::unique_ptr<ElectrodeTable> elecTable;
const std::string identifierText;
static std::vector<SizeType> emptyContainerIndexes;
inline const static std::string acquisitionPath = "/acquisition";
};

} // namespace AQNWB::NWB
16 changes: 16 additions & 0 deletions src/nwb/RecordingContainers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<SpikeEventSeries*>(getContainer(containerInd));

if (ses == nullptr)
return Status::Failure;

ses->writeSpike(numSamples, numChannels, data, timestamps);
}
18 changes: 17 additions & 1 deletion src/nwb/RecordingContainers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<std::unique_ptr<Container>> containers;
std::string name;
};
Expand Down
Loading
Loading