Skip to content

Commit

Permalink
Merge branch 'general_obj_lookup' into add_read_columns
Browse files Browse the repository at this point in the history
  • Loading branch information
oruebel authored Jan 23, 2025
2 parents 4014b61 + 1a713e0 commit a496583
Show file tree
Hide file tree
Showing 16 changed files with 388 additions and 5 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ add_library(
src/nwb/ecephys/SpikeEventSeries.cpp
src/nwb/file/ElectrodeGroup.cpp
src/nwb/file/ElectrodeTable.cpp
src/nwb/misc/AnnotationSeries.cpp
src/nwb/hdmf/base/Container.cpp
src/nwb/hdmf/base/Data.cpp
src/nwb/hdmf/table/DynamicTable.cpp
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![Codespell](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/codespell.yml/badge.svg)](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/codespell.yml)
[![Lint](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/lint.yml/badge.svg)](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/lint.yml)
[![Docs build](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/doxygen-gh-pages.yml/badge.svg)](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/doxygen-gh-pages.yml)
[![Coverage](https://codecov.io/github/NeurodataWithoutBorders/aqnwb/coverage.svg?branch=main)](https://app.codecov.io/github/NeurodataWithoutBorders/aqnwb?branch=main)

[![Docs](https://img.shields.io/badge/AqNWB-Docs-8A2BE2?style=flat)](https://neurodatawithoutborders.github.io/aqnwb/)
[![Code Stats](https://img.shields.io/badge/AqNWB-Code%20Statistics-8A2BE2?style=flat)](https://nwb-overview.readthedocs.io/en/latest/nwb-project-analytics/docs/source/code_stat_pages/AqNWB_stats.html)
Expand All @@ -30,4 +31,4 @@ See the [AqNWB Documentation](https://neurodatawithoutborders.github.io/aqnwb/)

For more information about the license, contributing guidelines, code of conduct
and other relevant documentation for developers please see the
[Developer Documentation](https://neurodatawithoutborders.github.io/aqnwb/devdocs.html).
[Developer Documentation](https://neurodatawithoutborders.github.io/aqnwb/devdocs.html).
2 changes: 1 addition & 1 deletion cmake/coverage.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# customization issues
set(
COVERAGE_TRACE_COMMAND
lcov -c -q
lcov -c --verbose
-o "${PROJECT_BINARY_DIR}/coverage.info"
-d "${PROJECT_BINARY_DIR}"
--include "${PROJECT_SOURCE_DIR}/src/*"
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/userdocs/read.dox
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@
* - \ref AQNWB::IO::BaseIO "BaseIO", \ref AQNWB::IO::HDF5::HDF5IO "HDF5IO" are responsible for
* i) reading type attribute and group information, ii) searching the file for typed objects via
* \ref AQNWB::IO::BaseIO::findTypes "findTypes()" methods, and iii) retrieving the paths of all
* object associated with a storage objects (e.g., a Group) via \ref AQNWB::IO::BaseIO::getStorageObjects "getStoragebjects()"
* object associated with a storage object (e.g., a Group) via \ref AQNWB::IO::BaseIO::getStorageObjects "getStorageObjects()"
*
* \subsubsection read_design_wrapper_registeredType RegisteredType
*
Expand Down
2 changes: 1 addition & 1 deletion src/Types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class Types
* \brief Helper struct to check if a value is a data field, i.e.,
* Dataset or Attribute
*
* This function is used to enforce constrains on templated functions that
* This function is used to enforce constraints on templated functions that
* should only be callable for valid StorageObjectType values
*/
template<StorageObjectType T>
Expand Down
3 changes: 2 additions & 1 deletion src/io/hdf5/HDF5RecordingData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ Status HDF5RecordingData::writeDataBlock(
|| type.type == BaseDataType::Type::T_STR)
{
std::cerr << "HDF5RecordingData::writeDataBlock called for string data, "
"use HDF5RecordingData::writeStringDataBlock instead."
"use HDF5RecordingData::writeDataBlock with a string array "
"data input instead of void* data."
<< std::endl;
return Status::Failure;
}
Expand Down
30 changes: 30 additions & 0 deletions src/nwb/NWBFile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "nwb/ecephys/ElectricalSeries.hpp"
#include "nwb/ecephys/SpikeEventSeries.hpp"
#include "nwb/file/ElectrodeGroup.hpp"
#include "nwb/misc/AnnotationSeries.hpp"
#include "spec/core.hpp"
#include "spec/hdmf_common.hpp"
#include "spec/hdmf_experimental.hpp"
Expand Down Expand Up @@ -299,6 +300,35 @@ Status NWBFile::createSpikeEventSeries(
return Status::Success;
}

Status NWBFile::createAnnotationSeries(std::vector<std::string> recordingNames,
RecordingContainers* recordingContainers,
std::vector<SizeType>& containerIndexes)
{
if (!m_io->canModifyObjects()) {
return Status::Failure;
}

for (size_t i = 0; i < recordingNames.size(); ++i) {
const std::string& recordingName = recordingNames[i];

std::string annotationSeriesPath =
AQNWB::mergePaths(acquisitionPath, recordingName);

// Setup annotation series datasets
auto annotationSeries =
std::make_unique<AnnotationSeries>(annotationSeriesPath, m_io);
annotationSeries->initialize(
"Stores user annotations made during an experiment",
"no comments",
SizeArray {0},
SizeArray {CHUNK_XSIZE});
recordingContainers->addContainer(std::move(annotationSeries));
containerIndexes.push_back(recordingContainers->size() - 1);
}

return Status::Success;
}

template<SizeType N>
void NWBFile::cacheSpecifications(
const std::string& specPath,
Expand Down
14 changes: 14 additions & 0 deletions src/nwb/NWBFile.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,20 @@ class NWBFile : public Container
const IO::BaseDataType& dataType = IO::BaseDataType::I16,
RecordingContainers* recordingContainers = nullptr,
std::vector<SizeType>& containerIndexes = emptyContainerIndexes);

/** @brief Create AnnotationSeries objects to record data into.
* Created objects are stored in recordingContainers.
* @param recordingNames vector indicating the names of the AnnotationSeries
* within the acquisition group
* @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 createAnnotationSeries(
std::vector<std::string> recordingNames,
RecordingContainers* recordingContainers = nullptr,
std::vector<SizeType>& containerIndexes = emptyContainerIndexes);

DEFINE_REGISTERED_FIELD(readElectrodeTable,
ElectrodeTable,
Expand Down
17 changes: 17 additions & 0 deletions src/nwb/RecordingContainers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "nwb/ecephys/ElectricalSeries.hpp"
#include "nwb/ecephys/SpikeEventSeries.hpp"
#include "nwb/hdmf/base/Container.hpp"
#include "nwb/misc/AnnotationSeries.hpp"

using namespace AQNWB::NWB;
// Recording Container
Expand Down Expand Up @@ -86,3 +87,19 @@ Status RecordingContainers::writeSpikeEventData(const SizeType& containerInd,
return ses->writeSpike(
numSamples, numChannels, data, timestamps, controlInput);
}

Status RecordingContainers::writeAnnotationSeriesData(
const SizeType& containerInd,
const SizeType& numSamples,
const std::vector<std::string> data,
const void* timestamps,
const void* controlInput)
{
AnnotationSeries* as =
dynamic_cast<AnnotationSeries*>(getContainer(containerInd));

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

return as->writeAnnotation(numSamples, data, timestamps, controlInput);
}
16 changes: 16 additions & 0 deletions src/nwb/RecordingContainers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,22 @@ class RecordingContainers
const void* timestamps,
const void* controlInput = nullptr);

/**
* @brief Write AnnotationSeries data to a recordingContainer dataset.
* @param containerInd The index of the AnnotationSeries dataset within the
* AnnotationSeries containers.
* @param numSamples Number of samples in the time for the single event.
* @param data A vector of strings of data to write.
* @param timestamps A pointer to the timestamps block
* @param controlInput A pointer to the control block data (optional)
* @return The status of the write operation.
*/
Status writeAnnotationSeriesData(const SizeType& containerInd,
const SizeType& numSamples,
const std::vector<std::string> data,
const void* timestamps,
const void* controlInput = nullptr);

/**
* @brief Get the number of recording containers
*/
Expand Down
71 changes: 71 additions & 0 deletions src/nwb/misc/AnnotationSeries.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@

#include "nwb/misc/AnnotationSeries.hpp"

#include "Utils.hpp"

using namespace AQNWB::NWB;

// AnnotationSeries
// Initialize the static registered_ member to trigger registration
REGISTER_SUBCLASS_IMPL(AnnotationSeries)

/** Constructor */
AnnotationSeries::AnnotationSeries(const std::string& path,
std::shared_ptr<IO::BaseIO> io)
: TimeSeries(path, io)
{
}

/** Destructor */
AnnotationSeries::~AnnotationSeries() {}

/** Initialization function*/
void AnnotationSeries::initialize(const std::string& description,
const std::string& comments,
const SizeArray& dsetSize,
const SizeArray& chunkSize)
{
TimeSeries::initialize(
IO::BaseDataType::V_STR, // fixed to string according to schema
"n/a", // unit fixed to "n/a"
description,
comments,
dsetSize,
chunkSize,
1.0f, // conversion fixed to 1.0, since unit is n/a
-1.0f, // resolution fixed to -1.0
0.0f); // offset fixed to 0.0, since unit is n/a
}

Status AnnotationSeries::writeAnnotation(const SizeType& numSamples,
std::vector<std::string> dataInput,
const void* timestampsInput,
const void* controlInput)
{
std::vector<SizeType> dataShape = {numSamples};
std::vector<SizeType> positionOffset = {this->m_samplesRecorded};

// Write timestamps
Status tsStatus = Status::Success;
tsStatus = this->timestamps->writeDataBlock(
dataShape, positionOffset, this->timestampsType, timestampsInput);

// Write the data
Status dataStatus = this->data->writeDataBlock(
dataShape, positionOffset, this->m_dataType, dataInput);

// Write the control data if it exists
if (controlInput != nullptr) {
tsStatus = this->control->writeDataBlock(
dataShape, positionOffset, this->controlType, controlInput);
}

// track samples recorded
m_samplesRecorded += numSamples;

if ((dataStatus != Status::Success) || (tsStatus != Status::Success)) {
return Status::Failure;
} else {
return Status::Success;
}
}
71 changes: 71 additions & 0 deletions src/nwb/misc/AnnotationSeries.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@

#pragma once

#include <string>

#include "Utils.hpp"
#include "io/BaseIO.hpp"
#include "io/ReadIO.hpp"
#include "nwb/base/TimeSeries.hpp"

namespace AQNWB::NWB
{
/**
* @brief TimeSeries storing text-based records about the experiment.
*/
class AnnotationSeries : public TimeSeries
{
public:
// Register the AnnotationSeries
REGISTER_SUBCLASS(AnnotationSeries, "core")

/**
* @brief Constructor.
* @param path The location of the AnnotationSeries in the file.
* @param io A shared pointer to the IO object.
*/
AnnotationSeries(const std::string& path, std::shared_ptr<IO::BaseIO> io);

/**
* @brief Destructor
*/
~AnnotationSeries();

/**
* @brief Initializes the AnnotationSeries
* @param description The description of the AnnotationSeries.
* @param dsetSize Initial size of the main dataset. This must be a vector
* with one element specifying the length in time.
* @param chunkSize Chunk size to use.
*/
void initialize(const std::string& description,
const std::string& comments,
const SizeArray& dsetSize,
const SizeArray& chunkSize);

/**
* @brief Writes a channel to an AnnotationSeries dataset.
* @param numSamples The number of samples to write (length in time).
* @param dataInput A vector of strings.
* @param timestampsInput A pointer to the timestamps block.
* @param controlInput A pointer to the control block data (optional)
* @return The status of the write operation.
*/
Status writeAnnotation(const SizeType& numSamples,
const std::vector<std::string> dataInput,
const void* timestampsInput,
const void* controlInput = nullptr);

DEFINE_FIELD(readData,
DatasetField,
std::string,
"data",
Annotations made during an experiment.)

private:
/**
* @brief The number of samples already written per channel.
*/
SizeType m_samplesRecorded = 0;
};
} // namespace AQNWB::NWB
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ add_executable(aqnwb_test
testElementIdentifiers.cpp
testFile.cpp
testHDF5IO.cpp
testMisc.cpp
testNWBFile.cpp
testRecordingWorkflow.cpp
testRegisteredType.cpp
Expand Down
32 changes: 32 additions & 0 deletions tests/testHDF5IO.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,38 @@ TEST_CASE("getStorageObjects", "[hdf5io]")
REQUIRE(groupContent.size() == 0);
}

SECTION("attribute")
{
int attrData1 = 42;
hdf5io.createAttribute(BaseDataType::I32, &attrData1, "/", "attr1");
auto attributeContent = hdf5io.getStorageObjects("/attr1");
REQUIRE(attributeContent.size() == 0);
}

SECTION("dataset w/o attribute")
{
// Dataset without attributes
hdf5io.createArrayDataSet(
BaseDataType::I32, SizeArray {0}, SizeArray {1}, "/data");
auto datasetContent = hdf5io.getStorageObjects("/data");
REQUIRE(datasetContent.size() == 0);

// Dataset with attribute
int attrData1 = 42;
hdf5io.createAttribute(BaseDataType::I32, &attrData1, "/data", "attr1");
auto dataContent2 = hdf5io.getStorageObjects("/data");
REQUIRE(dataContent2.size() == 1);
REQUIRE(
dataContent2[0]
== std::make_pair(std::string("attr1"), StorageObjectType::Attribute));
}

SECTION("invalid path")
{
auto invalidPathContent = hdf5io.getStorageObjects("/invalid/path");
REQUIRE(invalidPathContent.size() == 0);
}

SECTION("group with datasets, subgroups, and attributes")
{
hdf5io.createGroup("/data");
Expand Down
Loading

0 comments on commit a496583

Please sign in to comment.