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

Add SpikeEventSeries data type #92

Merged
merged 34 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
fbd4d4a
add spike event series
stephprince Aug 5, 2024
13515ad
add annotation series
stephprince Aug 5, 2024
5ca3546
add data type creation functions
stephprince Aug 5, 2024
f65ffc2
add data types to cmake lists
stephprince Aug 5, 2024
d55c0b4
add checks whether object exists
stephprince Aug 6, 2024
04d5d3a
skip adding electrode if it already exists
stephprince Aug 6, 2024
a1108ee
add electrodeTable and acquisitonPath to NWBFile
stephprince Aug 6, 2024
d41bb7c
update new data type inputs
stephprince Aug 6, 2024
5fcfa63
fix tests
stephprince Aug 6, 2024
a8cb93f
fix object exists logic
stephprince Aug 6, 2024
5468106
fix formatting
stephprince Aug 22, 2024
11ece6c
remove annotationseries for separate pr
stephprince Sep 9, 2024
915ba30
add writeSpikeEventSeries related functions
stephprince Sep 9, 2024
5bf4d59
isolate electrode table related functions
stephprince Sep 9, 2024
34c9aeb
update spikeEventSeries
stephprince Sep 9, 2024
91b7aa5
update spikeevent inputs
stephprince Sep 9, 2024
e9c23cb
setup eventseries for different dims
stephprince Sep 9, 2024
56f482c
update spike event tests - wip
stephprince Sep 9, 2024
01313a9
fix tests
stephprince Sep 9, 2024
052453b
remove annotation series
stephprince Sep 9, 2024
90055c3
remove python script
stephprince Sep 9, 2024
c060e13
update spikeeventseries for diff dims
stephprince Sep 9, 2024
ab6a995
fix formatting
stephprince Sep 9, 2024
a6d86f9
remove old conditional
stephprince Sep 9, 2024
ddde29a
Apply suggestions from code review
stephprince Sep 10, 2024
905547b
add check for device or electrode group existing
stephprince Sep 10, 2024
2687aec
separate group and source name
stephprince Sep 10, 2024
f68a490
test 2d and 3d event series
stephprince Sep 10, 2024
42788a0
test multiple ecephys dset creation
stephprince Sep 10, 2024
3d32106
remove nSamples as input
stephprince Sep 10, 2024
cd2d098
fix formatting
stephprince Sep 10, 2024
5d59c0b
update dset creation to take recordingNames as input
stephprince Sep 11, 2024
378a8e4
fix formatting
stephprince Sep 11, 2024
50004b7
update test to fix validation
stephprince Sep 11, 2024
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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/BaseIO.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,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 @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions src/hdf5/HDF5IO.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<AQNWB::BaseRecordingData> HDF5IO::getDataSet(
const std::string& path)
{
Expand Down
8 changes: 8 additions & 0 deletions src/hdf5/HDF5IO.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,14 @@ class HDF5IO : public BaseIO
std::unique_ptr<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
143 changes: 123 additions & 20 deletions src/nwb/NWBFile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<SizeType> NWBFile::emptyContainerIndexes = {};

Expand Down Expand Up @@ -99,6 +102,7 @@ Status NWBFile::createFileStructure(std::string description,

Status NWBFile::createElectricalSeries(
std::vector<Types::ChannelVector> recordingArrays,
std::vector<std::string> recordingNames,
const BaseDataType& dataType,
RecordingContainers* recordingContainers,
std::vector<SizeType>& containerIndexes)
Expand All @@ -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<ElectrodeTable>(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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now this seems fine. I assume this is part of #93 to address the groupName?

Copy link
Collaborator Author

@stephprince stephprince Sep 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By address the groupName do you mean allowing different options for Device and ElectrodeGroup?

From what I had in my notes we also discussed having a single groupName for a channelVector. That would be part of #37

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.


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<ElectricalSeries>(
Expand All @@ -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<Types::ChannelVector> recordingArrays,
std::vector<std::string> recordingNames,
const 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, "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<SpikeEventSeries>(
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;
}
Expand All @@ -161,7 +264,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
29 changes: 28 additions & 1 deletion src/nwb/NWBFile.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "Types.hpp"
#include "nwb/RecordingContainers.hpp"
#include "nwb/base/TimeSeries.hpp"
#include "nwb/file/ElectrodeTable.hpp"

/*!
* \namespace AQNWB::NWB
Expand Down Expand Up @@ -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<Types::ChannelVector> recordingArrays,
std::vector<std::string> recordingNames,
const BaseDataType& dataType = 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 BaseDataType& dataType = BaseDataType::I16,
RecordingContainers* recordingContainers = nullptr,
std::vector<SizeType>& containerIndexes = emptyContainerIndexes);
Expand Down Expand Up @@ -127,9 +152,11 @@ class NWBFile
const std::array<std::pair<std::string_view, std::string_view>, N>&
specVariables);

std::unique_ptr<ElectrodeTable> elecTable;
const std::string identifierText;
std::shared_ptr<BaseIO> io;
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