From 69eda3a9897a87c01df3053b5081493aa05b79c6 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:54:41 -0700 Subject: [PATCH 01/32] add aqnwb library --- libs/macos/bin/libaqnwb.0.1.0.dylib | Bin 0 -> 680424 bytes libs/macos/bin/libaqnwb.0.dylib | 1 + libs/macos/include/aqnwb/BaseIO.hpp | 404 ++++++++++++++++++ libs/macos/include/aqnwb/Channel.hpp | 102 +++++ libs/macos/include/aqnwb/Types.hpp | 42 ++ libs/macos/include/aqnwb/Utils.hpp | 96 +++++ libs/macos/include/aqnwb/aqnwb.hpp | 27 ++ libs/macos/include/aqnwb/hdf5/HDF5IO.hpp | 298 +++++++++++++ libs/macos/include/aqnwb/nwb/NWBFile.hpp | 175 ++++++++ libs/macos/include/aqnwb/nwb/NWBRecording.hpp | 85 ++++ .../include/aqnwb/nwb/base/TimeSeries.hpp | 148 +++++++ .../macos/include/aqnwb/nwb/device/Device.hpp | 63 +++ .../aqnwb/nwb/ecephys/ElectricalSeries.hpp | 98 +++++ .../include/aqnwb/nwb/file/ElectrodeGroup.hpp | 81 ++++ .../include/aqnwb/nwb/file/ElectrodeTable.hpp | 128 ++++++ .../include/aqnwb/nwb/hdmf/base/Container.hpp | 51 +++ .../include/aqnwb/nwb/hdmf/base/Data.hpp | 30 ++ .../aqnwb/nwb/hdmf/table/DynamicTable.hpp | 96 +++++ .../nwb/hdmf/table/ElementIdentifiers.hpp | 14 + .../aqnwb/nwb/hdmf/table/VectorData.hpp | 27 ++ libs/macos/lib/libaqnwb.dylib | 1 + 21 files changed, 1967 insertions(+) create mode 100755 libs/macos/bin/libaqnwb.0.1.0.dylib create mode 120000 libs/macos/bin/libaqnwb.0.dylib create mode 100644 libs/macos/include/aqnwb/BaseIO.hpp create mode 100644 libs/macos/include/aqnwb/Channel.hpp create mode 100644 libs/macos/include/aqnwb/Types.hpp create mode 100644 libs/macos/include/aqnwb/Utils.hpp create mode 100644 libs/macos/include/aqnwb/aqnwb.hpp create mode 100644 libs/macos/include/aqnwb/hdf5/HDF5IO.hpp create mode 100644 libs/macos/include/aqnwb/nwb/NWBFile.hpp create mode 100644 libs/macos/include/aqnwb/nwb/NWBRecording.hpp create mode 100644 libs/macos/include/aqnwb/nwb/base/TimeSeries.hpp create mode 100644 libs/macos/include/aqnwb/nwb/device/Device.hpp create mode 100644 libs/macos/include/aqnwb/nwb/ecephys/ElectricalSeries.hpp create mode 100644 libs/macos/include/aqnwb/nwb/file/ElectrodeGroup.hpp create mode 100644 libs/macos/include/aqnwb/nwb/file/ElectrodeTable.hpp create mode 100644 libs/macos/include/aqnwb/nwb/hdmf/base/Container.hpp create mode 100644 libs/macos/include/aqnwb/nwb/hdmf/base/Data.hpp create mode 100644 libs/macos/include/aqnwb/nwb/hdmf/table/DynamicTable.hpp create mode 100644 libs/macos/include/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp create mode 100644 libs/macos/include/aqnwb/nwb/hdmf/table/VectorData.hpp create mode 120000 libs/macos/lib/libaqnwb.dylib diff --git a/libs/macos/bin/libaqnwb.0.1.0.dylib b/libs/macos/bin/libaqnwb.0.1.0.dylib new file mode 100755 index 0000000000000000000000000000000000000000..1b27c56dd35a075d96968e395e7adb132173a3de GIT binary patch literal 680424 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 literal 0 HcmV?d00001 diff --git a/libs/macos/bin/libaqnwb.0.dylib b/libs/macos/bin/libaqnwb.0.dylib new file mode 120000 index 0000000..3a2323e --- /dev/null +++ b/libs/macos/bin/libaqnwb.0.dylib @@ -0,0 +1 @@ +libaqnwb.0.1.0.dylib \ No newline at end of file diff --git a/libs/macos/include/aqnwb/BaseIO.hpp b/libs/macos/include/aqnwb/BaseIO.hpp new file mode 100644 index 0000000..0f897e4 --- /dev/null +++ b/libs/macos/include/aqnwb/BaseIO.hpp @@ -0,0 +1,404 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "aqnwb/Types.hpp" + +#define DEFAULT_STR_SIZE 256 +#define DEFAULT_ARRAY_SIZE 1 + +using Status = AQNWB::Types::Status; +using SizeArray = AQNWB::Types::SizeArray; +using SizeType = AQNWB::Types::SizeType; + +namespace AQNWB +{ + +class BaseRecordingData; + +/** + * @brief Represents a base data type. + * + * This class provides an enumeration of different data types and their + * corresponding sizes. It also includes handy accessors for commonly used data + * types. + */ +class BaseDataType +{ +public: + /** + * @brief Enumeration of different data types. + */ + enum Type + { + T_U8, ///< Unsigned 8-bit integer + T_U16, ///< Unsigned 16-bit integer + T_U32, ///< Unsigned 32-bit integer + T_U64, ///< Unsigned 64-bit integer + T_I8, ///< Signed 8-bit integer + T_I16, ///< Signed 16-bit integer + T_I32, ///< Signed 32-bit integer + T_I64, ///< Signed 64-bit integer + T_F32, ///< 32-bit floating point + T_F64, ///< 64-bit floating point + T_STR, ///< String + V_STR, ///< Variable length string + }; + + /** + * @brief Constructs a BaseDataType object with the specified type and size. + * @param t The data type. + * @param s The size of the data type. + */ + BaseDataType(Type t = T_I32, SizeType s = 1); + + Type type; ///< The data type. + SizeType typeSize; ///< The size of the data type. + + // handy accessors + static const BaseDataType U8; ///< Accessor for unsigned 8-bit integer. + static const BaseDataType U16; ///< Accessor for unsigned 16-bit integer. + static const BaseDataType U32; ///< Accessor for unsigned 32-bit integer. + static const BaseDataType U64; ///< Accessor for unsigned 64-bit integer. + static const BaseDataType I8; ///< Accessor for signed 8-bit integer. + static const BaseDataType I16; ///< Accessor for signed 16-bit integer. + static const BaseDataType I32; ///< Accessor for signed 32-bit integer. + static const BaseDataType I64; ///< Accessor for signed 64-bit integer. + static const BaseDataType F32; ///< Accessor for 32-bit floating point. + static const BaseDataType F64; ///< Accessor for 64-bit floating point. + static const BaseDataType DSTR; ///< Accessor for dynamic string. + static BaseDataType STR( + SizeType size); ///< Accessor for string with specified size. +}; + +/** + * @brief The BaseIO class is an abstract base class that defines the interface + * for input/output (IO) operations on a file. + * + * This class provides pure virtual methods that must be implemented by all IO + * classes. It also includes other methods for common IO operations. + * + * @note This class cannot be instantiated directly as it is an abstract class. + */ +class BaseIO +{ +public: + /** + * @brief Constructor for the BaseIO class. + */ + BaseIO(); + + /** + * @brief Copy constructor is deleted to prevent construction-copying. + */ + BaseIO(const BaseIO&) = delete; + + /** + * @brief Assignment operator is deleted to prevent copying. + */ + BaseIO& operator=(const BaseIO&) = delete; + + /** + * @brief Destructor the BaseIO class. + */ + virtual ~BaseIO(); + + /** + * @brief Returns the full path to the file. + * @return The full path to the file. + */ + virtual std::string getFileName() = 0; + + /** + * @brief Opens the file for writing. + * @return The status of the file opening operation. + */ + virtual Status open() = 0; + + /** + * @brief Opens an existing file or creates a new file for writing. + * @param newfile Flag indicating whether to create a new file. + * @return The status of the file opening operation. + */ + virtual Status open(bool newfile) = 0; + + /** + * @brief Closes the file. + * @return The status of the file closing operation. + */ + virtual Status close() = 0; + + /** + * @brief Creates an attribute at a given location in the file. + * @param type The base data type of the attribute. + * @param data Pointer to the attribute data. + * @param path The location in the file to set the attribute. + * @param name The name of the attribute. + * @param size The size of the attribute (default is 1). + * @return The status of the attribute creation operation. + */ + virtual Status createAttribute(const BaseDataType& type, + const void* data, + const std::string& path, + const std::string& name, + const SizeType& size = 1) = 0; + + /** + * @brief Creates a string attribute at a given location in the file. + * @param data The string attribute data. + * @param path The location in the file to set the attribute. + * @param name The name of the attribute. + * @return The status of the attribute creation operation. + */ + virtual Status createAttribute(const std::string& data, + const std::string& path, + const std::string& name) = 0; + + /** + * @brief Creates a string array attribute at a given location in the file. + * @param data The string array attribute data. + * @param path The location in the file to set the attribute. + * @param name The name of the attribute. + * @return The status of the attribute creation operation. + */ + virtual Status createAttribute(const std::vector& data, + const std::string& path, + const std::string& name) = 0; + + /** + * @brief Creates a string array attribute at a given location in the file. + * @param data The string array attribute data. + * @param path The location in the file to set the attribute. + * @param name The name of the attribute. + * @param maxSize The maximum size of the string. + * @return The status of the attribute creation operation. + */ + virtual Status createAttribute(const std::vector& data, + const std::string& path, + const std::string& name, + const SizeType& maxSize) = 0; + + /** + * @brief Sets an object reference attribute for a given location in the file. + * @param referencePath The full path to the referenced group / dataset. + * @param path The location in the file to set the attribute. + * @param name The name of the attribute. + * @return The status of the attribute creation operation. + */ + virtual Status createReferenceAttribute(const std::string& referencePath, + const std::string& path, + const std::string& name) = 0; + + /** + * @brief Creates a new group in the file. + * @param path The location in the file of the new group. + * @return The status of the group creation operation. + */ + virtual Status createGroup(const std::string& path) = 0; + + /** + * @brief Creates a soft link to another location in the file. + * @param path The location in the file to the new link. + * @param reference The location in the file of the object that is being + * linked to. + * @return The status of the link creation operation. + */ + virtual Status createLink(const std::string& path, + const std::string& reference) = 0; + + /** + * @brief Creates a non-modifiable dataset with a string value. + * @param path The location in the file of the dataset. + * @param value The string value of the dataset. + * @return The status of the dataset creation operation. + */ + virtual Status createStringDataSet(const std::string& path, + const std::string& value) = 0; + + /** + * @brief Creates a dataset that holds an array of string values. + * @param path The location in the file of the dataset. + * @param values The vector of string values of the dataset. + * @return The status of the dataset creation operation. + */ + virtual Status createStringDataSet( + const std::string& path, const std::vector& values) = 0; + + /** + * @brief Creates a dataset that holds an array of references to groups within + * the file. + * @param path The location in the file of the new dataset. + * @param references The array of references. + * @return The status of the dataset creation operation. + */ + virtual Status createReferenceDataSet( + const std::string& path, const std::vector& references) = 0; + + /** + * @brief Creates an extendable dataset with a given base data type, size, + * chunking, and path. + * @param type The base data type of the dataset. + * @param size The size of the dataset. + * @param chunking The chunking size of the dataset. + * @param path The location in the file of the new dataset. + * @return A pointer to the created dataset. + */ + virtual std::unique_ptr createArrayDataSet( + const BaseDataType& type, + const SizeArray& size, + const SizeArray& chunking, + const std::string& path) = 0; + + /** + * @brief Returns a pointer to a dataset at a given path. + * @param path The location in the file of the dataset. + * @return A pointer to the dataset. + */ + virtual std::unique_ptr getDataSet( + const std::string& path) = 0; + + /** + * @brief Convenience function for creating NWB related attributes. + * @param path The location of the object in the file. + * @param objectNamespace The namespace of the object. + * @param neurodataType The neurodata type of the object. + * @param description The description of the object (default is empty). + * @return The status of the operation. + */ + Status createCommonNWBAttributes(const std::string& path, + const std::string& objectNamespace, + const std::string& neurodataType = "", + const std::string& description = ""); + + /** + * @brief Convenience function for creating data related attributes. + * @param path The location of the object in the file. + * @param conversion Scalar to multiply each element in data to convert it to + * the specified ‘unit’. + * @param resolution Smallest meaningful difference between values in data. + * @param unit Base unit of measurement for working with the data. + * @return The status of the operation. + */ + Status createDataAttributes(const std::string& path, + const float& conversion, + const float& resolution, + const std::string& unit); + + /** + * @brief Convenience function for creating timestamp related attributes. + * @param path The location of the object in the file. + * @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. + */ + bool isOpen() const; + + /** + * @brief Returns true if the file is able to be opened. + * @return True if the file is able to be opened, false otherwise. + */ + bool isReadyToOpen() const; + + /** + * @brief The name of the file. + */ + const std::string filename; + +protected: + /** + * @brief Creates a new group if it does not already exist. + * @param path The location of the group in the file. + * @return The status of the operation. + */ + virtual Status createGroupIfDoesNotExist(const std::string& path) = 0; + + /** + * @brief Whether the file is ready to be opened. + */ + bool readyToOpen; + + /** + * @brief Whether the file is currently open. + */ + bool opened; +}; + +/** + * @brief The base class to represent recording data that can be extended. + * + * This class provides functionality for writing 1D and 2D blocks of data. + */ +class BaseRecordingData +{ +public: + /** + * @brief Default constructor. + */ + BaseRecordingData(); + + /** + * @brief Deleted copy constructor to prevent construction-copying. + */ + BaseRecordingData(const BaseRecordingData&) = delete; + + /** + * @brief Deleted copy assignment operator to prevent copying. + */ + BaseRecordingData& operator=(const BaseRecordingData&) = delete; + + /** + * @brief Destructor. + */ + virtual ~BaseRecordingData(); + + /** + * @brief Writes a block of data using the stored position information. + * This is not intended to be overwritten by derived classes, but is a + * convenience function for writing data using the last recorded position. + * @param dataShape The size of the data block. + * @param type The data type of the elements in the data block. + * @param data A pointer to the data block. + * @return The status of the write operation. + */ + Status writeDataBlock(const std::vector& dataShape, + const BaseDataType& type, + const void* data); + + /** + * @brief Writes a block of data (any number of dimensions). + * @param dataShape The size of the data block. + * @param positionOffset The position of the data block to write to. + * @param type The data type of the elements in the data block. + * @param data A pointer to the data block. + * @return The status of the write operation. + */ + virtual Status writeDataBlock(const std::vector& dataShape, + const std::vector& positionOffset, + const BaseDataType& type, + const void* data) = 0; + +protected: + /** + * @brief The size of the dataset in each dimension. + */ + std::vector size; + + /** + * @brief The current position in the dataset. + */ + std::vector position; + + /** + * @brief The number of dimensions in the data block. + */ + SizeType nDimensions; +}; + +} // namespace AQNWB diff --git a/libs/macos/include/aqnwb/Channel.hpp b/libs/macos/include/aqnwb/Channel.hpp new file mode 100644 index 0000000..66fe06f --- /dev/null +++ b/libs/macos/include/aqnwb/Channel.hpp @@ -0,0 +1,102 @@ +#pragma once + +#include +#include + +#include "aqnwb/Types.hpp" + +using SizeType = AQNWB::Types::SizeType; + +namespace AQNWB +{ +/** + * @brief Class for storing acquisition system channel information. + */ +class Channel +{ +public: + /** + * @brief Constructor. + */ + Channel(const std::string name, + const std::string groupName, + const SizeType localIndex, + 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 std::array position = {0.f, 0.f, 0.f}, + const std::string comments = "no comments"); + + /** + * @brief Destructor + */ + ~Channel(); + + /** + * @brief Getter for conversion factor + * @return The conversion value. + */ + float getConversion() const; + + /** + * @brief Getter for samplingRate + * @return The samplingRate value. + */ + float getSamplingRate() const; + + /** + * @brief Getter for bitVolts + * @return The bitVolts value. + */ + float getBitVolts() const; + + /** + * @brief Name of the channel. + */ + std::string name; + + /** + * @brief Name of the array group the channel belongs to. + */ + std::string groupName; + + /** + * @brief Index of channel within the recording array. + */ + SizeType localIndex; + + /** + * @brief Index of channel across the recording system. + */ + SizeType globalIndex; + + /** + * @brief Coordinates of channel (x, y, z) within the recording array. + */ + std::array position; + + /** + * @brief Comments about the channel. + */ + std::string comments; + +private: + /** + * @brief Conversion factor. + */ + float conversion; + + /** + * @brief Sampling rate of the channel. + */ + float samplingRate; + + /** + * @brief floating point value of microvolts per bit + */ + float bitVolts; +}; +} // namespace AQNWB diff --git a/libs/macos/include/aqnwb/Types.hpp b/libs/macos/include/aqnwb/Types.hpp new file mode 100644 index 0000000..37bbd8a --- /dev/null +++ b/libs/macos/include/aqnwb/Types.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +namespace AQNWB +{ + +// Forward declaration of Channel +class Channel; + +/** + * @brief Provides definitions for various types used in the project. + */ +class Types +{ +public: + /** + * @brief Represents the status of an operation. + */ + enum Status + { + Success = 0, + Failure = -1 + }; + + /** + * @brief Alias for the size type used in the project. + */ + using SizeType = size_t; + + /** + * @brief Alias for an array of size types used in the project. + */ + using SizeArray = std::vector; + + /** + * @brief Alias for a vector of channels. + */ + using ChannelVector = std::vector; +}; +} // namespace AQNWB diff --git a/libs/macos/include/aqnwb/Utils.hpp b/libs/macos/include/aqnwb/Utils.hpp new file mode 100644 index 0000000..31a4717 --- /dev/null +++ b/libs/macos/include/aqnwb/Utils.hpp @@ -0,0 +1,96 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "boost/date_time/c_local_time_adjustor.hpp" + +#include "aqnwb/BaseIO.hpp" +#include "aqnwb/hdf5/HDF5IO.hpp" + +namespace AQNWB +{ +/** + * @brief Generates a UUID (Universally Unique Identifier) as a string. + * @return The generated UUID as a string. + */ +inline std::string generateUuid() +{ + boost::uuids::uuid uuid = boost::uuids::random_generator()(); + std::string uuidStr = boost::uuids::to_string(uuid); + + return uuidStr; +} + +/** + * @brief Get the current time in ISO 8601 format with the UTC offset. + * @return The current time as a string in ISO 8601 format. + */ +inline std::string getCurrentTime() +{ + // Set up boost time zone adjustment and time facet + using local_adj = + boost::date_time::c_local_adjustor; + boost::posix_time::time_facet* f = new boost::posix_time::time_facet(); + f->time_duration_format("%+%H:%M"); + + // get local time, utc time, and offset + auto now = boost::posix_time::microsec_clock::universal_time(); + auto utc_now = local_adj::utc_to_local(now); + boost::posix_time::time_duration td = utc_now - now; + + // Format the date and time in ISO 8601 format with the UTC offset + std::ostringstream oss_offset; + oss_offset.imbue(std::locale(oss_offset.getloc(), f)); + oss_offset << td; + + std::string currentTime = to_iso_extended_string(utc_now); + currentTime += oss_offset.str(); + + return currentTime; +} + +/** + * @brief Factory method to create an IO object. + * @return A pointer to a BaseIO object + */ +inline std::unique_ptr createIO(const std::string& type, + const std::string& filename) +{ + if (type == "HDF5") { + return std::make_unique(filename); + } else { + throw std::invalid_argument("Invalid IO type"); + } +} + +inline std::unique_ptr transformToInt16(SizeType numSamples, + float conversion_factor, + const float* data) +{ + std::unique_ptr scaledData = std::make_unique(numSamples); + std::unique_ptr intData = std::make_unique(numSamples); + + // copy data and multiply by scaling factor + double multFactor = 1 / (32767.0f * conversion_factor); + std::transform(data, + data + numSamples, + scaledData.get(), + [multFactor](float value) { return value * multFactor; }); + + // convert float to int16 + std::transform( + scaledData.get(), + scaledData.get() + numSamples, + intData.get(), + [](float value) + { return static_cast(std::clamp(value, -32768.0f, 32767.0f)); }); + + return intData; +} +} // namespace AQNWB diff --git a/libs/macos/include/aqnwb/aqnwb.hpp b/libs/macos/include/aqnwb/aqnwb.hpp new file mode 100644 index 0000000..585ebcc --- /dev/null +++ b/libs/macos/include/aqnwb/aqnwb.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "aqnwb/aqnwb_export.hpp" + +/** + * @brief Reports the name of the library + * + * Please see the note above for considerations when creating shared libraries. + */ +class AQNWB_EXPORT exported_class +{ +public: + /** + * @brief Initializes the name field to the name of the project + */ + exported_class(); + + /** + * @brief Returns a non-owning pointer to the string stored in this class + */ + auto name() const -> char const*; + +private: + std::string m_name; +}; \ No newline at end of file diff --git a/libs/macos/include/aqnwb/hdf5/HDF5IO.hpp b/libs/macos/include/aqnwb/hdf5/HDF5IO.hpp new file mode 100644 index 0000000..30e2137 --- /dev/null +++ b/libs/macos/include/aqnwb/hdf5/HDF5IO.hpp @@ -0,0 +1,298 @@ +#pragma once + +#include +#include +#include + +#include + +#include "aqnwb/BaseIO.hpp" +#include "aqnwb/Types.hpp" + +namespace H5 +{ +class DataSet; +class H5File; +class DataType; +class Exception; +} // namespace H5 + +namespace AQNWB::HDF5 +{ +class HDF5RecordingData; // declare here because gets used in HDF5IO class + +/** + * @brief The HDF5IO class provides an interface for reading and writing data to + * HDF5 files. + */ +class HDF5IO : public BaseIO +{ +public: + /** + * @brief Default constructor for the HDF5IO class. + */ + HDF5IO(); + + /** + * @brief Constructor for the HDF5IO class that takes a file name as input. + * @param fileName The name of the HDF5 file. + */ + HDF5IO(const std::string& fileName); + + /** + * @brief Destructor for the HDF5IO class. + */ + ~HDF5IO(); + + /** + * @brief Returns the full path to the HDF5 file. + * @return The full path to the HDF5 file. + */ + std::string getFileName() override; + + /** + * @brief Opens an existing file or creates a new file for writing. + * @return The status of the file opening operation. + */ + Status open() override; + + /** + * @brief Opens an existing file or creates a new file for writing. + * @param newfile Flag indicating whether to create a new file. + * @return The status of the file opening operation. + */ + Status open(bool newfile) override; + + /** + * @brief Closes the file. + * @return The status of the file closing operation. + */ + Status close() override; + + /** + * @brief Creates an attribute at a given location in the file. + * @param type The base data type of the attribute. + * @param data Pointer to the attribute data. + * @param path The location in the file to set the attribute. + * @param name The name of the attribute. + * @param size The size of the attribute (default is 1). + * @return The status of the attribute creation operation. + */ + Status createAttribute(const BaseDataType& type, + const void* data, + const std::string& path, + const std::string& name, + const SizeType& size = 1) override; + + /** + * @brief Creates a string attribute at a given location in the file. + * @param data The string attribute data. + * @param path The location in the file to set the attribute. + * @param name The name of the attribute. + * @return The status of the attribute creation operation. + */ + Status createAttribute(const std::string& data, + const std::string& path, + const std::string& name) override; + + /** + * @brief Creates a string array attribute at a given location in the file. + * @param data The string array attribute data. + * @param path The location in the file to set the attribute. + * @param name The name of the attribute. + * @return The status of the attribute creation operation. + */ + Status createAttribute(const std::vector& data, + const std::string& path, + const std::string& name) override; + + /** + * @brief Creates a string array attribute at a given location in the file. + * @param data The string array attribute data. + * @param path The location in the file to set the attribute. + * @param name The name of the attribute. + * @param maxSize The maximum size of the string. + * @return The status of the attribute creation operation. + */ + Status createAttribute(const std::vector& data, + const std::string& path, + const std::string& name, + const SizeType& maxSize) override; + + /** + * @brief Sets an object reference attribute for a given location in the file. + * @param referencePath The full path to the referenced group / dataset. + * @param path The location in the file to set the attribute. + * @param name The name of the attribute. + * @return The status of the attribute creation operation. + */ + Status createReferenceAttribute(const std::string& referencePath, + const std::string& path, + const std::string& name) override; + + /** + * @brief Creates a new group in the file. + * @param path The location in the file of the new group. + * @return The status of the group creation operation. + */ + Status createGroup(const std::string& path) override; + + /** + * @brief Creates a soft link to another location in the file. + * @param path The location in the file to the new link. + * @param reference The location in the file of the object that is being + * linked to. + * @return The status of the link creation operation. + */ + Status createLink(const std::string& path, + const std::string& reference) override; + + /** + * @brief Creates a non-modifiable dataset with a string value. + * @param path The location in the file of the dataset. + * @param value The string value of the dataset. + * @return The status of the dataset creation operation. + */ + Status createStringDataSet(const std::string& path, + const std::string& value) override; + + /** + * @brief Creates a dataset that holds an array of string values. + * @param path The location in the file of the dataset. + * @param values The vector of string values of the dataset. + * @return The status of the dataset creation operation. + */ + Status createStringDataSet(const std::string& path, + const std::vector& values) override; + + /** + * @brief Creates a dataset that holds an array of references to groups within + * the file. + * @param path The location in the file of the new dataset. + * @param references The array of references. + * @return The status of the dataset creation operation. + */ + Status createReferenceDataSet( + const std::string& path, + const std::vector& references) override; + + /** + * @brief Creates an extendable dataset with a given base data type, size, + * chunking, and path. + * @param type The base data type of the dataset. + * @param size The size of the dataset. + * @param chunking The chunking size of the dataset. + * @param path The location in the file of the new dataset. + * @return A pointer to the created dataset. + */ + std::unique_ptr createArrayDataSet( + const BaseDataType& type, + const SizeArray& size, + const SizeArray& chunking, + const std::string& path) override; + + /** + * @brief Returns a pointer to a dataset at a given path. + * @param path The location in the file of the dataset. + * @return A pointer to the dataset. + */ + std::unique_ptr getDataSet( + 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. + * @return The type of object at the given path. + */ + H5O_type_t getObjectType(const std::string& path); + + /** + * @brief Returns the HDF5 native data type for a given base data type. + * @param type The base data type. + * @return The HDF5 native data type. + */ + static H5::DataType getNativeType(BaseDataType type); + + /** + * @brief Returns the HDF5 data type for a given base data type. + * @param type The base data type. + * @return The HDF5 data type. + */ + static H5::DataType getH5Type(BaseDataType type); + +protected: + std::string filename; + + /** + * @brief Creates a new group if it does not exist. + * @param path The location in the file of the group. + * @return The status of the group creation operation. + */ + Status createGroupIfDoesNotExist(const std::string& path) override; + +private: + std::unique_ptr file; +}; + +/** + * @brief Represents an HDF5 Dataset that can be extended indefinitely + in blocks. +* +* This class provides functionality for reading and writing blocks of data +* to an HDF5 dataset. +*/ +class HDF5RecordingData : public BaseRecordingData +{ +public: + /** + * @brief Constructs an HDF5RecordingData object. + * @param data A pointer to the HDF5 dataset. + */ + HDF5RecordingData(std::unique_ptr data); + + /** + * @brief Deleted copy constructor to prevent construction-copying. + */ + HDF5RecordingData(const HDF5RecordingData&) = delete; + + /** + * @brief Deleted copy assignment operator to prevent copying. + */ + HDF5RecordingData& operator=(const HDF5RecordingData&) = delete; + + /** + * @brief Destroys the HDF5RecordingData object. + */ + ~HDF5RecordingData(); + + /** + * @brief Writes a block of data to the HDF5 dataset. + * @param dataShape The size of the data block. + * @param positionOffset The position of the data block to write to. + * @param type The data type of the elements in the data block. + * @param data A pointer to the data block. + * @return The status of the write operation. + */ + Status writeDataBlock(const std::vector& dataShape, + const std::vector& positionOffset, + const BaseDataType& type, + const void* data); + + /** + * @brief Gets a const pointer to the HDF5 dataset. + * @return A const pointer to the HDF5 dataset. + */ + const H5::DataSet* getDataSet(); + +private: + /** + * @brief Pointer to an extendable HDF5 dataset + */ + std::unique_ptr dSet; + + /** + * @brief Return status of HDF5 operations. + */ + Status checkStatus(int status); +}; +} // namespace AQNWB::HDF5 diff --git a/libs/macos/include/aqnwb/nwb/NWBFile.hpp b/libs/macos/include/aqnwb/nwb/NWBFile.hpp new file mode 100644 index 0000000..6c78d07 --- /dev/null +++ b/libs/macos/include/aqnwb/nwb/NWBFile.hpp @@ -0,0 +1,175 @@ +#pragma once + +#include +#include +#include + +#include "aqnwb/BaseIO.hpp" +#include "aqnwb/Types.hpp" +#include "aqnwb/nwb/base/TimeSeries.hpp" + +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. + */ +class NWBFile +{ +public: + /** + * @brief Constructor for NWBFile class. + * @param idText The identifier text for the NWBFile. + * @param io The shared pointer to the IO object. + */ + NWBFile(const std::string& idText, std::shared_ptr io); + + /** + * @brief Deleted copy constructor to prevent construction-copying. + */ + NWBFile(const NWBFile&) = delete; + + /** + * @brief Deleted copy assignment operator to prevent copying. + */ + NWBFile& operator=(const NWBFile&) = delete; + + /** + * @brief Destructor for NWBFile class. + */ + ~NWBFile(); + + /** + * @brief Initializes the NWB file by opening and setting up the file + * structure. + */ + void initialize(); + + /** + * @brief Finalizes the NWB file by closing it. + */ + void finalize(); + + /** + * @brief Create ElectricalSeries 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 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 Closes the relevant datasets. + */ + void stopRecording(); + + /** + * @brief Indicates the NWB schema version. + */ + const std::string NWBVersion = "2.7.0"; + + /** + * @brief Indicates the HDMF schema version. + */ + const std::string HDMFVersion = "1.8.0"; + + /** + * @brief Indicates the HDMF experimental version. + */ + const std::string HDMFExperimentalVersion = "0.5.0"; + + /** + * @brief Gets the TimeSeries object from the recordingContainers + * @param timeseriesInd The index of the timeseries dataset within the group. + */ + TimeSeries* getTimeSeries(const SizeType& timeseriesInd); + +protected: + /** + * @brief Creates the default file structure. + * @return Status The status of the file structure creation. + */ + Status createFileStructure(); + +private: + /** + * @brief Factory method for creating recording data. + * @param type The base data type. + * @param size The size of the dataset. + * @param chunking The chunking size of the dataset. + * @param path The location in the file of the new dataset. + * @return std::unique_ptr The unique pointer to the + * created recording data. + */ + std::unique_ptr createRecordingData( + BaseDataType type, + const SizeArray& size, + const SizeArray& chunking, + const std::string& path); + + /** + * @brief Saves the specification files for the schema. + * @param specPath The location in the file to store the spec information. + * @param versionNumber The version number of the specification files. + */ + void cacheSpecifications(const std::string& specPath, + const std::string& versionNumber); + + /** + * @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; +}; + +/** + * @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 diff --git a/libs/macos/include/aqnwb/nwb/NWBRecording.hpp b/libs/macos/include/aqnwb/nwb/NWBRecording.hpp new file mode 100644 index 0000000..6926863 --- /dev/null +++ b/libs/macos/include/aqnwb/nwb/NWBRecording.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include "aqnwb/Types.hpp" +#include "aqnwb/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 rootFolder The root folder where the file will be stored. + * @param baseName The base name of the file (will be appended with + * experiment number). + * @param experimentNumber The experiment number. + * @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& rootFolder, + const std::string& baseName, + int experimentNumber, + 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/libs/macos/include/aqnwb/nwb/base/TimeSeries.hpp b/libs/macos/include/aqnwb/nwb/base/TimeSeries.hpp new file mode 100644 index 0000000..004f258 --- /dev/null +++ b/libs/macos/include/aqnwb/nwb/base/TimeSeries.hpp @@ -0,0 +1,148 @@ +#pragma once + +#include + +#include "aqnwb/BaseIO.hpp" +#include "aqnwb/nwb/hdmf/base/Container.hpp" + +namespace AQNWB::NWB +{ +/** + * @brief General purpose time series. + */ +class TimeSeries : public Container +{ +public: + /** + * @brief Constructor. + * @param path The location of the TimeSeries in the file. + * @param io A shared pointer to the IO object. + * @param dataType The data type to use for storing the recorded signal + * @param unit Unit for the electrical signal. Must be "volts" + * @param description The description of the TimeSeries. + * @param comments Human-readable comments about the TimeSeries + * @param dsetSize Initial size of the main dataset + * @param chunkSize Chunk size to use + * @param conversion Scalar to multiply each element in data to convert it to + * the specified ‘unit’ + * @param resolution Smallest meaningful difference between values in data, + * stored in the specified by unit + * @param offset Scalar to add to the data after scaling by ‘conversion’ to + * finalize its coercion to the specified ‘unit' + */ + TimeSeries(const std::string& path, + std::shared_ptr io, + const BaseDataType& dataType, + const std::string& unit, + const std::string& description = "no description", + const std::string& comments = "no comments", + const SizeArray& dsetSize = SizeArray {0}, + const SizeArray& chunkSize = SizeArray {1}, + const float& conversion = 1.0f, + const float& resolution = -1.0f, + const float& offset = 0.0f); + + /** + * @brief Destructor + */ + ~TimeSeries(); + + /** + * @brief Writes a timeseries data block to the file. + * @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 in separate blocks. + * @return The status of the write operation. + */ + Status writeData(const std::vector& dataShape, + const std::vector& positionOffset, + const void* data, + const void* timestamps = nullptr); + + /** + * @brief Initializes the TimeSeries by creating NWB related attributes and + * writing the description and comment metadata. + */ + void initialize(); + + /** + * @brief Pointer to data values. + */ + std::unique_ptr data; + + /** + * @brief Pointer to timestamp values. + */ + std::unique_ptr timestamps; + + /** + * @brief Data type of the data. + */ + BaseDataType dataType; + + /** + * @brief Data type of the timestamps (float64). + */ + BaseDataType timestampsType = BaseDataType::F64; + + /** + * @brief Base unit of measurement for working with the data. Actual stored + * values are not necessarily stored in these units. To access the data in + * these units, multiply ‘data’ by ‘conversion’ and add ‘offset’. + */ + std::string unit; + + /** + * @brief The description of the TimeSeries. + */ + std::string description; + + /** + * @brief Human-readable comments about the TimeSeries. + */ + std::string comments; + + /** + * @brief Size used in dataset creation. Can be expanded when writing if + * needed. + */ + SizeArray dsetSize; + + /** + * @brief Chunking size used in dataset creation. + */ + SizeArray chunkSize; + + /** + * @brief Scalar to multiply each element in data to convert it to the + * specified ‘unit’. + */ + float conversion; + + /** + * @brief Smallest meaningful difference between values in data, stored in the + * specified by unit. + */ + float resolution; + + /** + * @brief Scalar to add to the data after scaling by ‘conversion’ to finalize + * its coercion to the specified ‘unit’. + */ + float offset; + + /** + * @brief The starting time of the TimeSeries. + */ + float startingTime = 0.0; + +private: + /** + * @brief The neurodataType of the TimeSeries. + */ + std::string neurodataType = "TimeSeries"; +}; +} // namespace AQNWB::NWB diff --git a/libs/macos/include/aqnwb/nwb/device/Device.hpp b/libs/macos/include/aqnwb/nwb/device/Device.hpp new file mode 100644 index 0000000..31acd7f --- /dev/null +++ b/libs/macos/include/aqnwb/nwb/device/Device.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include + +#include "aqnwb/BaseIO.hpp" +#include "aqnwb/nwb/hdmf/base/Container.hpp" + +namespace AQNWB::NWB +{ +/** + * @brief Metadata about a data acquisition device, e.g., recording system, + * electrode, microscope. + */ +class Device : public Container +{ +public: + /** + * @brief Constructor. + * @param path The location of the device in the file. + * @param io A shared pointer to the IO object. + * @param description The description of the device. + * @param manufacturer The manufacturer of the device. + */ + Device(const std::string& path, + std::shared_ptr io, + const std::string& description, + const std::string& manufacturer); + + /** + * @brief Destructor + */ + ~Device(); + + /** + * @brief Initializes the device by creating NWB related attributes and + * writing the manufactor and description metadata. + */ + void initialize(); + + /** + * @brief Gets the manufacturer of the device. + * @return The manufacturer of the device. + */ + std::string getManufacturer() const; + + /** + * @brief Gets the description of the device. + * @return The description of the device. + */ + std::string getDescription() const; + +private: + /** + * @brief The description of the device. + */ + std::string description; + + /** + * @brief The manufacturer of the device. + */ + std::string manufacturer; +}; +} // namespace AQNWB::NWB diff --git a/libs/macos/include/aqnwb/nwb/ecephys/ElectricalSeries.hpp b/libs/macos/include/aqnwb/nwb/ecephys/ElectricalSeries.hpp new file mode 100644 index 0000000..8224286 --- /dev/null +++ b/libs/macos/include/aqnwb/nwb/ecephys/ElectricalSeries.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include + +#include "aqnwb/BaseIO.hpp" +#include "aqnwb/Channel.hpp" +#include "aqnwb/nwb/base/TimeSeries.hpp" + +namespace AQNWB::NWB +{ +/** + * @brief General purpose time series. + */ +class ElectricalSeries : public TimeSeries +{ +public: + /** + * @brief Constructor. + * @param path The location of the ElectricalSeries in the file. + * @param io A shared pointer to the IO object. + * @param dataType The data type to use for storing the recorded voltage + * @param channelVector The electrodes to use for recording + * @param description The description of the TimeSeries. + * @param dsetSize Initial size of the main dataset. This must be a vector + * with two elements. The first element specifies the length + * in time and the second element must be equal to the + * length of channelVector + * @param chunkSize Chunk size to use. The number of elements must be two to + * specify the size of a chunk in the time and electrode + * dimension + * @param conversion Scalar to multiply each element in data to convert it to + * the specified ‘unit’ + * @param resolution Smallest meaningful difference between values in data, + * stored in the specified by unit + * @param offset Scalar to add to the data after scaling by ‘conversion’ to + * finalize its coercion to the specified ‘unit' + */ + ElectricalSeries(const std::string& path, + std::shared_ptr io, + const BaseDataType& dataType, + const Types::ChannelVector& channelVector, + const std::string& description, + const SizeArray& dsetSize, + const SizeArray& chunkSize, + const float& conversion = 1.0f, + const float& resolution = -1.0f, + const float& offset = 0.0f); + + /** + * @brief Destructor + */ + ~ElectricalSeries(); + + /** + * @brief Initializes the Electrical Series + */ + void initialize(); + + /** + * @brief Writes a channel to an ElectricalSeries dataset. + * @param channelInd The channel index within the ElectricalSeries + * @param numSamples The number of samples to write (length in time). + * @param data A pointer to the data block. + * @param timestamps A pointer to the timestamps block. + * @return The status of the write operation. + */ + Status writeChannel(SizeType channelInd, + const SizeType& numSamples, + const void* data, + const void* timestamps); + + /** + * @brief Channel group that this time series is associated with. + */ + Types::ChannelVector channelVector; + + /** + * @brief Pointer to channel-specific conversion factor dataset. + */ + std::unique_ptr channelConversion; + + /** + * @brief Pointer to electrodes dataset. + */ + std::unique_ptr electrodesDataset; + +private: + /** + * @brief The neurodataType of the TimeSeries. + */ + std::string neurodataType = "ElectricalSeries"; + + /** + * @brief The number of samples already written per channel. + */ + SizeArray samplesRecorded; +}; +} // namespace AQNWB::NWB diff --git a/libs/macos/include/aqnwb/nwb/file/ElectrodeGroup.hpp b/libs/macos/include/aqnwb/nwb/file/ElectrodeGroup.hpp new file mode 100644 index 0000000..7b46ee2 --- /dev/null +++ b/libs/macos/include/aqnwb/nwb/file/ElectrodeGroup.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include + +#include "aqnwb/BaseIO.hpp" +#include "aqnwb/nwb/device/Device.hpp" +#include "aqnwb/nwb/hdmf/base/Container.hpp" + +namespace AQNWB::NWB +{ +/** + * @brief The ElectrodeGroup class represents a physical grouping of electrodes, + * e.g. a shank of an array. + */ +class ElectrodeGroup : public Container +{ +public: + /** + * @brief Constructor. + * @param path The location in the file of the electrode group. + * @param io A shared pointer to the IO object. + * @param description The description of the electrode group. + * @param location The location of electrode group within the subject e.g. + * brain region. + * @param device The device associated with the electrode group. + */ + ElectrodeGroup(const std::string& path, + std::shared_ptr io, + const std::string& description, + const std::string& location, + const Device& device); + + /** + * @brief Destructor. + */ + ~ElectrodeGroup(); + + /** + * @brief Initializes the ElectrodeGroup object. + * + * Initializes the ElectrodeGroup by creating NWB related attributes and + * linking to the Device object. + */ + void initialize(); + + /** + * @brief Gets the description of the electrode group. + * @return The description of the electrode group. + */ + std::string getDescription() const; + + /** + * @brief Gets the location of the electrode group. + * @return The location of the electrode group. + */ + std::string getLocation() const; + + /** + * @brief Gets the device associated with the electrode group. + * @return The device associated with the electrode group. + */ + const Device& getDevice() const; + +private: + /** + * @brief The description of the electrode group. + */ + std::string description; + + /** + * @brief The location of electrode group within the subject e.g. brain + * region. + */ + std::string location; + + /** + * @brief The device associated with the electrode group. + */ + Device device; +}; +} // namespace AQNWB::NWB diff --git a/libs/macos/include/aqnwb/nwb/file/ElectrodeTable.hpp b/libs/macos/include/aqnwb/nwb/file/ElectrodeTable.hpp new file mode 100644 index 0000000..6ab89ce --- /dev/null +++ b/libs/macos/include/aqnwb/nwb/file/ElectrodeTable.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include + +#include "aqnwb/BaseIO.hpp" +#include "aqnwb/nwb/hdmf/table/DynamicTable.hpp" +#include "aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp" +#include "aqnwb/nwb/hdmf/table/VectorData.hpp" + +namespace AQNWB::NWB +{ +/** + * @brief Represents a table containing electrode metadata. + */ +class ElectrodeTable : public DynamicTable +{ +public: + /** + * @brief Constructor. + * @param io The shared pointer to the BaseIO object. + * @param description The description of the table (default: "metadata about + * extracellular electrodes"). + */ + ElectrodeTable(std::shared_ptr io, + const std::string& description = + "metadata about extracellular electrodes"); + + /** + * @brief Destructor. + */ + ~ElectrodeTable(); + + /** + * @brief Initializes the ElectrodeTable. + * + * Initializes the ElectrodeTable by creating NWB related attributes and + * adding required columns. + */ + void initialize(); + + /** + * @brief Finalizes the ElectrodeTable. + * + * Finalizes the ElectrodeTable by adding the required columns and writing + * the data to the file. + */ + void finalize(); + + /** + * @brief Sets up the ElectrodeTable by adding electrodes and their metadata. + * + */ + void addElectrodes(std::vector channels); + + /** + * @brief Gets the column names of the ElectrodeTable. + * @return The vector of column names. + */ + const std::vector& getColNames() override; + + /** + * @brief Sets the column names of the ElectrodeTable. + * @param newColNames The vector of new column names. + */ + void setColNames(const std::vector& newColNames); + + /** + * @brief Gets the group path of the ElectrodeTable. + * @return The group path. + */ + std::string getGroupPath() const; + + /** + * @brief Sets the group path of the ElectrodeTable. + * @param groupPath The new group path. + */ + void setGroupPath(const std::string& groupPath); + + std::unique_ptr electrodeDataset = + std::make_unique(); /**< The electrode dataset. */ + std::unique_ptr groupNamesDataset = + std::make_unique(); /**< The group names dataset. */ + std::unique_ptr locationsDataset = + std::make_unique(); /**< The locations dataset. */ + + /** + * @brief The path to the ElectrodeTable. + */ + inline const static std::string electrodeTablePath = + "/general/extracellular_ephys/electrodes/"; + +private: + /** + * @brief The channel information from the acquisition system. + */ + std::vector channels; + + /** + * @brief The global indices for each electrode. + */ + std::vector electrodeNumbers; + + /** + * @brief The names of the ElectrodeGroup object for each electrode. + */ + std::vector groupNames; + + /** + * @brief The location names for each electrode. + */ + std::vector locationNames; + + /** + * @brief The references to the ElectrodeGroup object for each electrode. + */ + std::vector groupReferences; + + /** + * @brief The vector of column names for the table. + */ + std::vector colNames = {"group", "group_name", "location"}; + + /** + * @brief The references path to the ElectrodeGroup + */ + std::string groupPathBase = "/general/extracellular_ephys/"; +}; +} // namespace AQNWB::NWB diff --git a/libs/macos/include/aqnwb/nwb/hdmf/base/Container.hpp b/libs/macos/include/aqnwb/nwb/hdmf/base/Container.hpp new file mode 100644 index 0000000..e0e1126 --- /dev/null +++ b/libs/macos/include/aqnwb/nwb/hdmf/base/Container.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include "aqnwb/BaseIO.hpp" + +namespace AQNWB::NWB +{ +/** + * @brief Abstract data type for a group storing collections of data and + * metadata + */ +class Container +{ +public: + /** + * @brief Constructor. + * @param path The path of the container. + * @param io A shared pointer to the IO object. + */ + Container(const std::string& path, std::shared_ptr io); + + /** + * @brief Destructor. + */ + virtual ~Container(); + + /** + * @brief Initialize the container. + */ + void initialize(); + + /** + * @brief Gets the path of the container. + * @return The path of the container. + */ + std::string getPath() const; + +protected: + /** + * @brief The path of the container. + */ + std::string path; + + /** + * @brief A shared pointer to the IO object. + */ + std::shared_ptr io; +}; +} // namespace AQNWB::NWB diff --git a/libs/macos/include/aqnwb/nwb/hdmf/base/Data.hpp b/libs/macos/include/aqnwb/nwb/hdmf/base/Data.hpp new file mode 100644 index 0000000..36eccac --- /dev/null +++ b/libs/macos/include/aqnwb/nwb/hdmf/base/Data.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include "aqnwb/BaseIO.hpp" + +namespace AQNWB::NWB +{ +/** + * @brief An abstract data type for a dataset. + */ +class Data +{ +public: + /** + * @brief Constructor. + */ + Data() {} + + /** + * @brief Destructor. + */ + ~Data() {} + + /** + * @brief Pointer to dataset. + */ + std::unique_ptr dataset; +}; +} // namespace AQNWB::NWB diff --git a/libs/macos/include/aqnwb/nwb/hdmf/table/DynamicTable.hpp b/libs/macos/include/aqnwb/nwb/hdmf/table/DynamicTable.hpp new file mode 100644 index 0000000..7352f42 --- /dev/null +++ b/libs/macos/include/aqnwb/nwb/hdmf/table/DynamicTable.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include + +#include "aqnwb/BaseIO.hpp" +#include "aqnwb/nwb/hdmf/base/Container.hpp" +#include "aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp" +#include "aqnwb/nwb/hdmf/table/VectorData.hpp" + +namespace AQNWB::NWB +{ +/** + * @brief Represents a group containing multiple datasets that are aligned on + * the first dimension + * + * This class inherits from the `Container` class and provides methods to add + * columns of different types of data to the table. + */ +class DynamicTable : public Container +{ +public: + /** + * @brief Constructor. + * @param path The location of the table in the file. + * @param io A shared pointer to the IO object. + * @param description The description of the table (optional). + */ + DynamicTable(const std::string& path, + std::shared_ptr io, + const std::string& description); + + /** + * @brief Destructor + */ + virtual ~DynamicTable(); + + /** + * @brief Initializes the `DynamicTable` object by creating NWB attributes and + * column names. + */ + void initialize(); + + /** + * @brief Adds a column of vector string data to the table. + * @param name The name of the column. + * @param colDescription The description of the column. + * @param vectorData A unique pointer to the `VectorData` dataset. + * @param values The vector of string values. + */ + void addColumn(const std::string& name, + const std::string& colDescription, + std::unique_ptr& vectorData, + const std::vector& values); + + /** + * @brief Adds a column of references to the table. + * @param name The name of the column. + * @param colDescription The description of the column. + * @param dataset The vector of string values representing the references. + */ + void addColumn(const std::string& name, + const std::string& colDescription, + const std::vector& dataset); + + /** + * @brief Adds a column of element identifiers to the table. + * @param elementIDs A unique pointer to the `ElementIdentifiers` dataset. + * @param values The vector of id values. + */ + void setRowIDs(std::unique_ptr& elementIDs, + const std::vector& values); + + /** + * @brief Gets the description of the table. + * @return The description of the table. + */ + std::string getDescription() const; + + /** + * @brief Gets the column names of the table. + * @return A vector of column names. + */ + virtual const std::vector& getColNames() = 0; + +private: + /** + * @brief Description of the DynamicTable. + */ + std::string description; + + /** + * @brief Names of the columns in the table. + */ + std::vector colNames; +}; +} // namespace AQNWB::NWB diff --git a/libs/macos/include/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp b/libs/macos/include/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp new file mode 100644 index 0000000..2f52d9e --- /dev/null +++ b/libs/macos/include/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "aqnwb/nwb/hdmf/base/Data.hpp" + +namespace AQNWB::NWB +{ +/** + * @brief A list of unique identifiers for values within a dataset, e.g. rows of + * a DynamicTable. + */ +class ElementIdentifiers : public Data +{ +}; +} // namespace AQNWB::NWB diff --git a/libs/macos/include/aqnwb/nwb/hdmf/table/VectorData.hpp b/libs/macos/include/aqnwb/nwb/hdmf/table/VectorData.hpp new file mode 100644 index 0000000..efb269f --- /dev/null +++ b/libs/macos/include/aqnwb/nwb/hdmf/table/VectorData.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "aqnwb/nwb/hdmf/base/Data.hpp" + +namespace AQNWB::NWB +{ +/** + * @brief An n-dimensional dataset representing a column of a DynamicTable. + */ +class VectorData : public Data +{ +public: + /** + * @brief Gets the description of the table. + * @return The description of the table. + */ + std::string getDescription() const; + +private: + /** + * @brief Description of VectorData. + */ + std::string description; +}; +} // namespace AQNWB::NWB diff --git a/libs/macos/lib/libaqnwb.dylib b/libs/macos/lib/libaqnwb.dylib new file mode 120000 index 0000000..61a5762 --- /dev/null +++ b/libs/macos/lib/libaqnwb.dylib @@ -0,0 +1 @@ +libaqnwb.0.dylib \ No newline at end of file From 7c9ddd5e5ae52be965477e37d5726052806abc60 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:55:15 -0700 Subject: [PATCH 02/32] update cmake to find aqnwb library --- CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b947a1..01bb918 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -108,6 +108,9 @@ if(NOT HDF5_FOUND) #if package finding fails, try manually find_path(HDF5_INCLUDE_DIRS H5Cpp.h) endif() +find_library(AQNWB_LIBRARIES NAMES aqnwb) +find_path(AQNWB_INCLUDE_DIRS aqnwb.hpp) + include_directories(${HDF5_INCLUDE_DIRS}) target_link_libraries(${PLUGIN_NAME} ${HDF5_LIBRARIES} ${HDF5_CXX_LIBRARIES}) From 4fbe80b38f0f613b9ccb650b9f8a016b40527826 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:48:47 -0700 Subject: [PATCH 03/32] update aqnwb lib --- libs/macos/bin/libaqnwb.0.1.0.dylib | Bin 680424 -> 681336 bytes libs/macos/include/aqnwb/BaseIO.hpp | 31 ++++++++++++++++++ libs/macos/include/aqnwb/hdf5/HDF5IO.hpp | 39 ++++++++++++++++++++++- libs/macos/include/aqnwb/nwb/NWBFile.hpp | 23 ++++++++++--- 4 files changed, 88 insertions(+), 5 deletions(-) diff --git a/libs/macos/bin/libaqnwb.0.1.0.dylib b/libs/macos/bin/libaqnwb.0.1.0.dylib index 1b27c56dd35a075d96968e395e7adb132173a3de..18b1098a62295af2d10f31de70e5b0d8dc742935 100755 GIT binary patch delta 119722 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

EW!*qFPQZCM1sCCTT!ypo9lRC0*VhBe zzs&mYL*gU_LAV@;<2yJSyU9l{6A9Q0r($26frD`l4!^?spHCu+f|EE7m*XUCX}~4H z9ykNH#o0I*=ivxkXeALtq68=63cLtgVN8)T8i}P?2F2ZSQtEH4g76mu(cI=R|C31cq97eVgV*C2d{RD6o0#=?{cq|8xDcyIUz#7qhj@qCc*`9nIt}= zAP0Yg^YCe0h=0Q+xSFRPK%{&FW1=yR#T{@e?rkNJLE<@_jVIzfJR29{rMLuth%4|m zY?1GnOdP`Q*!nXGZxX-b09@lCJ)%(jFpk8XaSZN@lkrfz48MSL)K-g?!~qK4!6kSd zuEaaAn|x1a;ydhx&tYHuCl1E7n&<(A+b#EhGZIl0bj5MFKhDAXT zVZ&bd6YOhj*5BPEf+;wL!|_ENg>U0{T=!u;qDK9QX5vab zADi!jO{~D)coX)+`)~w4fur#i9RDZl|9uk46g2SCBTB<j;(UA+7vZWc^#Dq-wK0iG6290)ex7Hd5B9`EurGcA z2jgTMj^D*m_+uQWwpw0{;y%~|55_)t5)Qzx;xPOcj>2nk9R3m~RWa|s5{F5oQE(Qo$A94MxMmwYq64@o zK8gKt8GZs+;^A1HKc+h{g@h-bgUwIDO}vYP@CF=)cj96AD2~PFa0b4K<@XQh25Pp| zBl4)G_QF26GY-Iy<1joJ55uuI7SG0sc!}C-Sw!Lk3Nr8)I2#|tx%dn&z}ImxuHH@$ zs2n%JcW@`WW&XK4=@CCpfj1t4{qSTQf|GC*UV@YH8k~kd!AEqNt~o0-dT6( z3SNZo;S5}_y&hpU_Q83$J1)de;SxMjY+wH?NKB`|sk-jK8`ulKkA3l{I2iB2;rIs} zg)iYae78O8e-ep$9rTD6;Z`^U_rTfsX`GA4;6nT&F2!%+8+c_0)_P4@WfwZ zUwjIO;Hx+SSMk*Yh`|kU0&a&>a8E0VbP~_tEIbY$z%ODK7v14h?2T7oKfDo#;5;0G zt>2SKAyJCc@lBkCYj)Hlti)c}v!?D)XB>$i$1!*?PEcDdu_RI`n2po%5}buUz~(Qj zOniY4;Dh)iK7-9)W|_E-@7OK(fAvm!L~gEH&;)zoPS_Vej)UAH_L%AkM>MaUp&Qm*54s z0u(jWJ^459AN5`Et$A4{svFL z=kaS|tJy#$iDmWz`R!NoBU2N*=+FrVb<+*_;qAB!?h~l(0k{AM;$Gdg{X_g|ce($~ z9@mziXqvc0fe)S$jD{J_$L^*o7C6178gz==2zn;)5)n1z?%J=lh;_0j&z@j#r3Q}73Bt7Q*~ zwG{k`*Wn(I>wq8QsrXa86X)O?*!&3BL||X-{{>FQU)nAA|1TsO$xlE{)C)Bo+6f@JT!iSK@cD%V6E1&#)gpX6%}5u_SAI4Y_p2p17y6wU+!_4ie9sgg6H$;ynBw zF2q}L2|kJ|@Nd{MM|YsMTzfOVJMJLvXA<5do}|DJPsAbE{93}g^3U~(+n9F^`OT;D zGEQi-?ZD$5i)39`G89<&>S+5>9E$yGYkNjLZC`>@ z@l{;tqU{6f*z2v9ToRvX!jj^y1uk`Uz<4|nr&90luI-hzwSET|o17|7_2`eM|HIOV}r8 zkb|Fd@KgscaPUVC-s|8q|5&$gx%E#%Z;nPy{@Fu&!q36O96Z&*DGpxl;LU%V?OTrh zozU%Desi$1yc5ek`p<3J%)w7Pc%p+3JNQX29q;e$TQ-qk`DOS1qQ22mfrh^?y!j^9Kib+QErxT|O-G z54jz@#lh8D*vI>)exQTXjIHu;P#Mb}c@72F9PHEb!2zFl@aGOLc5pSH2m3b{oAY1o z?;Tj^P_Wj)_Z<9Cs|N?{;@|-ej&X2uEBSQzzdLZ+p`g;i^;EOu@Ug+Q- zTFd$W-yLWnpL_n-Qyl!IgR9HOq5rLa*1@YCe9ghVtnD5gV7`M39Ne(|gY^*(e#60E zIrujRTU&Q{aKOB0W5 zIC$#=wsop-u5Wee{9qq%2M>1e3?cjI^uYJ(ZT7G*lVR&cW(55D`Kr~_VNB{@8IA8>VHnaBnPi@@OcM!4t#KbpS9cif9t_Yhl1@6{?5Vo z9PH8k!40%`aHxaBjsJH=jC3e?!ND^e{IY|Ws-6Cxbt@g(w>h+bEq-7{Jn7Kkl7sI% z*fZ$CBkthf5C;!-@T4Hizy}tTM2CVm)c+jO5{LFIhxRXQoBxn|rN|+D+QF9`{JVoK zJ?zKz&t_d5?C#))9&~R@^9K`_HV*FS;BF7tcC*sCQ^&~K^1YF~hRAEEyoSkZguJ5U zHBw%qm?f+_*}K?d)ymhn<6@vWM;3pUw~043R(fk=I4rPnE(l`QsINmD~DMIybiY z-f{M9ep`G;UU%hnPhR)s?2WRV@IBCo3QswOWddDW1YtGsH-tG2xA z$g8gH%{$J|Rnfz?!DKkdfn!8u;Tt8x_6vRy$`_lLsqsLBraca!WZJoY$s^+q7kZ1Kk z=XyaAGp5CdPnwy~O$K^0=82cKB{i)c-Q2@JaNL-wPfv@PG~wkzV<(RvmoW2*mu!1_ zS8uTGr(V_ldQ}-~+niM0)7I+M>Y@7Q#@mu#t?pdS)^Tr5H{09E)mz&VM%3(XJF}^J zE8F75)$7~*_SL9itGHUdw&n&TS9i1JCs*%Ky_@}DY(LMd-r43nr~0?sn!H|pcAdHb z_CtAg{J3c|VkS+UXxf|Dy40)D&~{;7^#=9+wSV@4ZSSU4pV{2DYD7)lOto<@jkQfL zt=`d=lOsEF_xtLNZ0^6iw6INmzq*I*{>JKFww&SBU2J~2)f?M>n_aV(ZRs)Tnz>hw z?X^59>3!R!j_q!z8bfRgzo_2MmRwY`;kHemRi8WZU;B5oW{rpD``4=>FPClp^=fYY z(^ly2+Mr5+tzL822DVwXT1I3I%C(Ix#apsHe58FyZP$jj(AM&&F`lj-Rf25OWfQjWc2e8b zcGTo6&3~#ibZtm~@Igg;O|LWGmwx*+0Z)__E z-QCvQTmJl(tNhu|zLQn`yV?AkNUo%nYg=ib^RWEc%k1nN*;-Zq?zT0af+SZtz&$>) zE$=q+pD;OLa%5H7yEbUpHRxZP>=IyV8`!3`krNPLYv10rwMX|J|1Pr~m2EWY64d=) zO+v6TJ#2G)WowU`Igsk%+Q@dj zp@t0eB@7~-Cb>M0{=CZtRRk;Jg|rDhh{Pp51C2W#ao7Yt)7hXLtFXJH8VTQ z%%Aqi@!Cr2xwf}OnHgy&vbn7G3)i`|T)5}!%Q*V4g{LS&0>usyw!nI-5!0xtdEd=&cdClB7 z-Tb=QTzy?zH}wDaMVEWZ6t?g3AE)&{CTO=Fezpz`U0VnH{l_`?|Mv_D^#Au14YcoU z+ra-gQ-S|+DQ#n#+E19dt*n7T|DHD8{r-I!y8p+%cMtmaK6mf&@0&2l?>}-~?U&W| zZcW#^@>$n$y2)9wzgO5#cMrcH+m1$Z=l;jB_ptSm_Y}Ek=6bd3p`$+VX4NC$-&aDe zu}s9qJ-YozQ=q?XT4R|)W8^-Sapi5aLC+rD|9u2Kg8n@*d;G^6h(e$-Mn$w&q{#Y{E>g8_vVVDIv#PG$R8{3I&%U;pw;TU%`sP!$o1W`x-GaK?j(XWovFy2rEzL{L z=@_&2`UjfhGVd&Av9$f-<=RJ95c{44cC)?ZB~9#DV4$85`Fd7wE&EVwn#x%jD|=?% zL1a(OYHYey4eF+w>|s9+x#~I_y6bGPg+1ch$Silw&3Yl{$xMmvdSa_~XOrFa97tc8 zpC0CI&wfhet2rgry7%a6i*|Qyxvfz%*L59(Z=G?=lmAnqk9;rp_jkk|lB+7Yj+UzO zok>;uU&c+5_F;U#_mRal!q2;^#qFJI)mw#B+w4K!BA*n;OrJVyY$v}?{_=`>SrV4y z`*Y`wu$27rT6ko&^DSK4E!&j(v)_{MrnG-;F-W#)@+tImIP5<%FM_UKh9F~>U zVcyE(f%W>%A9n2hUG2Vl_QUV~_^gK8wec0L2aNr6`Lu-Vy`n46T`e5?#s_uA_#P~* zlYMY((1aDOn_V08%hhX3vMxo=NH6~Uuld%;w&XZPF7|F3(&mmOaQxX7=|OV~o_RO? zaQfrTtrL!K{iY=F<|~sk#y)@ZOW%>BrlcOJwSHaNTQvjPRcxrZm^#o;Fl7Pq&))9{=n|pZ{42cQ)PPR>?gfWGMmKRg_=m68SMBFqS~}#D;&Bh#Rvh&# z+4O{icBpkB z=7*YB`^`9a;@OR>Y~RH8@A!4YarZ7g?|IJqvA+GUk9cTa!-org*x6!rjdL>sb3bnM zL-nIGJsv)?D8l#qK+ER?-i-}t<}xjG zLjLb%Tf(O8sNH_#`sdu-+?I(^gj%XceQWmHDaogW(Y`p;#94uAON zzE9_`F1uWCtLKmuYun_eOMBjk_0Eb1_lVHykMC+9u{ruBZZ*~>AdKc2ZXH{oQ9 zvKGht{;{W7jh=5=vsSI26ZGWgk4`Q8zUR|E_uh)X@nWm4qu(CuKCZ#?_g>pMKcTU8 zLbckr*97LA>fXJoSGSLcUkiKf-BI68e!AAWjlUk7`9}Ms5g$8cF1YV{CTxEhkm#kJ|<9p6>FX11vRbLZArl+Umo4=Y-zxc z&wLPlcz9*i3H9>c-M3@)@*}Ty$qM}3{gtf?D=(~Rl{~laM>8tsU%2(w*>#5}&6!v+ z@<7+nlg~}EzQ4ZrH-Gy2#C*5sh0ryZA0GPW`Bg7g)*rLXvwG}-T1S`HKicY2qeV6M z%0#M=iK>myfruU?Xo8A8_m1DukWQts=VhNJNwME`)eFE@?Dr!q6&%Zk))!zjku7s;iufM_6ABU%9NBSEK!ZZY*7XdqQ5ji;Z$!F86-< zLXB}dJdfQt7XPIC(DA!Gt=j{x96j^d4>9W+H(mGpWaqhw#r=mj7~b^yGrzt0OlCjp z?yr)jp1S?&TM;Lkj;rrA*X!>5zlH`*Dtd0nM=$QNCH_8freD=YV>>t{_K6CbC1m6QH|;! zxqRe{jpq+Oe6Z$AZ(Y6D<$kS*t(TiUJ^oC+7L%XZwIkZUQ*~>N8rNQX@8I$tlMj5A zUO!{=i4MJAJbI+v*>`UT&!6^J=&$JoLt1{nJ-X)jxtFi~G;;3`ub&7Fx_EKQ(Z05~ z&*ilD-`l;j>r|JY?tMPLS774~Z%;ne&|2rG#32hq@4u6=dv>+gUY>991oS*2w!rN_2-aC(Z z#d(Y#(P!*}_ot1S7oYsi-on*Im*(btHFE6er)KpGv(#PkXX~Yb(PeLs{k^2!S6e^4 z;(sV>>83=tfol(}u*N#Ki0?M?)>BWFUb}NBu-a2=&h%{llx=9jkdPCPzI|g?dVjBH z&h++ZR`%ZKV-oMg#;!=cUb+9qyp98xet*{bTcuOq2&d+)20XoHN|pXUe0wZ+ak+oD zZ=aa)E@O5lbe^4Ddk@`UefE^Cdwyn}xAOY+_@i4)TFj#CneE*_ z{jJL94Qf|j_`=eyy!o~-XEm?lva9$)>OKkM6aS82t5ufTu> zVU2g+UG?kAW6zvee!uBxkI=w2cl*tqRJvB(Q}3`_lX=7niFuQ&WP zsApp1yoT4}UU}-w(kEW|XpGmcqEpvT#N2)N$eQV&1kF5i)#I&~U);E@)uxm>X+5GZ zKk8BP`t>u5_lD`JobvqGLmsQ29`s%f_bTmUK3)Dsk8_*Ww|sxl z`ohQyvxY1=lU#Sq&`tCA|9WTJ%_{Y#?f=X7Sd~5dkIblL+i`2#gjidZA+CAWu<5S- zFCW>sDspE+;NkgS1{Xa&{DaLSeb)S1zI*SS!IkS4_4=%`!4IDfZ}r0)1vmB&U-HTa z+b8$>VdKq-^Lnmu>FKn8_@>pXdsW$V{`HTNV?O(JPt1(#u{B0J9s00dYU}w6H+|Fb zyQv+H)|>b5C^5h{|hwwA#Lk-&02J@SO8b^M$Lbo#<|R#4ee{8a zySGf;*I|16SoeO`q?FmiOY=glM_#B}oIa|<&b49LqZA0cwySIp%ZQ`x%z9%OBbI0aPOQcGmd2!ow_#gWtS@N zt$Xo$hrVgu4&D0Ib>`I?`x7nm;`Y|q)nr8B6D!ZX`%>LEw_a)3zUi3Qj~-k8P{#QS zt^4n|`t`E;-9l=uoWJ?XoW9j3)ZgEI`}^nL=y`VirSfQ}+HYF}emdl9v$lUa@n~%G zh8s7>?Pz~@)$|si;Pmp(P;j`xN+^D2r9XFeMBwv&H*k9#xU{Nv=8ot~>(^|$HY=S+{gwf9~@y|#%5 zy%wzde0r7gy2+R7<5V-Tj})D@dehm*Z=wcw6i-;j4ixVaCr5*C#E#* z7vZts!!FK&C2>M>N2=u@m;4+?+)~tky-N57qxzPV#)2^N9$eN==GAz=4bkx zn=~}@_91z4UU78$-Bk~MxyJguOT*sBCa&1F%X*|m^{d|;`hNjU2(tI@v5daedq_T=x(HXV+u;3l_(LvBhN(;2tYel1Kq?Dw#T!J_-2()d&XKDY#M9va>AD?x3FNP6r|DIx z%gns9E15%7<3Jg8QA2Qu-zLpOHA?r4DNLde5oSd@^cC%9L~%8E=7M7%OTuJ zu>!qc@xW#5#{PQBQlgkZlWedGJ@$I@4cdcqWSCnmTOS{*+iT}HPEB8nvb6?1pdXoB z^qu%{us#BC9f)$mqOB1wlWz$>X&=SJz?O9RyO@xS5Yi@*RBvxf|4Y(jpHZKDuX;Pz z#_aTaJ&0cw2@ZTdple6DwLX7*i5^wI(OmYUwjUs~wFSw8E`?c>2sAvW5qa0bWtnyEr?EYPZ{V4CqDbr{LG%45cHIH-!2MzKfdm#k zoh-1BKr3_y3z^rCfIEd!^tG##U*Jlnr#zZs!;~Hdx^$TitJRS(pQnH1dCBH%6jH8h zUBty4{r_yAS@r*JBp3`d7Px> zaFEchmI1ub{rm+D%pX|b-7p_Wh7cUNi0RhgUy)5%3<)u`f2G*WwW&)B22!S&D+<2m zDJBrLu!m>p1#0y)ZSa3ZXF;S^KCm-)z$|S!RfZEHp|~HSj&C?BYClxYDg9_>7ELgR zk=`=Y84BUPBR{a6h`qW`@4kX2%rWu)c_7_aA649&_2H3>6g~#mEAl1bF4@0(8X#JW zmcES$qrGstvng_KAeVbJd!ryWo}K4tS>L|r5X&aj_oDg!kY#_&S5I3fy;fN|_vlN- z_24Q7RS+I6=FKZ{bWsbQ3Amjk?_CEu>{X>2XX^l=fVzxwFa8>b#^;{SyF&0<$m;)7 z8=7X$Sne96|HW3xNLTKVzn-ScPmTP+l%z6B5<>jol^(U(#p;Yv0jRwNKr4+Kvh!GeNYw03gLUbgun6x zM|(ROUinu#bcGwET3YE)xH<(F>+OJbFDb7u+)^5k$j3-i#Nc+)@c@jnpR9y=RFuZ&SE~aA;MW3~_&J_T=pD>a0tE8O{~1hxxF|ydC3(IXLxLnM+S{Zo3nD znhZ;!7u$s<$uYft&XY}KL-@Pb@;2aA#-JP1W)W^7ShW@kOwqBt;qd;*l0Vlw90KgN ziU$Z_Nz#9bpic}53Ek${KoPhvguIh^#FLYwVdrJ5S>3z~iNj0f(>)geVRGqGPHnX1hIHcZ&hN^;$5pslU z+_@>5Ya?QYu26UJkfrI7kr*C3l(FX&FRSvvQUHHd@}IkgWj!Vu3t~d0esza(j)ESQ z1DC<}W`%utL^y;xShaNi53RXVp5ra75X2gvPQ-!+?vCNES9eW{cfU>ysF=L=Um%RV z<_D{J-QVqX>yu+RYOQ6^WS`s^q&2b%)$&Iiw066l2i7FvzkU3&YCnuWNyUBe7&+cz zIp2TD^(=P{^#)yS7uONoFrieCB}o`Rh2bOWK49-;@(+O<1}4Mvo0uQ}UKqbQ_5oj4S?Bo}(Y(|0tng{7{8w(n zH=0|!Oz_A}ysv;)RQA&BEVnD(8y>2o18;pgjPp$eM~}$f*~A^C~_SkN&Ifji-ZzOtUH$1TvkU_Jg~T)tBrtZA)jmZ#34{k1RWr zP$bOrlRUE60mRlz+?eA3N)43?j0Lf~reF6t$^P7Qd;31>q44WXY#Ks|R5hlb

    t!YiHX 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 b@WA-%3Lh?6edh~rc6fDnmp?92f}8&bh^=!& diff --git a/libs/macos/bin/libaqnwb.0.dylib b/libs/macos/bin/libaqnwb.0.dylib deleted file mode 120000 index 3a2323e..0000000 --- a/libs/macos/bin/libaqnwb.0.dylib +++ /dev/null @@ -1 +0,0 @@ -libaqnwb.0.1.0.dylib \ No newline at end of file diff --git a/libs/macos/include/aqnwb/aqnwb.hpp b/libs/macos/include/aqnwb/aqnwb.hpp deleted file mode 100644 index 585ebcc..0000000 --- a/libs/macos/include/aqnwb/aqnwb.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include - -#include "aqnwb/aqnwb_export.hpp" - -/** - * @brief Reports the name of the library - * - * Please see the note above for considerations when creating shared libraries. - */ -class AQNWB_EXPORT exported_class -{ -public: - /** - * @brief Initializes the name field to the name of the project - */ - exported_class(); - - /** - * @brief Returns a non-owning pointer to the string stored in this class - */ - auto name() const -> char const*; - -private: - std::string m_name; -}; \ No newline at end of file diff --git a/libs/macos/lib/libaqnwb.dylib b/libs/macos/lib/libaqnwb.dylib deleted file mode 120000 index 61a5762..0000000 --- a/libs/macos/lib/libaqnwb.dylib +++ /dev/null @@ -1 +0,0 @@ -libaqnwb.0.dylib \ No newline at end of file From 663dc934e3e9060e3eb43e8dea79ab9f017eb14e Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Tue, 27 Aug 2024 09:39:44 -0700 Subject: [PATCH 05/32] add spec to resources --- Resources/spec/core/2.7.0/nwb.base.json | 1 + Resources/spec/core/2.7.0/nwb.behavior.json | 1 + Resources/spec/core/2.7.0/nwb.device.json | 1 + Resources/spec/core/2.7.0/nwb.ecephys.json | 1 + Resources/spec/core/2.7.0/nwb.epoch.json | 1 + Resources/spec/core/2.7.0/nwb.file.json | 1 + Resources/spec/core/2.7.0/nwb.icephys.json | 1 + Resources/spec/core/2.7.0/nwb.image.json | 1 + Resources/spec/core/2.7.0/nwb.misc.json | 1 + Resources/spec/core/2.7.0/nwb.namespace.json | 1 + Resources/spec/core/2.7.0/nwb.ogen.json | 1 + Resources/spec/core/2.7.0/nwb.ophys.json | 1 + Resources/spec/core/2.7.0/nwb.retinotopy.json | 1 + Resources/spec/hdmf-common/1.8.0/base.json | 1 + Resources/spec/hdmf-common/1.8.0/namespace.json | 1 + Resources/spec/hdmf-common/1.8.0/sparse.json | 1 + Resources/spec/hdmf-common/1.8.0/table.json | 1 + Resources/spec/hdmf-experimental/0.5.0/experimental.json | 1 + Resources/spec/hdmf-experimental/0.5.0/namespace.json | 1 + Resources/spec/hdmf-experimental/0.5.0/resources.json | 1 + 20 files changed, 20 insertions(+) create mode 100644 Resources/spec/core/2.7.0/nwb.base.json create mode 100644 Resources/spec/core/2.7.0/nwb.behavior.json create mode 100644 Resources/spec/core/2.7.0/nwb.device.json create mode 100644 Resources/spec/core/2.7.0/nwb.ecephys.json create mode 100644 Resources/spec/core/2.7.0/nwb.epoch.json create mode 100644 Resources/spec/core/2.7.0/nwb.file.json create mode 100644 Resources/spec/core/2.7.0/nwb.icephys.json create mode 100644 Resources/spec/core/2.7.0/nwb.image.json create mode 100644 Resources/spec/core/2.7.0/nwb.misc.json create mode 100644 Resources/spec/core/2.7.0/nwb.namespace.json create mode 100644 Resources/spec/core/2.7.0/nwb.ogen.json create mode 100644 Resources/spec/core/2.7.0/nwb.ophys.json create mode 100644 Resources/spec/core/2.7.0/nwb.retinotopy.json create mode 100644 Resources/spec/hdmf-common/1.8.0/base.json create mode 100644 Resources/spec/hdmf-common/1.8.0/namespace.json create mode 100644 Resources/spec/hdmf-common/1.8.0/sparse.json create mode 100644 Resources/spec/hdmf-common/1.8.0/table.json create mode 100644 Resources/spec/hdmf-experimental/0.5.0/experimental.json create mode 100644 Resources/spec/hdmf-experimental/0.5.0/namespace.json create mode 100644 Resources/spec/hdmf-experimental/0.5.0/resources.json diff --git a/Resources/spec/core/2.7.0/nwb.base.json b/Resources/spec/core/2.7.0/nwb.base.json new file mode 100644 index 0000000..b068e87 --- /dev/null +++ b/Resources/spec/core/2.7.0/nwb.base.json @@ -0,0 +1 @@ +{"datasets":[{"neurodata_type_def":"NWBData","neurodata_type_inc":"Data","doc":"An abstract data type for a dataset."},{"neurodata_type_def":"TimeSeriesReferenceVectorData","neurodata_type_inc":"VectorData","default_name":"timeseries","dtype":[{"name":"idx_start","dtype":"int32","doc":"Start index into the TimeSeries 'data' and 'timestamp' datasets of the referenced TimeSeries. The first dimension of those arrays is always time."},{"name":"count","dtype":"int32","doc":"Number of data samples available in this time series, during this epoch"},{"name":"timeseries","dtype":{"target_type":"TimeSeries","reftype":"object"},"doc":"The TimeSeries that this index applies to"}],"doc":"Column storing references to a TimeSeries (rows). For each TimeSeries this VectorData column stores the start_index and count to indicate the range in time to be selected as well as an object reference to the TimeSeries."},{"neurodata_type_def":"Image","neurodata_type_inc":"NWBData","dtype":"numeric","dims":[["x","y"],["x","y","r, g, b"],["x","y","r, g, b, a"]],"shape":[[null,null],[null,null,3],[null,null,4]],"doc":"An abstract data type for an image. Shape can be 2-D (x, y), or 3-D where the third dimension can have three or four elements, e.g. (x, y, (r, g, b)) or (x, y, (r, g, b, a)).","attributes":[{"name":"resolution","dtype":"float32","doc":"Pixel resolution of the image, in pixels per centimeter.","required":false},{"name":"description","dtype":"text","doc":"Description of the image.","required":false}]},{"neurodata_type_def":"ImageReferences","neurodata_type_inc":"NWBData","dtype":{"target_type":"Image","reftype":"object"},"dims":["num_images"],"shape":[null],"doc":"Ordered dataset of references to Image objects."}],"groups":[{"neurodata_type_def":"NWBContainer","neurodata_type_inc":"Container","doc":"An abstract data type for a generic container storing collections of data and metadata. Base type for all data and metadata containers."},{"neurodata_type_def":"NWBDataInterface","neurodata_type_inc":"NWBContainer","doc":"An abstract data type for a generic container storing collections of data, as opposed to metadata."},{"neurodata_type_def":"TimeSeries","neurodata_type_inc":"NWBDataInterface","doc":"General purpose time series.","attributes":[{"name":"description","dtype":"text","default_value":"no description","doc":"Description of the time series.","required":false},{"name":"comments","dtype":"text","default_value":"no comments","doc":"Human-readable comments about the TimeSeries. This second descriptive field can be used to store additional information, or descriptive information if the primary description field is populated with a computer-readable string.","required":false}],"datasets":[{"name":"data","dims":[["num_times"],["num_times","num_DIM2"],["num_times","num_DIM2","num_DIM3"],["num_times","num_DIM2","num_DIM3","num_DIM4"]],"shape":[[null],[null,null],[null,null,null],[null,null,null,null]],"doc":"Data values. Data can be in 1-D, 2-D, 3-D, or 4-D. The first dimension should always represent time. This can also be used to store binary data (e.g., image frames). This can also be a link to data stored in an external file.","attributes":[{"name":"conversion","dtype":"float32","default_value":1.0,"doc":"Scalar to multiply each element in data to convert it to the specified 'unit'. If the data are stored in acquisition system units or other units that require a conversion to be interpretable, multiply the data by 'conversion' to convert the data to the specified 'unit'. e.g. if the data acquisition system stores values in this object as signed 16-bit integers (int16 range -32,768 to 32,767) that correspond to a 5V range (-2.5V to 2.5V), and the data acquisition system gain is 8000X, then the 'conversion' multiplier to get from raw data acquisition values to recorded volts is 2.5/32768/8000 = 9.5367e-9.","required":false},{"name":"offset","dtype":"float32","default_value":0.0,"doc":"Scalar to add to the data after scaling by 'conversion' to finalize its coercion to the specified 'unit'. Two common examples of this include (a) data stored in an unsigned type that requires a shift after scaling to re-center the data, and (b) specialized recording devices that naturally cause a scalar offset with respect to the true units.","required":false},{"name":"resolution","dtype":"float32","default_value":-1.0,"doc":"Smallest meaningful difference between values in data, stored in the specified by unit, e.g., the change in value of the least significant bit, or a larger number if signal noise is known to be present. If unknown, use -1.0.","required":false},{"name":"unit","dtype":"text","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."},{"name":"continuity","dtype":"text","doc":"Optionally describe the continuity of the data. Can be \"continuous\", \"instantaneous\", or \"step\". For example, a voltage trace would be \"continuous\", because samples are recorded from a continuous process. An array of lick times would be \"instantaneous\", because the data represents distinct moments in time. Times of image presentations would be \"step\" because the picture remains the same until the next timepoint. This field is optional, but is useful in providing information about the underlying data. It may inform the way this data is interpreted, the way it is visualized, and what analysis methods are applicable.","required":false}]},{"name":"starting_time","dtype":"float64","doc":"Timestamp of the first sample in seconds. When timestamps are uniformly spaced, the timestamp of the first sample can be specified and all subsequent ones calculated from the sampling rate attribute.","quantity":"?","attributes":[{"name":"rate","dtype":"float32","doc":"Sampling rate, in Hz."},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for time, which is fixed to 'seconds'."}]},{"name":"timestamps","dtype":"float64","dims":["num_times"],"shape":[null],"doc":"Timestamps for samples stored in data, in seconds, relative to the common experiment master-clock stored in NWBFile.timestamps_reference_time.","quantity":"?","attributes":[{"name":"interval","dtype":"int32","value":1,"doc":"Value is '1'"},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for timestamps, which is fixed to 'seconds'."}]},{"name":"control","dtype":"uint8","dims":["num_times"],"shape":[null],"doc":"Numerical labels that apply to each time point in data for the purpose of querying and slicing data by these values. If present, the length of this array should be the same size as the first dimension of data.","quantity":"?"},{"name":"control_description","dtype":"text","dims":["num_control_values"],"shape":[null],"doc":"Description of each control value. Must be present if control is present. If present, control_description[0] should describe time points where control == 0.","quantity":"?"}],"groups":[{"name":"sync","doc":"Lab-specific time and sync information as provided directly from hardware devices and that is necessary for aligning all acquired time information to a common timebase. The timestamp array stores time in the common timebase. This group will usually only be populated in TimeSeries that are stored external to the NWB file, in files storing raw data. Once timestamp data is calculated, the contents of 'sync' are mostly for archival purposes.","quantity":"?"}]},{"neurodata_type_def":"ProcessingModule","neurodata_type_inc":"NWBContainer","doc":"A collection of processed data.","attributes":[{"name":"description","dtype":"text","doc":"Description of this collection of processed data."}],"groups":[{"neurodata_type_inc":"NWBDataInterface","doc":"Data objects stored in this collection.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tables stored in this collection.","quantity":"*"}]},{"neurodata_type_def":"Images","neurodata_type_inc":"NWBDataInterface","default_name":"Images","doc":"A collection of images with an optional way to specify the order of the images using the \"order_of_images\" dataset. An order must be specified if the images are referenced by index, e.g., from an IndexSeries.","attributes":[{"name":"description","dtype":"text","doc":"Description of this collection of images."}],"datasets":[{"neurodata_type_inc":"Image","doc":"Images stored in this collection.","quantity":"+"},{"name":"order_of_images","neurodata_type_inc":"ImageReferences","doc":"Ordered dataset of references to Image objects stored in the parent group. Each Image object in the Images group should be stored once and only once, so the dataset should have the same length as the number of images.","quantity":"?"}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.behavior.json b/Resources/spec/core/2.7.0/nwb.behavior.json new file mode 100644 index 0000000..1ecff4d --- /dev/null +++ b/Resources/spec/core/2.7.0/nwb.behavior.json @@ -0,0 +1 @@ +{"groups":[{"neurodata_type_def":"SpatialSeries","neurodata_type_inc":"TimeSeries","doc":"Direction, e.g., of gaze or travel, or position. The TimeSeries::data field is a 2D array storing position or direction relative to some reference frame. Array structure: [num measurements] [num dimensions]. Each SpatialSeries has a text dataset reference_frame that indicates the zero-position, or the zero-axes for direction. For example, if representing gaze direction, 'straight-ahead' might be a specific pixel on the monitor, or some other point in space. For position data, the 0,0 point might be the top-left corner of an enclosure, as viewed from the tracking camera. The unit of data will indicate how to interpret SpatialSeries values.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","x"],["num_times","x,y"],["num_times","x,y,z"]],"shape":[[null],[null,1],[null,2],[null,3]],"doc":"1-D or 2-D array storing position or direction relative to some reference frame.","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Base unit of measurement for working with the data. The default value is 'meters'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'.","required":false}]},{"name":"reference_frame","dtype":"text","doc":"Description defining what exactly 'straight-ahead' means.","quantity":"?"}]},{"neurodata_type_def":"BehavioralEpochs","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralEpochs","doc":"TimeSeries for storing behavioral epochs. The objective of this and the other two Behavioral interfaces (e.g. BehavioralEvents and BehavioralTimeSeries) is to provide generic hooks for software tools/scripts. This allows a tool/script to take the output one specific interface (e.g., UnitTimes) and plot that data relative to another data modality (e.g., behavioral events) without having to define all possible modalities in advance. Declaring one of these interfaces means that one or more TimeSeries of the specified type is published. These TimeSeries should reside in a group having the same name as the interface. For example, if a BehavioralTimeSeries interface is declared, the module will have one or more TimeSeries defined in the module sub-group 'BehavioralTimeSeries'. BehavioralEpochs should use IntervalSeries. BehavioralEvents is used for irregular events. BehavioralTimeSeries is for continuous data.","groups":[{"neurodata_type_inc":"IntervalSeries","doc":"IntervalSeries object containing start and stop times of epochs.","quantity":"*"}]},{"neurodata_type_def":"BehavioralEvents","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralEvents","doc":"TimeSeries for storing behavioral events. See description of BehavioralEpochs for more details.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing behavioral events.","quantity":"*"}]},{"neurodata_type_def":"BehavioralTimeSeries","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralTimeSeries","doc":"TimeSeries for storing Behavoioral time series data. See description of BehavioralEpochs for more details.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing continuous behavioral data.","quantity":"*"}]},{"neurodata_type_def":"PupilTracking","neurodata_type_inc":"NWBDataInterface","default_name":"PupilTracking","doc":"Eye-tracking data, representing pupil size.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing time series data on pupil size.","quantity":"+"}]},{"neurodata_type_def":"EyeTracking","neurodata_type_inc":"NWBDataInterface","default_name":"EyeTracking","doc":"Eye-tracking data, representing direction of gaze.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing data measuring direction of gaze.","quantity":"*"}]},{"neurodata_type_def":"CompassDirection","neurodata_type_inc":"NWBDataInterface","default_name":"CompassDirection","doc":"With a CompassDirection interface, a module publishes a SpatialSeries object representing a floating point value for theta. The SpatialSeries::reference_frame field should indicate what direction corresponds to 0 and which is the direction of rotation (this should be clockwise). The si_unit for the SpatialSeries should be radians or degrees.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing direction of gaze travel.","quantity":"*"}]},{"neurodata_type_def":"Position","neurodata_type_inc":"NWBDataInterface","default_name":"Position","doc":"Position data, whether along the x, x/y or x/y/z axis.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing position data.","quantity":"+"}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.device.json b/Resources/spec/core/2.7.0/nwb.device.json new file mode 100644 index 0000000..e0e1cfb --- /dev/null +++ b/Resources/spec/core/2.7.0/nwb.device.json @@ -0,0 +1 @@ +{"groups":[{"neurodata_type_def":"Device","neurodata_type_inc":"NWBContainer","doc":"Metadata about a data acquisition device, e.g., recording system, electrode, microscope.","attributes":[{"name":"description","dtype":"text","doc":"Description of the device (e.g., model, firmware version, processing software version, etc.) as free-form text.","required":false},{"name":"manufacturer","dtype":"text","doc":"The name of the manufacturer of the device.","required":false}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.ecephys.json b/Resources/spec/core/2.7.0/nwb.ecephys.json new file mode 100644 index 0000000..999e4a6 --- /dev/null +++ b/Resources/spec/core/2.7.0/nwb.ecephys.json @@ -0,0 +1 @@ +{"groups":[{"neurodata_type_def":"ElectricalSeries","neurodata_type_inc":"TimeSeries","doc":"A time series of acquired voltage data from extracellular recordings. The data field is an int or float array storing data in volts. The first dimension should always represent time. The second dimension, if present, should represent channels.","attributes":[{"name":"filtering","dtype":"text","doc":"Filtering applied to all channels of the data. For example, if this ElectricalSeries represents high-pass-filtered data (also known as AP Band), then this value could be \"High-pass 4-pole Bessel filter at 500 Hz\". If this ElectricalSeries represents low-pass-filtered LFP data and the type of filter is unknown, then this value could be \"Low-pass filter at 300 Hz\". If a non-standard filter type is used, provide as much detail about the filter properties as possible.","required":false}],"datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_channels"],["num_times","num_channels","num_samples"]],"shape":[[null],[null,null],[null,null,null]],"doc":"Recorded voltage data.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. This value is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion', followed by 'channel_conversion' (if present), and then add 'offset'."}]},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the electrodes that this time series was generated from."},{"name":"channel_conversion","dtype":"float32","dims":["num_channels"],"shape":[null],"doc":"Channel-specific conversion factor. Multiply the data in the 'data' dataset by these values along the channel axis (as indicated by axis attribute) AND by the global conversion factor in the 'conversion' attribute of 'data' to get the data values in Volts, i.e, data in Volts = data * data.conversion * channel_conversion. This approach allows for both global and per-channel data conversion factors needed to support the storage of electrical recordings as native values generated by data acquisition systems. If this dataset is not present, then there is no channel-specific conversion factor, i.e. it is 1 for all channels.","quantity":"?","attributes":[{"name":"axis","dtype":"int32","value":1,"doc":"The zero-indexed axis of the 'data' dataset that the channel-specific conversion factor corresponds to. This value is fixed to 1."}]}]},{"neurodata_type_def":"SpikeEventSeries","neurodata_type_inc":"ElectricalSeries","doc":"Stores snapshots/snippets of recorded spike events (i.e., threshold crossings). This may also be raw data, as reported by ephys hardware. If so, the TimeSeries::description field should describe how events were detected. All SpikeEventSeries should reside in a module (under EventWaveform interface) even if the spikes were reported and stored by hardware. All events span the same recording channels and store snapshots of equal duration. TimeSeries::data array structure: [num events] [num channels] [num samples] (or [num events] [num samples] for single electrode).","datasets":[{"name":"data","dtype":"numeric","dims":[["num_events","num_samples"],["num_events","num_channels","num_samples"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveforms.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement for waveforms, which is fixed to 'volts'."}]},{"name":"timestamps","dtype":"float64","dims":["num_times"],"shape":[null],"doc":"Timestamps for samples stored in data, in seconds, relative to the common experiment master-clock stored in NWBFile.timestamps_reference_time. Timestamps are required for the events. Unlike for TimeSeries, timestamps are required for SpikeEventSeries and are thus re-specified here.","attributes":[{"name":"interval","dtype":"int32","value":1,"doc":"Value is '1'"},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for timestamps, which is fixed to 'seconds'."}]}]},{"neurodata_type_def":"FeatureExtraction","neurodata_type_inc":"NWBDataInterface","default_name":"FeatureExtraction","doc":"Features, such as PC1 and PC2, that are extracted from signals stored in a SpikeEventSeries or other source.","datasets":[{"name":"description","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Description of features (eg, ''PC1'') for each of the extracted features."},{"name":"features","dtype":"float32","dims":["num_events","num_channels","num_features"],"shape":[null,null,null],"doc":"Multi-dimensional array of features extracted from each event."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Times of events that features correspond to (can be a link)."},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the electrodes that this time series was generated from."}]},{"neurodata_type_def":"EventDetection","neurodata_type_inc":"NWBDataInterface","default_name":"EventDetection","doc":"Detected spike events from voltage trace(s).","datasets":[{"name":"detection_method","dtype":"text","doc":"Description of how events were detected, such as voltage threshold, or dV/dT threshold, as well as relevant values."},{"name":"source_idx","dtype":"int32","dims":["num_events"],"shape":[null],"doc":"Indices (zero-based) into source ElectricalSeries::data array corresponding to time of event. ''description'' should define what is meant by time of event (e.g., .25 ms before action potential peak, zero-crossing time, etc). The index points to each event from the raw data."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Timestamps of events, in seconds.","attributes":[{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for event times, which is fixed to 'seconds'."}]}],"links":[{"name":"source_electricalseries","target_type":"ElectricalSeries","doc":"Link to the ElectricalSeries that this data was calculated from. Metadata about electrodes and their position can be read from that ElectricalSeries so it's not necessary to include that information here."}]},{"neurodata_type_def":"EventWaveform","neurodata_type_inc":"NWBDataInterface","default_name":"EventWaveform","doc":"Represents either the waveforms of detected events, as extracted from a raw data trace in /acquisition, or the event waveforms that were stored during experiment acquisition.","groups":[{"neurodata_type_inc":"SpikeEventSeries","doc":"SpikeEventSeries object(s) containing detected spike event waveforms.","quantity":"*"}]},{"neurodata_type_def":"FilteredEphys","neurodata_type_inc":"NWBDataInterface","default_name":"FilteredEphys","doc":"Electrophysiology data from one or more channels that has been subjected to filtering. Examples of filtered data include Theta and Gamma (LFP has its own interface). FilteredEphys modules publish an ElectricalSeries for each filtered channel or set of channels. The name of each ElectricalSeries is arbitrary but should be informative. The source of the filtered data, whether this is from analysis of another time series or as acquired by hardware, should be noted in each's TimeSeries::description field. There is no assumed 1::1 correspondence between filtered ephys signals and electrodes, as a single signal can apply to many nearby electrodes, and one electrode may have different filtered (e.g., theta and/or gamma) signals represented. Filter properties should be noted in the ElectricalSeries 'filtering' attribute.","groups":[{"neurodata_type_inc":"ElectricalSeries","doc":"ElectricalSeries object(s) containing filtered electrophysiology data.","quantity":"+"}]},{"neurodata_type_def":"LFP","neurodata_type_inc":"NWBDataInterface","default_name":"LFP","doc":"LFP data from one or more channels. The electrode map in each published ElectricalSeries will identify which channels are providing LFP data. Filter properties should be noted in the ElectricalSeries 'filtering' attribute.","groups":[{"neurodata_type_inc":"ElectricalSeries","doc":"ElectricalSeries object(s) containing LFP data for one or more channels.","quantity":"+"}]},{"neurodata_type_def":"ElectrodeGroup","neurodata_type_inc":"NWBContainer","doc":"A physical grouping of electrodes, e.g. a shank of an array.","attributes":[{"name":"description","dtype":"text","doc":"Description of this electrode group."},{"name":"location","dtype":"text","doc":"Location of electrode group. Specify the area, layer, comments on estimation of area/layer, etc. Use standard atlas names for anatomical regions when possible."}],"datasets":[{"name":"position","dtype":[{"name":"x","dtype":"float32","doc":"x coordinate"},{"name":"y","dtype":"float32","doc":"y coordinate"},{"name":"z","dtype":"float32","doc":"z coordinate"}],"doc":"stereotaxic or common framework coordinates","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the device that was used to record from this electrode group."}]},{"neurodata_type_def":"ClusterWaveforms","neurodata_type_inc":"NWBDataInterface","default_name":"ClusterWaveforms","doc":"DEPRECATED The mean waveform shape, including standard deviation, of the different clusters. Ideally, the waveform analysis should be performed on data that is only high-pass filtered. This is a separate module because it is expected to require updating. For example, IMEC probes may require different storage requirements to store/display mean waveforms, requiring a new interface or an extension of this one.","datasets":[{"name":"waveform_filtering","dtype":"text","doc":"Filtering applied to data before generating mean/sd"},{"name":"waveform_mean","dtype":"float32","dims":["num_clusters","num_samples"],"shape":[null,null],"doc":"The mean waveform for each cluster, using the same indices for each wave as cluster numbers in the associated Clustering module (i.e, cluster 3 is in array slot [3]). Waveforms corresponding to gaps in cluster sequence should be empty (e.g., zero- filled)"},{"name":"waveform_sd","dtype":"float32","dims":["num_clusters","num_samples"],"shape":[null,null],"doc":"Stdev of waveforms for each cluster, using the same indices as in mean"}],"links":[{"name":"clustering_interface","target_type":"Clustering","doc":"Link to Clustering interface that was the source of the clustered data"}]},{"neurodata_type_def":"Clustering","neurodata_type_inc":"NWBDataInterface","default_name":"Clustering","doc":"DEPRECATED Clustered spike data, whether from automatic clustering tools (e.g., klustakwik) or as a result of manual sorting.","datasets":[{"name":"description","dtype":"text","doc":"Description of clusters or clustering, (e.g. cluster 0 is noise, clusters curated using Klusters, etc)"},{"name":"num","dtype":"int32","dims":["num_events"],"shape":[null],"doc":"Cluster number of each event"},{"name":"peak_over_rms","dtype":"float32","dims":["num_clusters"],"shape":[null],"doc":"Maximum ratio of waveform peak to RMS on any channel in the cluster (provides a basic clustering metric)."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Times of clustered events, in seconds. This may be a link to times field in associated FeatureExtraction module."}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.epoch.json b/Resources/spec/core/2.7.0/nwb.epoch.json new file mode 100644 index 0000000..ed46470 --- /dev/null +++ b/Resources/spec/core/2.7.0/nwb.epoch.json @@ -0,0 +1 @@ +{"groups":[{"neurodata_type_def":"TimeIntervals","neurodata_type_inc":"DynamicTable","doc":"A container for aggregating epoch data and the TimeSeries that each epoch applies to.","datasets":[{"name":"start_time","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Start time of epoch, in seconds."},{"name":"stop_time","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Stop time of epoch, in seconds."},{"name":"tags","neurodata_type_inc":"VectorData","dtype":"text","doc":"User-defined tags that identify or categorize events.","quantity":"?"},{"name":"tags_index","neurodata_type_inc":"VectorIndex","doc":"Index for tags.","quantity":"?"},{"name":"timeseries","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"An index into a TimeSeries object.","quantity":"?"},{"name":"timeseries_index","neurodata_type_inc":"VectorIndex","doc":"Index for timeseries.","quantity":"?"}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.file.json b/Resources/spec/core/2.7.0/nwb.file.json new file mode 100644 index 0000000..e029904 --- /dev/null +++ b/Resources/spec/core/2.7.0/nwb.file.json @@ -0,0 +1 @@ +{"groups":[{"neurodata_type_def":"NWBFile","neurodata_type_inc":"NWBContainer","name":"root","doc":"An NWB file storing cellular-based neurophysiology data from a single experimental session.","attributes":[{"name":"nwb_version","dtype":"text","value":"2.7.0-alpha","doc":"File version string. Use semantic versioning, e.g. 1.2.1. This will be the name of the format with trailing major, minor and patch numbers."}],"datasets":[{"name":"file_create_date","dtype":"isodatetime","dims":["num_modifications"],"shape":[null],"doc":"A record of the date the file was created and of subsequent modifications. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted strings: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds. The file can be created after the experiment was run, so this may differ from the experiment start time. Each modification to the nwb file adds a new entry to the array."},{"name":"identifier","dtype":"text","doc":"A unique text identifier for the file. For example, concatenated lab name, file creation date/time and experimentalist, or a hash of these and/or other values. The goal is that the string should be unique to all other files."},{"name":"session_description","dtype":"text","doc":"A description of the experimental session and data in the file."},{"name":"session_start_time","dtype":"isodatetime","doc":"Date and time of the experiment/session start. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted string: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds."},{"name":"timestamps_reference_time","dtype":"isodatetime","doc":"Date and time corresponding to time zero of all timestamps. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted string: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds. All times stored in the file use this time as reference (i.e., time zero)."}],"groups":[{"name":"acquisition","doc":"Data streams recorded from the system, including ephys, ophys, tracking, etc. This group should be read-only after the experiment is completed and timestamps are corrected to a common timebase. The data stored here may be links to raw data stored in external NWB files. This will allow keeping bulky raw data out of the file while preserving the option of keeping some/all in the file. Acquired data includes tracking and experimental data streams (i.e., everything measured from the system). If bulky data is stored in the /acquisition group, the data can exist in a separate NWB file that is linked to by the file being used for processing and analysis.","groups":[{"neurodata_type_inc":"NWBDataInterface","doc":"Acquired, raw data.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tabular data that is relevant to acquisition","quantity":"*"}]},{"name":"analysis","doc":"Lab-specific and custom scientific analysis of data. There is no defined format for the content of this group - the format is up to the individual user/lab. To facilitate sharing analysis data between labs, the contents here should be stored in standard types (e.g., neurodata_types) and appropriately documented. The file can store lab-specific and custom data analysis without restriction on its form or schema, reducing data formatting restrictions on end users. Such data should be placed in the analysis group. The analysis data should be documented so that it could be shared with other labs.","groups":[{"neurodata_type_inc":"NWBContainer","doc":"Custom analysis results.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tabular data that is relevant to data stored in analysis","quantity":"*"}]},{"name":"scratch","doc":"A place to store one-off analysis results. Data placed here is not intended for sharing. By placing data here, users acknowledge that there is no guarantee that their data meets any standard.","quantity":"?","groups":[{"neurodata_type_inc":"NWBContainer","doc":"Any one-off containers","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Any one-off tables","quantity":"*"}],"datasets":[{"neurodata_type_inc":"ScratchData","doc":"Any one-off datasets","quantity":"*"}]},{"name":"processing","doc":"The home for ProcessingModules. These modules perform intermediate analysis of data that is necessary to perform before scientific analysis. Examples include spike clustering, extracting position from tracking data, stitching together image slices. ProcessingModules can be large and express many data sets from relatively complex analysis (e.g., spike detection and clustering) or small, representing extraction of position information from tracking video, or even binary lick/no-lick decisions. Common software tools (e.g., klustakwik, MClust) are expected to read/write data here. 'Processing' refers to intermediate analysis of the acquired data to make it more amenable to scientific analysis.","groups":[{"neurodata_type_inc":"ProcessingModule","doc":"Intermediate analysis of acquired data.","quantity":"*"}]},{"name":"stimulus","doc":"Data pushed into the system (eg, video stimulus, sound, voltage, etc) and secondary representations of that data (eg, measurements of something used as a stimulus). This group should be made read-only after experiment complete and timestamps are corrected to common timebase. Stores both presented stimuli and stimulus templates, the latter in case the same stimulus is presented multiple times, or is pulled from an external stimulus library. Stimuli are here defined as any signal that is pushed into the system as part of the experiment (eg, sound, video, voltage, etc). Many different experiments can use the same stimuli, and stimuli can be re-used during an experiment. The stimulus group is organized so that one version of template stimuli can be stored and these be used multiple times. These templates can exist in the present file or can be linked to a remote library file.","groups":[{"name":"presentation","doc":"Stimuli presented during the experiment.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries objects containing data of presented stimuli.","quantity":"*"}]},{"name":"templates","doc":"Template stimuli. Timestamps in templates are based on stimulus design and are relative to the beginning of the stimulus. When templates are used, the stimulus instances must convert presentation times to the experiment`s time reference frame.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries objects containing template data of presented stimuli.","quantity":"*"},{"neurodata_type_inc":"Images","doc":"Images objects containing images of presented stimuli.","quantity":"*"}]}]},{"name":"general","doc":"Experimental metadata, including protocol, notes and description of hardware device(s). The metadata stored in this section should be used to describe the experiment. Metadata necessary for interpreting the data is stored with the data. General experimental metadata, including animal strain, experimental protocols, experimenter, devices, etc, are stored under 'general'. Core metadata (e.g., that required to interpret data fields) is stored with the data itself, and implicitly defined by the file specification (e.g., time is in seconds). The strategy used here for storing non-core metadata is to use free-form text fields, such as would appear in sentences or paragraphs from a Methods section. Metadata fields are text to enable them to be more general, for example to represent ranges instead of numerical values. Machine-readable metadata is stored as attributes to these free-form datasets. All entries in the below table are to be included when data is present. Unused groups (e.g., intracellular_ephys in an optophysiology experiment) should not be created unless there is data to store within them.","datasets":[{"name":"data_collection","dtype":"text","doc":"Notes about data collection and analysis.","quantity":"?"},{"name":"experiment_description","dtype":"text","doc":"General description of the experiment.","quantity":"?"},{"name":"experimenter","dtype":"text","doc":"Name of person(s) who performed the experiment. Can also specify roles of different people involved.","quantity":"?","dims":["num_experimenters"],"shape":[null]},{"name":"institution","dtype":"text","doc":"Institution(s) where experiment was performed.","quantity":"?"},{"name":"keywords","dtype":"text","dims":["num_keywords"],"shape":[null],"doc":"Terms to search over.","quantity":"?"},{"name":"lab","dtype":"text","doc":"Laboratory where experiment was performed.","quantity":"?"},{"name":"notes","dtype":"text","doc":"Notes about the experiment.","quantity":"?"},{"name":"pharmacology","dtype":"text","doc":"Description of drugs used, including how and when they were administered. Anesthesia(s), painkiller(s), etc., plus dosage, concentration, etc.","quantity":"?"},{"name":"protocol","dtype":"text","doc":"Experimental protocol, if applicable. e.g., include IACUC protocol number.","quantity":"?"},{"name":"related_publications","dtype":"text","doc":"Publication information. PMID, DOI, URL, etc.","dims":["num_publications"],"shape":[null],"quantity":"?"},{"name":"session_id","dtype":"text","doc":"Lab-specific ID for the session.","quantity":"?"},{"name":"slices","dtype":"text","doc":"Description of slices, including information about preparation thickness, orientation, temperature, and bath solution.","quantity":"?"},{"name":"source_script","dtype":"text","doc":"Script file or link to public source code used to create this NWB file.","quantity":"?","attributes":[{"name":"file_name","dtype":"text","doc":"Name of script file."}]},{"name":"stimulus","dtype":"text","doc":"Notes about stimuli, such as how and where they were presented.","quantity":"?"},{"name":"surgery","dtype":"text","doc":"Narrative description about surgery/surgeries, including date(s) and who performed surgery.","quantity":"?"},{"name":"virus","dtype":"text","doc":"Information about virus(es) used in experiments, including virus ID, source, date made, injection location, volume, etc.","quantity":"?"}],"groups":[{"neurodata_type_inc":"LabMetaData","doc":"Place-holder than can be extended so that lab-specific meta-data can be placed in /general.","quantity":"*"},{"name":"devices","doc":"Description of hardware devices used during experiment, e.g., monitors, ADC boards, microscopes, etc.","quantity":"?","groups":[{"neurodata_type_inc":"Device","doc":"Data acquisition devices.","quantity":"*"}]},{"name":"subject","neurodata_type_inc":"Subject","doc":"Information about the animal or person from which the data was measured.","quantity":"?"},{"name":"extracellular_ephys","doc":"Metadata related to extracellular electrophysiology.","quantity":"?","groups":[{"neurodata_type_inc":"ElectrodeGroup","doc":"Physical group of electrodes.","quantity":"*"},{"name":"electrodes","neurodata_type_inc":"DynamicTable","doc":"A table of all electrodes (i.e. channels) used for recording.","quantity":"?","datasets":[{"name":"x","neurodata_type_inc":"VectorData","dtype":"float32","doc":"x coordinate of the channel location in the brain (+x is posterior).","quantity":"?"},{"name":"y","neurodata_type_inc":"VectorData","dtype":"float32","doc":"y coordinate of the channel location in the brain (+y is inferior).","quantity":"?"},{"name":"z","neurodata_type_inc":"VectorData","dtype":"float32","doc":"z coordinate of the channel location in the brain (+z is right).","quantity":"?"},{"name":"imp","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Impedance of the channel, in ohms.","quantity":"?"},{"name":"location","neurodata_type_inc":"VectorData","dtype":"text","doc":"Location of the electrode (channel). Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."},{"name":"filtering","neurodata_type_inc":"VectorData","dtype":"text","doc":"Description of hardware filtering, including the filter name and frequency cutoffs.","quantity":"?"},{"name":"group","neurodata_type_inc":"VectorData","dtype":{"target_type":"ElectrodeGroup","reftype":"object"},"doc":"Reference to the ElectrodeGroup this electrode is a part of."},{"name":"group_name","neurodata_type_inc":"VectorData","dtype":"text","doc":"Name of the ElectrodeGroup this electrode is a part of."},{"name":"rel_x","neurodata_type_inc":"VectorData","dtype":"float32","doc":"x coordinate in electrode group","quantity":"?"},{"name":"rel_y","neurodata_type_inc":"VectorData","dtype":"float32","doc":"y coordinate in electrode group","quantity":"?"},{"name":"rel_z","neurodata_type_inc":"VectorData","dtype":"float32","doc":"z coordinate in electrode group","quantity":"?"},{"name":"reference","neurodata_type_inc":"VectorData","dtype":"text","doc":"Description of the reference electrode and/or reference scheme used for this electrode, e.g., \"stainless steel skull screw\" or \"online common average referencing\".","quantity":"?"}]}]},{"name":"intracellular_ephys","doc":"Metadata related to intracellular electrophysiology.","quantity":"?","datasets":[{"name":"filtering","dtype":"text","doc":"[DEPRECATED] Use IntracellularElectrode.filtering instead. Description of filtering used. Includes filtering type and parameters, frequency fall-off, etc. If this changes between TimeSeries, filter description should be stored as a text attribute for each TimeSeries.","quantity":"?"}],"groups":[{"neurodata_type_inc":"IntracellularElectrode","doc":"An intracellular electrode.","quantity":"*"},{"name":"sweep_table","neurodata_type_inc":"SweepTable","doc":"[DEPRECATED] Table used to group different PatchClampSeries. SweepTable is being replaced by IntracellularRecordingsTable and SimultaneousRecordingsTable tables. Additional SequentialRecordingsTable, RepetitionsTable and ExperimentalConditions tables provide enhanced support for experiment metadata.","quantity":"?"},{"name":"intracellular_recordings","neurodata_type_inc":"IntracellularRecordingsTable","doc":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording. Each row in the table represents a single recording consisting typically of a stimulus and a corresponding response. In some cases, however, only a stimulus or a response are recorded as as part of an experiment. In this case both, the stimulus and response will point to the same TimeSeries while the idx_start and count of the invalid column will be set to -1, thus, indicating that no values have been recorded for the stimulus or response, respectively. Note, a recording MUST contain at least a stimulus or a response. Typically the stimulus and response are PatchClampSeries. However, the use of AD/DA channels that are not associated to an electrode is also common in intracellular electrophysiology, in which case other TimeSeries may be used.","quantity":"?"},{"name":"simultaneous_recordings","neurodata_type_inc":"SimultaneousRecordingsTable","doc":"A table for grouping different intracellular recordings from the IntracellularRecordingsTable table together that were recorded simultaneously from different electrodes","quantity":"?"},{"name":"sequential_recordings","neurodata_type_inc":"SequentialRecordingsTable","doc":"A table for grouping different sequential recordings from the SimultaneousRecordingsTable table together. This is typically used to group together sequential recordings where the a sequence of stimuli of the same type with varying parameters have been presented in a sequence.","quantity":"?"},{"name":"repetitions","neurodata_type_inc":"RepetitionsTable","doc":"A table for grouping different sequential intracellular recordings together. With each SequentialRecording typically representing a particular type of stimulus, the RepetitionsTable table is typically used to group sets of stimuli applied in sequence.","quantity":"?"},{"name":"experimental_conditions","neurodata_type_inc":"ExperimentalConditionsTable","doc":"A table for grouping different intracellular recording repetitions together that belong to the same experimental experimental_conditions.","quantity":"?"}]},{"name":"optogenetics","doc":"Metadata describing optogenetic stimuluation.","quantity":"?","groups":[{"neurodata_type_inc":"OptogeneticStimulusSite","doc":"An optogenetic stimulation site.","quantity":"*"}]},{"name":"optophysiology","doc":"Metadata related to optophysiology.","quantity":"?","groups":[{"neurodata_type_inc":"ImagingPlane","doc":"An imaging plane.","quantity":"*"}]}]},{"name":"intervals","doc":"Experimental intervals, whether that be logically distinct sub-experiments having a particular scientific goal, trials (see trials subgroup) during an experiment, or epochs (see epochs subgroup) deriving from analysis of data.","quantity":"?","groups":[{"name":"epochs","neurodata_type_inc":"TimeIntervals","doc":"Divisions in time marking experimental stages or sub-divisions of a single recording session.","quantity":"?"},{"name":"trials","neurodata_type_inc":"TimeIntervals","doc":"Repeated experimental events that have a logical grouping.","quantity":"?"},{"name":"invalid_times","neurodata_type_inc":"TimeIntervals","doc":"Time intervals that should be removed from analysis.","quantity":"?"},{"neurodata_type_inc":"TimeIntervals","doc":"Optional additional table(s) for describing other experimental time intervals.","quantity":"*"}]},{"name":"units","neurodata_type_inc":"Units","doc":"Data about sorted spike units.","quantity":"?"}]},{"neurodata_type_def":"LabMetaData","neurodata_type_inc":"NWBContainer","doc":"Lab-specific meta-data."},{"neurodata_type_def":"Subject","neurodata_type_inc":"NWBContainer","doc":"Information about the animal or person from which the data was measured.","datasets":[{"name":"age","dtype":"text","doc":"Age of subject. Can be supplied instead of 'date_of_birth'.","quantity":"?","attributes":[{"name":"reference","doc":"Age is with reference to this event. Can be 'birth' or 'gestational'. If reference is omitted, 'birth' is implied.","dtype":"text","required":false,"default_value":"birth"}]},{"name":"date_of_birth","dtype":"isodatetime","doc":"Date of birth of subject. Can be supplied instead of 'age'.","quantity":"?"},{"name":"description","dtype":"text","doc":"Description of subject and where subject came from (e.g., breeder, if animal).","quantity":"?"},{"name":"genotype","dtype":"text","doc":"Genetic strain. If absent, assume Wild Type (WT).","quantity":"?"},{"name":"sex","dtype":"text","doc":"Gender of subject.","quantity":"?"},{"name":"species","dtype":"text","doc":"Species of subject.","quantity":"?"},{"name":"strain","dtype":"text","doc":"Strain of subject.","quantity":"?"},{"name":"subject_id","dtype":"text","doc":"ID of animal/person used/participating in experiment (lab convention).","quantity":"?"},{"name":"weight","dtype":"text","doc":"Weight at time of experiment, at time of surgery and at other important times.","quantity":"?"}]}],"datasets":[{"neurodata_type_def":"ScratchData","neurodata_type_inc":"NWBData","doc":"Any one-off datasets","attributes":[{"name":"notes","doc":"Any notes the user has about the dataset being stored","dtype":"text"}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.icephys.json b/Resources/spec/core/2.7.0/nwb.icephys.json new file mode 100644 index 0000000..0aa9188 --- /dev/null +++ b/Resources/spec/core/2.7.0/nwb.icephys.json @@ -0,0 +1 @@ +{"groups":[{"neurodata_type_def":"PatchClampSeries","neurodata_type_inc":"TimeSeries","doc":"An abstract base class for patch-clamp data - stimulus or response, current or voltage.","attributes":[{"name":"stimulus_description","dtype":"text","doc":"Protocol/stimulus name for this patch-clamp dataset."},{"name":"sweep_number","dtype":"uint32","doc":"Sweep number, allows to group different PatchClampSeries together.","required":false}],"datasets":[{"name":"data","dtype":"numeric","dims":["num_times"],"shape":[null],"doc":"Recorded voltage or current.","attributes":[{"name":"unit","dtype":"text","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"gain","dtype":"float32","doc":"Gain of the recording, in units Volt/Amp (v-clamp) or Volt/Volt (c-clamp).","quantity":"?"}],"links":[{"name":"electrode","target_type":"IntracellularElectrode","doc":"Link to IntracellularElectrode object that describes the electrode that was used to apply or record this data."}]},{"neurodata_type_def":"CurrentClampSeries","neurodata_type_inc":"PatchClampSeries","doc":"Voltage data from an intracellular current-clamp recording. A corresponding CurrentClampStimulusSeries (stored separately as a stimulus) is used to store the current injected.","datasets":[{"name":"data","doc":"Recorded voltage.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. which is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"bias_current","dtype":"float32","doc":"Bias current, in amps.","quantity":"?"},{"name":"bridge_balance","dtype":"float32","doc":"Bridge balance, in ohms.","quantity":"?"},{"name":"capacitance_compensation","dtype":"float32","doc":"Capacitance compensation, in farads.","quantity":"?"}]},{"neurodata_type_def":"IZeroClampSeries","neurodata_type_inc":"CurrentClampSeries","doc":"Voltage data from an intracellular recording when all current and amplifier settings are off (i.e., CurrentClampSeries fields will be zero). There is no CurrentClampStimulusSeries associated with an IZero series because the amplifier is disconnected and no stimulus can reach the cell.","attributes":[{"name":"stimulus_description","dtype":"text","doc":"An IZeroClampSeries has no stimulus, so this attribute is automatically set to \"N/A\"","value":"N/A"}],"datasets":[{"name":"bias_current","dtype":"float32","value":0.0,"doc":"Bias current, in amps, fixed to 0.0."},{"name":"bridge_balance","dtype":"float32","value":0.0,"doc":"Bridge balance, in ohms, fixed to 0.0."},{"name":"capacitance_compensation","dtype":"float32","value":0.0,"doc":"Capacitance compensation, in farads, fixed to 0.0."}]},{"neurodata_type_def":"CurrentClampStimulusSeries","neurodata_type_inc":"PatchClampSeries","doc":"Stimulus current applied during current clamp recording.","datasets":[{"name":"data","doc":"Stimulus current applied.","attributes":[{"name":"unit","dtype":"text","value":"amperes","doc":"Base unit of measurement for working with the data. which is fixed to 'amperes'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]}]},{"neurodata_type_def":"VoltageClampSeries","neurodata_type_inc":"PatchClampSeries","doc":"Current data from an intracellular voltage-clamp recording. A corresponding VoltageClampStimulusSeries (stored separately as a stimulus) is used to store the voltage injected.","datasets":[{"name":"data","doc":"Recorded current.","attributes":[{"name":"unit","dtype":"text","value":"amperes","doc":"Base unit of measurement for working with the data. which is fixed to 'amperes'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"capacitance_fast","dtype":"float32","doc":"Fast capacitance, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for capacitance_fast, which is fixed to 'farads'."}]},{"name":"capacitance_slow","dtype":"float32","doc":"Slow capacitance, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for capacitance_fast, which is fixed to 'farads'."}]},{"name":"resistance_comp_bandwidth","dtype":"float32","doc":"Resistance compensation bandwidth, in hertz.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"hertz","doc":"Unit of measurement for resistance_comp_bandwidth, which is fixed to 'hertz'."}]},{"name":"resistance_comp_correction","dtype":"float32","doc":"Resistance compensation correction, in percent.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"percent","doc":"Unit of measurement for resistance_comp_correction, which is fixed to 'percent'."}]},{"name":"resistance_comp_prediction","dtype":"float32","doc":"Resistance compensation prediction, in percent.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"percent","doc":"Unit of measurement for resistance_comp_prediction, which is fixed to 'percent'."}]},{"name":"whole_cell_capacitance_comp","dtype":"float32","doc":"Whole cell capacitance compensation, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for whole_cell_capacitance_comp, which is fixed to 'farads'."}]},{"name":"whole_cell_series_resistance_comp","dtype":"float32","doc":"Whole cell series resistance compensation, in ohms.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"ohms","doc":"Unit of measurement for whole_cell_series_resistance_comp, which is fixed to 'ohms'."}]}]},{"neurodata_type_def":"VoltageClampStimulusSeries","neurodata_type_inc":"PatchClampSeries","doc":"Stimulus voltage applied during a voltage clamp recording.","datasets":[{"name":"data","doc":"Stimulus voltage applied.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. which is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]}]},{"neurodata_type_def":"IntracellularElectrode","neurodata_type_inc":"NWBContainer","doc":"An intracellular electrode and its metadata.","datasets":[{"name":"cell_id","dtype":"text","doc":"unique ID of the cell","quantity":"?"},{"name":"description","dtype":"text","doc":"Description of electrode (e.g., whole-cell, sharp, etc.)."},{"name":"filtering","dtype":"text","doc":"Electrode specific filtering.","quantity":"?"},{"name":"initial_access_resistance","dtype":"text","doc":"Initial access resistance.","quantity":"?"},{"name":"location","dtype":"text","doc":"Location of the electrode. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible.","quantity":"?"},{"name":"resistance","dtype":"text","doc":"Electrode resistance, in ohms.","quantity":"?"},{"name":"seal","dtype":"text","doc":"Information about seal used for recording.","quantity":"?"},{"name":"slice","dtype":"text","doc":"Information about slice used for recording.","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Device that was used to record from this electrode."}]},{"neurodata_type_def":"SweepTable","neurodata_type_inc":"DynamicTable","doc":"[DEPRECATED] Table used to group different PatchClampSeries. SweepTable is being replaced by IntracellularRecordingsTable and SimultaneousRecordingsTable tables. Additional SequentialRecordingsTable, RepetitionsTable, and ExperimentalConditions tables provide enhanced support for experiment metadata.","datasets":[{"name":"sweep_number","neurodata_type_inc":"VectorData","dtype":"uint32","doc":"Sweep number of the PatchClampSeries in that row."},{"name":"series","neurodata_type_inc":"VectorData","dtype":{"target_type":"PatchClampSeries","reftype":"object"},"doc":"The PatchClampSeries with the sweep number in that row."},{"name":"series_index","neurodata_type_inc":"VectorIndex","doc":"Index for series."}]},{"neurodata_type_def":"IntracellularElectrodesTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular electrode related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular electrode related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"electrode","neurodata_type_inc":"VectorData","dtype":{"target_type":"IntracellularElectrode","reftype":"object"},"doc":"Column for storing the reference to the intracellular electrode."}]},{"neurodata_type_def":"IntracellularStimuliTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular stimulus related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular stimulus related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"stimulus","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the recorded stimulus for the recording (rows)."},{"name":"stimulus_template","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the stimulus template for the recording (rows).","quantity":"?"}]},{"neurodata_type_def":"IntracellularResponsesTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular response related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular response related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"response","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the recorded response for the recording (rows)"}]},{"neurodata_type_def":"IntracellularRecordingsTable","neurodata_type_inc":"AlignedDynamicTable","name":"intracellular_recordings","doc":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording. Each row in the table represents a single recording consisting typically of a stimulus and a corresponding response. In some cases, however, only a stimulus or a response is recorded as part of an experiment. In this case, both the stimulus and response will point to the same TimeSeries while the idx_start and count of the invalid column will be set to -1, thus, indicating that no values have been recorded for the stimulus or response, respectively. Note, a recording MUST contain at least a stimulus or a response. Typically the stimulus and response are PatchClampSeries. However, the use of AD/DA channels that are not associated to an electrode is also common in intracellular electrophysiology, in which case other TimeSeries may be used.","attributes":[{"name":"description","dtype":"text","value":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording and for storing metadata about the intracellular recording.","doc":"Description of the contents of this table. Inherited from AlignedDynamicTable and overwritten here to fix the value of the attribute."}],"groups":[{"name":"electrodes","neurodata_type_inc":"IntracellularElectrodesTable","doc":"Table for storing intracellular electrode related metadata."},{"name":"stimuli","neurodata_type_inc":"IntracellularStimuliTable","doc":"Table for storing intracellular stimulus related metadata."},{"name":"responses","neurodata_type_inc":"IntracellularResponsesTable","doc":"Table for storing intracellular response related metadata."}]},{"neurodata_type_def":"SimultaneousRecordingsTable","neurodata_type_inc":"DynamicTable","name":"simultaneous_recordings","doc":"A table for grouping different intracellular recordings from the IntracellularRecordingsTable table together that were recorded simultaneously from different electrodes.","datasets":[{"name":"recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the IntracellularRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"IntracellularRecordingsTable","reftype":"object"},"doc":"Reference to the IntracellularRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the recordings column."}]},{"neurodata_type_def":"SequentialRecordingsTable","neurodata_type_inc":"DynamicTable","name":"sequential_recordings","doc":"A table for grouping different sequential recordings from the SimultaneousRecordingsTable table together. This is typically used to group together sequential recordings where a sequence of stimuli of the same type with varying parameters have been presented in a sequence.","datasets":[{"name":"simultaneous_recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the SimultaneousRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"SimultaneousRecordingsTable","reftype":"object"},"doc":"Reference to the SimultaneousRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"simultaneous_recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the simultaneous_recordings column."},{"name":"stimulus_type","neurodata_type_inc":"VectorData","dtype":"text","doc":"The type of stimulus used for the sequential recording."}]},{"neurodata_type_def":"RepetitionsTable","neurodata_type_inc":"DynamicTable","name":"repetitions","doc":"A table for grouping different sequential intracellular recordings together. With each SequentialRecording typically representing a particular type of stimulus, the RepetitionsTable table is typically used to group sets of stimuli applied in sequence.","datasets":[{"name":"sequential_recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the SequentialRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"SequentialRecordingsTable","reftype":"object"},"doc":"Reference to the SequentialRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"sequential_recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the sequential_recordings column."}]},{"neurodata_type_def":"ExperimentalConditionsTable","neurodata_type_inc":"DynamicTable","name":"experimental_conditions","doc":"A table for grouping different intracellular recording repetitions together that belong to the same experimental condition.","datasets":[{"name":"repetitions","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the RepetitionsTable table.","attributes":[{"name":"table","dtype":{"target_type":"RepetitionsTable","reftype":"object"},"doc":"Reference to the RepetitionsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"repetitions_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the repetitions column."}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.image.json b/Resources/spec/core/2.7.0/nwb.image.json new file mode 100644 index 0000000..78cd4fd --- /dev/null +++ b/Resources/spec/core/2.7.0/nwb.image.json @@ -0,0 +1 @@ +{"datasets":[{"neurodata_type_def":"GrayscaleImage","neurodata_type_inc":"Image","dims":["x","y"],"shape":[null,null],"doc":"A grayscale image.","dtype":"numeric"},{"neurodata_type_def":"RGBImage","neurodata_type_inc":"Image","dims":["x","y","r, g, b"],"shape":[null,null,3],"doc":"A color image.","dtype":"numeric"},{"neurodata_type_def":"RGBAImage","neurodata_type_inc":"Image","dims":["x","y","r, g, b, a"],"shape":[null,null,4],"doc":"A color image with transparency.","dtype":"numeric"}],"groups":[{"neurodata_type_def":"ImageSeries","neurodata_type_inc":"TimeSeries","doc":"General image data that is common between acquisition and stimulus time series. Sometimes the image data is stored in the file in a raw format while other times it will be stored as a series of external image files in the host file system. The data field will either be binary data, if the data is stored in the NWB file, or empty, if the data is stored in an external image stack. [frame][x][y] or [frame][x][y][z].","datasets":[{"name":"data","dtype":"numeric","dims":[["frame","x","y"],["frame","x","y","z"]],"shape":[[null,null,null],[null,null,null,null]],"doc":"Binary data representing images across frames. If data are stored in an external file, this should be an empty 3D array."},{"name":"dimension","dtype":"int32","dims":["rank"],"shape":[null],"doc":"Number of pixels on x, y, (and z) axes.","quantity":"?"},{"name":"external_file","dtype":"text","dims":["num_files"],"shape":[null],"doc":"Paths to one or more external file(s). The field is only present if format='external'. This is only relevant if the image series is stored in the file system as one or more image file(s). This field should NOT be used if the image is stored in another NWB file and that file is linked to this file.","quantity":"?","attributes":[{"name":"starting_frame","dtype":"int32","dims":["num_files"],"shape":[null],"doc":"Each external image may contain one or more consecutive frames of the full ImageSeries. This attribute serves as an index to indicate which frames each file contains, to facilitate random access. The 'starting_frame' attribute, hence, contains a list of frame numbers within the full ImageSeries of the first frame of each file listed in the parent 'external_file' dataset. Zero-based indexing is used (hence, the first element will always be zero). For example, if the 'external_file' dataset has three paths to files and the first file has 5 frames, the second file has 10 frames, and the third file has 20 frames, then this attribute will have values [0, 5, 15]. If there is a single external file that holds all of the frames of the ImageSeries (and so there is a single element in the 'external_file' dataset), then this attribute should have value [0]."}]},{"name":"format","dtype":"text","default_value":"raw","doc":"Format of image. If this is 'external', then the attribute 'external_file' contains the path information to the image files. If this is 'raw', then the raw (single-channel) binary data is stored in the 'data' dataset. If this attribute is not present, then the default format='raw' case is assumed.","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the Device object that was used to capture these images.","quantity":"?"}]},{"neurodata_type_def":"ImageMaskSeries","neurodata_type_inc":"ImageSeries","doc":"An alpha mask that is applied to a presented visual stimulus. The 'data' array contains an array of mask values that are applied to the displayed image. Mask values are stored as RGBA. Mask can vary with time. The timestamps array indicates the starting time of a mask, and that mask pattern continues until it's explicitly changed.","links":[{"name":"masked_imageseries","target_type":"ImageSeries","doc":"Link to ImageSeries object that this image mask is applied to."}]},{"neurodata_type_def":"OpticalSeries","neurodata_type_inc":"ImageSeries","doc":"Image data that is presented or recorded. A stimulus template movie will be stored only as an image. When the image is presented as stimulus, additional data is required, such as field of view (e.g., how much of the visual field the image covers, or how what is the area of the target being imaged). If the OpticalSeries represents acquired imaging data, orientation is also important.","datasets":[{"name":"distance","dtype":"float32","doc":"Distance from camera/monitor to target/eye.","quantity":"?"},{"name":"field_of_view","dtype":"float32","dims":[["width, height"],["width, height, depth"]],"shape":[[2],[3]],"doc":"Width, height and depth of image, or imaged area, in meters.","quantity":"?"},{"name":"data","dtype":"numeric","dims":[["frame","x","y"],["frame","x","y","r, g, b"]],"shape":[[null,null,null],[null,null,null,3]],"doc":"Images presented to subject, either grayscale or RGB"},{"name":"orientation","dtype":"text","doc":"Description of image relative to some reference frame (e.g., which way is up). Must also specify frame of reference.","quantity":"?"}]},{"neurodata_type_def":"IndexSeries","neurodata_type_inc":"TimeSeries","doc":"Stores indices to image frames stored in an ImageSeries. The purpose of the IndexSeries is to allow a static image stack to be stored in an Images object, and the images in the stack to be referenced out-of-order. This can be for the display of individual images, or of movie segments (as a movie is simply a series of images). The data field stores the index of the frame in the referenced Images object, and the timestamps array indicates when that image was displayed.","datasets":[{"name":"data","dtype":"uint32","dims":["num_times"],"shape":[null],"doc":"Index of the image (using zero-indexing) in the linked Images object.","attributes":[{"name":"conversion","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"resolution","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"offset","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"unit","dtype":"text","value":"N/A","doc":"This field is unused by IndexSeries and has the value N/A."}]}],"links":[{"name":"indexed_timeseries","target_type":"ImageSeries","doc":"Link to ImageSeries object containing images that are indexed. Use of this link is discouraged and will be deprecated. Link to an Images type instead.","quantity":"?"},{"name":"indexed_images","target_type":"Images","doc":"Link to Images object containing an ordered set of images that are indexed. The Images object must contain a 'ordered_images' dataset specifying the order of the images in the Images type.","quantity":"?"}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.misc.json b/Resources/spec/core/2.7.0/nwb.misc.json new file mode 100644 index 0000000..5f84a34 --- /dev/null +++ b/Resources/spec/core/2.7.0/nwb.misc.json @@ -0,0 +1 @@ +{"groups":[{"neurodata_type_def":"AbstractFeatureSeries","neurodata_type_inc":"TimeSeries","doc":"Abstract features, such as quantitative descriptions of sensory stimuli. The TimeSeries::data field is a 2D array, storing those features (e.g., for visual grating stimulus this might be orientation, spatial frequency and contrast). Null stimuli (eg, uniform gray) can be marked as being an independent feature (eg, 1.0 for gray, 0.0 for actual stimulus) or by storing NaNs for feature values, or through use of the TimeSeries::control fields. A set of features is considered to persist until the next set of features is defined. The final set of features stored should be the null set. This is useful when storing the raw stimulus is impractical.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_features"]],"shape":[[null],[null,null]],"doc":"Values of each feature at each time.","attributes":[{"name":"unit","dtype":"text","default_value":"see 'feature_units'","doc":"Since there can be different units for different features, store the units in 'feature_units'. The default value for this attribute is \"see 'feature_units'\".","required":false}]},{"name":"feature_units","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Units of each feature.","quantity":"?"},{"name":"features","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Description of the features represented in TimeSeries::data."}]},{"neurodata_type_def":"AnnotationSeries","neurodata_type_inc":"TimeSeries","doc":"Stores user annotations made during an experiment. The data[] field stores a text array, and timestamps are stored for each annotation (ie, interval=1). This is largely an alias to a standard TimeSeries storing a text array but that is identifiable as storing annotations in a machine-readable way.","datasets":[{"name":"data","dtype":"text","dims":["num_times"],"shape":[null],"doc":"Annotations made during an experiment.","attributes":[{"name":"resolution","dtype":"float32","value":-1.0,"doc":"Smallest meaningful difference between values in data. Annotations have no units, so the value is fixed to -1.0."},{"name":"unit","dtype":"text","value":"n/a","doc":"Base unit of measurement for working with the data. Annotations have no units, so the value is fixed to 'n/a'."}]}]},{"neurodata_type_def":"IntervalSeries","neurodata_type_inc":"TimeSeries","doc":"Stores intervals of data. The timestamps field stores the beginning and end of intervals. The data field stores whether the interval just started (>0 value) or ended (<0 value). Different interval types can be represented in the same series by using multiple key values (eg, 1 for feature A, 2 for feature B, 3 for feature C, etc). The field data stores an 8-bit integer. This is largely an alias of a standard TimeSeries but that is identifiable as representing time intervals in a machine-readable way.","datasets":[{"name":"data","dtype":"int8","dims":["num_times"],"shape":[null],"doc":"Use values >0 if interval started, <0 if interval ended.","attributes":[{"name":"resolution","dtype":"float32","value":-1.0,"doc":"Smallest meaningful difference between values in data. Annotations have no units, so the value is fixed to -1.0."},{"name":"unit","dtype":"text","value":"n/a","doc":"Base unit of measurement for working with the data. Annotations have no units, so the value is fixed to 'n/a'."}]}]},{"neurodata_type_def":"DecompositionSeries","neurodata_type_inc":"TimeSeries","doc":"Spectral analysis of a time series, e.g. of an LFP or a speech signal.","datasets":[{"name":"data","dtype":"numeric","dims":["num_times","num_channels","num_bands"],"shape":[null,null,null],"doc":"Data decomposed into frequency bands.","attributes":[{"name":"unit","dtype":"text","default_value":"no unit","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion'."}]},{"name":"metric","dtype":"text","doc":"The metric used, e.g. phase, amplitude, power."},{"name":"source_channels","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the channels that this decomposition series was generated from.","quantity":"?"}],"groups":[{"name":"bands","neurodata_type_inc":"DynamicTable","doc":"Table for describing the bands that this series was generated from. There should be one row in this table for each band.","datasets":[{"name":"band_name","neurodata_type_inc":"VectorData","dtype":"text","doc":"Name of the band, e.g. theta."},{"name":"band_limits","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands","low, high"],"shape":[null,2],"doc":"Low and high limit of each band in Hz. If it is a Gaussian filter, use 2 SD on either side of the center."},{"name":"band_mean","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands"],"shape":[null],"doc":"The mean Gaussian filters, in Hz."},{"name":"band_stdev","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands"],"shape":[null],"doc":"The standard deviation of Gaussian filters, in Hz."}]}],"links":[{"name":"source_timeseries","target_type":"TimeSeries","doc":"Link to TimeSeries object that this data was calculated from. Metadata about electrodes and their position can be read from that ElectricalSeries so it is not necessary to store that information here.","quantity":"?"}]},{"neurodata_type_def":"Units","neurodata_type_inc":"DynamicTable","default_name":"Units","doc":"Data about spiking units. Event times of observed units (e.g. cell, synapse, etc.) should be concatenated and stored in spike_times.","datasets":[{"name":"spike_times_index","neurodata_type_inc":"VectorIndex","doc":"Index into the spike_times dataset.","quantity":"?"},{"name":"spike_times","neurodata_type_inc":"VectorData","dtype":"float64","doc":"Spike times for each unit in seconds.","quantity":"?","attributes":[{"name":"resolution","dtype":"float64","doc":"The smallest possible difference between two spike times. Usually 1 divided by the acquisition sampling rate from which spike times were extracted, but could be larger if the acquisition time series was downsampled or smaller if the acquisition time series was smoothed/interpolated and it is possible for the spike time to be between samples.","required":false}]},{"name":"obs_intervals_index","neurodata_type_inc":"VectorIndex","doc":"Index into the obs_intervals dataset.","quantity":"?"},{"name":"obs_intervals","neurodata_type_inc":"VectorData","dtype":"float64","dims":["num_intervals","start|end"],"shape":[null,2],"doc":"Observation intervals for each unit.","quantity":"?"},{"name":"electrodes_index","neurodata_type_inc":"VectorIndex","doc":"Index into electrodes.","quantity":"?"},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"Electrode that each spike unit came from, specified using a DynamicTableRegion.","quantity":"?"},{"name":"electrode_group","neurodata_type_inc":"VectorData","dtype":{"target_type":"ElectrodeGroup","reftype":"object"},"doc":"Electrode group that each spike unit came from.","quantity":"?"},{"name":"waveform_mean","neurodata_type_inc":"VectorData","dtype":"float32","dims":[["num_units","num_samples"],["num_units","num_samples","num_electrodes"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveform mean for each spike unit.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveform_sd","neurodata_type_inc":"VectorData","dtype":"float32","dims":[["num_units","num_samples"],["num_units","num_samples","num_electrodes"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveform standard deviation for each spike unit.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveforms","neurodata_type_inc":"VectorData","dtype":"numeric","dims":["num_waveforms","num_samples"],"shape":[null,null],"doc":"Individual waveforms for each spike on each electrode. This is a doubly indexed column. The 'waveforms_index' column indexes which waveforms in this column belong to the same spike event for a given unit, where each waveform was recorded from a different electrode. The 'waveforms_index_index' column indexes the 'waveforms_index' column to indicate which spike events belong to a given unit. For example, if the 'waveforms_index_index' column has values [2, 5, 6], then the first 2 elements of the 'waveforms_index' column correspond to the 2 spike events of the first unit, the next 3 elements of the 'waveforms_index' column correspond to the 3 spike events of the second unit, and the next 1 element of the 'waveforms_index' column corresponds to the 1 spike event of the third unit. If the 'waveforms_index' column has values [3, 6, 8, 10, 12, 13], then the first 3 elements of the 'waveforms' column contain the 3 spike waveforms that were recorded from 3 different electrodes for the first spike time of the first unit. See https://nwb-schema.readthedocs.io/en/stable/format_description.html#doubly-ragged-arrays for a graphical representation of this example. When there is only one electrode for each unit (i.e., each spike time is associated with a single waveform), then the 'waveforms_index' column will have values 1, 2, ..., N, where N is the number of spike events. The number of electrodes for each spike event should be the same within a given unit. The 'electrodes' column should be used to indicate which electrodes are associated with each unit, and the order of the waveforms within a given unit x spike event should be in the same order as the electrodes referenced in the 'electrodes' column of this table. The number of samples for each waveform must be the same.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveforms_index","neurodata_type_inc":"VectorIndex","doc":"Index into the waveforms dataset. One value for every spike event. See 'waveforms' for more detail.","quantity":"?"},{"name":"waveforms_index_index","neurodata_type_inc":"VectorIndex","doc":"Index into the waveforms_index dataset. One value for every unit (row in the table). See 'waveforms' for more detail.","quantity":"?"}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.namespace.json b/Resources/spec/core/2.7.0/nwb.namespace.json new file mode 100644 index 0000000..1ebae4b --- /dev/null +++ b/Resources/spec/core/2.7.0/nwb.namespace.json @@ -0,0 +1 @@ +{"namespaces":[{"name":"core","doc":"NWB namespace","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter","Keith Godfrey","Jeff Teeters"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov","keithg@alleninstitute.org","jteeters@berkeley.edu"],"full_name":"NWB core","schema":[{"namespace":"hdmf-common"},{"source":"nwb.base"},{"source":"nwb.device"},{"source":"nwb.epoch"},{"source":"nwb.image"},{"source":"nwb.file"},{"source":"nwb.misc"},{"source":"nwb.behavior"},{"source":"nwb.ecephys"},{"source":"nwb.icephys"},{"source":"nwb.ogen"},{"source":"nwb.ophys"},{"source":"nwb.retinotopy"}],"version":"2.7.0"}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.ogen.json b/Resources/spec/core/2.7.0/nwb.ogen.json new file mode 100644 index 0000000..6133dba --- /dev/null +++ b/Resources/spec/core/2.7.0/nwb.ogen.json @@ -0,0 +1 @@ +{"groups":[{"neurodata_type_def":"OptogeneticSeries","neurodata_type_inc":"TimeSeries","doc":"An optogenetic stimulus.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_rois"]],"shape":[[null],[null,null]],"doc":"Applied power for optogenetic stimulus, in watts. Shape can be 1D or 2D. 2D data is meant to be used in an extension of OptogeneticSeries that defines what the second dimension represents.","attributes":[{"name":"unit","dtype":"text","value":"watts","doc":"Unit of measurement for data, which is fixed to 'watts'."}]}],"links":[{"name":"site","target_type":"OptogeneticStimulusSite","doc":"Link to OptogeneticStimulusSite object that describes the site to which this stimulus was applied."}]},{"neurodata_type_def":"OptogeneticStimulusSite","neurodata_type_inc":"NWBContainer","doc":"A site of optogenetic stimulation.","datasets":[{"name":"description","dtype":"text","doc":"Description of stimulation site."},{"name":"excitation_lambda","dtype":"float32","doc":"Excitation wavelength, in nm."},{"name":"location","dtype":"text","doc":"Location of the stimulation site. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."}],"links":[{"name":"device","target_type":"Device","doc":"Device that generated the stimulus."}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.ophys.json b/Resources/spec/core/2.7.0/nwb.ophys.json new file mode 100644 index 0000000..026c31e --- /dev/null +++ b/Resources/spec/core/2.7.0/nwb.ophys.json @@ -0,0 +1 @@ +{"groups":[{"neurodata_type_def":"OnePhotonSeries","neurodata_type_inc":"ImageSeries","doc":"Image stack recorded over time from 1-photon microscope.","attributes":[{"name":"pmt_gain","dtype":"float32","doc":"Photomultiplier gain.","required":false},{"name":"scan_line_rate","dtype":"float32","doc":"Lines imaged per second. This is also stored in /general/optophysiology but is kept here as it is useful information for analysis, and so good to be stored w/ the actual data.","required":false},{"name":"exposure_time","dtype":"float32","doc":"Exposure time of the sample; often the inverse of the frequency.","required":false},{"name":"binning","dtype":"uint8","doc":"Amount of pixels combined into 'bins'; could be 1, 2, 4, 8, etc.","required":false},{"name":"power","dtype":"float32","doc":"Power of the excitation in mW, if known.","required":false},{"name":"intensity","dtype":"float32","doc":"Intensity of the excitation in mW/mm^2, if known.","required":false}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this TimeSeries data was generated."}]},{"neurodata_type_def":"TwoPhotonSeries","neurodata_type_inc":"ImageSeries","doc":"Image stack recorded over time from 2-photon microscope.","attributes":[{"name":"pmt_gain","dtype":"float32","doc":"Photomultiplier gain.","required":false},{"name":"scan_line_rate","dtype":"float32","doc":"Lines imaged per second. This is also stored in /general/optophysiology but is kept here as it is useful information for analysis, and so good to be stored w/ the actual data.","required":false}],"datasets":[{"name":"field_of_view","dtype":"float32","dims":[["width|height"],["width|height|depth"]],"shape":[[2],[3]],"doc":"Width, height and depth of image, or imaged area, in meters.","quantity":"?"}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this TimeSeries data was generated."}]},{"neurodata_type_def":"RoiResponseSeries","neurodata_type_inc":"TimeSeries","doc":"ROI responses over an imaging plane. The first dimension represents time. The second dimension, if present, represents ROIs.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_ROIs"]],"shape":[[null],[null,null]],"doc":"Signals from ROIs."},{"name":"rois","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion referencing into an ROITable containing information on the ROIs stored in this timeseries."}]},{"neurodata_type_def":"DfOverF","neurodata_type_inc":"NWBDataInterface","default_name":"DfOverF","doc":"dF/F information about a region of interest (ROI). Storage hierarchy of dF/F should be the same as for segmentation (i.e., same names for ROIs and for image planes).","groups":[{"neurodata_type_inc":"RoiResponseSeries","doc":"RoiResponseSeries object(s) containing dF/F for a ROI.","quantity":"+"}]},{"neurodata_type_def":"Fluorescence","neurodata_type_inc":"NWBDataInterface","default_name":"Fluorescence","doc":"Fluorescence information about a region of interest (ROI). Storage hierarchy of fluorescence should be the same as for segmentation (ie, same names for ROIs and for image planes).","groups":[{"neurodata_type_inc":"RoiResponseSeries","doc":"RoiResponseSeries object(s) containing fluorescence data for a ROI.","quantity":"+"}]},{"neurodata_type_def":"ImageSegmentation","neurodata_type_inc":"NWBDataInterface","default_name":"ImageSegmentation","doc":"Stores pixels in an image that represent different regions of interest (ROIs) or masks. All segmentation for a given imaging plane is stored together, with storage for multiple imaging planes (masks) supported. Each ROI is stored in its own subgroup, with the ROI group containing both a 2D mask and a list of pixels that make up this mask. Segments can also be used for masking neuropil. If segmentation is allowed to change with time, a new imaging plane (or module) is required and ROI names should remain consistent between them.","groups":[{"neurodata_type_inc":"PlaneSegmentation","doc":"Results from image segmentation of a specific imaging plane.","quantity":"+"}]},{"neurodata_type_def":"PlaneSegmentation","neurodata_type_inc":"DynamicTable","doc":"Results from image segmentation of a specific imaging plane.","datasets":[{"name":"image_mask","neurodata_type_inc":"VectorData","dims":[["num_roi","num_x","num_y"],["num_roi","num_x","num_y","num_z"]],"shape":[[null,null,null],[null,null,null,null]],"doc":"ROI masks for each ROI. Each image mask is the size of the original imaging plane (or volume) and members of the ROI are finite non-zero.","quantity":"?"},{"name":"pixel_mask_index","neurodata_type_inc":"VectorIndex","doc":"Index into pixel_mask.","quantity":"?"},{"name":"pixel_mask","neurodata_type_inc":"VectorData","dtype":[{"name":"x","dtype":"uint32","doc":"Pixel x-coordinate."},{"name":"y","dtype":"uint32","doc":"Pixel y-coordinate."},{"name":"weight","dtype":"float32","doc":"Weight of the pixel."}],"doc":"Pixel masks for each ROI: a list of indices and weights for the ROI. Pixel masks are concatenated and parsing of this dataset is maintained by the PlaneSegmentation","quantity":"?"},{"name":"voxel_mask_index","neurodata_type_inc":"VectorIndex","doc":"Index into voxel_mask.","quantity":"?"},{"name":"voxel_mask","neurodata_type_inc":"VectorData","dtype":[{"name":"x","dtype":"uint32","doc":"Voxel x-coordinate."},{"name":"y","dtype":"uint32","doc":"Voxel y-coordinate."},{"name":"z","dtype":"uint32","doc":"Voxel z-coordinate."},{"name":"weight","dtype":"float32","doc":"Weight of the voxel."}],"doc":"Voxel masks for each ROI: a list of indices and weights for the ROI. Voxel masks are concatenated and parsing of this dataset is maintained by the PlaneSegmentation","quantity":"?"}],"groups":[{"name":"reference_images","doc":"Image stacks that the segmentation masks apply to.","groups":[{"neurodata_type_inc":"ImageSeries","doc":"One or more image stacks that the masks apply to (can be one-element stack).","quantity":"*"}]}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this data was generated."}]},{"neurodata_type_def":"ImagingPlane","neurodata_type_inc":"NWBContainer","doc":"An imaging plane and its metadata.","datasets":[{"name":"description","dtype":"text","doc":"Description of the imaging plane.","quantity":"?"},{"name":"excitation_lambda","dtype":"float32","doc":"Excitation wavelength, in nm."},{"name":"imaging_rate","dtype":"float32","doc":"Rate that images are acquired, in Hz. If the corresponding TimeSeries is present, the rate should be stored there instead.","quantity":"?"},{"name":"indicator","dtype":"text","doc":"Calcium indicator."},{"name":"location","dtype":"text","doc":"Location of the imaging plane. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."},{"name":"manifold","dtype":"float32","dims":[["height","width","x, y, z"],["height","width","depth","x, y, z"]],"shape":[[null,null,3],[null,null,null,3]],"doc":"DEPRECATED Physical position of each pixel. 'xyz' represents the position of the pixel relative to the defined coordinate space. Deprecated in favor of origin_coords and grid_spacing.","quantity":"?","attributes":[{"name":"conversion","dtype":"float32","default_value":1.0,"doc":"Scalar to multiply each element in data to convert it to the specified 'unit'. If the data are stored in acquisition system units or other units that require a conversion to be interpretable, multiply the data by 'conversion' to convert the data to the specified 'unit'. e.g. if the data acquisition system stores values in this object as pixels from x = -500 to 499, y = -500 to 499 that correspond to a 2 m x 2 m range, then the 'conversion' multiplier to get from raw data acquisition pixel units to meters is 2/1000.","required":false},{"name":"unit","dtype":"text","default_value":"meters","doc":"Base unit of measurement for working with the data. The default value is 'meters'.","required":false}]},{"name":"origin_coords","dtype":"float32","dims":[["x, y"],["x, y, z"]],"shape":[[2],[3]],"doc":"Physical location of the first element of the imaging plane (0, 0) for 2-D data or (0, 0, 0) for 3-D data. See also reference_frame for what the physical location is relative to (e.g., bregma).","quantity":"?","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Measurement units for origin_coords. The default value is 'meters'."}]},{"name":"grid_spacing","dtype":"float32","dims":[["x, y"],["x, y, z"]],"shape":[[2],[3]],"doc":"Space between pixels in (x, y) or voxels in (x, y, z) directions, in the specified unit. Assumes imaging plane is a regular grid. See also reference_frame to interpret the grid.","quantity":"?","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Measurement units for grid_spacing. The default value is 'meters'."}]},{"name":"reference_frame","dtype":"text","doc":"Describes reference frame of origin_coords and grid_spacing. For example, this can be a text description of the anatomical location and orientation of the grid defined by origin_coords and grid_spacing or the vectors needed to transform or rotate the grid to a common anatomical axis (e.g., AP/DV/ML). This field is necessary to interpret origin_coords and grid_spacing. If origin_coords and grid_spacing are not present, then this field is not required. For example, if the microscope takes 10 x 10 x 2 images, where the first value of the data matrix (index (0, 0, 0)) corresponds to (-1.2, -0.6, -2) mm relative to bregma, the spacing between pixels is 0.2 mm in x, 0.2 mm in y and 0.5 mm in z, and larger numbers in x means more anterior, larger numbers in y means more rightward, and larger numbers in z means more ventral, then enter the following -- origin_coords = (-1.2, -0.6, -2) grid_spacing = (0.2, 0.2, 0.5) reference_frame = \"Origin coordinates are relative to bregma. First dimension corresponds to anterior-posterior axis (larger index = more anterior). Second dimension corresponds to medial-lateral axis (larger index = more rightward). Third dimension corresponds to dorsal-ventral axis (larger index = more ventral).\"","quantity":"?"}],"groups":[{"neurodata_type_inc":"OpticalChannel","doc":"An optical channel used to record from an imaging plane.","quantity":"+"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the Device object that was used to record from this electrode."}]},{"neurodata_type_def":"OpticalChannel","neurodata_type_inc":"NWBContainer","doc":"An optical channel used to record from an imaging plane.","datasets":[{"name":"description","dtype":"text","doc":"Description or other notes about the channel."},{"name":"emission_lambda","dtype":"float32","doc":"Emission wavelength for channel, in nm."}]},{"neurodata_type_def":"MotionCorrection","neurodata_type_inc":"NWBDataInterface","default_name":"MotionCorrection","doc":"An image stack where all frames are shifted (registered) to a common coordinate system, to account for movement and drift between frames. Note: each frame at each point in time is assumed to be 2-D (has only x & y dimensions).","groups":[{"neurodata_type_inc":"CorrectedImageStack","doc":"Results from motion correction of an image stack.","quantity":"+"}]},{"neurodata_type_def":"CorrectedImageStack","neurodata_type_inc":"NWBDataInterface","doc":"Results from motion correction of an image stack.","groups":[{"name":"corrected","neurodata_type_inc":"ImageSeries","doc":"Image stack with frames shifted to the common coordinates."},{"name":"xy_translation","neurodata_type_inc":"TimeSeries","doc":"Stores the x,y delta necessary to align each frame to the common coordinates, for example, to align each frame to a reference image."}],"links":[{"name":"original","target_type":"ImageSeries","doc":"Link to ImageSeries object that is being registered."}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.retinotopy.json b/Resources/spec/core/2.7.0/nwb.retinotopy.json new file mode 100644 index 0000000..895dae9 --- /dev/null +++ b/Resources/spec/core/2.7.0/nwb.retinotopy.json @@ -0,0 +1 @@ +{"groups":[{"neurodata_type_def":"ImagingRetinotopy","neurodata_type_inc":"NWBDataInterface","default_name":"ImagingRetinotopy","doc":"DEPRECATED. Intrinsic signal optical imaging or widefield imaging for measuring retinotopy. Stores orthogonal maps (e.g., altitude/azimuth; radius/theta) of responses to specific stimuli and a combined polarity map from which to identify visual areas. This group does not store the raw responses imaged during retinotopic mapping or the stimuli presented, but rather the resulting phase and power maps after applying a Fourier transform on the averaged responses. Note: for data consistency, all images and arrays are stored in the format [row][column] and [row, col], which equates to [y][x]. Field of view and dimension arrays may appear backward (i.e., y before x).","datasets":[{"name":"axis_1_phase_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Phase response to stimulus on the first measured axis.","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_1_power_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Power response on the first measured axis. Response is scaled so 0.0 is no power in the response and 1.0 is maximum relative power.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_2_phase_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Phase response to stimulus on the second measured axis.","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_2_power_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Power response on the second measured axis. Response is scaled so 0.0 is no power in the response and 1.0 is maximum relative power.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_descriptions","dtype":"text","dims":["axis_1, axis_2"],"shape":[2],"doc":"Two-element array describing the contents of the two response axis fields. Description should be something like ['altitude', 'azimuth'] or '['radius', 'theta']."},{"name":"focal_depth_image","dtype":"uint16","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Gray-scale image taken with same settings/parameters (e.g., focal depth, wavelength) as data collection. Array format: [rows][columns].","quantity":"?","attributes":[{"name":"bits_per_pixel","dtype":"int32","doc":"Number of bits used to represent each value. This is necessary to determine maximum (white) pixel value."},{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"focal_depth","dtype":"float32","doc":"Focal depth offset, in meters."},{"name":"format","dtype":"text","doc":"Format of image. Right now only 'raw' is supported."}]},{"name":"sign_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Sine of the angle between the direction of the gradient in axis_1 and axis_2.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."}]},{"name":"vasculature_image","dtype":"uint16","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Gray-scale anatomical image of cortical surface. Array structure: [rows][columns]","attributes":[{"name":"bits_per_pixel","dtype":"int32","doc":"Number of bits used to represent each value. This is necessary to determine maximum (white) pixel value"},{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"format","dtype":"text","doc":"Format of image. Right now only 'raw' is supported."}]}]}]} \ No newline at end of file diff --git a/Resources/spec/hdmf-common/1.8.0/base.json b/Resources/spec/hdmf-common/1.8.0/base.json new file mode 100644 index 0000000..6753495 --- /dev/null +++ b/Resources/spec/hdmf-common/1.8.0/base.json @@ -0,0 +1 @@ +{"datasets":[{"data_type_def":"Data","doc":"An abstract data type for a dataset."}],"groups":[{"data_type_def":"Container","doc":"An abstract data type for a group storing collections of data and metadata. Base type for all data and metadata containers."},{"data_type_def":"SimpleMultiContainer","data_type_inc":"Container","doc":"A simple Container for holding onto multiple containers.","datasets":[{"data_type_inc":"Data","quantity":"*","doc":"Data objects held within this SimpleMultiContainer."}],"groups":[{"data_type_inc":"Container","quantity":"*","doc":"Container objects held within this SimpleMultiContainer."}]}]} \ No newline at end of file diff --git a/Resources/spec/hdmf-common/1.8.0/namespace.json b/Resources/spec/hdmf-common/1.8.0/namespace.json new file mode 100644 index 0000000..0b921a4 --- /dev/null +++ b/Resources/spec/hdmf-common/1.8.0/namespace.json @@ -0,0 +1 @@ +{"namespaces":[{"name":"hdmf-common","doc":"Common data structures provided by HDMF","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov"],"full_name":"HDMF Common","schema":[{"source":"base"},{"source":"table"},{"source":"sparse"}],"version":"1.8.0"}]} \ No newline at end of file diff --git a/Resources/spec/hdmf-common/1.8.0/sparse.json b/Resources/spec/hdmf-common/1.8.0/sparse.json new file mode 100644 index 0000000..16cff5b --- /dev/null +++ b/Resources/spec/hdmf-common/1.8.0/sparse.json @@ -0,0 +1 @@ +{"groups":[{"data_type_def":"CSRMatrix","data_type_inc":"Container","doc":"A compressed sparse row matrix. Data are stored in the standard CSR format, where column indices for row i are stored in indices[indptr[i]:indptr[i+1]] and their corresponding values are stored in data[indptr[i]:indptr[i+1]].","attributes":[{"name":"shape","dtype":"uint","dims":["number of rows, number of columns"],"shape":[2],"doc":"The shape (number of rows, number of columns) of this sparse matrix."}],"datasets":[{"name":"indices","dtype":"uint","dims":["number of non-zero values"],"shape":[null],"doc":"The column indices."},{"name":"indptr","dtype":"uint","dims":["number of rows in the matrix + 1"],"shape":[null],"doc":"The row index pointer."},{"name":"data","dims":["number of non-zero values"],"shape":[null],"doc":"The non-zero values in the matrix."}]}]} \ No newline at end of file diff --git a/Resources/spec/hdmf-common/1.8.0/table.json b/Resources/spec/hdmf-common/1.8.0/table.json new file mode 100644 index 0000000..36a927d --- /dev/null +++ b/Resources/spec/hdmf-common/1.8.0/table.json @@ -0,0 +1 @@ +{"datasets":[{"data_type_def":"VectorData","data_type_inc":"Data","doc":"An n-dimensional dataset representing a column of a DynamicTable. If used without an accompanying VectorIndex, first dimension is along the rows of the DynamicTable and each step along the first dimension is a cell of the larger table. VectorData can also be used to represent a ragged array if paired with a VectorIndex. This allows for storing arrays of varying length in a single cell of the DynamicTable by indexing into this VectorData. The first vector is at VectorData[0:VectorIndex[0]]. The second vector is at VectorData[VectorIndex[0]:VectorIndex[1]], and so on.","dims":[["dim0"],["dim0","dim1"],["dim0","dim1","dim2"],["dim0","dim1","dim2","dim3"]],"shape":[[null],[null,null],[null,null,null],[null,null,null,null]],"attributes":[{"name":"description","dtype":"text","doc":"Description of what these vectors represent."}]},{"data_type_def":"VectorIndex","data_type_inc":"VectorData","dtype":"uint8","doc":"Used with VectorData to encode a ragged array. An array of indices into the first dimension of the target VectorData, and forming a map between the rows of a DynamicTable and the indices of the VectorData. The name of the VectorIndex is expected to be the name of the target VectorData object followed by \"_index\".","dims":["num_rows"],"shape":[null],"attributes":[{"name":"target","dtype":{"target_type":"VectorData","reftype":"object"},"doc":"Reference to the target dataset that this index applies to."}]},{"data_type_def":"ElementIdentifiers","data_type_inc":"Data","default_name":"element_id","dtype":"int","dims":["num_elements"],"shape":[null],"doc":"A list of unique identifiers for values within a dataset, e.g. rows of a DynamicTable."},{"data_type_def":"DynamicTableRegion","data_type_inc":"VectorData","dtype":"int","doc":"DynamicTableRegion provides a link from one table to an index or region of another. The `table` attribute is a link to another `DynamicTable`, indicating which table is referenced, and the data is int(s) indicating the row(s) (0-indexed) of the target array. `DynamicTableRegion`s can be used to associate rows with repeated meta-data without data duplication. They can also be used to create hierarchical relationships between multiple `DynamicTable`s. `DynamicTableRegion` objects may be paired with a `VectorIndex` object to create ragged references, so a single cell of a `DynamicTable` can reference many rows of another `DynamicTable`.","dims":["num_rows"],"shape":[null],"attributes":[{"name":"table","dtype":{"target_type":"DynamicTable","reftype":"object"},"doc":"Reference to the DynamicTable object that this region applies to."},{"name":"description","dtype":"text","doc":"Description of what this table region points to."}]}],"groups":[{"data_type_def":"DynamicTable","data_type_inc":"Container","doc":"A group containing multiple datasets that are aligned on the first dimension (Currently, this requirement if left up to APIs to check and enforce). These datasets represent different columns in the table. Apart from a column that contains unique identifiers for each row, there are no other required datasets. Users are free to add any number of custom VectorData objects (columns) here. DynamicTable also supports ragged array columns, where each element can be of a different size. To add a ragged array column, use a VectorIndex type to index the corresponding VectorData type. See documentation for VectorData and VectorIndex for more details. Unlike a compound data type, which is analogous to storing an array-of-structs, a DynamicTable can be thought of as a struct-of-arrays. This provides an alternative structure to choose from when optimizing storage for anticipated access patterns. Additionally, this type provides a way of creating a table without having to define a compound type up front. Although this convenience may be attractive, users should think carefully about how data will be accessed. DynamicTable is more appropriate for column-centric access, whereas a dataset with a compound type would be more appropriate for row-centric access. Finally, data size should also be taken into account. For small tables, performance loss may be an acceptable trade-off for the flexibility of a DynamicTable.","attributes":[{"name":"colnames","dtype":"text","dims":["num_columns"],"shape":[null],"doc":"The names of the columns in this table. This should be used to specify an order to the columns."},{"name":"description","dtype":"text","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"id","data_type_inc":"ElementIdentifiers","dtype":"int","dims":["num_rows"],"shape":[null],"doc":"Array of unique identifiers for the rows of this dynamic table."},{"data_type_inc":"VectorData","doc":"Vector columns, including index columns, of this dynamic table.","quantity":"*"}]},{"data_type_def":"AlignedDynamicTable","data_type_inc":"DynamicTable","doc":"DynamicTable container that supports storing a collection of sub-tables. Each sub-table is a DynamicTable itself that is aligned with the main table by row index. I.e., all DynamicTables stored in this group MUST have the same number of rows. This type effectively defines a 2-level table in which the main data is stored in the main table implemented by this type and additional columns of the table are grouped into categories, with each category being represented by a separate DynamicTable stored within the group.","attributes":[{"name":"categories","dtype":"text","dims":["num_categories"],"shape":[null],"doc":"The names of the categories in this AlignedDynamicTable. Each category is represented by one DynamicTable stored in the parent group. This attribute should be used to specify an order of categories and the category names must match the names of the corresponding DynamicTable in the group."}],"groups":[{"data_type_inc":"DynamicTable","doc":"A DynamicTable representing a particular category for columns in the AlignedDynamicTable parent container. The table MUST be aligned with (i.e., have the same number of rows) as all other DynamicTables stored in the AlignedDynamicTable parent container. The name of the category is given by the name of the DynamicTable and its description by the description attribute of the DynamicTable.","quantity":"*"}]}]} \ No newline at end of file diff --git a/Resources/spec/hdmf-experimental/0.5.0/experimental.json b/Resources/spec/hdmf-experimental/0.5.0/experimental.json new file mode 100644 index 0000000..25f3113 --- /dev/null +++ b/Resources/spec/hdmf-experimental/0.5.0/experimental.json @@ -0,0 +1 @@ +{"groups":[],"datasets":[{"data_type_def":"EnumData","data_type_inc":"VectorData","dtype":"uint8","doc":"Data that come from a fixed set of values. A data value of i corresponds to the i-th value in the VectorData referenced by the 'elements' attribute.","attributes":[{"name":"elements","dtype":{"target_type":"VectorData","reftype":"object"},"doc":"Reference to the VectorData object that contains the enumerable elements"}]}]} \ No newline at end of file diff --git a/Resources/spec/hdmf-experimental/0.5.0/namespace.json b/Resources/spec/hdmf-experimental/0.5.0/namespace.json new file mode 100644 index 0000000..26f1ac7 --- /dev/null +++ b/Resources/spec/hdmf-experimental/0.5.0/namespace.json @@ -0,0 +1 @@ +{"namespaces":[{"name":"hdmf-experimental","doc":"Experimental data structures provided by HDMF. These are not guaranteed to be available in the future.","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter","Matthew Avaylon"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov","mavaylon@lbl.gov"],"full_name":"HDMF Experimental","schema":[{"namespace":"hdmf-common"},{"source":"experimental"},{"source":"resources"}],"version":"0.5.0"}]} \ No newline at end of file diff --git a/Resources/spec/hdmf-experimental/0.5.0/resources.json b/Resources/spec/hdmf-experimental/0.5.0/resources.json new file mode 100644 index 0000000..0e7d71f --- /dev/null +++ b/Resources/spec/hdmf-experimental/0.5.0/resources.json @@ -0,0 +1 @@ +{"groups":[{"data_type_def":"HERD","data_type_inc":"Container","doc":"HDMF External Resources Data Structure. A set of six tables for tracking external resource references in a file or across multiple files.","datasets":[{"data_type_inc":"Data","name":"keys","doc":"A table for storing user terms that are used to refer to external resources.","dtype":[{"name":"key","dtype":"text","doc":"The user term that maps to one or more resources in the `resources` table, e.g., \"human\"."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"files","doc":"A table for storing object ids of files used in external resources.","dtype":[{"name":"file_object_id","dtype":"text","doc":"The object id (UUID) of a file that contains objects that refers to external resources."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"entities","doc":"A table for mapping user terms (i.e., keys) to resource entities.","dtype":[{"name":"entity_id","dtype":"text","doc":"The compact uniform resource identifier (CURIE) of the entity, in the form [prefix]:[unique local identifier], e.g., 'NCBI_TAXON:9606'."},{"name":"entity_uri","dtype":"text","doc":"The URI for the entity this reference applies to. This can be an empty string. e.g., https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=info&id=9606"}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"objects","doc":"A table for identifying which objects in a file contain references to external resources.","dtype":[{"name":"files_idx","dtype":"uint","doc":"The row index to the file in the `files` table containing the object."},{"name":"object_id","dtype":"text","doc":"The object id (UUID) of the object."},{"name":"object_type","dtype":"text","doc":"The data type of the object."},{"name":"relative_path","dtype":"text","doc":"The relative path from the data object with the `object_id` to the dataset or attribute with the value(s) that is associated with an external resource. This can be an empty string if the object is a dataset that contains the value(s) that is associated with an external resource."},{"name":"field","dtype":"text","doc":"The field within the compound data type using an external resource. This is used only if the dataset or attribute is a compound data type; otherwise this should be an empty string."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"object_keys","doc":"A table for identifying which objects use which keys.","dtype":[{"name":"objects_idx","dtype":"uint","doc":"The row index to the object in the `objects` table that holds the key"},{"name":"keys_idx","dtype":"uint","doc":"The row index to the key in the `keys` table."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"entity_keys","doc":"A table for identifying which keys use which entity.","dtype":[{"name":"entities_idx","dtype":"uint","doc":"The row index to the entity in the `entities` table."},{"name":"keys_idx","dtype":"uint","doc":"The row index to the key in the `keys` table."}],"dims":["num_rows"],"shape":[null]}]}]} \ No newline at end of file From 2139c47de84a33beb96d62a2c41c7be126b03018 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:16:08 -0700 Subject: [PATCH 06/32] update cmake for static lib --- CMakeLists.txt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 01bb918..7b825f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,9 +33,11 @@ set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS set(SOURCE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/Source) -file(GLOB_RECURSE SRC_FILES LIST_DIRECTORIES false "${SOURCE_PATH}/*.cpp" "${SOURCE_PATH}/*.h") +file(GLOB_RECURSE SRC_FILES LIST_DIRECTORIES false "${SOURCE_PATH}/*.cpp" "${SOURCE_PATH}/*.h" "${SOURCE_PATH}/*.hpp") set(GUI_COMMONLIB_DIR ${GUI_BASE_DIR}/installed_libs) +include_directories(${SOURCE_PATH}/aqnwb) + set(CONFIGURATION_FOLDER $<$:Debug>$<$>:Release>) list(APPEND CMAKE_PREFIX_PATH ${GUI_COMMONLIB_DIR} ${GUI_COMMONLIB_DIR}/${CONFIGURATION_FOLDER}) @@ -108,17 +110,13 @@ if(NOT HDF5_FOUND) #if package finding fails, try manually find_path(HDF5_INCLUDE_DIRS H5Cpp.h) endif() -find_library(AQNWB_LIBRARIES NAMES aqnwb) -find_path(AQNWB_INCLUDE_DIRS aqnwb.hpp) - -include_directories(${HDF5_INCLUDE_DIRS}) +find_package(Boost REQUIRED) -target_link_libraries(${PLUGIN_NAME} ${HDF5_LIBRARIES} ${HDF5_CXX_LIBRARIES}) -target_include_directories(${PLUGIN_NAME} PRIVATE ${HDF5_INCLUDE_DIRS}) +target_link_libraries(${PLUGIN_NAME} ${HDF5_LIBRARIES} ${HDF5_CXX_LIBRARIES} ${Boost_LIBRARIES}) +target_include_directories(${PLUGIN_NAME} PRIVATE ${HDF5_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS}) #target_include_directories(${PLUGIN_NAME} PUBLIC ../OpenEphysHDF5Lib/Source) - # Open Ephys common libraries include(link_open_ephys_lib.cmake) link_open_ephys_lib(${PLUGIN_NAME} OpenEphysHDF5) From edfeccbb25936e4641f32d2fb36b740b83ad34a2 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 28 Aug 2024 13:01:07 -0700 Subject: [PATCH 07/32] wip - update recording files --- Source/RecordEngine/NWBRecording.cpp | 313 +++++++++++++++++---------- Source/RecordEngine/NWBRecording.h | 37 ++-- 2 files changed, 218 insertions(+), 132 deletions(-) diff --git a/Source/RecordEngine/NWBRecording.cpp b/Source/RecordEngine/NWBRecording.cpp index fa603bf..1125252 100644 --- a/Source/RecordEngine/NWBRecording.cpp +++ b/Source/RecordEngine/NWBRecording.cpp @@ -21,7 +21,12 @@ */ - #include "NWBRecording.h" +#include +#include + +#include "NWBRecording.h" +#include "aqnwb/Channel.hpp" +#include "aqnwb/Utils.hpp" #include "../../plugin-GUI/Source/Processors/RecordNode/RecordNode.h" @@ -32,23 +37,22 @@ NWBRecordEngine::NWBRecordEngine() { - smpBuffer.malloc(MAX_BUFFER_SIZE); } NWBRecordEngine::~NWBRecordEngine() - { - if (nwb != nullptr) - { - spikeChannels.clear(); - eventChannels.clear(); - continuousChannelGroups.clear(); - datasetIndexes.clear(); - writeChannelIndexes.clear(); - - nwb->close(); - nwb.reset(); - } + { + // if (nwb != nullptr) + // { + // spikeChannels.clear(); + // eventChannels.clear(); + // continuousChannelGroups.clear(); + // datasetIndexes.clear(); + // writeChannelIndexes.clear(); + + // nwb->close(); + // nwb.reset(); + // } } RecordEngineManager* NWBRecordEngine::getEngineManager() @@ -56,164 +60,241 @@ RecordEngineManager* NWBRecordEngine::getEngineManager() //static factory that instantiates the engine manager, which allows to configure recording options among other things. See OriginalRecording to see how to create options for a record engine RecordEngineManager* man = new RecordEngineManager("NWB2", "NWB2", &(engineFactory)); EngineParameter* param; - param = new EngineParameter(EngineParameter::STR, 0, "Identifier Text", String()); - man->addParameter(param); + // param = new EngineParameter(EngineParameter::STR, 0, "Identifier Text", String()); + // man->addParameter(param); return man; } void NWBRecordEngine::openFiles(File rootFolder, int experimentNumber, int recordingNumber) - { + { + // setup file paths + char separator = std::filesystem::path::preferred_separator; + std::string separatorStr(1, separator); // Convert char to std::string + std::string filename = rootFolder.getFullPathName().toStdString() + separatorStr + + "experiment_aqnwb" + std::to_string(experimentNumber) + ".nwb"; + // std::string guiVersion = CoreServices::getGUIVersion(); + + // get pointers to all continuous channels for electrode table + std::vector channelVector; + for (int i = 0; i < recordNode->getNumOutputs(); i++) + { + const ContinuousChannel* channelInfo = getContinuousChannel(i); // channel info object + continuousChannels.add(channelInfo); + } + + int streamIndex = -1; + uint16 lastStreamId = 0; + + for (int ch = 0; ch < getNumRecordedContinuousChannels(); ch++) + { + int globalIndex = getGlobalIndex(ch); // the global channel index (across all channels entering the Record Node) + int localIndex = getLocalIndex(ch); // the local channel index (within a stream) + + const ContinuousChannel* channelInfo = getContinuousChannel(globalIndex); // channel info object + if (channelInfo->getStreamId() != lastStreamId) + { + streamIndex++; + ContinuousGroup newGroup; + continuousChannelGroups.add(newGroup); + } + + continuousChannelGroups.getReference(streamIndex).add(channelInfo); + lastStreamId = channelInfo->getStreamId(); + + // TODO - part I'm adding + std::string name = channelInfo->getName().toStdString(); + std::string groupName = channelInfo->getSourceNodeName().toStdString() + "-" + + std::to_string(channelInfo->getSourceNodeId()) + + "." + channelInfo->getStreamName().toStdString(); + + float channel_conversion = channelInfo->getBitVolts() * 1e6; + channelVector.push_back(AQNWB::Channel(name, groupName, streamIndex, localIndex, globalIndex, channel_conversion, channelInfo->getSampleRate(), channelInfo->getBitVolts())); + } + + // TODO - I don't think this properly separates different streams / recording arrays right now + this->recordingArrays = {channelVector}; + + // initialize nwbfile object and create base structure + nwbRecording.openFile(filename, recordingArrays); - if (recordingNumber == 0) // new file needed - { - - spikeChannels.clear(); - eventChannels.clear(); - continuousChannels.clear(); - continuousChannelGroups.clear(); - datasetIndexes.clear(); - writeChannelIndexes.clear(); - - // New file for each experiment, e.g. experiment1.nwb, epxperiment2.nwb, etc. - String basepath = rootFolder.getFullPathName() + - rootFolder.getSeparatorString() + - "experiment" + String(experimentNumber) + - ".nwb"; + + // if (recordingNumber == 0) // new file needed + // { + + // spikeChannels.clear(); + // eventChannels.clear(); + // continuousChannels.clear(); + // continuousChannelGroups.clear(); + // datasetIndexes.clear(); + // writeChannelIndexes.clear(); + + // // New file for each experiment, e.g. experiment1.nwb, epxperiment2.nwb, etc. + // String basepath = rootFolder.getFullPathName() + + // rootFolder.getSeparatorString() + + // "experiment" + String(experimentNumber) + + // ".nwb"; - if (nwb != nullptr) - { - nwb->close(); - nwb.reset(); - } + // if (nwb != nullptr) + // { + // nwb->close(); + // nwb.reset(); + // } - // create a unique identifier for the file if it doesn't exist - Uuid identifier; - identifierText = identifier.toString(); + // // create a unique identifier for the file if it doesn't exist + // Uuid identifier; + // identifierText = identifier.toString(); - nwb = std::make_unique(basepath, CoreServices::getGUIVersion(), identifierText); + // nwb = std::make_unique(basepath, CoreServices::getGUIVersion(), identifierText); - // get pointers to all continuous channels for electrode table - for (int i = 0; i < recordNode->getNumOutputs(); i++) - { - const ContinuousChannel* channelInfo = getContinuousChannel(i); // channel info object + // // get pointers to all continuous channels for electrode table + // for (int i = 0; i < recordNode->getNumOutputs(); i++) + // { + // const ContinuousChannel* channelInfo = getContinuousChannel(i); // channel info object - continuousChannels.add(channelInfo); - } + // continuousChannels.add(channelInfo); + // } - datasetIndexes.insertMultiple(0, 0, getNumRecordedContinuousChannels()); - writeChannelIndexes.insertMultiple(0, 0, getNumRecordedContinuousChannels()); - continuousChannelGroups.clear(); + // datasetIndexes.insertMultiple(0, 0, getNumRecordedContinuousChannels()); + // writeChannelIndexes.insertMultiple(0, 0, getNumRecordedContinuousChannels()); + // continuousChannelGroups.clear(); - int streamIndex = -1; - uint16 lastStreamId = 0; - int indexWithinStream = 0; + // int streamIndex = -1; + // uint16 lastStreamId = 0; + // int indexWithinStream = 0; - for (int ch = 0; ch < getNumRecordedContinuousChannels(); ch++) - { + // for (int ch = 0; ch < getNumRecordedContinuousChannels(); ch++) + // { - int globalIndex = getGlobalIndex(ch); // the global channel index (across all channels entering the Record Node) - int localIndex = getLocalIndex(ch); // the local channel index (within a stream) + // int globalIndex = getGlobalIndex(ch); // the global channel index (across all channels entering the Record Node) + // int localIndex = getLocalIndex(ch); // the local channel index (within a stream) - const ContinuousChannel* channelInfo = getContinuousChannel(globalIndex); // channel info object + // const ContinuousChannel* channelInfo = getContinuousChannel(globalIndex); // channel info object - int sourceId = channelInfo->getSourceNodeId(); - int streamId = channelInfo->getStreamId(); + // int sourceId = channelInfo->getSourceNodeId(); + // int streamId = channelInfo->getStreamId(); - if (streamId != lastStreamId) - { - streamIndex++; - indexWithinStream = 0; + // if (streamId != lastStreamId) + // { + // streamIndex++; + // indexWithinStream = 0; - ContinuousGroup newGroup; - continuousChannelGroups.add(newGroup); + // ContinuousGroup newGroup; + // continuousChannelGroups.add(newGroup); - } + // } - continuousChannelGroups.getReference(streamIndex).add(channelInfo); + // continuousChannelGroups.getReference(streamIndex).add(channelInfo); - datasetIndexes.set(ch, streamIndex); - writeChannelIndexes.set(ch, indexWithinStream++); + // datasetIndexes.set(ch, streamIndex); + // writeChannelIndexes.set(ch, indexWithinStream++); - lastStreamId = streamId; - } + // lastStreamId = streamId; + // } - for (int i = 0; i < getNumRecordedEventChannels(); i++) - eventChannels.add(getEventChannel(i)); + // for (int i = 0; i < getNumRecordedEventChannels(); i++) + // eventChannels.add(getEventChannel(i)); - for (int i = 0; i < getNumRecordedSpikeChannels(); i++) - spikeChannels.add(getSpikeChannel(i)); + // for (int i = 0; i < getNumRecordedSpikeChannels(); i++) + // spikeChannels.add(getSpikeChannel(i)); - //open the file - nwb->open(getNumRecordedContinuousChannels() + continuousChannelGroups.size() + eventChannels.size() + spikeChannels.size()); //total channels + timestamp arrays, to create a big enough buffer + // //open the file + // nwb->open(getNumRecordedContinuousChannels() + continuousChannelGroups.size() + eventChannels.size() + spikeChannels.size()); //total channels + timestamp arrays, to create a big enough buffer - //create the recording - nwb->startNewRecording(recordingNumber, continuousChannelGroups, continuousChannels, eventChannels, spikeChannels); - } + // //create the recording + // nwb->startNewRecording(recordingNumber, continuousChannelGroups, continuousChannels, eventChannels, spikeChannels); + // } } - + void NWBRecordEngine::closeFiles() { - nwb->stopRecording(); + nwbRecording.closeFile(); + // nwb->stopRecording(); } - - void NWBRecordEngine::writeContinuousData(int writeChannel, int realChannel, const float* dataBuffer, const double* timestampBuffer, int size) -{ - nwb->writeData(datasetIndexes[writeChannel], - writeChannelIndexes[writeChannel], - size, - dataBuffer, - getContinuousChannel(realChannel)->getBitVolts()); - - /* All channels in a dataset have the same number of samples and share timestamps. - But since this method is called asynchronously, the timestamps might not be - in sync during acquisition, so we chose a channel and write the timestamps - when writing that channel's data */ - if (writeChannelIndexes[writeChannel] == 0) - { - int64 baseTS = getLatestSampleNumber(writeChannel); - - for (int i = 0; i < size; i++) - { - smpBuffer[i] = baseTS + i; +{ + // get channel info - add this to RecordingArray or ChannelVector when we make it a class + AQNWB::Channel* channel = nullptr; + AQNWB::Types::SizeType datasetIndex = 0; + for (auto& channelVector : recordingArrays) { + for (auto& ch : channelVector) { + if (ch.globalIndex == realChannel) { + datasetIndex = ch.groupIndex; + channel = &ch; + break; + } } - - nwb->writeTimestamps(datasetIndexes[writeChannel], size, timestampBuffer); - nwb->writeSampleNumbers(datasetIndexes[writeChannel], size, smpBuffer); } + + // get data info + std::vector dataShape = {static_cast(size), 1}; + std::vector positionOffset = {static_cast(0), static_cast(writeChannel)}; + // TODO - update positionOffset tracking + + // write data + // std::unique_ptr intBuffer = AQNWB::transformToInt16(static_cast(size), channel->getBitVolts() / 1e6, dataBuffer); + nwbRecording.writeTimeseriesData("ElectricalSeries", + datasetIndex, + *channel, // to do, get recording channel information + dataShape, + positionOffset, + intBuffer.get(), + timestampBuffer); + + // nwb->writeData(datasetIndexes[writeChannel], + // writeChannelIndexes[writeChannel], + // size, + // dataBuffer, + // getContinuousChannel(realChannel)->getBitVolts()); + + // /* All channels in a dataset have the same number of samples and share timestamps. + // But since this method is called asynchronously, the timestamps might not be + // in sync during acquisition, so we chose a channel and write the timestamps + // when writing that channel's data */ + // if (writeChannelIndexes[writeChannel] == 0) + // { + // int64 baseTS = getLatestSampleNumber(writeChannel); + + // for (int i = 0; i < size; i++) + // { + // smpBuffer[i] = baseTS + i; + // } + + // nwb->writeTimestamps(datasetIndexes[writeChannel], size, timestampBuffer); + // nwb->writeSampleNumbers(datasetIndexes[writeChannel], size, smpBuffer); + // } } void NWBRecordEngine::writeEvent(int eventIndex, const MidiMessage& event) { - const EventChannel* channel = getEventChannel(eventIndex); - EventPtr eventStruct = Event::deserialize(event, channel); + // const EventChannel* channel = getEventChannel(eventIndex); + // EventPtr eventStruct = Event::deserialize(event, channel); - nwb->writeEvent(eventIndex, channel, eventStruct); + // nwb->writeEvent(eventIndex, channel, eventStruct); } void NWBRecordEngine::writeTimestampSyncText(uint64 streamId, int64 timestamp, float sourceSampleRate, String text) { - nwb->writeTimestampSyncText(streamId, timestamp, sourceSampleRate, text); + // nwb->writeTimestampSyncText(streamId, timestamp, sourceSampleRate, text); } void NWBRecordEngine::writeSpike(int electrodeIndex, const Spike* spike) { - const SpikeChannel* channel = getSpikeChannel(electrodeIndex); + // const SpikeChannel* channel = getSpikeChannel(electrodeIndex); - nwb->writeSpike(electrodeIndex, channel, spike); + // nwb->writeSpike(electrodeIndex, channel, spike); } -void NWBRecordEngine::setParameter(EngineParameter& parameter) -{ - strParameter(0, identifierText); -} +// void NWBRecordEngine::setParameter(EngineParameter& parameter) +// { +// strParameter(0, identifierText); +// } diff --git a/Source/RecordEngine/NWBRecording.h b/Source/RecordEngine/NWBRecording.h index a2aba61..1de1af5 100644 --- a/Source/RecordEngine/NWBRecording.h +++ b/Source/RecordEngine/NWBRecording.h @@ -26,7 +26,9 @@ #include -#include "NWBFormat.h" +#include "aqnwb/nwb/NWBRecording.hpp" + +typedef Array ContinuousGroup; namespace NWBRecording { @@ -73,27 +75,30 @@ namespace NWBRecording /** Write the timestamp sync text messages to disk*/ void writeTimestampSyncText(uint64 streamId, int64 timestamp, float sourceSampleRate, String text) override; - /** Allows the file identifier to be set externally*/ - void setParameter(EngineParameter ¶meter) override; + // /** Allows the file identifier to be set externally*/ + // void setParameter(EngineParameter ¶meter) override; private: - /** Pointer to the current NWB file */ - std::unique_ptr nwb; + /** NWB recording manager */ + AQNWB::NWB::NWBRecording nwbRecording; + + /** Holds channel information and ids */ + std::vector recordingArrays; - /** For each incoming recorded channel, which dataset (stream) is it associated with? */ - Array datasetIndexes; + // /** For each incoming recorded channel, which dataset (stream) is it associated with? */ + // Array datasetIndexes; - /** For each incoming recorded channel, what is the local index within a stream? */ - Array writeChannelIndexes; + // /** For each incoming recorded channel, what is the local index within a stream? */ + // Array writeChannelIndexes; /** Holds pointers to all recorded channels within a stream */ Array continuousChannelGroups; - /** Holds pointers to all recorded event channels*/ - Array eventChannels; + // /** Holds pointers to all recorded event channels*/ + // Array eventChannels; - /** Holds pointers to all recorded spike channels*/ - Array spikeChannels; + // /** Holds pointers to all recorded spike channels*/ + // Array spikeChannels; /** Holds pointers to all incoming continuous channels (used for electrode table)*/ Array continuousChannels; @@ -101,10 +106,10 @@ namespace NWBRecording /** Holds integer sample numbers for writing */ HeapBlock smpBuffer; - /** The identifier for the current file (can be set externally) */ - String identifierText; + // /** The identifier for the current file (can be set externally) */ + // String identifierText; - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(NWBRecordEngine); + // JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(NWBRecordEngine); }; } From d0c3137e2ab7224c569c526a4c97d34b0f4e57dc Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 28 Aug 2024 13:16:16 -0700 Subject: [PATCH 08/32] remove spec files from resources --- Resources/spec/core/2.7.0/nwb.base.json | 1 - Resources/spec/core/2.7.0/nwb.behavior.json | 1 - Resources/spec/core/2.7.0/nwb.device.json | 1 - Resources/spec/core/2.7.0/nwb.ecephys.json | 1 - Resources/spec/core/2.7.0/nwb.epoch.json | 1 - Resources/spec/core/2.7.0/nwb.file.json | 1 - Resources/spec/core/2.7.0/nwb.icephys.json | 1 - Resources/spec/core/2.7.0/nwb.image.json | 1 - Resources/spec/core/2.7.0/nwb.misc.json | 1 - Resources/spec/core/2.7.0/nwb.namespace.json | 1 - Resources/spec/core/2.7.0/nwb.ogen.json | 1 - Resources/spec/core/2.7.0/nwb.ophys.json | 1 - Resources/spec/core/2.7.0/nwb.retinotopy.json | 1 - Resources/spec/hdmf-common/1.8.0/base.json | 1 - Resources/spec/hdmf-common/1.8.0/namespace.json | 1 - Resources/spec/hdmf-common/1.8.0/sparse.json | 1 - Resources/spec/hdmf-common/1.8.0/table.json | 1 - Resources/spec/hdmf-experimental/0.5.0/experimental.json | 1 - Resources/spec/hdmf-experimental/0.5.0/namespace.json | 1 - Resources/spec/hdmf-experimental/0.5.0/resources.json | 1 - 20 files changed, 20 deletions(-) delete mode 100644 Resources/spec/core/2.7.0/nwb.base.json delete mode 100644 Resources/spec/core/2.7.0/nwb.behavior.json delete mode 100644 Resources/spec/core/2.7.0/nwb.device.json delete mode 100644 Resources/spec/core/2.7.0/nwb.ecephys.json delete mode 100644 Resources/spec/core/2.7.0/nwb.epoch.json delete mode 100644 Resources/spec/core/2.7.0/nwb.file.json delete mode 100644 Resources/spec/core/2.7.0/nwb.icephys.json delete mode 100644 Resources/spec/core/2.7.0/nwb.image.json delete mode 100644 Resources/spec/core/2.7.0/nwb.misc.json delete mode 100644 Resources/spec/core/2.7.0/nwb.namespace.json delete mode 100644 Resources/spec/core/2.7.0/nwb.ogen.json delete mode 100644 Resources/spec/core/2.7.0/nwb.ophys.json delete mode 100644 Resources/spec/core/2.7.0/nwb.retinotopy.json delete mode 100644 Resources/spec/hdmf-common/1.8.0/base.json delete mode 100644 Resources/spec/hdmf-common/1.8.0/namespace.json delete mode 100644 Resources/spec/hdmf-common/1.8.0/sparse.json delete mode 100644 Resources/spec/hdmf-common/1.8.0/table.json delete mode 100644 Resources/spec/hdmf-experimental/0.5.0/experimental.json delete mode 100644 Resources/spec/hdmf-experimental/0.5.0/namespace.json delete mode 100644 Resources/spec/hdmf-experimental/0.5.0/resources.json diff --git a/Resources/spec/core/2.7.0/nwb.base.json b/Resources/spec/core/2.7.0/nwb.base.json deleted file mode 100644 index b068e87..0000000 --- a/Resources/spec/core/2.7.0/nwb.base.json +++ /dev/null @@ -1 +0,0 @@ -{"datasets":[{"neurodata_type_def":"NWBData","neurodata_type_inc":"Data","doc":"An abstract data type for a dataset."},{"neurodata_type_def":"TimeSeriesReferenceVectorData","neurodata_type_inc":"VectorData","default_name":"timeseries","dtype":[{"name":"idx_start","dtype":"int32","doc":"Start index into the TimeSeries 'data' and 'timestamp' datasets of the referenced TimeSeries. The first dimension of those arrays is always time."},{"name":"count","dtype":"int32","doc":"Number of data samples available in this time series, during this epoch"},{"name":"timeseries","dtype":{"target_type":"TimeSeries","reftype":"object"},"doc":"The TimeSeries that this index applies to"}],"doc":"Column storing references to a TimeSeries (rows). For each TimeSeries this VectorData column stores the start_index and count to indicate the range in time to be selected as well as an object reference to the TimeSeries."},{"neurodata_type_def":"Image","neurodata_type_inc":"NWBData","dtype":"numeric","dims":[["x","y"],["x","y","r, g, b"],["x","y","r, g, b, a"]],"shape":[[null,null],[null,null,3],[null,null,4]],"doc":"An abstract data type for an image. Shape can be 2-D (x, y), or 3-D where the third dimension can have three or four elements, e.g. (x, y, (r, g, b)) or (x, y, (r, g, b, a)).","attributes":[{"name":"resolution","dtype":"float32","doc":"Pixel resolution of the image, in pixels per centimeter.","required":false},{"name":"description","dtype":"text","doc":"Description of the image.","required":false}]},{"neurodata_type_def":"ImageReferences","neurodata_type_inc":"NWBData","dtype":{"target_type":"Image","reftype":"object"},"dims":["num_images"],"shape":[null],"doc":"Ordered dataset of references to Image objects."}],"groups":[{"neurodata_type_def":"NWBContainer","neurodata_type_inc":"Container","doc":"An abstract data type for a generic container storing collections of data and metadata. Base type for all data and metadata containers."},{"neurodata_type_def":"NWBDataInterface","neurodata_type_inc":"NWBContainer","doc":"An abstract data type for a generic container storing collections of data, as opposed to metadata."},{"neurodata_type_def":"TimeSeries","neurodata_type_inc":"NWBDataInterface","doc":"General purpose time series.","attributes":[{"name":"description","dtype":"text","default_value":"no description","doc":"Description of the time series.","required":false},{"name":"comments","dtype":"text","default_value":"no comments","doc":"Human-readable comments about the TimeSeries. This second descriptive field can be used to store additional information, or descriptive information if the primary description field is populated with a computer-readable string.","required":false}],"datasets":[{"name":"data","dims":[["num_times"],["num_times","num_DIM2"],["num_times","num_DIM2","num_DIM3"],["num_times","num_DIM2","num_DIM3","num_DIM4"]],"shape":[[null],[null,null],[null,null,null],[null,null,null,null]],"doc":"Data values. Data can be in 1-D, 2-D, 3-D, or 4-D. The first dimension should always represent time. This can also be used to store binary data (e.g., image frames). This can also be a link to data stored in an external file.","attributes":[{"name":"conversion","dtype":"float32","default_value":1.0,"doc":"Scalar to multiply each element in data to convert it to the specified 'unit'. If the data are stored in acquisition system units or other units that require a conversion to be interpretable, multiply the data by 'conversion' to convert the data to the specified 'unit'. e.g. if the data acquisition system stores values in this object as signed 16-bit integers (int16 range -32,768 to 32,767) that correspond to a 5V range (-2.5V to 2.5V), and the data acquisition system gain is 8000X, then the 'conversion' multiplier to get from raw data acquisition values to recorded volts is 2.5/32768/8000 = 9.5367e-9.","required":false},{"name":"offset","dtype":"float32","default_value":0.0,"doc":"Scalar to add to the data after scaling by 'conversion' to finalize its coercion to the specified 'unit'. Two common examples of this include (a) data stored in an unsigned type that requires a shift after scaling to re-center the data, and (b) specialized recording devices that naturally cause a scalar offset with respect to the true units.","required":false},{"name":"resolution","dtype":"float32","default_value":-1.0,"doc":"Smallest meaningful difference between values in data, stored in the specified by unit, e.g., the change in value of the least significant bit, or a larger number if signal noise is known to be present. If unknown, use -1.0.","required":false},{"name":"unit","dtype":"text","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."},{"name":"continuity","dtype":"text","doc":"Optionally describe the continuity of the data. Can be \"continuous\", \"instantaneous\", or \"step\". For example, a voltage trace would be \"continuous\", because samples are recorded from a continuous process. An array of lick times would be \"instantaneous\", because the data represents distinct moments in time. Times of image presentations would be \"step\" because the picture remains the same until the next timepoint. This field is optional, but is useful in providing information about the underlying data. It may inform the way this data is interpreted, the way it is visualized, and what analysis methods are applicable.","required":false}]},{"name":"starting_time","dtype":"float64","doc":"Timestamp of the first sample in seconds. When timestamps are uniformly spaced, the timestamp of the first sample can be specified and all subsequent ones calculated from the sampling rate attribute.","quantity":"?","attributes":[{"name":"rate","dtype":"float32","doc":"Sampling rate, in Hz."},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for time, which is fixed to 'seconds'."}]},{"name":"timestamps","dtype":"float64","dims":["num_times"],"shape":[null],"doc":"Timestamps for samples stored in data, in seconds, relative to the common experiment master-clock stored in NWBFile.timestamps_reference_time.","quantity":"?","attributes":[{"name":"interval","dtype":"int32","value":1,"doc":"Value is '1'"},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for timestamps, which is fixed to 'seconds'."}]},{"name":"control","dtype":"uint8","dims":["num_times"],"shape":[null],"doc":"Numerical labels that apply to each time point in data for the purpose of querying and slicing data by these values. If present, the length of this array should be the same size as the first dimension of data.","quantity":"?"},{"name":"control_description","dtype":"text","dims":["num_control_values"],"shape":[null],"doc":"Description of each control value. Must be present if control is present. If present, control_description[0] should describe time points where control == 0.","quantity":"?"}],"groups":[{"name":"sync","doc":"Lab-specific time and sync information as provided directly from hardware devices and that is necessary for aligning all acquired time information to a common timebase. The timestamp array stores time in the common timebase. This group will usually only be populated in TimeSeries that are stored external to the NWB file, in files storing raw data. Once timestamp data is calculated, the contents of 'sync' are mostly for archival purposes.","quantity":"?"}]},{"neurodata_type_def":"ProcessingModule","neurodata_type_inc":"NWBContainer","doc":"A collection of processed data.","attributes":[{"name":"description","dtype":"text","doc":"Description of this collection of processed data."}],"groups":[{"neurodata_type_inc":"NWBDataInterface","doc":"Data objects stored in this collection.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tables stored in this collection.","quantity":"*"}]},{"neurodata_type_def":"Images","neurodata_type_inc":"NWBDataInterface","default_name":"Images","doc":"A collection of images with an optional way to specify the order of the images using the \"order_of_images\" dataset. An order must be specified if the images are referenced by index, e.g., from an IndexSeries.","attributes":[{"name":"description","dtype":"text","doc":"Description of this collection of images."}],"datasets":[{"neurodata_type_inc":"Image","doc":"Images stored in this collection.","quantity":"+"},{"name":"order_of_images","neurodata_type_inc":"ImageReferences","doc":"Ordered dataset of references to Image objects stored in the parent group. Each Image object in the Images group should be stored once and only once, so the dataset should have the same length as the number of images.","quantity":"?"}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.behavior.json b/Resources/spec/core/2.7.0/nwb.behavior.json deleted file mode 100644 index 1ecff4d..0000000 --- a/Resources/spec/core/2.7.0/nwb.behavior.json +++ /dev/null @@ -1 +0,0 @@ -{"groups":[{"neurodata_type_def":"SpatialSeries","neurodata_type_inc":"TimeSeries","doc":"Direction, e.g., of gaze or travel, or position. The TimeSeries::data field is a 2D array storing position or direction relative to some reference frame. Array structure: [num measurements] [num dimensions]. Each SpatialSeries has a text dataset reference_frame that indicates the zero-position, or the zero-axes for direction. For example, if representing gaze direction, 'straight-ahead' might be a specific pixel on the monitor, or some other point in space. For position data, the 0,0 point might be the top-left corner of an enclosure, as viewed from the tracking camera. The unit of data will indicate how to interpret SpatialSeries values.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","x"],["num_times","x,y"],["num_times","x,y,z"]],"shape":[[null],[null,1],[null,2],[null,3]],"doc":"1-D or 2-D array storing position or direction relative to some reference frame.","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Base unit of measurement for working with the data. The default value is 'meters'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'.","required":false}]},{"name":"reference_frame","dtype":"text","doc":"Description defining what exactly 'straight-ahead' means.","quantity":"?"}]},{"neurodata_type_def":"BehavioralEpochs","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralEpochs","doc":"TimeSeries for storing behavioral epochs. The objective of this and the other two Behavioral interfaces (e.g. BehavioralEvents and BehavioralTimeSeries) is to provide generic hooks for software tools/scripts. This allows a tool/script to take the output one specific interface (e.g., UnitTimes) and plot that data relative to another data modality (e.g., behavioral events) without having to define all possible modalities in advance. Declaring one of these interfaces means that one or more TimeSeries of the specified type is published. These TimeSeries should reside in a group having the same name as the interface. For example, if a BehavioralTimeSeries interface is declared, the module will have one or more TimeSeries defined in the module sub-group 'BehavioralTimeSeries'. BehavioralEpochs should use IntervalSeries. BehavioralEvents is used for irregular events. BehavioralTimeSeries is for continuous data.","groups":[{"neurodata_type_inc":"IntervalSeries","doc":"IntervalSeries object containing start and stop times of epochs.","quantity":"*"}]},{"neurodata_type_def":"BehavioralEvents","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralEvents","doc":"TimeSeries for storing behavioral events. See description of BehavioralEpochs for more details.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing behavioral events.","quantity":"*"}]},{"neurodata_type_def":"BehavioralTimeSeries","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralTimeSeries","doc":"TimeSeries for storing Behavoioral time series data. See description of BehavioralEpochs for more details.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing continuous behavioral data.","quantity":"*"}]},{"neurodata_type_def":"PupilTracking","neurodata_type_inc":"NWBDataInterface","default_name":"PupilTracking","doc":"Eye-tracking data, representing pupil size.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing time series data on pupil size.","quantity":"+"}]},{"neurodata_type_def":"EyeTracking","neurodata_type_inc":"NWBDataInterface","default_name":"EyeTracking","doc":"Eye-tracking data, representing direction of gaze.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing data measuring direction of gaze.","quantity":"*"}]},{"neurodata_type_def":"CompassDirection","neurodata_type_inc":"NWBDataInterface","default_name":"CompassDirection","doc":"With a CompassDirection interface, a module publishes a SpatialSeries object representing a floating point value for theta. The SpatialSeries::reference_frame field should indicate what direction corresponds to 0 and which is the direction of rotation (this should be clockwise). The si_unit for the SpatialSeries should be radians or degrees.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing direction of gaze travel.","quantity":"*"}]},{"neurodata_type_def":"Position","neurodata_type_inc":"NWBDataInterface","default_name":"Position","doc":"Position data, whether along the x, x/y or x/y/z axis.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing position data.","quantity":"+"}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.device.json b/Resources/spec/core/2.7.0/nwb.device.json deleted file mode 100644 index e0e1cfb..0000000 --- a/Resources/spec/core/2.7.0/nwb.device.json +++ /dev/null @@ -1 +0,0 @@ -{"groups":[{"neurodata_type_def":"Device","neurodata_type_inc":"NWBContainer","doc":"Metadata about a data acquisition device, e.g., recording system, electrode, microscope.","attributes":[{"name":"description","dtype":"text","doc":"Description of the device (e.g., model, firmware version, processing software version, etc.) as free-form text.","required":false},{"name":"manufacturer","dtype":"text","doc":"The name of the manufacturer of the device.","required":false}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.ecephys.json b/Resources/spec/core/2.7.0/nwb.ecephys.json deleted file mode 100644 index 999e4a6..0000000 --- a/Resources/spec/core/2.7.0/nwb.ecephys.json +++ /dev/null @@ -1 +0,0 @@ -{"groups":[{"neurodata_type_def":"ElectricalSeries","neurodata_type_inc":"TimeSeries","doc":"A time series of acquired voltage data from extracellular recordings. The data field is an int or float array storing data in volts. The first dimension should always represent time. The second dimension, if present, should represent channels.","attributes":[{"name":"filtering","dtype":"text","doc":"Filtering applied to all channels of the data. For example, if this ElectricalSeries represents high-pass-filtered data (also known as AP Band), then this value could be \"High-pass 4-pole Bessel filter at 500 Hz\". If this ElectricalSeries represents low-pass-filtered LFP data and the type of filter is unknown, then this value could be \"Low-pass filter at 300 Hz\". If a non-standard filter type is used, provide as much detail about the filter properties as possible.","required":false}],"datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_channels"],["num_times","num_channels","num_samples"]],"shape":[[null],[null,null],[null,null,null]],"doc":"Recorded voltage data.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. This value is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion', followed by 'channel_conversion' (if present), and then add 'offset'."}]},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the electrodes that this time series was generated from."},{"name":"channel_conversion","dtype":"float32","dims":["num_channels"],"shape":[null],"doc":"Channel-specific conversion factor. Multiply the data in the 'data' dataset by these values along the channel axis (as indicated by axis attribute) AND by the global conversion factor in the 'conversion' attribute of 'data' to get the data values in Volts, i.e, data in Volts = data * data.conversion * channel_conversion. This approach allows for both global and per-channel data conversion factors needed to support the storage of electrical recordings as native values generated by data acquisition systems. If this dataset is not present, then there is no channel-specific conversion factor, i.e. it is 1 for all channels.","quantity":"?","attributes":[{"name":"axis","dtype":"int32","value":1,"doc":"The zero-indexed axis of the 'data' dataset that the channel-specific conversion factor corresponds to. This value is fixed to 1."}]}]},{"neurodata_type_def":"SpikeEventSeries","neurodata_type_inc":"ElectricalSeries","doc":"Stores snapshots/snippets of recorded spike events (i.e., threshold crossings). This may also be raw data, as reported by ephys hardware. If so, the TimeSeries::description field should describe how events were detected. All SpikeEventSeries should reside in a module (under EventWaveform interface) even if the spikes were reported and stored by hardware. All events span the same recording channels and store snapshots of equal duration. TimeSeries::data array structure: [num events] [num channels] [num samples] (or [num events] [num samples] for single electrode).","datasets":[{"name":"data","dtype":"numeric","dims":[["num_events","num_samples"],["num_events","num_channels","num_samples"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveforms.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement for waveforms, which is fixed to 'volts'."}]},{"name":"timestamps","dtype":"float64","dims":["num_times"],"shape":[null],"doc":"Timestamps for samples stored in data, in seconds, relative to the common experiment master-clock stored in NWBFile.timestamps_reference_time. Timestamps are required for the events. Unlike for TimeSeries, timestamps are required for SpikeEventSeries and are thus re-specified here.","attributes":[{"name":"interval","dtype":"int32","value":1,"doc":"Value is '1'"},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for timestamps, which is fixed to 'seconds'."}]}]},{"neurodata_type_def":"FeatureExtraction","neurodata_type_inc":"NWBDataInterface","default_name":"FeatureExtraction","doc":"Features, such as PC1 and PC2, that are extracted from signals stored in a SpikeEventSeries or other source.","datasets":[{"name":"description","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Description of features (eg, ''PC1'') for each of the extracted features."},{"name":"features","dtype":"float32","dims":["num_events","num_channels","num_features"],"shape":[null,null,null],"doc":"Multi-dimensional array of features extracted from each event."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Times of events that features correspond to (can be a link)."},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the electrodes that this time series was generated from."}]},{"neurodata_type_def":"EventDetection","neurodata_type_inc":"NWBDataInterface","default_name":"EventDetection","doc":"Detected spike events from voltage trace(s).","datasets":[{"name":"detection_method","dtype":"text","doc":"Description of how events were detected, such as voltage threshold, or dV/dT threshold, as well as relevant values."},{"name":"source_idx","dtype":"int32","dims":["num_events"],"shape":[null],"doc":"Indices (zero-based) into source ElectricalSeries::data array corresponding to time of event. ''description'' should define what is meant by time of event (e.g., .25 ms before action potential peak, zero-crossing time, etc). The index points to each event from the raw data."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Timestamps of events, in seconds.","attributes":[{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for event times, which is fixed to 'seconds'."}]}],"links":[{"name":"source_electricalseries","target_type":"ElectricalSeries","doc":"Link to the ElectricalSeries that this data was calculated from. Metadata about electrodes and their position can be read from that ElectricalSeries so it's not necessary to include that information here."}]},{"neurodata_type_def":"EventWaveform","neurodata_type_inc":"NWBDataInterface","default_name":"EventWaveform","doc":"Represents either the waveforms of detected events, as extracted from a raw data trace in /acquisition, or the event waveforms that were stored during experiment acquisition.","groups":[{"neurodata_type_inc":"SpikeEventSeries","doc":"SpikeEventSeries object(s) containing detected spike event waveforms.","quantity":"*"}]},{"neurodata_type_def":"FilteredEphys","neurodata_type_inc":"NWBDataInterface","default_name":"FilteredEphys","doc":"Electrophysiology data from one or more channels that has been subjected to filtering. Examples of filtered data include Theta and Gamma (LFP has its own interface). FilteredEphys modules publish an ElectricalSeries for each filtered channel or set of channels. The name of each ElectricalSeries is arbitrary but should be informative. The source of the filtered data, whether this is from analysis of another time series or as acquired by hardware, should be noted in each's TimeSeries::description field. There is no assumed 1::1 correspondence between filtered ephys signals and electrodes, as a single signal can apply to many nearby electrodes, and one electrode may have different filtered (e.g., theta and/or gamma) signals represented. Filter properties should be noted in the ElectricalSeries 'filtering' attribute.","groups":[{"neurodata_type_inc":"ElectricalSeries","doc":"ElectricalSeries object(s) containing filtered electrophysiology data.","quantity":"+"}]},{"neurodata_type_def":"LFP","neurodata_type_inc":"NWBDataInterface","default_name":"LFP","doc":"LFP data from one or more channels. The electrode map in each published ElectricalSeries will identify which channels are providing LFP data. Filter properties should be noted in the ElectricalSeries 'filtering' attribute.","groups":[{"neurodata_type_inc":"ElectricalSeries","doc":"ElectricalSeries object(s) containing LFP data for one or more channels.","quantity":"+"}]},{"neurodata_type_def":"ElectrodeGroup","neurodata_type_inc":"NWBContainer","doc":"A physical grouping of electrodes, e.g. a shank of an array.","attributes":[{"name":"description","dtype":"text","doc":"Description of this electrode group."},{"name":"location","dtype":"text","doc":"Location of electrode group. Specify the area, layer, comments on estimation of area/layer, etc. Use standard atlas names for anatomical regions when possible."}],"datasets":[{"name":"position","dtype":[{"name":"x","dtype":"float32","doc":"x coordinate"},{"name":"y","dtype":"float32","doc":"y coordinate"},{"name":"z","dtype":"float32","doc":"z coordinate"}],"doc":"stereotaxic or common framework coordinates","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the device that was used to record from this electrode group."}]},{"neurodata_type_def":"ClusterWaveforms","neurodata_type_inc":"NWBDataInterface","default_name":"ClusterWaveforms","doc":"DEPRECATED The mean waveform shape, including standard deviation, of the different clusters. Ideally, the waveform analysis should be performed on data that is only high-pass filtered. This is a separate module because it is expected to require updating. For example, IMEC probes may require different storage requirements to store/display mean waveforms, requiring a new interface or an extension of this one.","datasets":[{"name":"waveform_filtering","dtype":"text","doc":"Filtering applied to data before generating mean/sd"},{"name":"waveform_mean","dtype":"float32","dims":["num_clusters","num_samples"],"shape":[null,null],"doc":"The mean waveform for each cluster, using the same indices for each wave as cluster numbers in the associated Clustering module (i.e, cluster 3 is in array slot [3]). Waveforms corresponding to gaps in cluster sequence should be empty (e.g., zero- filled)"},{"name":"waveform_sd","dtype":"float32","dims":["num_clusters","num_samples"],"shape":[null,null],"doc":"Stdev of waveforms for each cluster, using the same indices as in mean"}],"links":[{"name":"clustering_interface","target_type":"Clustering","doc":"Link to Clustering interface that was the source of the clustered data"}]},{"neurodata_type_def":"Clustering","neurodata_type_inc":"NWBDataInterface","default_name":"Clustering","doc":"DEPRECATED Clustered spike data, whether from automatic clustering tools (e.g., klustakwik) or as a result of manual sorting.","datasets":[{"name":"description","dtype":"text","doc":"Description of clusters or clustering, (e.g. cluster 0 is noise, clusters curated using Klusters, etc)"},{"name":"num","dtype":"int32","dims":["num_events"],"shape":[null],"doc":"Cluster number of each event"},{"name":"peak_over_rms","dtype":"float32","dims":["num_clusters"],"shape":[null],"doc":"Maximum ratio of waveform peak to RMS on any channel in the cluster (provides a basic clustering metric)."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Times of clustered events, in seconds. This may be a link to times field in associated FeatureExtraction module."}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.epoch.json b/Resources/spec/core/2.7.0/nwb.epoch.json deleted file mode 100644 index ed46470..0000000 --- a/Resources/spec/core/2.7.0/nwb.epoch.json +++ /dev/null @@ -1 +0,0 @@ -{"groups":[{"neurodata_type_def":"TimeIntervals","neurodata_type_inc":"DynamicTable","doc":"A container for aggregating epoch data and the TimeSeries that each epoch applies to.","datasets":[{"name":"start_time","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Start time of epoch, in seconds."},{"name":"stop_time","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Stop time of epoch, in seconds."},{"name":"tags","neurodata_type_inc":"VectorData","dtype":"text","doc":"User-defined tags that identify or categorize events.","quantity":"?"},{"name":"tags_index","neurodata_type_inc":"VectorIndex","doc":"Index for tags.","quantity":"?"},{"name":"timeseries","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"An index into a TimeSeries object.","quantity":"?"},{"name":"timeseries_index","neurodata_type_inc":"VectorIndex","doc":"Index for timeseries.","quantity":"?"}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.file.json b/Resources/spec/core/2.7.0/nwb.file.json deleted file mode 100644 index e029904..0000000 --- a/Resources/spec/core/2.7.0/nwb.file.json +++ /dev/null @@ -1 +0,0 @@ -{"groups":[{"neurodata_type_def":"NWBFile","neurodata_type_inc":"NWBContainer","name":"root","doc":"An NWB file storing cellular-based neurophysiology data from a single experimental session.","attributes":[{"name":"nwb_version","dtype":"text","value":"2.7.0-alpha","doc":"File version string. Use semantic versioning, e.g. 1.2.1. This will be the name of the format with trailing major, minor and patch numbers."}],"datasets":[{"name":"file_create_date","dtype":"isodatetime","dims":["num_modifications"],"shape":[null],"doc":"A record of the date the file was created and of subsequent modifications. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted strings: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds. The file can be created after the experiment was run, so this may differ from the experiment start time. Each modification to the nwb file adds a new entry to the array."},{"name":"identifier","dtype":"text","doc":"A unique text identifier for the file. For example, concatenated lab name, file creation date/time and experimentalist, or a hash of these and/or other values. The goal is that the string should be unique to all other files."},{"name":"session_description","dtype":"text","doc":"A description of the experimental session and data in the file."},{"name":"session_start_time","dtype":"isodatetime","doc":"Date and time of the experiment/session start. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted string: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds."},{"name":"timestamps_reference_time","dtype":"isodatetime","doc":"Date and time corresponding to time zero of all timestamps. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted string: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds. All times stored in the file use this time as reference (i.e., time zero)."}],"groups":[{"name":"acquisition","doc":"Data streams recorded from the system, including ephys, ophys, tracking, etc. This group should be read-only after the experiment is completed and timestamps are corrected to a common timebase. The data stored here may be links to raw data stored in external NWB files. This will allow keeping bulky raw data out of the file while preserving the option of keeping some/all in the file. Acquired data includes tracking and experimental data streams (i.e., everything measured from the system). If bulky data is stored in the /acquisition group, the data can exist in a separate NWB file that is linked to by the file being used for processing and analysis.","groups":[{"neurodata_type_inc":"NWBDataInterface","doc":"Acquired, raw data.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tabular data that is relevant to acquisition","quantity":"*"}]},{"name":"analysis","doc":"Lab-specific and custom scientific analysis of data. There is no defined format for the content of this group - the format is up to the individual user/lab. To facilitate sharing analysis data between labs, the contents here should be stored in standard types (e.g., neurodata_types) and appropriately documented. The file can store lab-specific and custom data analysis without restriction on its form or schema, reducing data formatting restrictions on end users. Such data should be placed in the analysis group. The analysis data should be documented so that it could be shared with other labs.","groups":[{"neurodata_type_inc":"NWBContainer","doc":"Custom analysis results.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tabular data that is relevant to data stored in analysis","quantity":"*"}]},{"name":"scratch","doc":"A place to store one-off analysis results. Data placed here is not intended for sharing. By placing data here, users acknowledge that there is no guarantee that their data meets any standard.","quantity":"?","groups":[{"neurodata_type_inc":"NWBContainer","doc":"Any one-off containers","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Any one-off tables","quantity":"*"}],"datasets":[{"neurodata_type_inc":"ScratchData","doc":"Any one-off datasets","quantity":"*"}]},{"name":"processing","doc":"The home for ProcessingModules. These modules perform intermediate analysis of data that is necessary to perform before scientific analysis. Examples include spike clustering, extracting position from tracking data, stitching together image slices. ProcessingModules can be large and express many data sets from relatively complex analysis (e.g., spike detection and clustering) or small, representing extraction of position information from tracking video, or even binary lick/no-lick decisions. Common software tools (e.g., klustakwik, MClust) are expected to read/write data here. 'Processing' refers to intermediate analysis of the acquired data to make it more amenable to scientific analysis.","groups":[{"neurodata_type_inc":"ProcessingModule","doc":"Intermediate analysis of acquired data.","quantity":"*"}]},{"name":"stimulus","doc":"Data pushed into the system (eg, video stimulus, sound, voltage, etc) and secondary representations of that data (eg, measurements of something used as a stimulus). This group should be made read-only after experiment complete and timestamps are corrected to common timebase. Stores both presented stimuli and stimulus templates, the latter in case the same stimulus is presented multiple times, or is pulled from an external stimulus library. Stimuli are here defined as any signal that is pushed into the system as part of the experiment (eg, sound, video, voltage, etc). Many different experiments can use the same stimuli, and stimuli can be re-used during an experiment. The stimulus group is organized so that one version of template stimuli can be stored and these be used multiple times. These templates can exist in the present file or can be linked to a remote library file.","groups":[{"name":"presentation","doc":"Stimuli presented during the experiment.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries objects containing data of presented stimuli.","quantity":"*"}]},{"name":"templates","doc":"Template stimuli. Timestamps in templates are based on stimulus design and are relative to the beginning of the stimulus. When templates are used, the stimulus instances must convert presentation times to the experiment`s time reference frame.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries objects containing template data of presented stimuli.","quantity":"*"},{"neurodata_type_inc":"Images","doc":"Images objects containing images of presented stimuli.","quantity":"*"}]}]},{"name":"general","doc":"Experimental metadata, including protocol, notes and description of hardware device(s). The metadata stored in this section should be used to describe the experiment. Metadata necessary for interpreting the data is stored with the data. General experimental metadata, including animal strain, experimental protocols, experimenter, devices, etc, are stored under 'general'. Core metadata (e.g., that required to interpret data fields) is stored with the data itself, and implicitly defined by the file specification (e.g., time is in seconds). The strategy used here for storing non-core metadata is to use free-form text fields, such as would appear in sentences or paragraphs from a Methods section. Metadata fields are text to enable them to be more general, for example to represent ranges instead of numerical values. Machine-readable metadata is stored as attributes to these free-form datasets. All entries in the below table are to be included when data is present. Unused groups (e.g., intracellular_ephys in an optophysiology experiment) should not be created unless there is data to store within them.","datasets":[{"name":"data_collection","dtype":"text","doc":"Notes about data collection and analysis.","quantity":"?"},{"name":"experiment_description","dtype":"text","doc":"General description of the experiment.","quantity":"?"},{"name":"experimenter","dtype":"text","doc":"Name of person(s) who performed the experiment. Can also specify roles of different people involved.","quantity":"?","dims":["num_experimenters"],"shape":[null]},{"name":"institution","dtype":"text","doc":"Institution(s) where experiment was performed.","quantity":"?"},{"name":"keywords","dtype":"text","dims":["num_keywords"],"shape":[null],"doc":"Terms to search over.","quantity":"?"},{"name":"lab","dtype":"text","doc":"Laboratory where experiment was performed.","quantity":"?"},{"name":"notes","dtype":"text","doc":"Notes about the experiment.","quantity":"?"},{"name":"pharmacology","dtype":"text","doc":"Description of drugs used, including how and when they were administered. Anesthesia(s), painkiller(s), etc., plus dosage, concentration, etc.","quantity":"?"},{"name":"protocol","dtype":"text","doc":"Experimental protocol, if applicable. e.g., include IACUC protocol number.","quantity":"?"},{"name":"related_publications","dtype":"text","doc":"Publication information. PMID, DOI, URL, etc.","dims":["num_publications"],"shape":[null],"quantity":"?"},{"name":"session_id","dtype":"text","doc":"Lab-specific ID for the session.","quantity":"?"},{"name":"slices","dtype":"text","doc":"Description of slices, including information about preparation thickness, orientation, temperature, and bath solution.","quantity":"?"},{"name":"source_script","dtype":"text","doc":"Script file or link to public source code used to create this NWB file.","quantity":"?","attributes":[{"name":"file_name","dtype":"text","doc":"Name of script file."}]},{"name":"stimulus","dtype":"text","doc":"Notes about stimuli, such as how and where they were presented.","quantity":"?"},{"name":"surgery","dtype":"text","doc":"Narrative description about surgery/surgeries, including date(s) and who performed surgery.","quantity":"?"},{"name":"virus","dtype":"text","doc":"Information about virus(es) used in experiments, including virus ID, source, date made, injection location, volume, etc.","quantity":"?"}],"groups":[{"neurodata_type_inc":"LabMetaData","doc":"Place-holder than can be extended so that lab-specific meta-data can be placed in /general.","quantity":"*"},{"name":"devices","doc":"Description of hardware devices used during experiment, e.g., monitors, ADC boards, microscopes, etc.","quantity":"?","groups":[{"neurodata_type_inc":"Device","doc":"Data acquisition devices.","quantity":"*"}]},{"name":"subject","neurodata_type_inc":"Subject","doc":"Information about the animal or person from which the data was measured.","quantity":"?"},{"name":"extracellular_ephys","doc":"Metadata related to extracellular electrophysiology.","quantity":"?","groups":[{"neurodata_type_inc":"ElectrodeGroup","doc":"Physical group of electrodes.","quantity":"*"},{"name":"electrodes","neurodata_type_inc":"DynamicTable","doc":"A table of all electrodes (i.e. channels) used for recording.","quantity":"?","datasets":[{"name":"x","neurodata_type_inc":"VectorData","dtype":"float32","doc":"x coordinate of the channel location in the brain (+x is posterior).","quantity":"?"},{"name":"y","neurodata_type_inc":"VectorData","dtype":"float32","doc":"y coordinate of the channel location in the brain (+y is inferior).","quantity":"?"},{"name":"z","neurodata_type_inc":"VectorData","dtype":"float32","doc":"z coordinate of the channel location in the brain (+z is right).","quantity":"?"},{"name":"imp","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Impedance of the channel, in ohms.","quantity":"?"},{"name":"location","neurodata_type_inc":"VectorData","dtype":"text","doc":"Location of the electrode (channel). Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."},{"name":"filtering","neurodata_type_inc":"VectorData","dtype":"text","doc":"Description of hardware filtering, including the filter name and frequency cutoffs.","quantity":"?"},{"name":"group","neurodata_type_inc":"VectorData","dtype":{"target_type":"ElectrodeGroup","reftype":"object"},"doc":"Reference to the ElectrodeGroup this electrode is a part of."},{"name":"group_name","neurodata_type_inc":"VectorData","dtype":"text","doc":"Name of the ElectrodeGroup this electrode is a part of."},{"name":"rel_x","neurodata_type_inc":"VectorData","dtype":"float32","doc":"x coordinate in electrode group","quantity":"?"},{"name":"rel_y","neurodata_type_inc":"VectorData","dtype":"float32","doc":"y coordinate in electrode group","quantity":"?"},{"name":"rel_z","neurodata_type_inc":"VectorData","dtype":"float32","doc":"z coordinate in electrode group","quantity":"?"},{"name":"reference","neurodata_type_inc":"VectorData","dtype":"text","doc":"Description of the reference electrode and/or reference scheme used for this electrode, e.g., \"stainless steel skull screw\" or \"online common average referencing\".","quantity":"?"}]}]},{"name":"intracellular_ephys","doc":"Metadata related to intracellular electrophysiology.","quantity":"?","datasets":[{"name":"filtering","dtype":"text","doc":"[DEPRECATED] Use IntracellularElectrode.filtering instead. Description of filtering used. Includes filtering type and parameters, frequency fall-off, etc. If this changes between TimeSeries, filter description should be stored as a text attribute for each TimeSeries.","quantity":"?"}],"groups":[{"neurodata_type_inc":"IntracellularElectrode","doc":"An intracellular electrode.","quantity":"*"},{"name":"sweep_table","neurodata_type_inc":"SweepTable","doc":"[DEPRECATED] Table used to group different PatchClampSeries. SweepTable is being replaced by IntracellularRecordingsTable and SimultaneousRecordingsTable tables. Additional SequentialRecordingsTable, RepetitionsTable and ExperimentalConditions tables provide enhanced support for experiment metadata.","quantity":"?"},{"name":"intracellular_recordings","neurodata_type_inc":"IntracellularRecordingsTable","doc":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording. Each row in the table represents a single recording consisting typically of a stimulus and a corresponding response. In some cases, however, only a stimulus or a response are recorded as as part of an experiment. In this case both, the stimulus and response will point to the same TimeSeries while the idx_start and count of the invalid column will be set to -1, thus, indicating that no values have been recorded for the stimulus or response, respectively. Note, a recording MUST contain at least a stimulus or a response. Typically the stimulus and response are PatchClampSeries. However, the use of AD/DA channels that are not associated to an electrode is also common in intracellular electrophysiology, in which case other TimeSeries may be used.","quantity":"?"},{"name":"simultaneous_recordings","neurodata_type_inc":"SimultaneousRecordingsTable","doc":"A table for grouping different intracellular recordings from the IntracellularRecordingsTable table together that were recorded simultaneously from different electrodes","quantity":"?"},{"name":"sequential_recordings","neurodata_type_inc":"SequentialRecordingsTable","doc":"A table for grouping different sequential recordings from the SimultaneousRecordingsTable table together. This is typically used to group together sequential recordings where the a sequence of stimuli of the same type with varying parameters have been presented in a sequence.","quantity":"?"},{"name":"repetitions","neurodata_type_inc":"RepetitionsTable","doc":"A table for grouping different sequential intracellular recordings together. With each SequentialRecording typically representing a particular type of stimulus, the RepetitionsTable table is typically used to group sets of stimuli applied in sequence.","quantity":"?"},{"name":"experimental_conditions","neurodata_type_inc":"ExperimentalConditionsTable","doc":"A table for grouping different intracellular recording repetitions together that belong to the same experimental experimental_conditions.","quantity":"?"}]},{"name":"optogenetics","doc":"Metadata describing optogenetic stimuluation.","quantity":"?","groups":[{"neurodata_type_inc":"OptogeneticStimulusSite","doc":"An optogenetic stimulation site.","quantity":"*"}]},{"name":"optophysiology","doc":"Metadata related to optophysiology.","quantity":"?","groups":[{"neurodata_type_inc":"ImagingPlane","doc":"An imaging plane.","quantity":"*"}]}]},{"name":"intervals","doc":"Experimental intervals, whether that be logically distinct sub-experiments having a particular scientific goal, trials (see trials subgroup) during an experiment, or epochs (see epochs subgroup) deriving from analysis of data.","quantity":"?","groups":[{"name":"epochs","neurodata_type_inc":"TimeIntervals","doc":"Divisions in time marking experimental stages or sub-divisions of a single recording session.","quantity":"?"},{"name":"trials","neurodata_type_inc":"TimeIntervals","doc":"Repeated experimental events that have a logical grouping.","quantity":"?"},{"name":"invalid_times","neurodata_type_inc":"TimeIntervals","doc":"Time intervals that should be removed from analysis.","quantity":"?"},{"neurodata_type_inc":"TimeIntervals","doc":"Optional additional table(s) for describing other experimental time intervals.","quantity":"*"}]},{"name":"units","neurodata_type_inc":"Units","doc":"Data about sorted spike units.","quantity":"?"}]},{"neurodata_type_def":"LabMetaData","neurodata_type_inc":"NWBContainer","doc":"Lab-specific meta-data."},{"neurodata_type_def":"Subject","neurodata_type_inc":"NWBContainer","doc":"Information about the animal or person from which the data was measured.","datasets":[{"name":"age","dtype":"text","doc":"Age of subject. Can be supplied instead of 'date_of_birth'.","quantity":"?","attributes":[{"name":"reference","doc":"Age is with reference to this event. Can be 'birth' or 'gestational'. If reference is omitted, 'birth' is implied.","dtype":"text","required":false,"default_value":"birth"}]},{"name":"date_of_birth","dtype":"isodatetime","doc":"Date of birth of subject. Can be supplied instead of 'age'.","quantity":"?"},{"name":"description","dtype":"text","doc":"Description of subject and where subject came from (e.g., breeder, if animal).","quantity":"?"},{"name":"genotype","dtype":"text","doc":"Genetic strain. If absent, assume Wild Type (WT).","quantity":"?"},{"name":"sex","dtype":"text","doc":"Gender of subject.","quantity":"?"},{"name":"species","dtype":"text","doc":"Species of subject.","quantity":"?"},{"name":"strain","dtype":"text","doc":"Strain of subject.","quantity":"?"},{"name":"subject_id","dtype":"text","doc":"ID of animal/person used/participating in experiment (lab convention).","quantity":"?"},{"name":"weight","dtype":"text","doc":"Weight at time of experiment, at time of surgery and at other important times.","quantity":"?"}]}],"datasets":[{"neurodata_type_def":"ScratchData","neurodata_type_inc":"NWBData","doc":"Any one-off datasets","attributes":[{"name":"notes","doc":"Any notes the user has about the dataset being stored","dtype":"text"}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.icephys.json b/Resources/spec/core/2.7.0/nwb.icephys.json deleted file mode 100644 index 0aa9188..0000000 --- a/Resources/spec/core/2.7.0/nwb.icephys.json +++ /dev/null @@ -1 +0,0 @@ -{"groups":[{"neurodata_type_def":"PatchClampSeries","neurodata_type_inc":"TimeSeries","doc":"An abstract base class for patch-clamp data - stimulus or response, current or voltage.","attributes":[{"name":"stimulus_description","dtype":"text","doc":"Protocol/stimulus name for this patch-clamp dataset."},{"name":"sweep_number","dtype":"uint32","doc":"Sweep number, allows to group different PatchClampSeries together.","required":false}],"datasets":[{"name":"data","dtype":"numeric","dims":["num_times"],"shape":[null],"doc":"Recorded voltage or current.","attributes":[{"name":"unit","dtype":"text","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"gain","dtype":"float32","doc":"Gain of the recording, in units Volt/Amp (v-clamp) or Volt/Volt (c-clamp).","quantity":"?"}],"links":[{"name":"electrode","target_type":"IntracellularElectrode","doc":"Link to IntracellularElectrode object that describes the electrode that was used to apply or record this data."}]},{"neurodata_type_def":"CurrentClampSeries","neurodata_type_inc":"PatchClampSeries","doc":"Voltage data from an intracellular current-clamp recording. A corresponding CurrentClampStimulusSeries (stored separately as a stimulus) is used to store the current injected.","datasets":[{"name":"data","doc":"Recorded voltage.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. which is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"bias_current","dtype":"float32","doc":"Bias current, in amps.","quantity":"?"},{"name":"bridge_balance","dtype":"float32","doc":"Bridge balance, in ohms.","quantity":"?"},{"name":"capacitance_compensation","dtype":"float32","doc":"Capacitance compensation, in farads.","quantity":"?"}]},{"neurodata_type_def":"IZeroClampSeries","neurodata_type_inc":"CurrentClampSeries","doc":"Voltage data from an intracellular recording when all current and amplifier settings are off (i.e., CurrentClampSeries fields will be zero). There is no CurrentClampStimulusSeries associated with an IZero series because the amplifier is disconnected and no stimulus can reach the cell.","attributes":[{"name":"stimulus_description","dtype":"text","doc":"An IZeroClampSeries has no stimulus, so this attribute is automatically set to \"N/A\"","value":"N/A"}],"datasets":[{"name":"bias_current","dtype":"float32","value":0.0,"doc":"Bias current, in amps, fixed to 0.0."},{"name":"bridge_balance","dtype":"float32","value":0.0,"doc":"Bridge balance, in ohms, fixed to 0.0."},{"name":"capacitance_compensation","dtype":"float32","value":0.0,"doc":"Capacitance compensation, in farads, fixed to 0.0."}]},{"neurodata_type_def":"CurrentClampStimulusSeries","neurodata_type_inc":"PatchClampSeries","doc":"Stimulus current applied during current clamp recording.","datasets":[{"name":"data","doc":"Stimulus current applied.","attributes":[{"name":"unit","dtype":"text","value":"amperes","doc":"Base unit of measurement for working with the data. which is fixed to 'amperes'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]}]},{"neurodata_type_def":"VoltageClampSeries","neurodata_type_inc":"PatchClampSeries","doc":"Current data from an intracellular voltage-clamp recording. A corresponding VoltageClampStimulusSeries (stored separately as a stimulus) is used to store the voltage injected.","datasets":[{"name":"data","doc":"Recorded current.","attributes":[{"name":"unit","dtype":"text","value":"amperes","doc":"Base unit of measurement for working with the data. which is fixed to 'amperes'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"capacitance_fast","dtype":"float32","doc":"Fast capacitance, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for capacitance_fast, which is fixed to 'farads'."}]},{"name":"capacitance_slow","dtype":"float32","doc":"Slow capacitance, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for capacitance_fast, which is fixed to 'farads'."}]},{"name":"resistance_comp_bandwidth","dtype":"float32","doc":"Resistance compensation bandwidth, in hertz.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"hertz","doc":"Unit of measurement for resistance_comp_bandwidth, which is fixed to 'hertz'."}]},{"name":"resistance_comp_correction","dtype":"float32","doc":"Resistance compensation correction, in percent.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"percent","doc":"Unit of measurement for resistance_comp_correction, which is fixed to 'percent'."}]},{"name":"resistance_comp_prediction","dtype":"float32","doc":"Resistance compensation prediction, in percent.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"percent","doc":"Unit of measurement for resistance_comp_prediction, which is fixed to 'percent'."}]},{"name":"whole_cell_capacitance_comp","dtype":"float32","doc":"Whole cell capacitance compensation, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for whole_cell_capacitance_comp, which is fixed to 'farads'."}]},{"name":"whole_cell_series_resistance_comp","dtype":"float32","doc":"Whole cell series resistance compensation, in ohms.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"ohms","doc":"Unit of measurement for whole_cell_series_resistance_comp, which is fixed to 'ohms'."}]}]},{"neurodata_type_def":"VoltageClampStimulusSeries","neurodata_type_inc":"PatchClampSeries","doc":"Stimulus voltage applied during a voltage clamp recording.","datasets":[{"name":"data","doc":"Stimulus voltage applied.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. which is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]}]},{"neurodata_type_def":"IntracellularElectrode","neurodata_type_inc":"NWBContainer","doc":"An intracellular electrode and its metadata.","datasets":[{"name":"cell_id","dtype":"text","doc":"unique ID of the cell","quantity":"?"},{"name":"description","dtype":"text","doc":"Description of electrode (e.g., whole-cell, sharp, etc.)."},{"name":"filtering","dtype":"text","doc":"Electrode specific filtering.","quantity":"?"},{"name":"initial_access_resistance","dtype":"text","doc":"Initial access resistance.","quantity":"?"},{"name":"location","dtype":"text","doc":"Location of the electrode. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible.","quantity":"?"},{"name":"resistance","dtype":"text","doc":"Electrode resistance, in ohms.","quantity":"?"},{"name":"seal","dtype":"text","doc":"Information about seal used for recording.","quantity":"?"},{"name":"slice","dtype":"text","doc":"Information about slice used for recording.","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Device that was used to record from this electrode."}]},{"neurodata_type_def":"SweepTable","neurodata_type_inc":"DynamicTable","doc":"[DEPRECATED] Table used to group different PatchClampSeries. SweepTable is being replaced by IntracellularRecordingsTable and SimultaneousRecordingsTable tables. Additional SequentialRecordingsTable, RepetitionsTable, and ExperimentalConditions tables provide enhanced support for experiment metadata.","datasets":[{"name":"sweep_number","neurodata_type_inc":"VectorData","dtype":"uint32","doc":"Sweep number of the PatchClampSeries in that row."},{"name":"series","neurodata_type_inc":"VectorData","dtype":{"target_type":"PatchClampSeries","reftype":"object"},"doc":"The PatchClampSeries with the sweep number in that row."},{"name":"series_index","neurodata_type_inc":"VectorIndex","doc":"Index for series."}]},{"neurodata_type_def":"IntracellularElectrodesTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular electrode related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular electrode related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"electrode","neurodata_type_inc":"VectorData","dtype":{"target_type":"IntracellularElectrode","reftype":"object"},"doc":"Column for storing the reference to the intracellular electrode."}]},{"neurodata_type_def":"IntracellularStimuliTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular stimulus related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular stimulus related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"stimulus","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the recorded stimulus for the recording (rows)."},{"name":"stimulus_template","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the stimulus template for the recording (rows).","quantity":"?"}]},{"neurodata_type_def":"IntracellularResponsesTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular response related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular response related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"response","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the recorded response for the recording (rows)"}]},{"neurodata_type_def":"IntracellularRecordingsTable","neurodata_type_inc":"AlignedDynamicTable","name":"intracellular_recordings","doc":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording. Each row in the table represents a single recording consisting typically of a stimulus and a corresponding response. In some cases, however, only a stimulus or a response is recorded as part of an experiment. In this case, both the stimulus and response will point to the same TimeSeries while the idx_start and count of the invalid column will be set to -1, thus, indicating that no values have been recorded for the stimulus or response, respectively. Note, a recording MUST contain at least a stimulus or a response. Typically the stimulus and response are PatchClampSeries. However, the use of AD/DA channels that are not associated to an electrode is also common in intracellular electrophysiology, in which case other TimeSeries may be used.","attributes":[{"name":"description","dtype":"text","value":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording and for storing metadata about the intracellular recording.","doc":"Description of the contents of this table. Inherited from AlignedDynamicTable and overwritten here to fix the value of the attribute."}],"groups":[{"name":"electrodes","neurodata_type_inc":"IntracellularElectrodesTable","doc":"Table for storing intracellular electrode related metadata."},{"name":"stimuli","neurodata_type_inc":"IntracellularStimuliTable","doc":"Table for storing intracellular stimulus related metadata."},{"name":"responses","neurodata_type_inc":"IntracellularResponsesTable","doc":"Table for storing intracellular response related metadata."}]},{"neurodata_type_def":"SimultaneousRecordingsTable","neurodata_type_inc":"DynamicTable","name":"simultaneous_recordings","doc":"A table for grouping different intracellular recordings from the IntracellularRecordingsTable table together that were recorded simultaneously from different electrodes.","datasets":[{"name":"recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the IntracellularRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"IntracellularRecordingsTable","reftype":"object"},"doc":"Reference to the IntracellularRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the recordings column."}]},{"neurodata_type_def":"SequentialRecordingsTable","neurodata_type_inc":"DynamicTable","name":"sequential_recordings","doc":"A table for grouping different sequential recordings from the SimultaneousRecordingsTable table together. This is typically used to group together sequential recordings where a sequence of stimuli of the same type with varying parameters have been presented in a sequence.","datasets":[{"name":"simultaneous_recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the SimultaneousRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"SimultaneousRecordingsTable","reftype":"object"},"doc":"Reference to the SimultaneousRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"simultaneous_recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the simultaneous_recordings column."},{"name":"stimulus_type","neurodata_type_inc":"VectorData","dtype":"text","doc":"The type of stimulus used for the sequential recording."}]},{"neurodata_type_def":"RepetitionsTable","neurodata_type_inc":"DynamicTable","name":"repetitions","doc":"A table for grouping different sequential intracellular recordings together. With each SequentialRecording typically representing a particular type of stimulus, the RepetitionsTable table is typically used to group sets of stimuli applied in sequence.","datasets":[{"name":"sequential_recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the SequentialRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"SequentialRecordingsTable","reftype":"object"},"doc":"Reference to the SequentialRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"sequential_recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the sequential_recordings column."}]},{"neurodata_type_def":"ExperimentalConditionsTable","neurodata_type_inc":"DynamicTable","name":"experimental_conditions","doc":"A table for grouping different intracellular recording repetitions together that belong to the same experimental condition.","datasets":[{"name":"repetitions","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the RepetitionsTable table.","attributes":[{"name":"table","dtype":{"target_type":"RepetitionsTable","reftype":"object"},"doc":"Reference to the RepetitionsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"repetitions_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the repetitions column."}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.image.json b/Resources/spec/core/2.7.0/nwb.image.json deleted file mode 100644 index 78cd4fd..0000000 --- a/Resources/spec/core/2.7.0/nwb.image.json +++ /dev/null @@ -1 +0,0 @@ -{"datasets":[{"neurodata_type_def":"GrayscaleImage","neurodata_type_inc":"Image","dims":["x","y"],"shape":[null,null],"doc":"A grayscale image.","dtype":"numeric"},{"neurodata_type_def":"RGBImage","neurodata_type_inc":"Image","dims":["x","y","r, g, b"],"shape":[null,null,3],"doc":"A color image.","dtype":"numeric"},{"neurodata_type_def":"RGBAImage","neurodata_type_inc":"Image","dims":["x","y","r, g, b, a"],"shape":[null,null,4],"doc":"A color image with transparency.","dtype":"numeric"}],"groups":[{"neurodata_type_def":"ImageSeries","neurodata_type_inc":"TimeSeries","doc":"General image data that is common between acquisition and stimulus time series. Sometimes the image data is stored in the file in a raw format while other times it will be stored as a series of external image files in the host file system. The data field will either be binary data, if the data is stored in the NWB file, or empty, if the data is stored in an external image stack. [frame][x][y] or [frame][x][y][z].","datasets":[{"name":"data","dtype":"numeric","dims":[["frame","x","y"],["frame","x","y","z"]],"shape":[[null,null,null],[null,null,null,null]],"doc":"Binary data representing images across frames. If data are stored in an external file, this should be an empty 3D array."},{"name":"dimension","dtype":"int32","dims":["rank"],"shape":[null],"doc":"Number of pixels on x, y, (and z) axes.","quantity":"?"},{"name":"external_file","dtype":"text","dims":["num_files"],"shape":[null],"doc":"Paths to one or more external file(s). The field is only present if format='external'. This is only relevant if the image series is stored in the file system as one or more image file(s). This field should NOT be used if the image is stored in another NWB file and that file is linked to this file.","quantity":"?","attributes":[{"name":"starting_frame","dtype":"int32","dims":["num_files"],"shape":[null],"doc":"Each external image may contain one or more consecutive frames of the full ImageSeries. This attribute serves as an index to indicate which frames each file contains, to facilitate random access. The 'starting_frame' attribute, hence, contains a list of frame numbers within the full ImageSeries of the first frame of each file listed in the parent 'external_file' dataset. Zero-based indexing is used (hence, the first element will always be zero). For example, if the 'external_file' dataset has three paths to files and the first file has 5 frames, the second file has 10 frames, and the third file has 20 frames, then this attribute will have values [0, 5, 15]. If there is a single external file that holds all of the frames of the ImageSeries (and so there is a single element in the 'external_file' dataset), then this attribute should have value [0]."}]},{"name":"format","dtype":"text","default_value":"raw","doc":"Format of image. If this is 'external', then the attribute 'external_file' contains the path information to the image files. If this is 'raw', then the raw (single-channel) binary data is stored in the 'data' dataset. If this attribute is not present, then the default format='raw' case is assumed.","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the Device object that was used to capture these images.","quantity":"?"}]},{"neurodata_type_def":"ImageMaskSeries","neurodata_type_inc":"ImageSeries","doc":"An alpha mask that is applied to a presented visual stimulus. The 'data' array contains an array of mask values that are applied to the displayed image. Mask values are stored as RGBA. Mask can vary with time. The timestamps array indicates the starting time of a mask, and that mask pattern continues until it's explicitly changed.","links":[{"name":"masked_imageseries","target_type":"ImageSeries","doc":"Link to ImageSeries object that this image mask is applied to."}]},{"neurodata_type_def":"OpticalSeries","neurodata_type_inc":"ImageSeries","doc":"Image data that is presented or recorded. A stimulus template movie will be stored only as an image. When the image is presented as stimulus, additional data is required, such as field of view (e.g., how much of the visual field the image covers, or how what is the area of the target being imaged). If the OpticalSeries represents acquired imaging data, orientation is also important.","datasets":[{"name":"distance","dtype":"float32","doc":"Distance from camera/monitor to target/eye.","quantity":"?"},{"name":"field_of_view","dtype":"float32","dims":[["width, height"],["width, height, depth"]],"shape":[[2],[3]],"doc":"Width, height and depth of image, or imaged area, in meters.","quantity":"?"},{"name":"data","dtype":"numeric","dims":[["frame","x","y"],["frame","x","y","r, g, b"]],"shape":[[null,null,null],[null,null,null,3]],"doc":"Images presented to subject, either grayscale or RGB"},{"name":"orientation","dtype":"text","doc":"Description of image relative to some reference frame (e.g., which way is up). Must also specify frame of reference.","quantity":"?"}]},{"neurodata_type_def":"IndexSeries","neurodata_type_inc":"TimeSeries","doc":"Stores indices to image frames stored in an ImageSeries. The purpose of the IndexSeries is to allow a static image stack to be stored in an Images object, and the images in the stack to be referenced out-of-order. This can be for the display of individual images, or of movie segments (as a movie is simply a series of images). The data field stores the index of the frame in the referenced Images object, and the timestamps array indicates when that image was displayed.","datasets":[{"name":"data","dtype":"uint32","dims":["num_times"],"shape":[null],"doc":"Index of the image (using zero-indexing) in the linked Images object.","attributes":[{"name":"conversion","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"resolution","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"offset","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"unit","dtype":"text","value":"N/A","doc":"This field is unused by IndexSeries and has the value N/A."}]}],"links":[{"name":"indexed_timeseries","target_type":"ImageSeries","doc":"Link to ImageSeries object containing images that are indexed. Use of this link is discouraged and will be deprecated. Link to an Images type instead.","quantity":"?"},{"name":"indexed_images","target_type":"Images","doc":"Link to Images object containing an ordered set of images that are indexed. The Images object must contain a 'ordered_images' dataset specifying the order of the images in the Images type.","quantity":"?"}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.misc.json b/Resources/spec/core/2.7.0/nwb.misc.json deleted file mode 100644 index 5f84a34..0000000 --- a/Resources/spec/core/2.7.0/nwb.misc.json +++ /dev/null @@ -1 +0,0 @@ -{"groups":[{"neurodata_type_def":"AbstractFeatureSeries","neurodata_type_inc":"TimeSeries","doc":"Abstract features, such as quantitative descriptions of sensory stimuli. The TimeSeries::data field is a 2D array, storing those features (e.g., for visual grating stimulus this might be orientation, spatial frequency and contrast). Null stimuli (eg, uniform gray) can be marked as being an independent feature (eg, 1.0 for gray, 0.0 for actual stimulus) or by storing NaNs for feature values, or through use of the TimeSeries::control fields. A set of features is considered to persist until the next set of features is defined. The final set of features stored should be the null set. This is useful when storing the raw stimulus is impractical.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_features"]],"shape":[[null],[null,null]],"doc":"Values of each feature at each time.","attributes":[{"name":"unit","dtype":"text","default_value":"see 'feature_units'","doc":"Since there can be different units for different features, store the units in 'feature_units'. The default value for this attribute is \"see 'feature_units'\".","required":false}]},{"name":"feature_units","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Units of each feature.","quantity":"?"},{"name":"features","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Description of the features represented in TimeSeries::data."}]},{"neurodata_type_def":"AnnotationSeries","neurodata_type_inc":"TimeSeries","doc":"Stores user annotations made during an experiment. The data[] field stores a text array, and timestamps are stored for each annotation (ie, interval=1). This is largely an alias to a standard TimeSeries storing a text array but that is identifiable as storing annotations in a machine-readable way.","datasets":[{"name":"data","dtype":"text","dims":["num_times"],"shape":[null],"doc":"Annotations made during an experiment.","attributes":[{"name":"resolution","dtype":"float32","value":-1.0,"doc":"Smallest meaningful difference between values in data. Annotations have no units, so the value is fixed to -1.0."},{"name":"unit","dtype":"text","value":"n/a","doc":"Base unit of measurement for working with the data. Annotations have no units, so the value is fixed to 'n/a'."}]}]},{"neurodata_type_def":"IntervalSeries","neurodata_type_inc":"TimeSeries","doc":"Stores intervals of data. The timestamps field stores the beginning and end of intervals. The data field stores whether the interval just started (>0 value) or ended (<0 value). Different interval types can be represented in the same series by using multiple key values (eg, 1 for feature A, 2 for feature B, 3 for feature C, etc). The field data stores an 8-bit integer. This is largely an alias of a standard TimeSeries but that is identifiable as representing time intervals in a machine-readable way.","datasets":[{"name":"data","dtype":"int8","dims":["num_times"],"shape":[null],"doc":"Use values >0 if interval started, <0 if interval ended.","attributes":[{"name":"resolution","dtype":"float32","value":-1.0,"doc":"Smallest meaningful difference between values in data. Annotations have no units, so the value is fixed to -1.0."},{"name":"unit","dtype":"text","value":"n/a","doc":"Base unit of measurement for working with the data. Annotations have no units, so the value is fixed to 'n/a'."}]}]},{"neurodata_type_def":"DecompositionSeries","neurodata_type_inc":"TimeSeries","doc":"Spectral analysis of a time series, e.g. of an LFP or a speech signal.","datasets":[{"name":"data","dtype":"numeric","dims":["num_times","num_channels","num_bands"],"shape":[null,null,null],"doc":"Data decomposed into frequency bands.","attributes":[{"name":"unit","dtype":"text","default_value":"no unit","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion'."}]},{"name":"metric","dtype":"text","doc":"The metric used, e.g. phase, amplitude, power."},{"name":"source_channels","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the channels that this decomposition series was generated from.","quantity":"?"}],"groups":[{"name":"bands","neurodata_type_inc":"DynamicTable","doc":"Table for describing the bands that this series was generated from. There should be one row in this table for each band.","datasets":[{"name":"band_name","neurodata_type_inc":"VectorData","dtype":"text","doc":"Name of the band, e.g. theta."},{"name":"band_limits","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands","low, high"],"shape":[null,2],"doc":"Low and high limit of each band in Hz. If it is a Gaussian filter, use 2 SD on either side of the center."},{"name":"band_mean","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands"],"shape":[null],"doc":"The mean Gaussian filters, in Hz."},{"name":"band_stdev","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands"],"shape":[null],"doc":"The standard deviation of Gaussian filters, in Hz."}]}],"links":[{"name":"source_timeseries","target_type":"TimeSeries","doc":"Link to TimeSeries object that this data was calculated from. Metadata about electrodes and their position can be read from that ElectricalSeries so it is not necessary to store that information here.","quantity":"?"}]},{"neurodata_type_def":"Units","neurodata_type_inc":"DynamicTable","default_name":"Units","doc":"Data about spiking units. Event times of observed units (e.g. cell, synapse, etc.) should be concatenated and stored in spike_times.","datasets":[{"name":"spike_times_index","neurodata_type_inc":"VectorIndex","doc":"Index into the spike_times dataset.","quantity":"?"},{"name":"spike_times","neurodata_type_inc":"VectorData","dtype":"float64","doc":"Spike times for each unit in seconds.","quantity":"?","attributes":[{"name":"resolution","dtype":"float64","doc":"The smallest possible difference between two spike times. Usually 1 divided by the acquisition sampling rate from which spike times were extracted, but could be larger if the acquisition time series was downsampled or smaller if the acquisition time series was smoothed/interpolated and it is possible for the spike time to be between samples.","required":false}]},{"name":"obs_intervals_index","neurodata_type_inc":"VectorIndex","doc":"Index into the obs_intervals dataset.","quantity":"?"},{"name":"obs_intervals","neurodata_type_inc":"VectorData","dtype":"float64","dims":["num_intervals","start|end"],"shape":[null,2],"doc":"Observation intervals for each unit.","quantity":"?"},{"name":"electrodes_index","neurodata_type_inc":"VectorIndex","doc":"Index into electrodes.","quantity":"?"},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"Electrode that each spike unit came from, specified using a DynamicTableRegion.","quantity":"?"},{"name":"electrode_group","neurodata_type_inc":"VectorData","dtype":{"target_type":"ElectrodeGroup","reftype":"object"},"doc":"Electrode group that each spike unit came from.","quantity":"?"},{"name":"waveform_mean","neurodata_type_inc":"VectorData","dtype":"float32","dims":[["num_units","num_samples"],["num_units","num_samples","num_electrodes"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveform mean for each spike unit.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveform_sd","neurodata_type_inc":"VectorData","dtype":"float32","dims":[["num_units","num_samples"],["num_units","num_samples","num_electrodes"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveform standard deviation for each spike unit.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveforms","neurodata_type_inc":"VectorData","dtype":"numeric","dims":["num_waveforms","num_samples"],"shape":[null,null],"doc":"Individual waveforms for each spike on each electrode. This is a doubly indexed column. The 'waveforms_index' column indexes which waveforms in this column belong to the same spike event for a given unit, where each waveform was recorded from a different electrode. The 'waveforms_index_index' column indexes the 'waveforms_index' column to indicate which spike events belong to a given unit. For example, if the 'waveforms_index_index' column has values [2, 5, 6], then the first 2 elements of the 'waveforms_index' column correspond to the 2 spike events of the first unit, the next 3 elements of the 'waveforms_index' column correspond to the 3 spike events of the second unit, and the next 1 element of the 'waveforms_index' column corresponds to the 1 spike event of the third unit. If the 'waveforms_index' column has values [3, 6, 8, 10, 12, 13], then the first 3 elements of the 'waveforms' column contain the 3 spike waveforms that were recorded from 3 different electrodes for the first spike time of the first unit. See https://nwb-schema.readthedocs.io/en/stable/format_description.html#doubly-ragged-arrays for a graphical representation of this example. When there is only one electrode for each unit (i.e., each spike time is associated with a single waveform), then the 'waveforms_index' column will have values 1, 2, ..., N, where N is the number of spike events. The number of electrodes for each spike event should be the same within a given unit. The 'electrodes' column should be used to indicate which electrodes are associated with each unit, and the order of the waveforms within a given unit x spike event should be in the same order as the electrodes referenced in the 'electrodes' column of this table. The number of samples for each waveform must be the same.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveforms_index","neurodata_type_inc":"VectorIndex","doc":"Index into the waveforms dataset. One value for every spike event. See 'waveforms' for more detail.","quantity":"?"},{"name":"waveforms_index_index","neurodata_type_inc":"VectorIndex","doc":"Index into the waveforms_index dataset. One value for every unit (row in the table). See 'waveforms' for more detail.","quantity":"?"}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.namespace.json b/Resources/spec/core/2.7.0/nwb.namespace.json deleted file mode 100644 index 1ebae4b..0000000 --- a/Resources/spec/core/2.7.0/nwb.namespace.json +++ /dev/null @@ -1 +0,0 @@ -{"namespaces":[{"name":"core","doc":"NWB namespace","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter","Keith Godfrey","Jeff Teeters"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov","keithg@alleninstitute.org","jteeters@berkeley.edu"],"full_name":"NWB core","schema":[{"namespace":"hdmf-common"},{"source":"nwb.base"},{"source":"nwb.device"},{"source":"nwb.epoch"},{"source":"nwb.image"},{"source":"nwb.file"},{"source":"nwb.misc"},{"source":"nwb.behavior"},{"source":"nwb.ecephys"},{"source":"nwb.icephys"},{"source":"nwb.ogen"},{"source":"nwb.ophys"},{"source":"nwb.retinotopy"}],"version":"2.7.0"}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.ogen.json b/Resources/spec/core/2.7.0/nwb.ogen.json deleted file mode 100644 index 6133dba..0000000 --- a/Resources/spec/core/2.7.0/nwb.ogen.json +++ /dev/null @@ -1 +0,0 @@ -{"groups":[{"neurodata_type_def":"OptogeneticSeries","neurodata_type_inc":"TimeSeries","doc":"An optogenetic stimulus.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_rois"]],"shape":[[null],[null,null]],"doc":"Applied power for optogenetic stimulus, in watts. Shape can be 1D or 2D. 2D data is meant to be used in an extension of OptogeneticSeries that defines what the second dimension represents.","attributes":[{"name":"unit","dtype":"text","value":"watts","doc":"Unit of measurement for data, which is fixed to 'watts'."}]}],"links":[{"name":"site","target_type":"OptogeneticStimulusSite","doc":"Link to OptogeneticStimulusSite object that describes the site to which this stimulus was applied."}]},{"neurodata_type_def":"OptogeneticStimulusSite","neurodata_type_inc":"NWBContainer","doc":"A site of optogenetic stimulation.","datasets":[{"name":"description","dtype":"text","doc":"Description of stimulation site."},{"name":"excitation_lambda","dtype":"float32","doc":"Excitation wavelength, in nm."},{"name":"location","dtype":"text","doc":"Location of the stimulation site. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."}],"links":[{"name":"device","target_type":"Device","doc":"Device that generated the stimulus."}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.ophys.json b/Resources/spec/core/2.7.0/nwb.ophys.json deleted file mode 100644 index 026c31e..0000000 --- a/Resources/spec/core/2.7.0/nwb.ophys.json +++ /dev/null @@ -1 +0,0 @@ -{"groups":[{"neurodata_type_def":"OnePhotonSeries","neurodata_type_inc":"ImageSeries","doc":"Image stack recorded over time from 1-photon microscope.","attributes":[{"name":"pmt_gain","dtype":"float32","doc":"Photomultiplier gain.","required":false},{"name":"scan_line_rate","dtype":"float32","doc":"Lines imaged per second. This is also stored in /general/optophysiology but is kept here as it is useful information for analysis, and so good to be stored w/ the actual data.","required":false},{"name":"exposure_time","dtype":"float32","doc":"Exposure time of the sample; often the inverse of the frequency.","required":false},{"name":"binning","dtype":"uint8","doc":"Amount of pixels combined into 'bins'; could be 1, 2, 4, 8, etc.","required":false},{"name":"power","dtype":"float32","doc":"Power of the excitation in mW, if known.","required":false},{"name":"intensity","dtype":"float32","doc":"Intensity of the excitation in mW/mm^2, if known.","required":false}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this TimeSeries data was generated."}]},{"neurodata_type_def":"TwoPhotonSeries","neurodata_type_inc":"ImageSeries","doc":"Image stack recorded over time from 2-photon microscope.","attributes":[{"name":"pmt_gain","dtype":"float32","doc":"Photomultiplier gain.","required":false},{"name":"scan_line_rate","dtype":"float32","doc":"Lines imaged per second. This is also stored in /general/optophysiology but is kept here as it is useful information for analysis, and so good to be stored w/ the actual data.","required":false}],"datasets":[{"name":"field_of_view","dtype":"float32","dims":[["width|height"],["width|height|depth"]],"shape":[[2],[3]],"doc":"Width, height and depth of image, or imaged area, in meters.","quantity":"?"}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this TimeSeries data was generated."}]},{"neurodata_type_def":"RoiResponseSeries","neurodata_type_inc":"TimeSeries","doc":"ROI responses over an imaging plane. The first dimension represents time. The second dimension, if present, represents ROIs.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_ROIs"]],"shape":[[null],[null,null]],"doc":"Signals from ROIs."},{"name":"rois","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion referencing into an ROITable containing information on the ROIs stored in this timeseries."}]},{"neurodata_type_def":"DfOverF","neurodata_type_inc":"NWBDataInterface","default_name":"DfOverF","doc":"dF/F information about a region of interest (ROI). Storage hierarchy of dF/F should be the same as for segmentation (i.e., same names for ROIs and for image planes).","groups":[{"neurodata_type_inc":"RoiResponseSeries","doc":"RoiResponseSeries object(s) containing dF/F for a ROI.","quantity":"+"}]},{"neurodata_type_def":"Fluorescence","neurodata_type_inc":"NWBDataInterface","default_name":"Fluorescence","doc":"Fluorescence information about a region of interest (ROI). Storage hierarchy of fluorescence should be the same as for segmentation (ie, same names for ROIs and for image planes).","groups":[{"neurodata_type_inc":"RoiResponseSeries","doc":"RoiResponseSeries object(s) containing fluorescence data for a ROI.","quantity":"+"}]},{"neurodata_type_def":"ImageSegmentation","neurodata_type_inc":"NWBDataInterface","default_name":"ImageSegmentation","doc":"Stores pixels in an image that represent different regions of interest (ROIs) or masks. All segmentation for a given imaging plane is stored together, with storage for multiple imaging planes (masks) supported. Each ROI is stored in its own subgroup, with the ROI group containing both a 2D mask and a list of pixels that make up this mask. Segments can also be used for masking neuropil. If segmentation is allowed to change with time, a new imaging plane (or module) is required and ROI names should remain consistent between them.","groups":[{"neurodata_type_inc":"PlaneSegmentation","doc":"Results from image segmentation of a specific imaging plane.","quantity":"+"}]},{"neurodata_type_def":"PlaneSegmentation","neurodata_type_inc":"DynamicTable","doc":"Results from image segmentation of a specific imaging plane.","datasets":[{"name":"image_mask","neurodata_type_inc":"VectorData","dims":[["num_roi","num_x","num_y"],["num_roi","num_x","num_y","num_z"]],"shape":[[null,null,null],[null,null,null,null]],"doc":"ROI masks for each ROI. Each image mask is the size of the original imaging plane (or volume) and members of the ROI are finite non-zero.","quantity":"?"},{"name":"pixel_mask_index","neurodata_type_inc":"VectorIndex","doc":"Index into pixel_mask.","quantity":"?"},{"name":"pixel_mask","neurodata_type_inc":"VectorData","dtype":[{"name":"x","dtype":"uint32","doc":"Pixel x-coordinate."},{"name":"y","dtype":"uint32","doc":"Pixel y-coordinate."},{"name":"weight","dtype":"float32","doc":"Weight of the pixel."}],"doc":"Pixel masks for each ROI: a list of indices and weights for the ROI. Pixel masks are concatenated and parsing of this dataset is maintained by the PlaneSegmentation","quantity":"?"},{"name":"voxel_mask_index","neurodata_type_inc":"VectorIndex","doc":"Index into voxel_mask.","quantity":"?"},{"name":"voxel_mask","neurodata_type_inc":"VectorData","dtype":[{"name":"x","dtype":"uint32","doc":"Voxel x-coordinate."},{"name":"y","dtype":"uint32","doc":"Voxel y-coordinate."},{"name":"z","dtype":"uint32","doc":"Voxel z-coordinate."},{"name":"weight","dtype":"float32","doc":"Weight of the voxel."}],"doc":"Voxel masks for each ROI: a list of indices and weights for the ROI. Voxel masks are concatenated and parsing of this dataset is maintained by the PlaneSegmentation","quantity":"?"}],"groups":[{"name":"reference_images","doc":"Image stacks that the segmentation masks apply to.","groups":[{"neurodata_type_inc":"ImageSeries","doc":"One or more image stacks that the masks apply to (can be one-element stack).","quantity":"*"}]}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this data was generated."}]},{"neurodata_type_def":"ImagingPlane","neurodata_type_inc":"NWBContainer","doc":"An imaging plane and its metadata.","datasets":[{"name":"description","dtype":"text","doc":"Description of the imaging plane.","quantity":"?"},{"name":"excitation_lambda","dtype":"float32","doc":"Excitation wavelength, in nm."},{"name":"imaging_rate","dtype":"float32","doc":"Rate that images are acquired, in Hz. If the corresponding TimeSeries is present, the rate should be stored there instead.","quantity":"?"},{"name":"indicator","dtype":"text","doc":"Calcium indicator."},{"name":"location","dtype":"text","doc":"Location of the imaging plane. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."},{"name":"manifold","dtype":"float32","dims":[["height","width","x, y, z"],["height","width","depth","x, y, z"]],"shape":[[null,null,3],[null,null,null,3]],"doc":"DEPRECATED Physical position of each pixel. 'xyz' represents the position of the pixel relative to the defined coordinate space. Deprecated in favor of origin_coords and grid_spacing.","quantity":"?","attributes":[{"name":"conversion","dtype":"float32","default_value":1.0,"doc":"Scalar to multiply each element in data to convert it to the specified 'unit'. If the data are stored in acquisition system units or other units that require a conversion to be interpretable, multiply the data by 'conversion' to convert the data to the specified 'unit'. e.g. if the data acquisition system stores values in this object as pixels from x = -500 to 499, y = -500 to 499 that correspond to a 2 m x 2 m range, then the 'conversion' multiplier to get from raw data acquisition pixel units to meters is 2/1000.","required":false},{"name":"unit","dtype":"text","default_value":"meters","doc":"Base unit of measurement for working with the data. The default value is 'meters'.","required":false}]},{"name":"origin_coords","dtype":"float32","dims":[["x, y"],["x, y, z"]],"shape":[[2],[3]],"doc":"Physical location of the first element of the imaging plane (0, 0) for 2-D data or (0, 0, 0) for 3-D data. See also reference_frame for what the physical location is relative to (e.g., bregma).","quantity":"?","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Measurement units for origin_coords. The default value is 'meters'."}]},{"name":"grid_spacing","dtype":"float32","dims":[["x, y"],["x, y, z"]],"shape":[[2],[3]],"doc":"Space between pixels in (x, y) or voxels in (x, y, z) directions, in the specified unit. Assumes imaging plane is a regular grid. See also reference_frame to interpret the grid.","quantity":"?","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Measurement units for grid_spacing. The default value is 'meters'."}]},{"name":"reference_frame","dtype":"text","doc":"Describes reference frame of origin_coords and grid_spacing. For example, this can be a text description of the anatomical location and orientation of the grid defined by origin_coords and grid_spacing or the vectors needed to transform or rotate the grid to a common anatomical axis (e.g., AP/DV/ML). This field is necessary to interpret origin_coords and grid_spacing. If origin_coords and grid_spacing are not present, then this field is not required. For example, if the microscope takes 10 x 10 x 2 images, where the first value of the data matrix (index (0, 0, 0)) corresponds to (-1.2, -0.6, -2) mm relative to bregma, the spacing between pixels is 0.2 mm in x, 0.2 mm in y and 0.5 mm in z, and larger numbers in x means more anterior, larger numbers in y means more rightward, and larger numbers in z means more ventral, then enter the following -- origin_coords = (-1.2, -0.6, -2) grid_spacing = (0.2, 0.2, 0.5) reference_frame = \"Origin coordinates are relative to bregma. First dimension corresponds to anterior-posterior axis (larger index = more anterior). Second dimension corresponds to medial-lateral axis (larger index = more rightward). Third dimension corresponds to dorsal-ventral axis (larger index = more ventral).\"","quantity":"?"}],"groups":[{"neurodata_type_inc":"OpticalChannel","doc":"An optical channel used to record from an imaging plane.","quantity":"+"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the Device object that was used to record from this electrode."}]},{"neurodata_type_def":"OpticalChannel","neurodata_type_inc":"NWBContainer","doc":"An optical channel used to record from an imaging plane.","datasets":[{"name":"description","dtype":"text","doc":"Description or other notes about the channel."},{"name":"emission_lambda","dtype":"float32","doc":"Emission wavelength for channel, in nm."}]},{"neurodata_type_def":"MotionCorrection","neurodata_type_inc":"NWBDataInterface","default_name":"MotionCorrection","doc":"An image stack where all frames are shifted (registered) to a common coordinate system, to account for movement and drift between frames. Note: each frame at each point in time is assumed to be 2-D (has only x & y dimensions).","groups":[{"neurodata_type_inc":"CorrectedImageStack","doc":"Results from motion correction of an image stack.","quantity":"+"}]},{"neurodata_type_def":"CorrectedImageStack","neurodata_type_inc":"NWBDataInterface","doc":"Results from motion correction of an image stack.","groups":[{"name":"corrected","neurodata_type_inc":"ImageSeries","doc":"Image stack with frames shifted to the common coordinates."},{"name":"xy_translation","neurodata_type_inc":"TimeSeries","doc":"Stores the x,y delta necessary to align each frame to the common coordinates, for example, to align each frame to a reference image."}],"links":[{"name":"original","target_type":"ImageSeries","doc":"Link to ImageSeries object that is being registered."}]}]} \ No newline at end of file diff --git a/Resources/spec/core/2.7.0/nwb.retinotopy.json b/Resources/spec/core/2.7.0/nwb.retinotopy.json deleted file mode 100644 index 895dae9..0000000 --- a/Resources/spec/core/2.7.0/nwb.retinotopy.json +++ /dev/null @@ -1 +0,0 @@ -{"groups":[{"neurodata_type_def":"ImagingRetinotopy","neurodata_type_inc":"NWBDataInterface","default_name":"ImagingRetinotopy","doc":"DEPRECATED. Intrinsic signal optical imaging or widefield imaging for measuring retinotopy. Stores orthogonal maps (e.g., altitude/azimuth; radius/theta) of responses to specific stimuli and a combined polarity map from which to identify visual areas. This group does not store the raw responses imaged during retinotopic mapping or the stimuli presented, but rather the resulting phase and power maps after applying a Fourier transform on the averaged responses. Note: for data consistency, all images and arrays are stored in the format [row][column] and [row, col], which equates to [y][x]. Field of view and dimension arrays may appear backward (i.e., y before x).","datasets":[{"name":"axis_1_phase_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Phase response to stimulus on the first measured axis.","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_1_power_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Power response on the first measured axis. Response is scaled so 0.0 is no power in the response and 1.0 is maximum relative power.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_2_phase_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Phase response to stimulus on the second measured axis.","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_2_power_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Power response on the second measured axis. Response is scaled so 0.0 is no power in the response and 1.0 is maximum relative power.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_descriptions","dtype":"text","dims":["axis_1, axis_2"],"shape":[2],"doc":"Two-element array describing the contents of the two response axis fields. Description should be something like ['altitude', 'azimuth'] or '['radius', 'theta']."},{"name":"focal_depth_image","dtype":"uint16","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Gray-scale image taken with same settings/parameters (e.g., focal depth, wavelength) as data collection. Array format: [rows][columns].","quantity":"?","attributes":[{"name":"bits_per_pixel","dtype":"int32","doc":"Number of bits used to represent each value. This is necessary to determine maximum (white) pixel value."},{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"focal_depth","dtype":"float32","doc":"Focal depth offset, in meters."},{"name":"format","dtype":"text","doc":"Format of image. Right now only 'raw' is supported."}]},{"name":"sign_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Sine of the angle between the direction of the gradient in axis_1 and axis_2.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."}]},{"name":"vasculature_image","dtype":"uint16","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Gray-scale anatomical image of cortical surface. Array structure: [rows][columns]","attributes":[{"name":"bits_per_pixel","dtype":"int32","doc":"Number of bits used to represent each value. This is necessary to determine maximum (white) pixel value"},{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"format","dtype":"text","doc":"Format of image. Right now only 'raw' is supported."}]}]}]} \ No newline at end of file diff --git a/Resources/spec/hdmf-common/1.8.0/base.json b/Resources/spec/hdmf-common/1.8.0/base.json deleted file mode 100644 index 6753495..0000000 --- a/Resources/spec/hdmf-common/1.8.0/base.json +++ /dev/null @@ -1 +0,0 @@ -{"datasets":[{"data_type_def":"Data","doc":"An abstract data type for a dataset."}],"groups":[{"data_type_def":"Container","doc":"An abstract data type for a group storing collections of data and metadata. Base type for all data and metadata containers."},{"data_type_def":"SimpleMultiContainer","data_type_inc":"Container","doc":"A simple Container for holding onto multiple containers.","datasets":[{"data_type_inc":"Data","quantity":"*","doc":"Data objects held within this SimpleMultiContainer."}],"groups":[{"data_type_inc":"Container","quantity":"*","doc":"Container objects held within this SimpleMultiContainer."}]}]} \ No newline at end of file diff --git a/Resources/spec/hdmf-common/1.8.0/namespace.json b/Resources/spec/hdmf-common/1.8.0/namespace.json deleted file mode 100644 index 0b921a4..0000000 --- a/Resources/spec/hdmf-common/1.8.0/namespace.json +++ /dev/null @@ -1 +0,0 @@ -{"namespaces":[{"name":"hdmf-common","doc":"Common data structures provided by HDMF","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov"],"full_name":"HDMF Common","schema":[{"source":"base"},{"source":"table"},{"source":"sparse"}],"version":"1.8.0"}]} \ No newline at end of file diff --git a/Resources/spec/hdmf-common/1.8.0/sparse.json b/Resources/spec/hdmf-common/1.8.0/sparse.json deleted file mode 100644 index 16cff5b..0000000 --- a/Resources/spec/hdmf-common/1.8.0/sparse.json +++ /dev/null @@ -1 +0,0 @@ -{"groups":[{"data_type_def":"CSRMatrix","data_type_inc":"Container","doc":"A compressed sparse row matrix. Data are stored in the standard CSR format, where column indices for row i are stored in indices[indptr[i]:indptr[i+1]] and their corresponding values are stored in data[indptr[i]:indptr[i+1]].","attributes":[{"name":"shape","dtype":"uint","dims":["number of rows, number of columns"],"shape":[2],"doc":"The shape (number of rows, number of columns) of this sparse matrix."}],"datasets":[{"name":"indices","dtype":"uint","dims":["number of non-zero values"],"shape":[null],"doc":"The column indices."},{"name":"indptr","dtype":"uint","dims":["number of rows in the matrix + 1"],"shape":[null],"doc":"The row index pointer."},{"name":"data","dims":["number of non-zero values"],"shape":[null],"doc":"The non-zero values in the matrix."}]}]} \ No newline at end of file diff --git a/Resources/spec/hdmf-common/1.8.0/table.json b/Resources/spec/hdmf-common/1.8.0/table.json deleted file mode 100644 index 36a927d..0000000 --- a/Resources/spec/hdmf-common/1.8.0/table.json +++ /dev/null @@ -1 +0,0 @@ -{"datasets":[{"data_type_def":"VectorData","data_type_inc":"Data","doc":"An n-dimensional dataset representing a column of a DynamicTable. If used without an accompanying VectorIndex, first dimension is along the rows of the DynamicTable and each step along the first dimension is a cell of the larger table. VectorData can also be used to represent a ragged array if paired with a VectorIndex. This allows for storing arrays of varying length in a single cell of the DynamicTable by indexing into this VectorData. The first vector is at VectorData[0:VectorIndex[0]]. The second vector is at VectorData[VectorIndex[0]:VectorIndex[1]], and so on.","dims":[["dim0"],["dim0","dim1"],["dim0","dim1","dim2"],["dim0","dim1","dim2","dim3"]],"shape":[[null],[null,null],[null,null,null],[null,null,null,null]],"attributes":[{"name":"description","dtype":"text","doc":"Description of what these vectors represent."}]},{"data_type_def":"VectorIndex","data_type_inc":"VectorData","dtype":"uint8","doc":"Used with VectorData to encode a ragged array. An array of indices into the first dimension of the target VectorData, and forming a map between the rows of a DynamicTable and the indices of the VectorData. The name of the VectorIndex is expected to be the name of the target VectorData object followed by \"_index\".","dims":["num_rows"],"shape":[null],"attributes":[{"name":"target","dtype":{"target_type":"VectorData","reftype":"object"},"doc":"Reference to the target dataset that this index applies to."}]},{"data_type_def":"ElementIdentifiers","data_type_inc":"Data","default_name":"element_id","dtype":"int","dims":["num_elements"],"shape":[null],"doc":"A list of unique identifiers for values within a dataset, e.g. rows of a DynamicTable."},{"data_type_def":"DynamicTableRegion","data_type_inc":"VectorData","dtype":"int","doc":"DynamicTableRegion provides a link from one table to an index or region of another. The `table` attribute is a link to another `DynamicTable`, indicating which table is referenced, and the data is int(s) indicating the row(s) (0-indexed) of the target array. `DynamicTableRegion`s can be used to associate rows with repeated meta-data without data duplication. They can also be used to create hierarchical relationships between multiple `DynamicTable`s. `DynamicTableRegion` objects may be paired with a `VectorIndex` object to create ragged references, so a single cell of a `DynamicTable` can reference many rows of another `DynamicTable`.","dims":["num_rows"],"shape":[null],"attributes":[{"name":"table","dtype":{"target_type":"DynamicTable","reftype":"object"},"doc":"Reference to the DynamicTable object that this region applies to."},{"name":"description","dtype":"text","doc":"Description of what this table region points to."}]}],"groups":[{"data_type_def":"DynamicTable","data_type_inc":"Container","doc":"A group containing multiple datasets that are aligned on the first dimension (Currently, this requirement if left up to APIs to check and enforce). These datasets represent different columns in the table. Apart from a column that contains unique identifiers for each row, there are no other required datasets. Users are free to add any number of custom VectorData objects (columns) here. DynamicTable also supports ragged array columns, where each element can be of a different size. To add a ragged array column, use a VectorIndex type to index the corresponding VectorData type. See documentation for VectorData and VectorIndex for more details. Unlike a compound data type, which is analogous to storing an array-of-structs, a DynamicTable can be thought of as a struct-of-arrays. This provides an alternative structure to choose from when optimizing storage for anticipated access patterns. Additionally, this type provides a way of creating a table without having to define a compound type up front. Although this convenience may be attractive, users should think carefully about how data will be accessed. DynamicTable is more appropriate for column-centric access, whereas a dataset with a compound type would be more appropriate for row-centric access. Finally, data size should also be taken into account. For small tables, performance loss may be an acceptable trade-off for the flexibility of a DynamicTable.","attributes":[{"name":"colnames","dtype":"text","dims":["num_columns"],"shape":[null],"doc":"The names of the columns in this table. This should be used to specify an order to the columns."},{"name":"description","dtype":"text","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"id","data_type_inc":"ElementIdentifiers","dtype":"int","dims":["num_rows"],"shape":[null],"doc":"Array of unique identifiers for the rows of this dynamic table."},{"data_type_inc":"VectorData","doc":"Vector columns, including index columns, of this dynamic table.","quantity":"*"}]},{"data_type_def":"AlignedDynamicTable","data_type_inc":"DynamicTable","doc":"DynamicTable container that supports storing a collection of sub-tables. Each sub-table is a DynamicTable itself that is aligned with the main table by row index. I.e., all DynamicTables stored in this group MUST have the same number of rows. This type effectively defines a 2-level table in which the main data is stored in the main table implemented by this type and additional columns of the table are grouped into categories, with each category being represented by a separate DynamicTable stored within the group.","attributes":[{"name":"categories","dtype":"text","dims":["num_categories"],"shape":[null],"doc":"The names of the categories in this AlignedDynamicTable. Each category is represented by one DynamicTable stored in the parent group. This attribute should be used to specify an order of categories and the category names must match the names of the corresponding DynamicTable in the group."}],"groups":[{"data_type_inc":"DynamicTable","doc":"A DynamicTable representing a particular category for columns in the AlignedDynamicTable parent container. The table MUST be aligned with (i.e., have the same number of rows) as all other DynamicTables stored in the AlignedDynamicTable parent container. The name of the category is given by the name of the DynamicTable and its description by the description attribute of the DynamicTable.","quantity":"*"}]}]} \ No newline at end of file diff --git a/Resources/spec/hdmf-experimental/0.5.0/experimental.json b/Resources/spec/hdmf-experimental/0.5.0/experimental.json deleted file mode 100644 index 25f3113..0000000 --- a/Resources/spec/hdmf-experimental/0.5.0/experimental.json +++ /dev/null @@ -1 +0,0 @@ -{"groups":[],"datasets":[{"data_type_def":"EnumData","data_type_inc":"VectorData","dtype":"uint8","doc":"Data that come from a fixed set of values. A data value of i corresponds to the i-th value in the VectorData referenced by the 'elements' attribute.","attributes":[{"name":"elements","dtype":{"target_type":"VectorData","reftype":"object"},"doc":"Reference to the VectorData object that contains the enumerable elements"}]}]} \ No newline at end of file diff --git a/Resources/spec/hdmf-experimental/0.5.0/namespace.json b/Resources/spec/hdmf-experimental/0.5.0/namespace.json deleted file mode 100644 index 26f1ac7..0000000 --- a/Resources/spec/hdmf-experimental/0.5.0/namespace.json +++ /dev/null @@ -1 +0,0 @@ -{"namespaces":[{"name":"hdmf-experimental","doc":"Experimental data structures provided by HDMF. These are not guaranteed to be available in the future.","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter","Matthew Avaylon"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov","mavaylon@lbl.gov"],"full_name":"HDMF Experimental","schema":[{"namespace":"hdmf-common"},{"source":"experimental"},{"source":"resources"}],"version":"0.5.0"}]} \ No newline at end of file diff --git a/Resources/spec/hdmf-experimental/0.5.0/resources.json b/Resources/spec/hdmf-experimental/0.5.0/resources.json deleted file mode 100644 index 0e7d71f..0000000 --- a/Resources/spec/hdmf-experimental/0.5.0/resources.json +++ /dev/null @@ -1 +0,0 @@ -{"groups":[{"data_type_def":"HERD","data_type_inc":"Container","doc":"HDMF External Resources Data Structure. A set of six tables for tracking external resource references in a file or across multiple files.","datasets":[{"data_type_inc":"Data","name":"keys","doc":"A table for storing user terms that are used to refer to external resources.","dtype":[{"name":"key","dtype":"text","doc":"The user term that maps to one or more resources in the `resources` table, e.g., \"human\"."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"files","doc":"A table for storing object ids of files used in external resources.","dtype":[{"name":"file_object_id","dtype":"text","doc":"The object id (UUID) of a file that contains objects that refers to external resources."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"entities","doc":"A table for mapping user terms (i.e., keys) to resource entities.","dtype":[{"name":"entity_id","dtype":"text","doc":"The compact uniform resource identifier (CURIE) of the entity, in the form [prefix]:[unique local identifier], e.g., 'NCBI_TAXON:9606'."},{"name":"entity_uri","dtype":"text","doc":"The URI for the entity this reference applies to. This can be an empty string. e.g., https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=info&id=9606"}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"objects","doc":"A table for identifying which objects in a file contain references to external resources.","dtype":[{"name":"files_idx","dtype":"uint","doc":"The row index to the file in the `files` table containing the object."},{"name":"object_id","dtype":"text","doc":"The object id (UUID) of the object."},{"name":"object_type","dtype":"text","doc":"The data type of the object."},{"name":"relative_path","dtype":"text","doc":"The relative path from the data object with the `object_id` to the dataset or attribute with the value(s) that is associated with an external resource. This can be an empty string if the object is a dataset that contains the value(s) that is associated with an external resource."},{"name":"field","dtype":"text","doc":"The field within the compound data type using an external resource. This is used only if the dataset or attribute is a compound data type; otherwise this should be an empty string."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"object_keys","doc":"A table for identifying which objects use which keys.","dtype":[{"name":"objects_idx","dtype":"uint","doc":"The row index to the object in the `objects` table that holds the key"},{"name":"keys_idx","dtype":"uint","doc":"The row index to the key in the `keys` table."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"entity_keys","doc":"A table for identifying which keys use which entity.","dtype":[{"name":"entities_idx","dtype":"uint","doc":"The row index to the entity in the `entities` table."},{"name":"keys_idx","dtype":"uint","doc":"The row index to the key in the `keys` table."}],"dims":["num_rows"],"shape":[null]}]}]} \ No newline at end of file From 025a093c2b5f76c053b80b85e0e4c53967e6b48f Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 28 Aug 2024 13:16:32 -0700 Subject: [PATCH 09/32] remove extra include from cmake --- CMakeLists.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b825f5..f3a0de2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,8 +36,6 @@ set(SOURCE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/Source) file(GLOB_RECURSE SRC_FILES LIST_DIRECTORIES false "${SOURCE_PATH}/*.cpp" "${SOURCE_PATH}/*.h" "${SOURCE_PATH}/*.hpp") set(GUI_COMMONLIB_DIR ${GUI_BASE_DIR}/installed_libs) -include_directories(${SOURCE_PATH}/aqnwb) - set(CONFIGURATION_FOLDER $<$:Debug>$<$>:Release>) list(APPEND CMAKE_PREFIX_PATH ${GUI_COMMONLIB_DIR} ${GUI_COMMONLIB_DIR}/${CONFIGURATION_FOLDER}) From 549bc710aa80cd7ab51b5c30a3745a6067e38cec Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 28 Aug 2024 13:17:06 -0700 Subject: [PATCH 10/32] update aqnwb source files --- Source/RecordEngine/NWBRecording.cpp | 6 +- Source/RecordEngine/NWBRecording.h | 2 +- Source/aqnwb/{aqnwb => }/BaseIO.cpp | 4 +- Source/aqnwb/{aqnwb => }/BaseIO.hpp | 2 +- Source/aqnwb/{aqnwb => }/Channel.cpp | 4 +- Source/aqnwb/{aqnwb => }/Channel.hpp | 8 ++- Source/aqnwb/{aqnwb => }/Types.hpp | 0 Source/aqnwb/{aqnwb => }/Utils.hpp | 5 +- Source/aqnwb/aqnwb.hpp | 3 + Source/aqnwb/aqnwb/aqnwb.hpp | 6 -- Source/aqnwb/aqnwb/aqnwb_export.hpp | 42 ------------ Source/aqnwb/{aqnwb => }/hdf5/HDF5IO.cpp | 5 +- Source/aqnwb/{aqnwb => }/hdf5/HDF5IO.hpp | 4 +- Source/aqnwb/{aqnwb => }/nwb/NWBFile.cpp | 68 ++++++++----------- Source/aqnwb/{aqnwb => }/nwb/NWBFile.hpp | 28 +++----- Source/aqnwb/{aqnwb => }/nwb/NWBRecording.cpp | 13 ++-- Source/aqnwb/{aqnwb => }/nwb/NWBRecording.hpp | 7 +- .../aqnwb/{aqnwb => }/nwb/base/TimeSeries.cpp | 2 +- .../aqnwb/{aqnwb => }/nwb/base/TimeSeries.hpp | 4 +- .../aqnwb/{aqnwb => }/nwb/device/Device.cpp | 2 +- .../aqnwb/{aqnwb => }/nwb/device/Device.hpp | 4 +- .../nwb/ecephys/ElectricalSeries.cpp | 5 +- .../nwb/ecephys/ElectricalSeries.hpp | 6 +- .../{aqnwb => }/nwb/file/ElectrodeGroup.cpp | 2 +- .../{aqnwb => }/nwb/file/ElectrodeGroup.hpp | 6 +- .../{aqnwb => }/nwb/file/ElectrodeTable.cpp | 5 +- .../{aqnwb => }/nwb/file/ElectrodeTable.hpp | 8 +-- .../{aqnwb => }/nwb/hdmf/base/Container.cpp | 2 +- .../{aqnwb => }/nwb/hdmf/base/Container.hpp | 2 +- .../aqnwb/{aqnwb => }/nwb/hdmf/base/Data.hpp | 2 +- .../nwb/hdmf/table/DynamicTable.cpp | 9 +-- .../nwb/hdmf/table/DynamicTable.hpp | 8 +-- .../nwb/hdmf/table/ElementIdentifiers.hpp | 2 +- .../{aqnwb => }/nwb/hdmf/table/VectorData.cpp | 2 +- .../{aqnwb => }/nwb/hdmf/table/VectorData.hpp | 2 +- Source/aqnwb/spec/core.hpp | 64 +++++++++++++++++ Source/aqnwb/spec/hdmf_common.hpp | 28 ++++++++ Source/aqnwb/spec/hdmf_experimental.hpp | 24 +++++++ 38 files changed, 227 insertions(+), 169 deletions(-) rename Source/aqnwb/{aqnwb => }/BaseIO.cpp (98%) rename Source/aqnwb/{aqnwb => }/BaseIO.hpp (99%) rename Source/aqnwb/{aqnwb => }/Channel.cpp (90%) rename Source/aqnwb/{aqnwb => }/Channel.hpp (93%) rename Source/aqnwb/{aqnwb => }/Types.hpp (100%) rename Source/aqnwb/{aqnwb => }/Utils.hpp (97%) create mode 100644 Source/aqnwb/aqnwb.hpp delete mode 100644 Source/aqnwb/aqnwb/aqnwb.hpp delete mode 100644 Source/aqnwb/aqnwb/aqnwb_export.hpp rename Source/aqnwb/{aqnwb => }/hdf5/HDF5IO.cpp (99%) rename Source/aqnwb/{aqnwb => }/hdf5/HDF5IO.hpp (99%) rename Source/aqnwb/{aqnwb => }/nwb/NWBFile.cpp (73%) rename Source/aqnwb/{aqnwb => }/nwb/NWBFile.hpp (90%) rename Source/aqnwb/{aqnwb => }/nwb/NWBRecording.cpp (89%) rename Source/aqnwb/{aqnwb => }/nwb/NWBRecording.hpp (94%) rename Source/aqnwb/{aqnwb => }/nwb/base/TimeSeries.cpp (98%) rename Source/aqnwb/{aqnwb => }/nwb/base/TimeSeries.hpp (98%) rename Source/aqnwb/{aqnwb => }/nwb/device/Device.cpp (95%) rename Source/aqnwb/{aqnwb => }/nwb/device/Device.hpp (94%) rename Source/aqnwb/{aqnwb => }/nwb/ecephys/ElectricalSeries.cpp (97%) rename Source/aqnwb/{aqnwb => }/nwb/ecephys/ElectricalSeries.hpp (97%) rename Source/aqnwb/{aqnwb => }/nwb/file/ElectrodeGroup.cpp (95%) rename Source/aqnwb/{aqnwb => }/nwb/file/ElectrodeGroup.hpp (94%) rename Source/aqnwb/{aqnwb => }/nwb/file/ElectrodeTable.cpp (97%) rename Source/aqnwb/{aqnwb => }/nwb/file/ElectrodeTable.hpp (94%) rename Source/aqnwb/{aqnwb => }/nwb/hdmf/base/Container.cpp (89%) rename Source/aqnwb/{aqnwb => }/nwb/hdmf/base/Container.hpp (96%) rename Source/aqnwb/{aqnwb => }/nwb/hdmf/base/Data.hpp (91%) rename Source/aqnwb/{aqnwb => }/nwb/hdmf/table/DynamicTable.cpp (90%) rename Source/aqnwb/{aqnwb => }/nwb/hdmf/table/DynamicTable.hpp (93%) rename Source/aqnwb/{aqnwb => }/nwb/hdmf/table/ElementIdentifiers.hpp (84%) rename Source/aqnwb/{aqnwb => }/nwb/hdmf/table/VectorData.cpp (71%) rename Source/aqnwb/{aqnwb => }/nwb/hdmf/table/VectorData.hpp (91%) create mode 100644 Source/aqnwb/spec/core.hpp create mode 100644 Source/aqnwb/spec/hdmf_common.hpp create mode 100644 Source/aqnwb/spec/hdmf_experimental.hpp diff --git a/Source/RecordEngine/NWBRecording.cpp b/Source/RecordEngine/NWBRecording.cpp index 1125252..ec3bead 100644 --- a/Source/RecordEngine/NWBRecording.cpp +++ b/Source/RecordEngine/NWBRecording.cpp @@ -25,8 +25,8 @@ #include #include "NWBRecording.h" -#include "aqnwb/Channel.hpp" -#include "aqnwb/Utils.hpp" +#include "../aqnwb/Channel.hpp" +#include "../aqnwb/Utils.hpp" #include "../../plugin-GUI/Source/Processors/RecordNode/RecordNode.h" @@ -238,7 +238,7 @@ void NWBRecordEngine::writeContinuousData(int writeChannel, // TODO - update positionOffset tracking // write data - // std::unique_ptr intBuffer = AQNWB::transformToInt16(static_cast(size), channel->getBitVolts() / 1e6, dataBuffer); + std::unique_ptr intBuffer = AQNWB::transformToInt16(static_cast(size), channel->getBitVolts() / 1e6, dataBuffer); nwbRecording.writeTimeseriesData("ElectricalSeries", datasetIndex, *channel, // to do, get recording channel information diff --git a/Source/RecordEngine/NWBRecording.h b/Source/RecordEngine/NWBRecording.h index 1de1af5..2322b18 100644 --- a/Source/RecordEngine/NWBRecording.h +++ b/Source/RecordEngine/NWBRecording.h @@ -26,7 +26,7 @@ #include -#include "aqnwb/nwb/NWBRecording.hpp" +#include "../aqnwb/nwb/NWBRecording.hpp" typedef Array ContinuousGroup; diff --git a/Source/aqnwb/aqnwb/BaseIO.cpp b/Source/aqnwb/BaseIO.cpp similarity index 98% rename from Source/aqnwb/aqnwb/BaseIO.cpp rename to Source/aqnwb/BaseIO.cpp index 1cd0f45..353f709 100644 --- a/Source/aqnwb/aqnwb/BaseIO.cpp +++ b/Source/aqnwb/BaseIO.cpp @@ -1,6 +1,6 @@ -#include "aqnwb/BaseIO.hpp" +#include "BaseIO.hpp" -#include "aqnwb/Utils.hpp" +#include "Utils.hpp" using namespace AQNWB; diff --git a/Source/aqnwb/aqnwb/BaseIO.hpp b/Source/aqnwb/BaseIO.hpp similarity index 99% rename from Source/aqnwb/aqnwb/BaseIO.hpp rename to Source/aqnwb/BaseIO.hpp index 6e571a4..3dd375d 100644 --- a/Source/aqnwb/aqnwb/BaseIO.hpp +++ b/Source/aqnwb/BaseIO.hpp @@ -6,7 +6,7 @@ #include #include -#include "aqnwb/Types.hpp" +#include "Types.hpp" #define DEFAULT_STR_SIZE 256 #define DEFAULT_ARRAY_SIZE 1 diff --git a/Source/aqnwb/aqnwb/Channel.cpp b/Source/aqnwb/Channel.cpp similarity index 90% rename from Source/aqnwb/aqnwb/Channel.cpp rename to Source/aqnwb/Channel.cpp index ccc3f72..d8f9357 100644 --- a/Source/aqnwb/aqnwb/Channel.cpp +++ b/Source/aqnwb/Channel.cpp @@ -1,11 +1,12 @@ #include -#include "aqnwb/Channel.hpp" +#include "Channel.hpp" using namespace AQNWB; Channel::Channel(const std::string name, const std::string groupName, + const SizeType groupIndex, const SizeType localIndex, const SizeType globalIndex, const float conversion, @@ -15,6 +16,7 @@ Channel::Channel(const std::string name, const std::string comments) : name(name) , groupName(groupName) + , groupIndex(groupIndex) , localIndex(localIndex) , globalIndex(globalIndex) , position(position) diff --git a/Source/aqnwb/aqnwb/Channel.hpp b/Source/aqnwb/Channel.hpp similarity index 93% rename from Source/aqnwb/aqnwb/Channel.hpp rename to Source/aqnwb/Channel.hpp index 66fe06f..fccdf22 100644 --- a/Source/aqnwb/aqnwb/Channel.hpp +++ b/Source/aqnwb/Channel.hpp @@ -3,7 +3,7 @@ #include #include -#include "aqnwb/Types.hpp" +#include "Types.hpp" using SizeType = AQNWB::Types::SizeType; @@ -20,6 +20,7 @@ class Channel */ Channel(const std::string name, const std::string groupName, + const SizeType groupIndex, const SizeType localIndex, const SizeType globalIndex, const float conversion = 1e6f, // uV to V @@ -63,6 +64,11 @@ class Channel */ std::string groupName; + /** + * @brief Index of array group the channel belongs to. + */ + SizeType groupIndex; + /** * @brief Index of channel within the recording array. */ diff --git a/Source/aqnwb/aqnwb/Types.hpp b/Source/aqnwb/Types.hpp similarity index 100% rename from Source/aqnwb/aqnwb/Types.hpp rename to Source/aqnwb/Types.hpp diff --git a/Source/aqnwb/aqnwb/Utils.hpp b/Source/aqnwb/Utils.hpp similarity index 97% rename from Source/aqnwb/aqnwb/Utils.hpp rename to Source/aqnwb/Utils.hpp index 31a4717..b1a8581 100644 --- a/Source/aqnwb/aqnwb/Utils.hpp +++ b/Source/aqnwb/Utils.hpp @@ -8,10 +8,9 @@ #include #include +#include "BaseIO.hpp" #include "boost/date_time/c_local_time_adjustor.hpp" - -#include "aqnwb/BaseIO.hpp" -#include "aqnwb/hdf5/HDF5IO.hpp" +#include "hdf5/HDF5IO.hpp" namespace AQNWB { diff --git a/Source/aqnwb/aqnwb.hpp b/Source/aqnwb/aqnwb.hpp new file mode 100644 index 0000000..dad5802 --- /dev/null +++ b/Source/aqnwb/aqnwb.hpp @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/Source/aqnwb/aqnwb/aqnwb.hpp b/Source/aqnwb/aqnwb/aqnwb.hpp deleted file mode 100644 index 45c083f..0000000 --- a/Source/aqnwb/aqnwb/aqnwb.hpp +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include - -#include "aqnwb/nwb/NWBRecording.hpp" -#include "aqnwb/nwb/NWBFile.hpp" diff --git a/Source/aqnwb/aqnwb/aqnwb_export.hpp b/Source/aqnwb/aqnwb/aqnwb_export.hpp deleted file mode 100644 index 1be4353..0000000 --- a/Source/aqnwb/aqnwb/aqnwb_export.hpp +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef AQNWB_EXPORT_H -#define AQNWB_EXPORT_H - -#ifdef AQNWB_STATIC_DEFINE -# define AQNWB_EXPORT -# define AQNWB_NO_EXPORT -#else -# ifndef AQNWB_EXPORT -# ifdef aqnwb_aqnwb_EXPORTS - /* We are building this library */ -# define AQNWB_EXPORT -# else - /* We are using this library */ -# define AQNWB_EXPORT -# endif -# endif - -# ifndef AQNWB_NO_EXPORT -# define AQNWB_NO_EXPORT -# endif -#endif - -#ifndef AQNWB_DEPRECATED -# define AQNWB_DEPRECATED __attribute__ ((__deprecated__)) -#endif - -#ifndef AQNWB_DEPRECATED_EXPORT -# define AQNWB_DEPRECATED_EXPORT AQNWB_EXPORT AQNWB_DEPRECATED -#endif - -#ifndef AQNWB_DEPRECATED_NO_EXPORT -# define AQNWB_DEPRECATED_NO_EXPORT AQNWB_NO_EXPORT AQNWB_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef AQNWB_NO_DEPRECATED -# define AQNWB_NO_DEPRECATED -# endif -#endif - -#endif /* AQNWB_EXPORT_H */ diff --git a/Source/aqnwb/aqnwb/hdf5/HDF5IO.cpp b/Source/aqnwb/hdf5/HDF5IO.cpp similarity index 99% rename from Source/aqnwb/aqnwb/hdf5/HDF5IO.cpp rename to Source/aqnwb/hdf5/HDF5IO.cpp index 1228948..ef7cc60 100644 --- a/Source/aqnwb/aqnwb/hdf5/HDF5IO.cpp +++ b/Source/aqnwb/hdf5/HDF5IO.cpp @@ -4,11 +4,12 @@ #include #include +#include "HDF5IO.hpp" + #include #include -#include "aqnwb/Utils.hpp" -#include "aqnwb/hdf5/HDF5IO.hpp" +#include "../Utils.hpp" using namespace H5; using namespace AQNWB::HDF5; diff --git a/Source/aqnwb/aqnwb/hdf5/HDF5IO.hpp b/Source/aqnwb/hdf5/HDF5IO.hpp similarity index 99% rename from Source/aqnwb/aqnwb/hdf5/HDF5IO.hpp rename to Source/aqnwb/hdf5/HDF5IO.hpp index f98408c..8eaca67 100644 --- a/Source/aqnwb/aqnwb/hdf5/HDF5IO.hpp +++ b/Source/aqnwb/hdf5/HDF5IO.hpp @@ -6,8 +6,8 @@ #include -#include "aqnwb/BaseIO.hpp" -#include "aqnwb/Types.hpp" +#include "../BaseIO.hpp" +#include "../Types.hpp" namespace H5 { diff --git a/Source/aqnwb/aqnwb/nwb/NWBFile.cpp b/Source/aqnwb/nwb/NWBFile.cpp similarity index 73% rename from Source/aqnwb/aqnwb/nwb/NWBFile.cpp rename to Source/aqnwb/nwb/NWBFile.cpp index c29169c..f277a7c 100644 --- a/Source/aqnwb/aqnwb/nwb/NWBFile.cpp +++ b/Source/aqnwb/nwb/NWBFile.cpp @@ -6,15 +6,18 @@ #include #include - -#include "aqnwb/BaseIO.hpp" -#include "aqnwb/Channel.hpp" -#include "aqnwb/Utils.hpp" -#include "aqnwb/nwb/device/Device.hpp" -#include "aqnwb/nwb/ecephys/ElectricalSeries.hpp" -#include "aqnwb/nwb/file/ElectrodeGroup.hpp" -#include "aqnwb/nwb/file/ElectrodeTable.hpp" -#include "aqnwb/nwb/NWBFile.hpp" +#include "NWBFile.hpp" + +#include "../BaseIO.hpp" +#include "../Channel.hpp" +#include "../Utils.hpp" +#include "../spec/core.hpp" +#include "../spec/hdmf_common.hpp" +#include "../spec/hdmf_experimental.hpp" +#include "device/Device.hpp" +#include "ecephys/ElectricalSeries.hpp" +#include "file/ElectrodeGroup.hpp" +#include "file/ElectrodeTable.hpp" using namespace AQNWB::NWB; @@ -53,7 +56,7 @@ Status NWBFile::createFileStructure() } io->createCommonNWBAttributes("/", "core", "NWBFile", ""); - io->createAttribute(NWBVersion, "/", "nwb_version"); + io->createAttribute(AQNWB::spec::core::version, "/", "nwb_version"); io->createGroup("/acquisition"); io->createGroup("/analysis"); @@ -67,9 +70,9 @@ Status NWBFile::createFileStructure() io->createGroup("/specifications"); io->createReferenceAttribute("/specifications", "/", ".specloc"); - cacheSpecifications("core/", NWBVersion); - cacheSpecifications("hdmf-common/", HDMFVersion); - cacheSpecifications("hdmf-experimental/", HDMFExperimentalVersion); + cacheSpecifications("core", spec::core::version, spec::core::registerVariables); + cacheSpecifications("hdmf-common", spec::hdmf_common::version, spec::hdmf_common::registerVariables); + cacheSpecifications("hdmf-experimental", spec::hdmf_experimental::version, spec::hdmf_experimental::registerVariables); std::string time = getCurrentTime(); std::vector timeVec = {time}; @@ -146,34 +149,19 @@ void NWBFile::stopRecording() io->stopRecording(); } -void NWBFile::cacheSpecifications(const std::string& specPath, - const std::string& versionNumber) +void NWBFile::cacheSpecifications(const std::string& specPath, + const std::string& version, + void (*registerFunc)(std::map&)) { - io->createGroup("/specifications/" + specPath); - io->createGroup("/specifications/" + specPath + versionNumber); - - std::filesystem::path currentFile = __FILE__; - std::filesystem::path schemaDir = - currentFile.parent_path().parent_path().parent_path().parent_path().parent_path() / "Resources/spec" - / specPath / versionNumber; - - for (auto const& entry : std::filesystem::directory_iterator {schemaDir}) - if (std::filesystem::is_regular_file(entry) - && entry.path().extension() == ".json") - { - std::string specName = - entry.path().filename().replace_extension("").string(); - if (specName.find("namespace") != std::string::npos) - specName = "namespace"; - - std::ifstream schemaFile(entry.path()); - std::stringstream buffer; - buffer << schemaFile.rdbuf(); - - io->createStringDataSet( - "/specifications/" + specPath + versionNumber + "/" + specName, - buffer.str()); - } + std::map registry; + registerFunc(registry); + + io->createGroup("/specifications/" + specPath + "/"); + io->createGroup("/specifications/" + specPath + "/" + version); + + for (const auto& [name, content] : registry) { + io->createStringDataSet("/specifications/" + specPath + "/" + version + "/" + name, *content); + } } // recording data factory method / diff --git a/Source/aqnwb/aqnwb/nwb/NWBFile.hpp b/Source/aqnwb/nwb/NWBFile.hpp similarity index 90% rename from Source/aqnwb/aqnwb/nwb/NWBFile.hpp rename to Source/aqnwb/nwb/NWBFile.hpp index d645f5f..d629504 100644 --- a/Source/aqnwb/aqnwb/nwb/NWBFile.hpp +++ b/Source/aqnwb/nwb/NWBFile.hpp @@ -1,13 +1,13 @@ #pragma once #include +#include #include #include -#include "aqnwb/aqnwb_export.hpp" -#include "aqnwb/BaseIO.hpp" -#include "aqnwb/Types.hpp" -#include "aqnwb/nwb/base/TimeSeries.hpp" +#include "../BaseIO.hpp" +#include "../Types.hpp" +#include "base/TimeSeries.hpp" /*! * \namespace AQNWB::NWB @@ -84,21 +84,6 @@ class NWBFile */ void stopRecording(); - /** - * @brief Indicates the NWB schema version. - */ - const std::string NWBVersion = "2.7.0"; - - /** - * @brief Indicates the HDMF schema version. - */ - const std::string HDMFVersion = "1.8.0"; - - /** - * @brief Indicates the HDMF experimental version. - */ - const std::string HDMFExperimentalVersion = "0.5.0"; - /** * @brief Gets the TimeSeries object from the recordingContainers * @param timeseriesInd The index of the timeseries dataset within the group. @@ -134,10 +119,13 @@ class NWBFile /** * @brief Saves the specification files for the schema. * @param specPath The location in the file to store the spec information. + * @param version The version number of the specification files. + * @param registry The registry of specification files. * @param versionNumber The version number of the specification files. */ void cacheSpecifications(const std::string& specPath, - const std::string& versionNumber); + const std::string& version, + void (*registerFunc)(std::map&)); /** * @brief Holds the Container (usually TimeSeries) objects that have been diff --git a/Source/aqnwb/aqnwb/nwb/NWBRecording.cpp b/Source/aqnwb/nwb/NWBRecording.cpp similarity index 89% rename from Source/aqnwb/aqnwb/nwb/NWBRecording.cpp rename to Source/aqnwb/nwb/NWBRecording.cpp index 9f4178d..bbcff44 100644 --- a/Source/aqnwb/aqnwb/nwb/NWBRecording.cpp +++ b/Source/aqnwb/nwb/NWBRecording.cpp @@ -1,7 +1,8 @@ -#include "aqnwb/Channel.hpp" -#include "aqnwb/nwb/NWBRecording.hpp" -#include "aqnwb/Utils.hpp" -#include "aqnwb/hdf5/HDF5IO.hpp" +#include "NWBRecording.hpp" + +#include "../Channel.hpp" +#include "../Utils.hpp" +#include "../hdf5/HDF5IO.hpp" using namespace AQNWB::NWB; @@ -20,8 +21,8 @@ Status NWBRecording::openFile(const std::string& filename, const std::string& IOType) { // close any existing files - if (nwbfile != nullptr){ - nwbfile->finalize(); + if (nwbfile != nullptr) { + nwbfile->finalize(); } // initialize nwbfile object and create base structure diff --git a/Source/aqnwb/aqnwb/nwb/NWBRecording.hpp b/Source/aqnwb/nwb/NWBRecording.hpp similarity index 94% rename from Source/aqnwb/aqnwb/nwb/NWBRecording.hpp rename to Source/aqnwb/nwb/NWBRecording.hpp index 792899f..c844e37 100644 --- a/Source/aqnwb/aqnwb/nwb/NWBRecording.hpp +++ b/Source/aqnwb/nwb/NWBRecording.hpp @@ -1,8 +1,7 @@ #pragma once -#include "aqnwb/aqnwb_export.hpp" -#include "aqnwb/Types.hpp" -#include "aqnwb/nwb/NWBFile.hpp" +#include "../Types.hpp" +#include "NWBFile.hpp" namespace AQNWB::NWB { @@ -10,7 +9,7 @@ namespace AQNWB::NWB * @brief The NWBRecording class manages the recording process */ -class AQNWB_EXPORT NWBRecording +class NWBRecording { public: /** diff --git a/Source/aqnwb/aqnwb/nwb/base/TimeSeries.cpp b/Source/aqnwb/nwb/base/TimeSeries.cpp similarity index 98% rename from Source/aqnwb/aqnwb/nwb/base/TimeSeries.cpp rename to Source/aqnwb/nwb/base/TimeSeries.cpp index 80128ae..254aa69 100644 --- a/Source/aqnwb/aqnwb/nwb/base/TimeSeries.cpp +++ b/Source/aqnwb/nwb/base/TimeSeries.cpp @@ -1,4 +1,4 @@ -#include "aqnwb/nwb/base/TimeSeries.hpp" +#include "TimeSeries.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/aqnwb/nwb/base/TimeSeries.hpp b/Source/aqnwb/nwb/base/TimeSeries.hpp similarity index 98% rename from Source/aqnwb/aqnwb/nwb/base/TimeSeries.hpp rename to Source/aqnwb/nwb/base/TimeSeries.hpp index 004f258..fb53024 100644 --- a/Source/aqnwb/aqnwb/nwb/base/TimeSeries.hpp +++ b/Source/aqnwb/nwb/base/TimeSeries.hpp @@ -2,8 +2,8 @@ #include -#include "aqnwb/BaseIO.hpp" -#include "aqnwb/nwb/hdmf/base/Container.hpp" +#include "../../BaseIO.hpp" +#include "../hdmf/base/Container.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/aqnwb/nwb/device/Device.cpp b/Source/aqnwb/nwb/device/Device.cpp similarity index 95% rename from Source/aqnwb/aqnwb/nwb/device/Device.cpp rename to Source/aqnwb/nwb/device/Device.cpp index 5102f03..98fc90e 100644 --- a/Source/aqnwb/aqnwb/nwb/device/Device.cpp +++ b/Source/aqnwb/nwb/device/Device.cpp @@ -1,4 +1,4 @@ -#include "aqnwb/nwb/device/Device.hpp" +#include "Device.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/aqnwb/nwb/device/Device.hpp b/Source/aqnwb/nwb/device/Device.hpp similarity index 94% rename from Source/aqnwb/aqnwb/nwb/device/Device.hpp rename to Source/aqnwb/nwb/device/Device.hpp index 31acd7f..67eed81 100644 --- a/Source/aqnwb/aqnwb/nwb/device/Device.hpp +++ b/Source/aqnwb/nwb/device/Device.hpp @@ -2,8 +2,8 @@ #include -#include "aqnwb/BaseIO.hpp" -#include "aqnwb/nwb/hdmf/base/Container.hpp" +#include "../../BaseIO.hpp" +#include "../hdmf/base/Container.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/aqnwb/nwb/ecephys/ElectricalSeries.cpp b/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp similarity index 97% rename from Source/aqnwb/aqnwb/nwb/ecephys/ElectricalSeries.cpp rename to Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp index 3f1fb2d..9d215c1 100644 --- a/Source/aqnwb/aqnwb/nwb/ecephys/ElectricalSeries.cpp +++ b/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp @@ -1,6 +1,7 @@ -#include "aqnwb/nwb/ecephys/ElectricalSeries.hpp" -#include "aqnwb/nwb/file/ElectrodeTable.hpp" +#include "ElectricalSeries.hpp" + +#include "../file/ElectrodeTable.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/aqnwb/nwb/ecephys/ElectricalSeries.hpp b/Source/aqnwb/nwb/ecephys/ElectricalSeries.hpp similarity index 97% rename from Source/aqnwb/aqnwb/nwb/ecephys/ElectricalSeries.hpp rename to Source/aqnwb/nwb/ecephys/ElectricalSeries.hpp index 8224286..9e594a4 100644 --- a/Source/aqnwb/aqnwb/nwb/ecephys/ElectricalSeries.hpp +++ b/Source/aqnwb/nwb/ecephys/ElectricalSeries.hpp @@ -2,9 +2,9 @@ #include -#include "aqnwb/BaseIO.hpp" -#include "aqnwb/Channel.hpp" -#include "aqnwb/nwb/base/TimeSeries.hpp" +#include "../../BaseIO.hpp" +#include "../../Channel.hpp" +#include "../base/TimeSeries.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/aqnwb/nwb/file/ElectrodeGroup.cpp b/Source/aqnwb/nwb/file/ElectrodeGroup.cpp similarity index 95% rename from Source/aqnwb/aqnwb/nwb/file/ElectrodeGroup.cpp rename to Source/aqnwb/nwb/file/ElectrodeGroup.cpp index ac5e017..ffe469a 100644 --- a/Source/aqnwb/aqnwb/nwb/file/ElectrodeGroup.cpp +++ b/Source/aqnwb/nwb/file/ElectrodeGroup.cpp @@ -1,4 +1,4 @@ -#include "aqnwb/nwb/file/ElectrodeGroup.hpp" +#include "ElectrodeGroup.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/aqnwb/nwb/file/ElectrodeGroup.hpp b/Source/aqnwb/nwb/file/ElectrodeGroup.hpp similarity index 94% rename from Source/aqnwb/aqnwb/nwb/file/ElectrodeGroup.hpp rename to Source/aqnwb/nwb/file/ElectrodeGroup.hpp index 7b46ee2..352e481 100644 --- a/Source/aqnwb/aqnwb/nwb/file/ElectrodeGroup.hpp +++ b/Source/aqnwb/nwb/file/ElectrodeGroup.hpp @@ -2,9 +2,9 @@ #include -#include "aqnwb/BaseIO.hpp" -#include "aqnwb/nwb/device/Device.hpp" -#include "aqnwb/nwb/hdmf/base/Container.hpp" +#include "../../BaseIO.hpp" +#include "../device/Device.hpp" +#include "../hdmf/base/Container.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/aqnwb/nwb/file/ElectrodeTable.cpp b/Source/aqnwb/nwb/file/ElectrodeTable.cpp similarity index 97% rename from Source/aqnwb/aqnwb/nwb/file/ElectrodeTable.cpp rename to Source/aqnwb/nwb/file/ElectrodeTable.cpp index f68231c..6f5e7e8 100644 --- a/Source/aqnwb/aqnwb/nwb/file/ElectrodeTable.cpp +++ b/Source/aqnwb/nwb/file/ElectrodeTable.cpp @@ -1,6 +1,7 @@ -#include "aqnwb/nwb/file/ElectrodeTable.hpp" -#include "aqnwb/Channel.hpp" +#include "ElectrodeTable.hpp" + +#include "../../Channel.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/aqnwb/nwb/file/ElectrodeTable.hpp b/Source/aqnwb/nwb/file/ElectrodeTable.hpp similarity index 94% rename from Source/aqnwb/aqnwb/nwb/file/ElectrodeTable.hpp rename to Source/aqnwb/nwb/file/ElectrodeTable.hpp index 6ab89ce..0161698 100644 --- a/Source/aqnwb/aqnwb/nwb/file/ElectrodeTable.hpp +++ b/Source/aqnwb/nwb/file/ElectrodeTable.hpp @@ -2,10 +2,10 @@ #include -#include "aqnwb/BaseIO.hpp" -#include "aqnwb/nwb/hdmf/table/DynamicTable.hpp" -#include "aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp" -#include "aqnwb/nwb/hdmf/table/VectorData.hpp" +#include "../../BaseIO.hpp" +#include "../hdmf/table/DynamicTable.hpp" +#include "../hdmf/table/ElementIdentifiers.hpp" +#include "../hdmf/table/VectorData.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/aqnwb/nwb/hdmf/base/Container.cpp b/Source/aqnwb/nwb/hdmf/base/Container.cpp similarity index 89% rename from Source/aqnwb/aqnwb/nwb/hdmf/base/Container.cpp rename to Source/aqnwb/nwb/hdmf/base/Container.cpp index 8cd2865..e47d3ee 100644 --- a/Source/aqnwb/aqnwb/nwb/hdmf/base/Container.cpp +++ b/Source/aqnwb/nwb/hdmf/base/Container.cpp @@ -1,4 +1,4 @@ -#include "aqnwb/nwb/hdmf/base/Container.hpp" +#include "Container.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/aqnwb/nwb/hdmf/base/Container.hpp b/Source/aqnwb/nwb/hdmf/base/Container.hpp similarity index 96% rename from Source/aqnwb/aqnwb/nwb/hdmf/base/Container.hpp rename to Source/aqnwb/nwb/hdmf/base/Container.hpp index e0e1126..c9528b5 100644 --- a/Source/aqnwb/aqnwb/nwb/hdmf/base/Container.hpp +++ b/Source/aqnwb/nwb/hdmf/base/Container.hpp @@ -3,7 +3,7 @@ #include #include -#include "aqnwb/BaseIO.hpp" +#include "../../../BaseIO.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/aqnwb/nwb/hdmf/base/Data.hpp b/Source/aqnwb/nwb/hdmf/base/Data.hpp similarity index 91% rename from Source/aqnwb/aqnwb/nwb/hdmf/base/Data.hpp rename to Source/aqnwb/nwb/hdmf/base/Data.hpp index 36eccac..6b3f855 100644 --- a/Source/aqnwb/aqnwb/nwb/hdmf/base/Data.hpp +++ b/Source/aqnwb/nwb/hdmf/base/Data.hpp @@ -2,7 +2,7 @@ #include -#include "aqnwb/BaseIO.hpp" +#include "../../../BaseIO.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/aqnwb/nwb/hdmf/table/DynamicTable.cpp b/Source/aqnwb/nwb/hdmf/table/DynamicTable.cpp similarity index 90% rename from Source/aqnwb/aqnwb/nwb/hdmf/table/DynamicTable.cpp rename to Source/aqnwb/nwb/hdmf/table/DynamicTable.cpp index 8c2dec7..c61fae8 100644 --- a/Source/aqnwb/aqnwb/nwb/hdmf/table/DynamicTable.cpp +++ b/Source/aqnwb/nwb/hdmf/table/DynamicTable.cpp @@ -1,4 +1,4 @@ -#include "aqnwb/nwb/hdmf/table/DynamicTable.hpp" +#include "DynamicTable.hpp" using namespace AQNWB::NWB; @@ -37,9 +37,10 @@ void DynamicTable::addColumn(const std::string& name, } else { // write in loop because variable length string for (SizeType i = 0; i < values.size(); i++) - vectorData->dataset->writeDataBlock(std::vector(1, 1), - BaseDataType::STR(values[i].size()), - &values[i]); + vectorData->dataset->writeDataBlock( + std::vector(1, 1), + BaseDataType::STR(values[i].size() + 1), + values[i].c_str()); // TODO - add tests for this io->createCommonNWBAttributes( path + name, "hdmf-common", "VectorData", colDescription); } diff --git a/Source/aqnwb/aqnwb/nwb/hdmf/table/DynamicTable.hpp b/Source/aqnwb/nwb/hdmf/table/DynamicTable.hpp similarity index 93% rename from Source/aqnwb/aqnwb/nwb/hdmf/table/DynamicTable.hpp rename to Source/aqnwb/nwb/hdmf/table/DynamicTable.hpp index 7352f42..defe156 100644 --- a/Source/aqnwb/aqnwb/nwb/hdmf/table/DynamicTable.hpp +++ b/Source/aqnwb/nwb/hdmf/table/DynamicTable.hpp @@ -2,10 +2,10 @@ #include -#include "aqnwb/BaseIO.hpp" -#include "aqnwb/nwb/hdmf/base/Container.hpp" -#include "aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp" -#include "aqnwb/nwb/hdmf/table/VectorData.hpp" +#include "../../../BaseIO.hpp" +#include "../base/Container.hpp" +#include "ElementIdentifiers.hpp" +#include "VectorData.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp b/Source/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp similarity index 84% rename from Source/aqnwb/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp rename to Source/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp index 2f52d9e..36ab29c 100644 --- a/Source/aqnwb/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp +++ b/Source/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp @@ -1,6 +1,6 @@ #pragma once -#include "aqnwb/nwb/hdmf/base/Data.hpp" +#include "../base/Data.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/aqnwb/nwb/hdmf/table/VectorData.cpp b/Source/aqnwb/nwb/hdmf/table/VectorData.cpp similarity index 71% rename from Source/aqnwb/aqnwb/nwb/hdmf/table/VectorData.cpp rename to Source/aqnwb/nwb/hdmf/table/VectorData.cpp index 5c71ebb..ef4a40b 100644 --- a/Source/aqnwb/aqnwb/nwb/hdmf/table/VectorData.cpp +++ b/Source/aqnwb/nwb/hdmf/table/VectorData.cpp @@ -1,4 +1,4 @@ -#include "aqnwb/nwb/hdmf/table/VectorData.hpp" +#include "VectorData.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/aqnwb/nwb/hdmf/table/VectorData.hpp b/Source/aqnwb/nwb/hdmf/table/VectorData.hpp similarity index 91% rename from Source/aqnwb/aqnwb/nwb/hdmf/table/VectorData.hpp rename to Source/aqnwb/nwb/hdmf/table/VectorData.hpp index efb269f..870696a 100644 --- a/Source/aqnwb/aqnwb/nwb/hdmf/table/VectorData.hpp +++ b/Source/aqnwb/nwb/hdmf/table/VectorData.hpp @@ -2,7 +2,7 @@ #include -#include "aqnwb/nwb/hdmf/base/Data.hpp" +#include "../base/Data.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/spec/core.hpp b/Source/aqnwb/spec/core.hpp new file mode 100644 index 0000000..3bb5f31 --- /dev/null +++ b/Source/aqnwb/spec/core.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include + +namespace AQNWB::spec::core +{ + +const std::string version = "2.7.0"; + +const std::string nwb_base = R"delimiter( +{"datasets":[{"neurodata_type_def":"NWBData","neurodata_type_inc":"Data","doc":"An abstract data type for a dataset."},{"neurodata_type_def":"TimeSeriesReferenceVectorData","neurodata_type_inc":"VectorData","default_name":"timeseries","dtype":[{"name":"idx_start","dtype":"int32","doc":"Start index into the TimeSeries 'data' and 'timestamp' datasets of the referenced TimeSeries. The first dimension of those arrays is always time."},{"name":"count","dtype":"int32","doc":"Number of data samples available in this time series, during this epoch"},{"name":"timeseries","dtype":{"target_type":"TimeSeries","reftype":"object"},"doc":"The TimeSeries that this index applies to"}],"doc":"Column storing references to a TimeSeries (rows). For each TimeSeries this VectorData column stores the start_index and count to indicate the range in time to be selected as well as an object reference to the TimeSeries."},{"neurodata_type_def":"Image","neurodata_type_inc":"NWBData","dtype":"numeric","dims":[["x","y"],["x","y","r, g, b"],["x","y","r, g, b, a"]],"shape":[[null,null],[null,null,3],[null,null,4]],"doc":"An abstract data type for an image. Shape can be 2-D (x, y), or 3-D where the third dimension can have three or four elements, e.g. (x, y, (r, g, b)) or (x, y, (r, g, b, a)).","attributes":[{"name":"resolution","dtype":"float32","doc":"Pixel resolution of the image, in pixels per centimeter.","required":false},{"name":"description","dtype":"text","doc":"Description of the image.","required":false}]},{"neurodata_type_def":"ImageReferences","neurodata_type_inc":"NWBData","dtype":{"target_type":"Image","reftype":"object"},"dims":["num_images"],"shape":[null],"doc":"Ordered dataset of references to Image objects."}],"groups":[{"neurodata_type_def":"NWBContainer","neurodata_type_inc":"Container","doc":"An abstract data type for a generic container storing collections of data and metadata. Base type for all data and metadata containers."},{"neurodata_type_def":"NWBDataInterface","neurodata_type_inc":"NWBContainer","doc":"An abstract data type for a generic container storing collections of data, as opposed to metadata."},{"neurodata_type_def":"TimeSeries","neurodata_type_inc":"NWBDataInterface","doc":"General purpose time series.","attributes":[{"name":"description","dtype":"text","default_value":"no description","doc":"Description of the time series.","required":false},{"name":"comments","dtype":"text","default_value":"no comments","doc":"Human-readable comments about the TimeSeries. This second descriptive field can be used to store additional information, or descriptive information if the primary description field is populated with a computer-readable string.","required":false}],"datasets":[{"name":"data","dims":[["num_times"],["num_times","num_DIM2"],["num_times","num_DIM2","num_DIM3"],["num_times","num_DIM2","num_DIM3","num_DIM4"]],"shape":[[null],[null,null],[null,null,null],[null,null,null,null]],"doc":"Data values. Data can be in 1-D, 2-D, 3-D, or 4-D. The first dimension should always represent time. This can also be used to store binary data (e.g., image frames). This can also be a link to data stored in an external file.","attributes":[{"name":"conversion","dtype":"float32","default_value":1.0,"doc":"Scalar to multiply each element in data to convert it to the specified 'unit'. If the data are stored in acquisition system units or other units that require a conversion to be interpretable, multiply the data by 'conversion' to convert the data to the specified 'unit'. e.g. if the data acquisition system stores values in this object as signed 16-bit integers (int16 range -32,768 to 32,767) that correspond to a 5V range (-2.5V to 2.5V), and the data acquisition system gain is 8000X, then the 'conversion' multiplier to get from raw data acquisition values to recorded volts is 2.5/32768/8000 = 9.5367e-9.","required":false},{"name":"offset","dtype":"float32","default_value":0.0,"doc":"Scalar to add to the data after scaling by 'conversion' to finalize its coercion to the specified 'unit'. Two common examples of this include (a) data stored in an unsigned type that requires a shift after scaling to re-center the data, and (b) specialized recording devices that naturally cause a scalar offset with respect to the true units.","required":false},{"name":"resolution","dtype":"float32","default_value":-1.0,"doc":"Smallest meaningful difference between values in data, stored in the specified by unit, e.g., the change in value of the least significant bit, or a larger number if signal noise is known to be present. If unknown, use -1.0.","required":false},{"name":"unit","dtype":"text","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."},{"name":"continuity","dtype":"text","doc":"Optionally describe the continuity of the data. Can be \"continuous\", \"instantaneous\", or \"step\". For example, a voltage trace would be \"continuous\", because samples are recorded from a continuous process. An array of lick times would be \"instantaneous\", because the data represents distinct moments in time. Times of image presentations would be \"step\" because the picture remains the same until the next timepoint. This field is optional, but is useful in providing information about the underlying data. It may inform the way this data is interpreted, the way it is visualized, and what analysis methods are applicable.","required":false}]},{"name":"starting_time","dtype":"float64","doc":"Timestamp of the first sample in seconds. When timestamps are uniformly spaced, the timestamp of the first sample can be specified and all subsequent ones calculated from the sampling rate attribute.","quantity":"?","attributes":[{"name":"rate","dtype":"float32","doc":"Sampling rate, in Hz."},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for time, which is fixed to 'seconds'."}]},{"name":"timestamps","dtype":"float64","dims":["num_times"],"shape":[null],"doc":"Timestamps for samples stored in data, in seconds, relative to the common experiment master-clock stored in NWBFile.timestamps_reference_time.","quantity":"?","attributes":[{"name":"interval","dtype":"int32","value":1,"doc":"Value is '1'"},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for timestamps, which is fixed to 'seconds'."}]},{"name":"control","dtype":"uint8","dims":["num_times"],"shape":[null],"doc":"Numerical labels that apply to each time point in data for the purpose of querying and slicing data by these values. If present, the length of this array should be the same size as the first dimension of data.","quantity":"?"},{"name":"control_description","dtype":"text","dims":["num_control_values"],"shape":[null],"doc":"Description of each control value. Must be present if control is present. If present, control_description[0] should describe time points where control == 0.","quantity":"?"}],"groups":[{"name":"sync","doc":"Lab-specific time and sync information as provided directly from hardware devices and that is necessary for aligning all acquired time information to a common timebase. The timestamp array stores time in the common timebase. This group will usually only be populated in TimeSeries that are stored external to the NWB file, in files storing raw data. Once timestamp data is calculated, the contents of 'sync' are mostly for archival purposes.","quantity":"?"}]},{"neurodata_type_def":"ProcessingModule","neurodata_type_inc":"NWBContainer","doc":"A collection of processed data.","attributes":[{"name":"description","dtype":"text","doc":"Description of this collection of processed data."}],"groups":[{"neurodata_type_inc":"NWBDataInterface","doc":"Data objects stored in this collection.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tables stored in this collection.","quantity":"*"}]},{"neurodata_type_def":"Images","neurodata_type_inc":"NWBDataInterface","default_name":"Images","doc":"A collection of images with an optional way to specify the order of the images using the \"order_of_images\" dataset. An order must be specified if the images are referenced by index, e.g., from an IndexSeries.","attributes":[{"name":"description","dtype":"text","doc":"Description of this collection of images."}],"datasets":[{"neurodata_type_inc":"Image","doc":"Images stored in this collection.","quantity":"+"},{"name":"order_of_images","neurodata_type_inc":"ImageReferences","doc":"Ordered dataset of references to Image objects stored in the parent group. Each Image object in the Images group should be stored once and only once, so the dataset should have the same length as the number of images.","quantity":"?"}]}]})delimiter"; + +const std::string nwb_device = R"delimiter( +{"groups":[{"neurodata_type_def":"Device","neurodata_type_inc":"NWBContainer","doc":"Metadata about a data acquisition device, e.g., recording system, electrode, microscope.","attributes":[{"name":"description","dtype":"text","doc":"Description of the device (e.g., model, firmware version, processing software version, etc.) as free-form text.","required":false},{"name":"manufacturer","dtype":"text","doc":"The name of the manufacturer of the device.","required":false}]}]})delimiter"; + +const std::string nwb_epoch = R"delimiter( +{"groups":[{"neurodata_type_def":"TimeIntervals","neurodata_type_inc":"DynamicTable","doc":"A container for aggregating epoch data and the TimeSeries that each epoch applies to.","datasets":[{"name":"start_time","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Start time of epoch, in seconds."},{"name":"stop_time","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Stop time of epoch, in seconds."},{"name":"tags","neurodata_type_inc":"VectorData","dtype":"text","doc":"User-defined tags that identify or categorize events.","quantity":"?"},{"name":"tags_index","neurodata_type_inc":"VectorIndex","doc":"Index for tags.","quantity":"?"},{"name":"timeseries","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"An index into a TimeSeries object.","quantity":"?"},{"name":"timeseries_index","neurodata_type_inc":"VectorIndex","doc":"Index for timeseries.","quantity":"?"}]}]})delimiter"; + +const std::string nwb_image = R"delimiter( +{"datasets":[{"neurodata_type_def":"GrayscaleImage","neurodata_type_inc":"Image","dims":["x","y"],"shape":[null,null],"doc":"A grayscale image.","dtype":"numeric"},{"neurodata_type_def":"RGBImage","neurodata_type_inc":"Image","dims":["x","y","r, g, b"],"shape":[null,null,3],"doc":"A color image.","dtype":"numeric"},{"neurodata_type_def":"RGBAImage","neurodata_type_inc":"Image","dims":["x","y","r, g, b, a"],"shape":[null,null,4],"doc":"A color image with transparency.","dtype":"numeric"}],"groups":[{"neurodata_type_def":"ImageSeries","neurodata_type_inc":"TimeSeries","doc":"General image data that is common between acquisition and stimulus time series. Sometimes the image data is stored in the file in a raw format while other times it will be stored as a series of external image files in the host file system. The data field will either be binary data, if the data is stored in the NWB file, or empty, if the data is stored in an external image stack. [frame][x][y] or [frame][x][y][z].","datasets":[{"name":"data","dtype":"numeric","dims":[["frame","x","y"],["frame","x","y","z"]],"shape":[[null,null,null],[null,null,null,null]],"doc":"Binary data representing images across frames. If data are stored in an external file, this should be an empty 3D array."},{"name":"dimension","dtype":"int32","dims":["rank"],"shape":[null],"doc":"Number of pixels on x, y, (and z) axes.","quantity":"?"},{"name":"external_file","dtype":"text","dims":["num_files"],"shape":[null],"doc":"Paths to one or more external file(s). The field is only present if format='external'. This is only relevant if the image series is stored in the file system as one or more image file(s). This field should NOT be used if the image is stored in another NWB file and that file is linked to this file.","quantity":"?","attributes":[{"name":"starting_frame","dtype":"int32","dims":["num_files"],"shape":[null],"doc":"Each external image may contain one or more consecutive frames of the full ImageSeries. This attribute serves as an index to indicate which frames each file contains, to facilitate random access. The 'starting_frame' attribute, hence, contains a list of frame numbers within the full ImageSeries of the first frame of each file listed in the parent 'external_file' dataset. Zero-based indexing is used (hence, the first element will always be zero). For example, if the 'external_file' dataset has three paths to files and the first file has 5 frames, the second file has 10 frames, and the third file has 20 frames, then this attribute will have values [0, 5, 15]. If there is a single external file that holds all of the frames of the ImageSeries (and so there is a single element in the 'external_file' dataset), then this attribute should have value [0]."}]},{"name":"format","dtype":"text","default_value":"raw","doc":"Format of image. If this is 'external', then the attribute 'external_file' contains the path information to the image files. If this is 'raw', then the raw (single-channel) binary data is stored in the 'data' dataset. If this attribute is not present, then the default format='raw' case is assumed.","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the Device object that was used to capture these images.","quantity":"?"}]},{"neurodata_type_def":"ImageMaskSeries","neurodata_type_inc":"ImageSeries","doc":"An alpha mask that is applied to a presented visual stimulus. The 'data' array contains an array of mask values that are applied to the displayed image. Mask values are stored as RGBA. Mask can vary with time. The timestamps array indicates the starting time of a mask, and that mask pattern continues until it's explicitly changed.","links":[{"name":"masked_imageseries","target_type":"ImageSeries","doc":"Link to ImageSeries object that this image mask is applied to."}]},{"neurodata_type_def":"OpticalSeries","neurodata_type_inc":"ImageSeries","doc":"Image data that is presented or recorded. A stimulus template movie will be stored only as an image. When the image is presented as stimulus, additional data is required, such as field of view (e.g., how much of the visual field the image covers, or how what is the area of the target being imaged). If the OpticalSeries represents acquired imaging data, orientation is also important.","datasets":[{"name":"distance","dtype":"float32","doc":"Distance from camera/monitor to target/eye.","quantity":"?"},{"name":"field_of_view","dtype":"float32","dims":[["width, height"],["width, height, depth"]],"shape":[[2],[3]],"doc":"Width, height and depth of image, or imaged area, in meters.","quantity":"?"},{"name":"data","dtype":"numeric","dims":[["frame","x","y"],["frame","x","y","r, g, b"]],"shape":[[null,null,null],[null,null,null,3]],"doc":"Images presented to subject, either grayscale or RGB"},{"name":"orientation","dtype":"text","doc":"Description of image relative to some reference frame (e.g., which way is up). Must also specify frame of reference.","quantity":"?"}]},{"neurodata_type_def":"IndexSeries","neurodata_type_inc":"TimeSeries","doc":"Stores indices to image frames stored in an ImageSeries. The purpose of the IndexSeries is to allow a static image stack to be stored in an Images object, and the images in the stack to be referenced out-of-order. This can be for the display of individual images, or of movie segments (as a movie is simply a series of images). The data field stores the index of the frame in the referenced Images object, and the timestamps array indicates when that image was displayed.","datasets":[{"name":"data","dtype":"uint32","dims":["num_times"],"shape":[null],"doc":"Index of the image (using zero-indexing) in the linked Images object.","attributes":[{"name":"conversion","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"resolution","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"offset","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"unit","dtype":"text","value":"N/A","doc":"This field is unused by IndexSeries and has the value N/A."}]}],"links":[{"name":"indexed_timeseries","target_type":"ImageSeries","doc":"Link to ImageSeries object containing images that are indexed. Use of this link is discouraged and will be deprecated. Link to an Images type instead.","quantity":"?"},{"name":"indexed_images","target_type":"Images","doc":"Link to Images object containing an ordered set of images that are indexed. The Images object must contain a 'ordered_images' dataset specifying the order of the images in the Images type.","quantity":"?"}]}]})delimiter"; + +const std::string nwb_file = R"delimiter( +{"groups":[{"neurodata_type_def":"NWBFile","neurodata_type_inc":"NWBContainer","name":"root","doc":"An NWB file storing cellular-based neurophysiology data from a single experimental session.","attributes":[{"name":"nwb_version","dtype":"text","value":"2.7.0-alpha","doc":"File version string. Use semantic versioning, e.g. 1.2.1. This will be the name of the format with trailing major, minor and patch numbers."}],"datasets":[{"name":"file_create_date","dtype":"isodatetime","dims":["num_modifications"],"shape":[null],"doc":"A record of the date the file was created and of subsequent modifications. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted strings: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds. The file can be created after the experiment was run, so this may differ from the experiment start time. Each modification to the nwb file adds a new entry to the array."},{"name":"identifier","dtype":"text","doc":"A unique text identifier for the file. For example, concatenated lab name, file creation date/time and experimentalist, or a hash of these and/or other values. The goal is that the string should be unique to all other files."},{"name":"session_description","dtype":"text","doc":"A description of the experimental session and data in the file."},{"name":"session_start_time","dtype":"isodatetime","doc":"Date and time of the experiment/session start. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted string: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds."},{"name":"timestamps_reference_time","dtype":"isodatetime","doc":"Date and time corresponding to time zero of all timestamps. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted string: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds. All times stored in the file use this time as reference (i.e., time zero)."}],"groups":[{"name":"acquisition","doc":"Data streams recorded from the system, including ephys, ophys, tracking, etc. This group should be read-only after the experiment is completed and timestamps are corrected to a common timebase. The data stored here may be links to raw data stored in external NWB files. This will allow keeping bulky raw data out of the file while preserving the option of keeping some/all in the file. Acquired data includes tracking and experimental data streams (i.e., everything measured from the system). If bulky data is stored in the /acquisition group, the data can exist in a separate NWB file that is linked to by the file being used for processing and analysis.","groups":[{"neurodata_type_inc":"NWBDataInterface","doc":"Acquired, raw data.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tabular data that is relevant to acquisition","quantity":"*"}]},{"name":"analysis","doc":"Lab-specific and custom scientific analysis of data. There is no defined format for the content of this group - the format is up to the individual user/lab. To facilitate sharing analysis data between labs, the contents here should be stored in standard types (e.g., neurodata_types) and appropriately documented. The file can store lab-specific and custom data analysis without restriction on its form or schema, reducing data formatting restrictions on end users. Such data should be placed in the analysis group. The analysis data should be documented so that it could be shared with other labs.","groups":[{"neurodata_type_inc":"NWBContainer","doc":"Custom analysis results.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tabular data that is relevant to data stored in analysis","quantity":"*"}]},{"name":"scratch","doc":"A place to store one-off analysis results. Data placed here is not intended for sharing. By placing data here, users acknowledge that there is no guarantee that their data meets any standard.","quantity":"?","groups":[{"neurodata_type_inc":"NWBContainer","doc":"Any one-off containers","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Any one-off tables","quantity":"*"}],"datasets":[{"neurodata_type_inc":"ScratchData","doc":"Any one-off datasets","quantity":"*"}]},{"name":"processing","doc":"The home for ProcessingModules. These modules perform intermediate analysis of data that is necessary to perform before scientific analysis. Examples include spike clustering, extracting position from tracking data, stitching together image slices. ProcessingModules can be large and express many data sets from relatively complex analysis (e.g., spike detection and clustering) or small, representing extraction of position information from tracking video, or even binary lick/no-lick decisions. Common software tools (e.g., klustakwik, MClust) are expected to read/write data here. 'Processing' refers to intermediate analysis of the acquired data to make it more amenable to scientific analysis.","groups":[{"neurodata_type_inc":"ProcessingModule","doc":"Intermediate analysis of acquired data.","quantity":"*"}]},{"name":"stimulus","doc":"Data pushed into the system (eg, video stimulus, sound, voltage, etc) and secondary representations of that data (eg, measurements of something used as a stimulus). This group should be made read-only after experiment complete and timestamps are corrected to common timebase. Stores both presented stimuli and stimulus templates, the latter in case the same stimulus is presented multiple times, or is pulled from an external stimulus library. Stimuli are here defined as any signal that is pushed into the system as part of the experiment (eg, sound, video, voltage, etc). Many different experiments can use the same stimuli, and stimuli can be re-used during an experiment. The stimulus group is organized so that one version of template stimuli can be stored and these be used multiple times. These templates can exist in the present file or can be linked to a remote library file.","groups":[{"name":"presentation","doc":"Stimuli presented during the experiment.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries objects containing data of presented stimuli.","quantity":"*"}]},{"name":"templates","doc":"Template stimuli. Timestamps in templates are based on stimulus design and are relative to the beginning of the stimulus. When templates are used, the stimulus instances must convert presentation times to the experiment`s time reference frame.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries objects containing template data of presented stimuli.","quantity":"*"},{"neurodata_type_inc":"Images","doc":"Images objects containing images of presented stimuli.","quantity":"*"}]}]},{"name":"general","doc":"Experimental metadata, including protocol, notes and description of hardware device(s). The metadata stored in this section should be used to describe the experiment. Metadata necessary for interpreting the data is stored with the data. General experimental metadata, including animal strain, experimental protocols, experimenter, devices, etc, are stored under 'general'. Core metadata (e.g., that required to interpret data fields) is stored with the data itself, and implicitly defined by the file specification (e.g., time is in seconds). The strategy used here for storing non-core metadata is to use free-form text fields, such as would appear in sentences or paragraphs from a Methods section. Metadata fields are text to enable them to be more general, for example to represent ranges instead of numerical values. Machine-readable metadata is stored as attributes to these free-form datasets. All entries in the below table are to be included when data is present. Unused groups (e.g., intracellular_ephys in an optophysiology experiment) should not be created unless there is data to store within them.","datasets":[{"name":"data_collection","dtype":"text","doc":"Notes about data collection and analysis.","quantity":"?"},{"name":"experiment_description","dtype":"text","doc":"General description of the experiment.","quantity":"?"},{"name":"experimenter","dtype":"text","doc":"Name of person(s) who performed the experiment. Can also specify roles of different people involved.","quantity":"?","dims":["num_experimenters"],"shape":[null]},{"name":"institution","dtype":"text","doc":"Institution(s) where experiment was performed.","quantity":"?"},{"name":"keywords","dtype":"text","dims":["num_keywords"],"shape":[null],"doc":"Terms to search over.","quantity":"?"},{"name":"lab","dtype":"text","doc":"Laboratory where experiment was performed.","quantity":"?"},{"name":"notes","dtype":"text","doc":"Notes about the experiment.","quantity":"?"},{"name":"pharmacology","dtype":"text","doc":"Description of drugs used, including how and when they were administered. Anesthesia(s), painkiller(s), etc., plus dosage, concentration, etc.","quantity":"?"},{"name":"protocol","dtype":"text","doc":"Experimental protocol, if applicable. e.g., include IACUC protocol number.","quantity":"?"},{"name":"related_publications","dtype":"text","doc":"Publication information. PMID, DOI, URL, etc.","dims":["num_publications"],"shape":[null],"quantity":"?"},{"name":"session_id","dtype":"text","doc":"Lab-specific ID for the session.","quantity":"?"},{"name":"slices","dtype":"text","doc":"Description of slices, including information about preparation thickness, orientation, temperature, and bath solution.","quantity":"?"},{"name":"source_script","dtype":"text","doc":"Script file or link to public source code used to create this NWB file.","quantity":"?","attributes":[{"name":"file_name","dtype":"text","doc":"Name of script file."}]},{"name":"stimulus","dtype":"text","doc":"Notes about stimuli, such as how and where they were presented.","quantity":"?"},{"name":"surgery","dtype":"text","doc":"Narrative description about surgery/surgeries, including date(s) and who performed surgery.","quantity":"?"},{"name":"virus","dtype":"text","doc":"Information about virus(es) used in experiments, including virus ID, source, date made, injection location, volume, etc.","quantity":"?"}],"groups":[{"neurodata_type_inc":"LabMetaData","doc":"Place-holder than can be extended so that lab-specific meta-data can be placed in /general.","quantity":"*"},{"name":"devices","doc":"Description of hardware devices used during experiment, e.g., monitors, ADC boards, microscopes, etc.","quantity":"?","groups":[{"neurodata_type_inc":"Device","doc":"Data acquisition devices.","quantity":"*"}]},{"name":"subject","neurodata_type_inc":"Subject","doc":"Information about the animal or person from which the data was measured.","quantity":"?"},{"name":"extracellular_ephys","doc":"Metadata related to extracellular electrophysiology.","quantity":"?","groups":[{"neurodata_type_inc":"ElectrodeGroup","doc":"Physical group of electrodes.","quantity":"*"},{"name":"electrodes","neurodata_type_inc":"DynamicTable","doc":"A table of all electrodes (i.e. channels) used for recording.","quantity":"?","datasets":[{"name":"x","neurodata_type_inc":"VectorData","dtype":"float32","doc":"x coordinate of the channel location in the brain (+x is posterior).","quantity":"?"},{"name":"y","neurodata_type_inc":"VectorData","dtype":"float32","doc":"y coordinate of the channel location in the brain (+y is inferior).","quantity":"?"},{"name":"z","neurodata_type_inc":"VectorData","dtype":"float32","doc":"z coordinate of the channel location in the brain (+z is right).","quantity":"?"},{"name":"imp","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Impedance of the channel, in ohms.","quantity":"?"},{"name":"location","neurodata_type_inc":"VectorData","dtype":"text","doc":"Location of the electrode (channel). Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."},{"name":"filtering","neurodata_type_inc":"VectorData","dtype":"text","doc":"Description of hardware filtering, including the filter name and frequency cutoffs.","quantity":"?"},{"name":"group","neurodata_type_inc":"VectorData","dtype":{"target_type":"ElectrodeGroup","reftype":"object"},"doc":"Reference to the ElectrodeGroup this electrode is a part of."},{"name":"group_name","neurodata_type_inc":"VectorData","dtype":"text","doc":"Name of the ElectrodeGroup this electrode is a part of."},{"name":"rel_x","neurodata_type_inc":"VectorData","dtype":"float32","doc":"x coordinate in electrode group","quantity":"?"},{"name":"rel_y","neurodata_type_inc":"VectorData","dtype":"float32","doc":"y coordinate in electrode group","quantity":"?"},{"name":"rel_z","neurodata_type_inc":"VectorData","dtype":"float32","doc":"z coordinate in electrode group","quantity":"?"},{"name":"reference","neurodata_type_inc":"VectorData","dtype":"text","doc":"Description of the reference electrode and/or reference scheme used for this electrode, e.g., \"stainless steel skull screw\" or \"online common average referencing\".","quantity":"?"}]}]},{"name":"intracellular_ephys","doc":"Metadata related to intracellular electrophysiology.","quantity":"?","datasets":[{"name":"filtering","dtype":"text","doc":"[DEPRECATED] Use IntracellularElectrode.filtering instead. Description of filtering used. Includes filtering type and parameters, frequency fall-off, etc. If this changes between TimeSeries, filter description should be stored as a text attribute for each TimeSeries.","quantity":"?"}],"groups":[{"neurodata_type_inc":"IntracellularElectrode","doc":"An intracellular electrode.","quantity":"*"},{"name":"sweep_table","neurodata_type_inc":"SweepTable","doc":"[DEPRECATED] Table used to group different PatchClampSeries. SweepTable is being replaced by IntracellularRecordingsTable and SimultaneousRecordingsTable tables. Additional SequentialRecordingsTable, RepetitionsTable and ExperimentalConditions tables provide enhanced support for experiment metadata.","quantity":"?"},{"name":"intracellular_recordings","neurodata_type_inc":"IntracellularRecordingsTable","doc":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording. Each row in the table represents a single recording consisting typically of a stimulus and a corresponding response. In some cases, however, only a stimulus or a response are recorded as as part of an experiment. In this case both, the stimulus and response will point to the same TimeSeries while the idx_start and count of the invalid column will be set to -1, thus, indicating that no values have been recorded for the stimulus or response, respectively. Note, a recording MUST contain at least a stimulus or a response. Typically the stimulus and response are PatchClampSeries. However, the use of AD/DA channels that are not associated to an electrode is also common in intracellular electrophysiology, in which case other TimeSeries may be used.","quantity":"?"},{"name":"simultaneous_recordings","neurodata_type_inc":"SimultaneousRecordingsTable","doc":"A table for grouping different intracellular recordings from the IntracellularRecordingsTable table together that were recorded simultaneously from different electrodes","quantity":"?"},{"name":"sequential_recordings","neurodata_type_inc":"SequentialRecordingsTable","doc":"A table for grouping different sequential recordings from the SimultaneousRecordingsTable table together. This is typically used to group together sequential recordings where the a sequence of stimuli of the same type with varying parameters have been presented in a sequence.","quantity":"?"},{"name":"repetitions","neurodata_type_inc":"RepetitionsTable","doc":"A table for grouping different sequential intracellular recordings together. With each SequentialRecording typically representing a particular type of stimulus, the RepetitionsTable table is typically used to group sets of stimuli applied in sequence.","quantity":"?"},{"name":"experimental_conditions","neurodata_type_inc":"ExperimentalConditionsTable","doc":"A table for grouping different intracellular recording repetitions together that belong to the same experimental experimental_conditions.","quantity":"?"}]},{"name":"optogenetics","doc":"Metadata describing optogenetic stimuluation.","quantity":"?","groups":[{"neurodata_type_inc":"OptogeneticStimulusSite","doc":"An optogenetic stimulation site.","quantity":"*"}]},{"name":"optophysiology","doc":"Metadata related to optophysiology.","quantity":"?","groups":[{"neurodata_type_inc":"ImagingPlane","doc":"An imaging plane.","quantity":"*"}]}]},{"name":"intervals","doc":"Experimental intervals, whether that be logically distinct sub-experiments having a particular scientific goal, trials (see trials subgroup) during an experiment, or epochs (see epochs subgroup) deriving from analysis of data.","quantity":"?","groups":[{"name":"epochs","neurodata_type_inc":"TimeIntervals","doc":"Divisions in time marking experimental stages or sub-divisions of a single recording session.","quantity":"?"},{"name":"trials","neurodata_type_inc":"TimeIntervals","doc":"Repeated experimental events that have a logical grouping.","quantity":"?"},{"name":"invalid_times","neurodata_type_inc":"TimeIntervals","doc":"Time intervals that should be removed from analysis.","quantity":"?"},{"neurodata_type_inc":"TimeIntervals","doc":"Optional additional table(s) for describing other experimental time intervals.","quantity":"*"}]},{"name":"units","neurodata_type_inc":"Units","doc":"Data about sorted spike units.","quantity":"?"}]},{"neurodata_type_def":"LabMetaData","neurodata_type_inc":"NWBContainer","doc":"Lab-specific meta-data."},{"neurodata_type_def":"Subject","neurodata_type_inc":"NWBContainer","doc":"Information about the animal or person from which the data was measured.","datasets":[{"name":"age","dtype":"text","doc":"Age of subject. Can be supplied instead of 'date_of_birth'.","quantity":"?","attributes":[{"name":"reference","doc":"Age is with reference to this event. Can be 'birth' or 'gestational'. If reference is omitted, 'birth' is implied.","dtype":"text","required":false,"default_value":"birth"}]},{"name":"date_of_birth","dtype":"isodatetime","doc":"Date of birth of subject. Can be supplied instead of 'age'.","quantity":"?"},{"name":"description","dtype":"text","doc":"Description of subject and where subject came from (e.g., breeder, if animal).","quantity":"?"},{"name":"genotype","dtype":"text","doc":"Genetic strain. If absent, assume Wild Type (WT).","quantity":"?"},{"name":"sex","dtype":"text","doc":"Gender of subject.","quantity":"?"},{"name":"species","dtype":"text","doc":"Species of subject.","quantity":"?"},{"name":"strain","dtype":"text","doc":"Strain of subject.","quantity":"?"},{"name":"subject_id","dtype":"text","doc":"ID of animal/person used/participating in experiment (lab convention).","quantity":"?"},{"name":"weight","dtype":"text","doc":"Weight at time of experiment, at time of surgery and at other important times.","quantity":"?"}]}],"datasets":[{"neurodata_type_def":"ScratchData","neurodata_type_inc":"NWBData","doc":"Any one-off datasets","attributes":[{"name":"notes","doc":"Any notes the user has about the dataset being stored","dtype":"text"}]}]})delimiter"; + +const std::string nwb_misc = R"delimiter( +{"groups":[{"neurodata_type_def":"AbstractFeatureSeries","neurodata_type_inc":"TimeSeries","doc":"Abstract features, such as quantitative descriptions of sensory stimuli. The TimeSeries::data field is a 2D array, storing those features (e.g., for visual grating stimulus this might be orientation, spatial frequency and contrast). Null stimuli (eg, uniform gray) can be marked as being an independent feature (eg, 1.0 for gray, 0.0 for actual stimulus) or by storing NaNs for feature values, or through use of the TimeSeries::control fields. A set of features is considered to persist until the next set of features is defined. The final set of features stored should be the null set. This is useful when storing the raw stimulus is impractical.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_features"]],"shape":[[null],[null,null]],"doc":"Values of each feature at each time.","attributes":[{"name":"unit","dtype":"text","default_value":"see 'feature_units'","doc":"Since there can be different units for different features, store the units in 'feature_units'. The default value for this attribute is \"see 'feature_units'\".","required":false}]},{"name":"feature_units","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Units of each feature.","quantity":"?"},{"name":"features","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Description of the features represented in TimeSeries::data."}]},{"neurodata_type_def":"AnnotationSeries","neurodata_type_inc":"TimeSeries","doc":"Stores user annotations made during an experiment. The data[] field stores a text array, and timestamps are stored for each annotation (ie, interval=1). This is largely an alias to a standard TimeSeries storing a text array but that is identifiable as storing annotations in a machine-readable way.","datasets":[{"name":"data","dtype":"text","dims":["num_times"],"shape":[null],"doc":"Annotations made during an experiment.","attributes":[{"name":"resolution","dtype":"float32","value":-1.0,"doc":"Smallest meaningful difference between values in data. Annotations have no units, so the value is fixed to -1.0."},{"name":"unit","dtype":"text","value":"n/a","doc":"Base unit of measurement for working with the data. Annotations have no units, so the value is fixed to 'n/a'."}]}]},{"neurodata_type_def":"IntervalSeries","neurodata_type_inc":"TimeSeries","doc":"Stores intervals of data. The timestamps field stores the beginning and end of intervals. The data field stores whether the interval just started (>0 value) or ended (<0 value). Different interval types can be represented in the same series by using multiple key values (eg, 1 for feature A, 2 for feature B, 3 for feature C, etc). The field data stores an 8-bit integer. This is largely an alias of a standard TimeSeries but that is identifiable as representing time intervals in a machine-readable way.","datasets":[{"name":"data","dtype":"int8","dims":["num_times"],"shape":[null],"doc":"Use values >0 if interval started, <0 if interval ended.","attributes":[{"name":"resolution","dtype":"float32","value":-1.0,"doc":"Smallest meaningful difference between values in data. Annotations have no units, so the value is fixed to -1.0."},{"name":"unit","dtype":"text","value":"n/a","doc":"Base unit of measurement for working with the data. Annotations have no units, so the value is fixed to 'n/a'."}]}]},{"neurodata_type_def":"DecompositionSeries","neurodata_type_inc":"TimeSeries","doc":"Spectral analysis of a time series, e.g. of an LFP or a speech signal.","datasets":[{"name":"data","dtype":"numeric","dims":["num_times","num_channels","num_bands"],"shape":[null,null,null],"doc":"Data decomposed into frequency bands.","attributes":[{"name":"unit","dtype":"text","default_value":"no unit","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion'."}]},{"name":"metric","dtype":"text","doc":"The metric used, e.g. phase, amplitude, power."},{"name":"source_channels","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the channels that this decomposition series was generated from.","quantity":"?"}],"groups":[{"name":"bands","neurodata_type_inc":"DynamicTable","doc":"Table for describing the bands that this series was generated from. There should be one row in this table for each band.","datasets":[{"name":"band_name","neurodata_type_inc":"VectorData","dtype":"text","doc":"Name of the band, e.g. theta."},{"name":"band_limits","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands","low, high"],"shape":[null,2],"doc":"Low and high limit of each band in Hz. If it is a Gaussian filter, use 2 SD on either side of the center."},{"name":"band_mean","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands"],"shape":[null],"doc":"The mean Gaussian filters, in Hz."},{"name":"band_stdev","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands"],"shape":[null],"doc":"The standard deviation of Gaussian filters, in Hz."}]}],"links":[{"name":"source_timeseries","target_type":"TimeSeries","doc":"Link to TimeSeries object that this data was calculated from. Metadata about electrodes and their position can be read from that ElectricalSeries so it is not necessary to store that information here.","quantity":"?"}]},{"neurodata_type_def":"Units","neurodata_type_inc":"DynamicTable","default_name":"Units","doc":"Data about spiking units. Event times of observed units (e.g. cell, synapse, etc.) should be concatenated and stored in spike_times.","datasets":[{"name":"spike_times_index","neurodata_type_inc":"VectorIndex","doc":"Index into the spike_times dataset.","quantity":"?"},{"name":"spike_times","neurodata_type_inc":"VectorData","dtype":"float64","doc":"Spike times for each unit in seconds.","quantity":"?","attributes":[{"name":"resolution","dtype":"float64","doc":"The smallest possible difference between two spike times. Usually 1 divided by the acquisition sampling rate from which spike times were extracted, but could be larger if the acquisition time series was downsampled or smaller if the acquisition time series was smoothed/interpolated and it is possible for the spike time to be between samples.","required":false}]},{"name":"obs_intervals_index","neurodata_type_inc":"VectorIndex","doc":"Index into the obs_intervals dataset.","quantity":"?"},{"name":"obs_intervals","neurodata_type_inc":"VectorData","dtype":"float64","dims":["num_intervals","start|end"],"shape":[null,2],"doc":"Observation intervals for each unit.","quantity":"?"},{"name":"electrodes_index","neurodata_type_inc":"VectorIndex","doc":"Index into electrodes.","quantity":"?"},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"Electrode that each spike unit came from, specified using a DynamicTableRegion.","quantity":"?"},{"name":"electrode_group","neurodata_type_inc":"VectorData","dtype":{"target_type":"ElectrodeGroup","reftype":"object"},"doc":"Electrode group that each spike unit came from.","quantity":"?"},{"name":"waveform_mean","neurodata_type_inc":"VectorData","dtype":"float32","dims":[["num_units","num_samples"],["num_units","num_samples","num_electrodes"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveform mean for each spike unit.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveform_sd","neurodata_type_inc":"VectorData","dtype":"float32","dims":[["num_units","num_samples"],["num_units","num_samples","num_electrodes"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveform standard deviation for each spike unit.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveforms","neurodata_type_inc":"VectorData","dtype":"numeric","dims":["num_waveforms","num_samples"],"shape":[null,null],"doc":"Individual waveforms for each spike on each electrode. This is a doubly indexed column. The 'waveforms_index' column indexes which waveforms in this column belong to the same spike event for a given unit, where each waveform was recorded from a different electrode. The 'waveforms_index_index' column indexes the 'waveforms_index' column to indicate which spike events belong to a given unit. For example, if the 'waveforms_index_index' column has values [2, 5, 6], then the first 2 elements of the 'waveforms_index' column correspond to the 2 spike events of the first unit, the next 3 elements of the 'waveforms_index' column correspond to the 3 spike events of the second unit, and the next 1 element of the 'waveforms_index' column corresponds to the 1 spike event of the third unit. If the 'waveforms_index' column has values [3, 6, 8, 10, 12, 13], then the first 3 elements of the 'waveforms' column contain the 3 spike waveforms that were recorded from 3 different electrodes for the first spike time of the first unit. See https://nwb-schema.readthedocs.io/en/stable/format_description.html#doubly-ragged-arrays for a graphical representation of this example. When there is only one electrode for each unit (i.e., each spike time is associated with a single waveform), then the 'waveforms_index' column will have values 1, 2, ..., N, where N is the number of spike events. The number of electrodes for each spike event should be the same within a given unit. The 'electrodes' column should be used to indicate which electrodes are associated with each unit, and the order of the waveforms within a given unit x spike event should be in the same order as the electrodes referenced in the 'electrodes' column of this table. The number of samples for each waveform must be the same.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveforms_index","neurodata_type_inc":"VectorIndex","doc":"Index into the waveforms dataset. One value for every spike event. See 'waveforms' for more detail.","quantity":"?"},{"name":"waveforms_index_index","neurodata_type_inc":"VectorIndex","doc":"Index into the waveforms_index dataset. One value for every unit (row in the table). See 'waveforms' for more detail.","quantity":"?"}]}]})delimiter"; + +const std::string nwb_behavior = R"delimiter( +{"groups":[{"neurodata_type_def":"SpatialSeries","neurodata_type_inc":"TimeSeries","doc":"Direction, e.g., of gaze or travel, or position. The TimeSeries::data field is a 2D array storing position or direction relative to some reference frame. Array structure: [num measurements] [num dimensions]. Each SpatialSeries has a text dataset reference_frame that indicates the zero-position, or the zero-axes for direction. For example, if representing gaze direction, 'straight-ahead' might be a specific pixel on the monitor, or some other point in space. For position data, the 0,0 point might be the top-left corner of an enclosure, as viewed from the tracking camera. The unit of data will indicate how to interpret SpatialSeries values.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","x"],["num_times","x,y"],["num_times","x,y,z"]],"shape":[[null],[null,1],[null,2],[null,3]],"doc":"1-D or 2-D array storing position or direction relative to some reference frame.","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Base unit of measurement for working with the data. The default value is 'meters'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'.","required":false}]},{"name":"reference_frame","dtype":"text","doc":"Description defining what exactly 'straight-ahead' means.","quantity":"?"}]},{"neurodata_type_def":"BehavioralEpochs","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralEpochs","doc":"TimeSeries for storing behavioral epochs. The objective of this and the other two Behavioral interfaces (e.g. BehavioralEvents and BehavioralTimeSeries) is to provide generic hooks for software tools/scripts. This allows a tool/script to take the output one specific interface (e.g., UnitTimes) and plot that data relative to another data modality (e.g., behavioral events) without having to define all possible modalities in advance. Declaring one of these interfaces means that one or more TimeSeries of the specified type is published. These TimeSeries should reside in a group having the same name as the interface. For example, if a BehavioralTimeSeries interface is declared, the module will have one or more TimeSeries defined in the module sub-group 'BehavioralTimeSeries'. BehavioralEpochs should use IntervalSeries. BehavioralEvents is used for irregular events. BehavioralTimeSeries is for continuous data.","groups":[{"neurodata_type_inc":"IntervalSeries","doc":"IntervalSeries object containing start and stop times of epochs.","quantity":"*"}]},{"neurodata_type_def":"BehavioralEvents","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralEvents","doc":"TimeSeries for storing behavioral events. See description of BehavioralEpochs for more details.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing behavioral events.","quantity":"*"}]},{"neurodata_type_def":"BehavioralTimeSeries","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralTimeSeries","doc":"TimeSeries for storing Behavoioral time series data. See description of BehavioralEpochs for more details.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing continuous behavioral data.","quantity":"*"}]},{"neurodata_type_def":"PupilTracking","neurodata_type_inc":"NWBDataInterface","default_name":"PupilTracking","doc":"Eye-tracking data, representing pupil size.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing time series data on pupil size.","quantity":"+"}]},{"neurodata_type_def":"EyeTracking","neurodata_type_inc":"NWBDataInterface","default_name":"EyeTracking","doc":"Eye-tracking data, representing direction of gaze.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing data measuring direction of gaze.","quantity":"*"}]},{"neurodata_type_def":"CompassDirection","neurodata_type_inc":"NWBDataInterface","default_name":"CompassDirection","doc":"With a CompassDirection interface, a module publishes a SpatialSeries object representing a floating point value for theta. The SpatialSeries::reference_frame field should indicate what direction corresponds to 0 and which is the direction of rotation (this should be clockwise). The si_unit for the SpatialSeries should be radians or degrees.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing direction of gaze travel.","quantity":"*"}]},{"neurodata_type_def":"Position","neurodata_type_inc":"NWBDataInterface","default_name":"Position","doc":"Position data, whether along the x, x/y or x/y/z axis.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing position data.","quantity":"+"}]}]})delimiter"; + +const std::string nwb_ecephys = R"delimiter( +{"groups":[{"neurodata_type_def":"ElectricalSeries","neurodata_type_inc":"TimeSeries","doc":"A time series of acquired voltage data from extracellular recordings. The data field is an int or float array storing data in volts. The first dimension should always represent time. The second dimension, if present, should represent channels.","attributes":[{"name":"filtering","dtype":"text","doc":"Filtering applied to all channels of the data. For example, if this ElectricalSeries represents high-pass-filtered data (also known as AP Band), then this value could be \"High-pass 4-pole Bessel filter at 500 Hz\". If this ElectricalSeries represents low-pass-filtered LFP data and the type of filter is unknown, then this value could be \"Low-pass filter at 300 Hz\". If a non-standard filter type is used, provide as much detail about the filter properties as possible.","required":false}],"datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_channels"],["num_times","num_channels","num_samples"]],"shape":[[null],[null,null],[null,null,null]],"doc":"Recorded voltage data.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. This value is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion', followed by 'channel_conversion' (if present), and then add 'offset'."}]},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the electrodes that this time series was generated from."},{"name":"channel_conversion","dtype":"float32","dims":["num_channels"],"shape":[null],"doc":"Channel-specific conversion factor. Multiply the data in the 'data' dataset by these values along the channel axis (as indicated by axis attribute) AND by the global conversion factor in the 'conversion' attribute of 'data' to get the data values in Volts, i.e, data in Volts = data * data.conversion * channel_conversion. This approach allows for both global and per-channel data conversion factors needed to support the storage of electrical recordings as native values generated by data acquisition systems. If this dataset is not present, then there is no channel-specific conversion factor, i.e. it is 1 for all channels.","quantity":"?","attributes":[{"name":"axis","dtype":"int32","value":1,"doc":"The zero-indexed axis of the 'data' dataset that the channel-specific conversion factor corresponds to. This value is fixed to 1."}]}]},{"neurodata_type_def":"SpikeEventSeries","neurodata_type_inc":"ElectricalSeries","doc":"Stores snapshots/snippets of recorded spike events (i.e., threshold crossings). This may also be raw data, as reported by ephys hardware. If so, the TimeSeries::description field should describe how events were detected. All SpikeEventSeries should reside in a module (under EventWaveform interface) even if the spikes were reported and stored by hardware. All events span the same recording channels and store snapshots of equal duration. TimeSeries::data array structure: [num events] [num channels] [num samples] (or [num events] [num samples] for single electrode).","datasets":[{"name":"data","dtype":"numeric","dims":[["num_events","num_samples"],["num_events","num_channels","num_samples"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveforms.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement for waveforms, which is fixed to 'volts'."}]},{"name":"timestamps","dtype":"float64","dims":["num_times"],"shape":[null],"doc":"Timestamps for samples stored in data, in seconds, relative to the common experiment master-clock stored in NWBFile.timestamps_reference_time. Timestamps are required for the events. Unlike for TimeSeries, timestamps are required for SpikeEventSeries and are thus re-specified here.","attributes":[{"name":"interval","dtype":"int32","value":1,"doc":"Value is '1'"},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for timestamps, which is fixed to 'seconds'."}]}]},{"neurodata_type_def":"FeatureExtraction","neurodata_type_inc":"NWBDataInterface","default_name":"FeatureExtraction","doc":"Features, such as PC1 and PC2, that are extracted from signals stored in a SpikeEventSeries or other source.","datasets":[{"name":"description","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Description of features (eg, ''PC1'') for each of the extracted features."},{"name":"features","dtype":"float32","dims":["num_events","num_channels","num_features"],"shape":[null,null,null],"doc":"Multi-dimensional array of features extracted from each event."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Times of events that features correspond to (can be a link)."},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the electrodes that this time series was generated from."}]},{"neurodata_type_def":"EventDetection","neurodata_type_inc":"NWBDataInterface","default_name":"EventDetection","doc":"Detected spike events from voltage trace(s).","datasets":[{"name":"detection_method","dtype":"text","doc":"Description of how events were detected, such as voltage threshold, or dV/dT threshold, as well as relevant values."},{"name":"source_idx","dtype":"int32","dims":["num_events"],"shape":[null],"doc":"Indices (zero-based) into source ElectricalSeries::data array corresponding to time of event. ''description'' should define what is meant by time of event (e.g., .25 ms before action potential peak, zero-crossing time, etc). The index points to each event from the raw data."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Timestamps of events, in seconds.","attributes":[{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for event times, which is fixed to 'seconds'."}]}],"links":[{"name":"source_electricalseries","target_type":"ElectricalSeries","doc":"Link to the ElectricalSeries that this data was calculated from. Metadata about electrodes and their position can be read from that ElectricalSeries so it's not necessary to include that information here."}]},{"neurodata_type_def":"EventWaveform","neurodata_type_inc":"NWBDataInterface","default_name":"EventWaveform","doc":"Represents either the waveforms of detected events, as extracted from a raw data trace in /acquisition, or the event waveforms that were stored during experiment acquisition.","groups":[{"neurodata_type_inc":"SpikeEventSeries","doc":"SpikeEventSeries object(s) containing detected spike event waveforms.","quantity":"*"}]},{"neurodata_type_def":"FilteredEphys","neurodata_type_inc":"NWBDataInterface","default_name":"FilteredEphys","doc":"Electrophysiology data from one or more channels that has been subjected to filtering. Examples of filtered data include Theta and Gamma (LFP has its own interface). FilteredEphys modules publish an ElectricalSeries for each filtered channel or set of channels. The name of each ElectricalSeries is arbitrary but should be informative. The source of the filtered data, whether this is from analysis of another time series or as acquired by hardware, should be noted in each's TimeSeries::description field. There is no assumed 1::1 correspondence between filtered ephys signals and electrodes, as a single signal can apply to many nearby electrodes, and one electrode may have different filtered (e.g., theta and/or gamma) signals represented. Filter properties should be noted in the ElectricalSeries 'filtering' attribute.","groups":[{"neurodata_type_inc":"ElectricalSeries","doc":"ElectricalSeries object(s) containing filtered electrophysiology data.","quantity":"+"}]},{"neurodata_type_def":"LFP","neurodata_type_inc":"NWBDataInterface","default_name":"LFP","doc":"LFP data from one or more channels. The electrode map in each published ElectricalSeries will identify which channels are providing LFP data. Filter properties should be noted in the ElectricalSeries 'filtering' attribute.","groups":[{"neurodata_type_inc":"ElectricalSeries","doc":"ElectricalSeries object(s) containing LFP data for one or more channels.","quantity":"+"}]},{"neurodata_type_def":"ElectrodeGroup","neurodata_type_inc":"NWBContainer","doc":"A physical grouping of electrodes, e.g. a shank of an array.","attributes":[{"name":"description","dtype":"text","doc":"Description of this electrode group."},{"name":"location","dtype":"text","doc":"Location of electrode group. Specify the area, layer, comments on estimation of area/layer, etc. Use standard atlas names for anatomical regions when possible."}],"datasets":[{"name":"position","dtype":[{"name":"x","dtype":"float32","doc":"x coordinate"},{"name":"y","dtype":"float32","doc":"y coordinate"},{"name":"z","dtype":"float32","doc":"z coordinate"}],"doc":"stereotaxic or common framework coordinates","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the device that was used to record from this electrode group."}]},{"neurodata_type_def":"ClusterWaveforms","neurodata_type_inc":"NWBDataInterface","default_name":"ClusterWaveforms","doc":"DEPRECATED The mean waveform shape, including standard deviation, of the different clusters. Ideally, the waveform analysis should be performed on data that is only high-pass filtered. This is a separate module because it is expected to require updating. For example, IMEC probes may require different storage requirements to store/display mean waveforms, requiring a new interface or an extension of this one.","datasets":[{"name":"waveform_filtering","dtype":"text","doc":"Filtering applied to data before generating mean/sd"},{"name":"waveform_mean","dtype":"float32","dims":["num_clusters","num_samples"],"shape":[null,null],"doc":"The mean waveform for each cluster, using the same indices for each wave as cluster numbers in the associated Clustering module (i.e, cluster 3 is in array slot [3]). Waveforms corresponding to gaps in cluster sequence should be empty (e.g., zero- filled)"},{"name":"waveform_sd","dtype":"float32","dims":["num_clusters","num_samples"],"shape":[null,null],"doc":"Stdev of waveforms for each cluster, using the same indices as in mean"}],"links":[{"name":"clustering_interface","target_type":"Clustering","doc":"Link to Clustering interface that was the source of the clustered data"}]},{"neurodata_type_def":"Clustering","neurodata_type_inc":"NWBDataInterface","default_name":"Clustering","doc":"DEPRECATED Clustered spike data, whether from automatic clustering tools (e.g., klustakwik) or as a result of manual sorting.","datasets":[{"name":"description","dtype":"text","doc":"Description of clusters or clustering, (e.g. cluster 0 is noise, clusters curated using Klusters, etc)"},{"name":"num","dtype":"int32","dims":["num_events"],"shape":[null],"doc":"Cluster number of each event"},{"name":"peak_over_rms","dtype":"float32","dims":["num_clusters"],"shape":[null],"doc":"Maximum ratio of waveform peak to RMS on any channel in the cluster (provides a basic clustering metric)."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Times of clustered events, in seconds. This may be a link to times field in associated FeatureExtraction module."}]}]})delimiter"; + +const std::string nwb_icephys = R"delimiter( +{"groups":[{"neurodata_type_def":"PatchClampSeries","neurodata_type_inc":"TimeSeries","doc":"An abstract base class for patch-clamp data - stimulus or response, current or voltage.","attributes":[{"name":"stimulus_description","dtype":"text","doc":"Protocol/stimulus name for this patch-clamp dataset."},{"name":"sweep_number","dtype":"uint32","doc":"Sweep number, allows to group different PatchClampSeries together.","required":false}],"datasets":[{"name":"data","dtype":"numeric","dims":["num_times"],"shape":[null],"doc":"Recorded voltage or current.","attributes":[{"name":"unit","dtype":"text","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"gain","dtype":"float32","doc":"Gain of the recording, in units Volt/Amp (v-clamp) or Volt/Volt (c-clamp).","quantity":"?"}],"links":[{"name":"electrode","target_type":"IntracellularElectrode","doc":"Link to IntracellularElectrode object that describes the electrode that was used to apply or record this data."}]},{"neurodata_type_def":"CurrentClampSeries","neurodata_type_inc":"PatchClampSeries","doc":"Voltage data from an intracellular current-clamp recording. A corresponding CurrentClampStimulusSeries (stored separately as a stimulus) is used to store the current injected.","datasets":[{"name":"data","doc":"Recorded voltage.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. which is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"bias_current","dtype":"float32","doc":"Bias current, in amps.","quantity":"?"},{"name":"bridge_balance","dtype":"float32","doc":"Bridge balance, in ohms.","quantity":"?"},{"name":"capacitance_compensation","dtype":"float32","doc":"Capacitance compensation, in farads.","quantity":"?"}]},{"neurodata_type_def":"IZeroClampSeries","neurodata_type_inc":"CurrentClampSeries","doc":"Voltage data from an intracellular recording when all current and amplifier settings are off (i.e., CurrentClampSeries fields will be zero). There is no CurrentClampStimulusSeries associated with an IZero series because the amplifier is disconnected and no stimulus can reach the cell.","attributes":[{"name":"stimulus_description","dtype":"text","doc":"An IZeroClampSeries has no stimulus, so this attribute is automatically set to \"N/A\"","value":"N/A"}],"datasets":[{"name":"bias_current","dtype":"float32","value":0.0,"doc":"Bias current, in amps, fixed to 0.0."},{"name":"bridge_balance","dtype":"float32","value":0.0,"doc":"Bridge balance, in ohms, fixed to 0.0."},{"name":"capacitance_compensation","dtype":"float32","value":0.0,"doc":"Capacitance compensation, in farads, fixed to 0.0."}]},{"neurodata_type_def":"CurrentClampStimulusSeries","neurodata_type_inc":"PatchClampSeries","doc":"Stimulus current applied during current clamp recording.","datasets":[{"name":"data","doc":"Stimulus current applied.","attributes":[{"name":"unit","dtype":"text","value":"amperes","doc":"Base unit of measurement for working with the data. which is fixed to 'amperes'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]}]},{"neurodata_type_def":"VoltageClampSeries","neurodata_type_inc":"PatchClampSeries","doc":"Current data from an intracellular voltage-clamp recording. A corresponding VoltageClampStimulusSeries (stored separately as a stimulus) is used to store the voltage injected.","datasets":[{"name":"data","doc":"Recorded current.","attributes":[{"name":"unit","dtype":"text","value":"amperes","doc":"Base unit of measurement for working with the data. which is fixed to 'amperes'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"capacitance_fast","dtype":"float32","doc":"Fast capacitance, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for capacitance_fast, which is fixed to 'farads'."}]},{"name":"capacitance_slow","dtype":"float32","doc":"Slow capacitance, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for capacitance_fast, which is fixed to 'farads'."}]},{"name":"resistance_comp_bandwidth","dtype":"float32","doc":"Resistance compensation bandwidth, in hertz.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"hertz","doc":"Unit of measurement for resistance_comp_bandwidth, which is fixed to 'hertz'."}]},{"name":"resistance_comp_correction","dtype":"float32","doc":"Resistance compensation correction, in percent.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"percent","doc":"Unit of measurement for resistance_comp_correction, which is fixed to 'percent'."}]},{"name":"resistance_comp_prediction","dtype":"float32","doc":"Resistance compensation prediction, in percent.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"percent","doc":"Unit of measurement for resistance_comp_prediction, which is fixed to 'percent'."}]},{"name":"whole_cell_capacitance_comp","dtype":"float32","doc":"Whole cell capacitance compensation, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for whole_cell_capacitance_comp, which is fixed to 'farads'."}]},{"name":"whole_cell_series_resistance_comp","dtype":"float32","doc":"Whole cell series resistance compensation, in ohms.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"ohms","doc":"Unit of measurement for whole_cell_series_resistance_comp, which is fixed to 'ohms'."}]}]},{"neurodata_type_def":"VoltageClampStimulusSeries","neurodata_type_inc":"PatchClampSeries","doc":"Stimulus voltage applied during a voltage clamp recording.","datasets":[{"name":"data","doc":"Stimulus voltage applied.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. which is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]}]},{"neurodata_type_def":"IntracellularElectrode","neurodata_type_inc":"NWBContainer","doc":"An intracellular electrode and its metadata.","datasets":[{"name":"cell_id","dtype":"text","doc":"unique ID of the cell","quantity":"?"},{"name":"description","dtype":"text","doc":"Description of electrode (e.g., whole-cell, sharp, etc.)."},{"name":"filtering","dtype":"text","doc":"Electrode specific filtering.","quantity":"?"},{"name":"initial_access_resistance","dtype":"text","doc":"Initial access resistance.","quantity":"?"},{"name":"location","dtype":"text","doc":"Location of the electrode. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible.","quantity":"?"},{"name":"resistance","dtype":"text","doc":"Electrode resistance, in ohms.","quantity":"?"},{"name":"seal","dtype":"text","doc":"Information about seal used for recording.","quantity":"?"},{"name":"slice","dtype":"text","doc":"Information about slice used for recording.","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Device that was used to record from this electrode."}]},{"neurodata_type_def":"SweepTable","neurodata_type_inc":"DynamicTable","doc":"[DEPRECATED] Table used to group different PatchClampSeries. SweepTable is being replaced by IntracellularRecordingsTable and SimultaneousRecordingsTable tables. Additional SequentialRecordingsTable, RepetitionsTable, and ExperimentalConditions tables provide enhanced support for experiment metadata.","datasets":[{"name":"sweep_number","neurodata_type_inc":"VectorData","dtype":"uint32","doc":"Sweep number of the PatchClampSeries in that row."},{"name":"series","neurodata_type_inc":"VectorData","dtype":{"target_type":"PatchClampSeries","reftype":"object"},"doc":"The PatchClampSeries with the sweep number in that row."},{"name":"series_index","neurodata_type_inc":"VectorIndex","doc":"Index for series."}]},{"neurodata_type_def":"IntracellularElectrodesTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular electrode related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular electrode related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"electrode","neurodata_type_inc":"VectorData","dtype":{"target_type":"IntracellularElectrode","reftype":"object"},"doc":"Column for storing the reference to the intracellular electrode."}]},{"neurodata_type_def":"IntracellularStimuliTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular stimulus related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular stimulus related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"stimulus","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the recorded stimulus for the recording (rows)."},{"name":"stimulus_template","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the stimulus template for the recording (rows).","quantity":"?"}]},{"neurodata_type_def":"IntracellularResponsesTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular response related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular response related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"response","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the recorded response for the recording (rows)"}]},{"neurodata_type_def":"IntracellularRecordingsTable","neurodata_type_inc":"AlignedDynamicTable","name":"intracellular_recordings","doc":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording. Each row in the table represents a single recording consisting typically of a stimulus and a corresponding response. In some cases, however, only a stimulus or a response is recorded as part of an experiment. In this case, both the stimulus and response will point to the same TimeSeries while the idx_start and count of the invalid column will be set to -1, thus, indicating that no values have been recorded for the stimulus or response, respectively. Note, a recording MUST contain at least a stimulus or a response. Typically the stimulus and response are PatchClampSeries. However, the use of AD/DA channels that are not associated to an electrode is also common in intracellular electrophysiology, in which case other TimeSeries may be used.","attributes":[{"name":"description","dtype":"text","value":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording and for storing metadata about the intracellular recording.","doc":"Description of the contents of this table. Inherited from AlignedDynamicTable and overwritten here to fix the value of the attribute."}],"groups":[{"name":"electrodes","neurodata_type_inc":"IntracellularElectrodesTable","doc":"Table for storing intracellular electrode related metadata."},{"name":"stimuli","neurodata_type_inc":"IntracellularStimuliTable","doc":"Table for storing intracellular stimulus related metadata."},{"name":"responses","neurodata_type_inc":"IntracellularResponsesTable","doc":"Table for storing intracellular response related metadata."}]},{"neurodata_type_def":"SimultaneousRecordingsTable","neurodata_type_inc":"DynamicTable","name":"simultaneous_recordings","doc":"A table for grouping different intracellular recordings from the IntracellularRecordingsTable table together that were recorded simultaneously from different electrodes.","datasets":[{"name":"recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the IntracellularRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"IntracellularRecordingsTable","reftype":"object"},"doc":"Reference to the IntracellularRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the recordings column."}]},{"neurodata_type_def":"SequentialRecordingsTable","neurodata_type_inc":"DynamicTable","name":"sequential_recordings","doc":"A table for grouping different sequential recordings from the SimultaneousRecordingsTable table together. This is typically used to group together sequential recordings where a sequence of stimuli of the same type with varying parameters have been presented in a sequence.","datasets":[{"name":"simultaneous_recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the SimultaneousRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"SimultaneousRecordingsTable","reftype":"object"},"doc":"Reference to the SimultaneousRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"simultaneous_recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the simultaneous_recordings column."},{"name":"stimulus_type","neurodata_type_inc":"VectorData","dtype":"text","doc":"The type of stimulus used for the sequential recording."}]},{"neurodata_type_def":"RepetitionsTable","neurodata_type_inc":"DynamicTable","name":"repetitions","doc":"A table for grouping different sequential intracellular recordings together. With each SequentialRecording typically representing a particular type of stimulus, the RepetitionsTable table is typically used to group sets of stimuli applied in sequence.","datasets":[{"name":"sequential_recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the SequentialRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"SequentialRecordingsTable","reftype":"object"},"doc":"Reference to the SequentialRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"sequential_recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the sequential_recordings column."}]},{"neurodata_type_def":"ExperimentalConditionsTable","neurodata_type_inc":"DynamicTable","name":"experimental_conditions","doc":"A table for grouping different intracellular recording repetitions together that belong to the same experimental condition.","datasets":[{"name":"repetitions","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the RepetitionsTable table.","attributes":[{"name":"table","dtype":{"target_type":"RepetitionsTable","reftype":"object"},"doc":"Reference to the RepetitionsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"repetitions_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the repetitions column."}]}]})delimiter"; + +const std::string nwb_ogen = R"delimiter( +{"groups":[{"neurodata_type_def":"OptogeneticSeries","neurodata_type_inc":"TimeSeries","doc":"An optogenetic stimulus.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_rois"]],"shape":[[null],[null,null]],"doc":"Applied power for optogenetic stimulus, in watts. Shape can be 1D or 2D. 2D data is meant to be used in an extension of OptogeneticSeries that defines what the second dimension represents.","attributes":[{"name":"unit","dtype":"text","value":"watts","doc":"Unit of measurement for data, which is fixed to 'watts'."}]}],"links":[{"name":"site","target_type":"OptogeneticStimulusSite","doc":"Link to OptogeneticStimulusSite object that describes the site to which this stimulus was applied."}]},{"neurodata_type_def":"OptogeneticStimulusSite","neurodata_type_inc":"NWBContainer","doc":"A site of optogenetic stimulation.","datasets":[{"name":"description","dtype":"text","doc":"Description of stimulation site."},{"name":"excitation_lambda","dtype":"float32","doc":"Excitation wavelength, in nm."},{"name":"location","dtype":"text","doc":"Location of the stimulation site. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."}],"links":[{"name":"device","target_type":"Device","doc":"Device that generated the stimulus."}]}]})delimiter"; + +const std::string nwb_ophys = R"delimiter( +{"groups":[{"neurodata_type_def":"OnePhotonSeries","neurodata_type_inc":"ImageSeries","doc":"Image stack recorded over time from 1-photon microscope.","attributes":[{"name":"pmt_gain","dtype":"float32","doc":"Photomultiplier gain.","required":false},{"name":"scan_line_rate","dtype":"float32","doc":"Lines imaged per second. This is also stored in /general/optophysiology but is kept here as it is useful information for analysis, and so good to be stored w/ the actual data.","required":false},{"name":"exposure_time","dtype":"float32","doc":"Exposure time of the sample; often the inverse of the frequency.","required":false},{"name":"binning","dtype":"uint8","doc":"Amount of pixels combined into 'bins'; could be 1, 2, 4, 8, etc.","required":false},{"name":"power","dtype":"float32","doc":"Power of the excitation in mW, if known.","required":false},{"name":"intensity","dtype":"float32","doc":"Intensity of the excitation in mW/mm^2, if known.","required":false}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this TimeSeries data was generated."}]},{"neurodata_type_def":"TwoPhotonSeries","neurodata_type_inc":"ImageSeries","doc":"Image stack recorded over time from 2-photon microscope.","attributes":[{"name":"pmt_gain","dtype":"float32","doc":"Photomultiplier gain.","required":false},{"name":"scan_line_rate","dtype":"float32","doc":"Lines imaged per second. This is also stored in /general/optophysiology but is kept here as it is useful information for analysis, and so good to be stored w/ the actual data.","required":false}],"datasets":[{"name":"field_of_view","dtype":"float32","dims":[["width|height"],["width|height|depth"]],"shape":[[2],[3]],"doc":"Width, height and depth of image, or imaged area, in meters.","quantity":"?"}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this TimeSeries data was generated."}]},{"neurodata_type_def":"RoiResponseSeries","neurodata_type_inc":"TimeSeries","doc":"ROI responses over an imaging plane. The first dimension represents time. The second dimension, if present, represents ROIs.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_ROIs"]],"shape":[[null],[null,null]],"doc":"Signals from ROIs."},{"name":"rois","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion referencing into an ROITable containing information on the ROIs stored in this timeseries."}]},{"neurodata_type_def":"DfOverF","neurodata_type_inc":"NWBDataInterface","default_name":"DfOverF","doc":"dF/F information about a region of interest (ROI). Storage hierarchy of dF/F should be the same as for segmentation (i.e., same names for ROIs and for image planes).","groups":[{"neurodata_type_inc":"RoiResponseSeries","doc":"RoiResponseSeries object(s) containing dF/F for a ROI.","quantity":"+"}]},{"neurodata_type_def":"Fluorescence","neurodata_type_inc":"NWBDataInterface","default_name":"Fluorescence","doc":"Fluorescence information about a region of interest (ROI). Storage hierarchy of fluorescence should be the same as for segmentation (ie, same names for ROIs and for image planes).","groups":[{"neurodata_type_inc":"RoiResponseSeries","doc":"RoiResponseSeries object(s) containing fluorescence data for a ROI.","quantity":"+"}]},{"neurodata_type_def":"ImageSegmentation","neurodata_type_inc":"NWBDataInterface","default_name":"ImageSegmentation","doc":"Stores pixels in an image that represent different regions of interest (ROIs) or masks. All segmentation for a given imaging plane is stored together, with storage for multiple imaging planes (masks) supported. Each ROI is stored in its own subgroup, with the ROI group containing both a 2D mask and a list of pixels that make up this mask. Segments can also be used for masking neuropil. If segmentation is allowed to change with time, a new imaging plane (or module) is required and ROI names should remain consistent between them.","groups":[{"neurodata_type_inc":"PlaneSegmentation","doc":"Results from image segmentation of a specific imaging plane.","quantity":"+"}]},{"neurodata_type_def":"PlaneSegmentation","neurodata_type_inc":"DynamicTable","doc":"Results from image segmentation of a specific imaging plane.","datasets":[{"name":"image_mask","neurodata_type_inc":"VectorData","dims":[["num_roi","num_x","num_y"],["num_roi","num_x","num_y","num_z"]],"shape":[[null,null,null],[null,null,null,null]],"doc":"ROI masks for each ROI. Each image mask is the size of the original imaging plane (or volume) and members of the ROI are finite non-zero.","quantity":"?"},{"name":"pixel_mask_index","neurodata_type_inc":"VectorIndex","doc":"Index into pixel_mask.","quantity":"?"},{"name":"pixel_mask","neurodata_type_inc":"VectorData","dtype":[{"name":"x","dtype":"uint32","doc":"Pixel x-coordinate."},{"name":"y","dtype":"uint32","doc":"Pixel y-coordinate."},{"name":"weight","dtype":"float32","doc":"Weight of the pixel."}],"doc":"Pixel masks for each ROI: a list of indices and weights for the ROI. Pixel masks are concatenated and parsing of this dataset is maintained by the PlaneSegmentation","quantity":"?"},{"name":"voxel_mask_index","neurodata_type_inc":"VectorIndex","doc":"Index into voxel_mask.","quantity":"?"},{"name":"voxel_mask","neurodata_type_inc":"VectorData","dtype":[{"name":"x","dtype":"uint32","doc":"Voxel x-coordinate."},{"name":"y","dtype":"uint32","doc":"Voxel y-coordinate."},{"name":"z","dtype":"uint32","doc":"Voxel z-coordinate."},{"name":"weight","dtype":"float32","doc":"Weight of the voxel."}],"doc":"Voxel masks for each ROI: a list of indices and weights for the ROI. Voxel masks are concatenated and parsing of this dataset is maintained by the PlaneSegmentation","quantity":"?"}],"groups":[{"name":"reference_images","doc":"Image stacks that the segmentation masks apply to.","groups":[{"neurodata_type_inc":"ImageSeries","doc":"One or more image stacks that the masks apply to (can be one-element stack).","quantity":"*"}]}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this data was generated."}]},{"neurodata_type_def":"ImagingPlane","neurodata_type_inc":"NWBContainer","doc":"An imaging plane and its metadata.","datasets":[{"name":"description","dtype":"text","doc":"Description of the imaging plane.","quantity":"?"},{"name":"excitation_lambda","dtype":"float32","doc":"Excitation wavelength, in nm."},{"name":"imaging_rate","dtype":"float32","doc":"Rate that images are acquired, in Hz. If the corresponding TimeSeries is present, the rate should be stored there instead.","quantity":"?"},{"name":"indicator","dtype":"text","doc":"Calcium indicator."},{"name":"location","dtype":"text","doc":"Location of the imaging plane. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."},{"name":"manifold","dtype":"float32","dims":[["height","width","x, y, z"],["height","width","depth","x, y, z"]],"shape":[[null,null,3],[null,null,null,3]],"doc":"DEPRECATED Physical position of each pixel. 'xyz' represents the position of the pixel relative to the defined coordinate space. Deprecated in favor of origin_coords and grid_spacing.","quantity":"?","attributes":[{"name":"conversion","dtype":"float32","default_value":1.0,"doc":"Scalar to multiply each element in data to convert it to the specified 'unit'. If the data are stored in acquisition system units or other units that require a conversion to be interpretable, multiply the data by 'conversion' to convert the data to the specified 'unit'. e.g. if the data acquisition system stores values in this object as pixels from x = -500 to 499, y = -500 to 499 that correspond to a 2 m x 2 m range, then the 'conversion' multiplier to get from raw data acquisition pixel units to meters is 2/1000.","required":false},{"name":"unit","dtype":"text","default_value":"meters","doc":"Base unit of measurement for working with the data. The default value is 'meters'.","required":false}]},{"name":"origin_coords","dtype":"float32","dims":[["x, y"],["x, y, z"]],"shape":[[2],[3]],"doc":"Physical location of the first element of the imaging plane (0, 0) for 2-D data or (0, 0, 0) for 3-D data. See also reference_frame for what the physical location is relative to (e.g., bregma).","quantity":"?","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Measurement units for origin_coords. The default value is 'meters'."}]},{"name":"grid_spacing","dtype":"float32","dims":[["x, y"],["x, y, z"]],"shape":[[2],[3]],"doc":"Space between pixels in (x, y) or voxels in (x, y, z) directions, in the specified unit. Assumes imaging plane is a regular grid. See also reference_frame to interpret the grid.","quantity":"?","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Measurement units for grid_spacing. The default value is 'meters'."}]},{"name":"reference_frame","dtype":"text","doc":"Describes reference frame of origin_coords and grid_spacing. For example, this can be a text description of the anatomical location and orientation of the grid defined by origin_coords and grid_spacing or the vectors needed to transform or rotate the grid to a common anatomical axis (e.g., AP/DV/ML). This field is necessary to interpret origin_coords and grid_spacing. If origin_coords and grid_spacing are not present, then this field is not required. For example, if the microscope takes 10 x 10 x 2 images, where the first value of the data matrix (index (0, 0, 0)) corresponds to (-1.2, -0.6, -2) mm relative to bregma, the spacing between pixels is 0.2 mm in x, 0.2 mm in y and 0.5 mm in z, and larger numbers in x means more anterior, larger numbers in y means more rightward, and larger numbers in z means more ventral, then enter the following -- origin_coords = (-1.2, -0.6, -2) grid_spacing = (0.2, 0.2, 0.5) reference_frame = \"Origin coordinates are relative to bregma. First dimension corresponds to anterior-posterior axis (larger index = more anterior). Second dimension corresponds to medial-lateral axis (larger index = more rightward). Third dimension corresponds to dorsal-ventral axis (larger index = more ventral).\"","quantity":"?"}],"groups":[{"neurodata_type_inc":"OpticalChannel","doc":"An optical channel used to record from an imaging plane.","quantity":"+"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the Device object that was used to record from this electrode."}]},{"neurodata_type_def":"OpticalChannel","neurodata_type_inc":"NWBContainer","doc":"An optical channel used to record from an imaging plane.","datasets":[{"name":"description","dtype":"text","doc":"Description or other notes about the channel."},{"name":"emission_lambda","dtype":"float32","doc":"Emission wavelength for channel, in nm."}]},{"neurodata_type_def":"MotionCorrection","neurodata_type_inc":"NWBDataInterface","default_name":"MotionCorrection","doc":"An image stack where all frames are shifted (registered) to a common coordinate system, to account for movement and drift between frames. Note: each frame at each point in time is assumed to be 2-D (has only x & y dimensions).","groups":[{"neurodata_type_inc":"CorrectedImageStack","doc":"Results from motion correction of an image stack.","quantity":"+"}]},{"neurodata_type_def":"CorrectedImageStack","neurodata_type_inc":"NWBDataInterface","doc":"Results from motion correction of an image stack.","groups":[{"name":"corrected","neurodata_type_inc":"ImageSeries","doc":"Image stack with frames shifted to the common coordinates."},{"name":"xy_translation","neurodata_type_inc":"TimeSeries","doc":"Stores the x,y delta necessary to align each frame to the common coordinates, for example, to align each frame to a reference image."}],"links":[{"name":"original","target_type":"ImageSeries","doc":"Link to ImageSeries object that is being registered."}]}]})delimiter"; + +const std::string nwb_retinotopy = R"delimiter( +{"groups":[{"neurodata_type_def":"ImagingRetinotopy","neurodata_type_inc":"NWBDataInterface","default_name":"ImagingRetinotopy","doc":"DEPRECATED. Intrinsic signal optical imaging or widefield imaging for measuring retinotopy. Stores orthogonal maps (e.g., altitude/azimuth; radius/theta) of responses to specific stimuli and a combined polarity map from which to identify visual areas. This group does not store the raw responses imaged during retinotopic mapping or the stimuli presented, but rather the resulting phase and power maps after applying a Fourier transform on the averaged responses. Note: for data consistency, all images and arrays are stored in the format [row][column] and [row, col], which equates to [y][x]. Field of view and dimension arrays may appear backward (i.e., y before x).","datasets":[{"name":"axis_1_phase_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Phase response to stimulus on the first measured axis.","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_1_power_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Power response on the first measured axis. Response is scaled so 0.0 is no power in the response and 1.0 is maximum relative power.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_2_phase_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Phase response to stimulus on the second measured axis.","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_2_power_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Power response on the second measured axis. Response is scaled so 0.0 is no power in the response and 1.0 is maximum relative power.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_descriptions","dtype":"text","dims":["axis_1, axis_2"],"shape":[2],"doc":"Two-element array describing the contents of the two response axis fields. Description should be something like ['altitude', 'azimuth'] or '['radius', 'theta']."},{"name":"focal_depth_image","dtype":"uint16","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Gray-scale image taken with same settings/parameters (e.g., focal depth, wavelength) as data collection. Array format: [rows][columns].","quantity":"?","attributes":[{"name":"bits_per_pixel","dtype":"int32","doc":"Number of bits used to represent each value. This is necessary to determine maximum (white) pixel value."},{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"focal_depth","dtype":"float32","doc":"Focal depth offset, in meters."},{"name":"format","dtype":"text","doc":"Format of image. Right now only 'raw' is supported."}]},{"name":"sign_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Sine of the angle between the direction of the gradient in axis_1 and axis_2.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."}]},{"name":"vasculature_image","dtype":"uint16","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Gray-scale anatomical image of cortical surface. Array structure: [rows][columns]","attributes":[{"name":"bits_per_pixel","dtype":"int32","doc":"Number of bits used to represent each value. This is necessary to determine maximum (white) pixel value"},{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"format","dtype":"text","doc":"Format of image. Right now only 'raw' is supported."}]}]}]})delimiter"; + +const std::string namespaces = R"delimiter( +{"namespaces":[{"name":"core","doc":"NWB namespace","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter","Keith Godfrey","Jeff Teeters"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov","keithg@alleninstitute.org","jteeters@berkeley.edu"],"full_name":"NWB core","schema":[{"namespace":"hdmf-common"},{"source":"nwb.base"},{"source":"nwb.device"},{"source":"nwb.epoch"},{"source":"nwb.image"},{"source":"nwb.file"},{"source":"nwb.misc"},{"source":"nwb.behavior"},{"source":"nwb.ecephys"},{"source":"nwb.icephys"},{"source":"nwb.ogen"},{"source":"nwb.ophys"},{"source":"nwb.retinotopy"}],"version":"2.7.0"}]})delimiter"; + +void registerVariables(std::map& registry) { + registry["nwb.base"] = &nwb_base; + registry["nwb.device"] = &nwb_device; + registry["nwb.epoch"] = &nwb_epoch; + registry["nwb.image"] = &nwb_image; + registry["nwb.file"] = &nwb_file; + registry["nwb.misc"] = &nwb_misc; + registry["nwb.behavior"] = &nwb_behavior; + registry["nwb.ecephys"] = &nwb_ecephys; + registry["nwb.icephys"] = &nwb_icephys; + registry["nwb.ogen"] = &nwb_ogen; + registry["nwb.ophys"] = &nwb_ophys; + registry["nwb.retinotopy"] = &nwb_retinotopy; + registry["namespace"] = &namespaces; +}; +} // namespace AQNWB::spec::core diff --git a/Source/aqnwb/spec/hdmf_common.hpp b/Source/aqnwb/spec/hdmf_common.hpp new file mode 100644 index 0000000..90ea926 --- /dev/null +++ b/Source/aqnwb/spec/hdmf_common.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +namespace AQNWB::spec::hdmf_common +{ + +const std::string version = "1.8.0"; + +const std::string base = R"delimiter( +{"datasets":[{"data_type_def":"Data","doc":"An abstract data type for a dataset."}],"groups":[{"data_type_def":"Container","doc":"An abstract data type for a group storing collections of data and metadata. Base type for all data and metadata containers."},{"data_type_def":"SimpleMultiContainer","data_type_inc":"Container","doc":"A simple Container for holding onto multiple containers.","datasets":[{"data_type_inc":"Data","quantity":"*","doc":"Data objects held within this SimpleMultiContainer."}],"groups":[{"data_type_inc":"Container","quantity":"*","doc":"Container objects held within this SimpleMultiContainer."}]}]})delimiter"; + +const std::string table = R"delimiter( +{"datasets":[{"data_type_def":"VectorData","data_type_inc":"Data","doc":"An n-dimensional dataset representing a column of a DynamicTable. If used without an accompanying VectorIndex, first dimension is along the rows of the DynamicTable and each step along the first dimension is a cell of the larger table. VectorData can also be used to represent a ragged array if paired with a VectorIndex. This allows for storing arrays of varying length in a single cell of the DynamicTable by indexing into this VectorData. The first vector is at VectorData[0:VectorIndex[0]]. The second vector is at VectorData[VectorIndex[0]:VectorIndex[1]], and so on.","dims":[["dim0"],["dim0","dim1"],["dim0","dim1","dim2"],["dim0","dim1","dim2","dim3"]],"shape":[[null],[null,null],[null,null,null],[null,null,null,null]],"attributes":[{"name":"description","dtype":"text","doc":"Description of what these vectors represent."}]},{"data_type_def":"VectorIndex","data_type_inc":"VectorData","dtype":"uint8","doc":"Used with VectorData to encode a ragged array. An array of indices into the first dimension of the target VectorData, and forming a map between the rows of a DynamicTable and the indices of the VectorData. The name of the VectorIndex is expected to be the name of the target VectorData object followed by \"_index\".","dims":["num_rows"],"shape":[null],"attributes":[{"name":"target","dtype":{"target_type":"VectorData","reftype":"object"},"doc":"Reference to the target dataset that this index applies to."}]},{"data_type_def":"ElementIdentifiers","data_type_inc":"Data","default_name":"element_id","dtype":"int","dims":["num_elements"],"shape":[null],"doc":"A list of unique identifiers for values within a dataset, e.g. rows of a DynamicTable."},{"data_type_def":"DynamicTableRegion","data_type_inc":"VectorData","dtype":"int","doc":"DynamicTableRegion provides a link from one table to an index or region of another. The `table` attribute is a link to another `DynamicTable`, indicating which table is referenced, and the data is int(s) indicating the row(s) (0-indexed) of the target array. `DynamicTableRegion`s can be used to associate rows with repeated meta-data without data duplication. They can also be used to create hierarchical relationships between multiple `DynamicTable`s. `DynamicTableRegion` objects may be paired with a `VectorIndex` object to create ragged references, so a single cell of a `DynamicTable` can reference many rows of another `DynamicTable`.","dims":["num_rows"],"shape":[null],"attributes":[{"name":"table","dtype":{"target_type":"DynamicTable","reftype":"object"},"doc":"Reference to the DynamicTable object that this region applies to."},{"name":"description","dtype":"text","doc":"Description of what this table region points to."}]}],"groups":[{"data_type_def":"DynamicTable","data_type_inc":"Container","doc":"A group containing multiple datasets that are aligned on the first dimension (Currently, this requirement if left up to APIs to check and enforce). These datasets represent different columns in the table. Apart from a column that contains unique identifiers for each row, there are no other required datasets. Users are free to add any number of custom VectorData objects (columns) here. DynamicTable also supports ragged array columns, where each element can be of a different size. To add a ragged array column, use a VectorIndex type to index the corresponding VectorData type. See documentation for VectorData and VectorIndex for more details. Unlike a compound data type, which is analogous to storing an array-of-structs, a DynamicTable can be thought of as a struct-of-arrays. This provides an alternative structure to choose from when optimizing storage for anticipated access patterns. Additionally, this type provides a way of creating a table without having to define a compound type up front. Although this convenience may be attractive, users should think carefully about how data will be accessed. DynamicTable is more appropriate for column-centric access, whereas a dataset with a compound type would be more appropriate for row-centric access. Finally, data size should also be taken into account. For small tables, performance loss may be an acceptable trade-off for the flexibility of a DynamicTable.","attributes":[{"name":"colnames","dtype":"text","dims":["num_columns"],"shape":[null],"doc":"The names of the columns in this table. This should be used to specify an order to the columns."},{"name":"description","dtype":"text","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"id","data_type_inc":"ElementIdentifiers","dtype":"int","dims":["num_rows"],"shape":[null],"doc":"Array of unique identifiers for the rows of this dynamic table."},{"data_type_inc":"VectorData","doc":"Vector columns, including index columns, of this dynamic table.","quantity":"*"}]},{"data_type_def":"AlignedDynamicTable","data_type_inc":"DynamicTable","doc":"DynamicTable container that supports storing a collection of sub-tables. Each sub-table is a DynamicTable itself that is aligned with the main table by row index. I.e., all DynamicTables stored in this group MUST have the same number of rows. This type effectively defines a 2-level table in which the main data is stored in the main table implemented by this type and additional columns of the table are grouped into categories, with each category being represented by a separate DynamicTable stored within the group.","attributes":[{"name":"categories","dtype":"text","dims":["num_categories"],"shape":[null],"doc":"The names of the categories in this AlignedDynamicTable. Each category is represented by one DynamicTable stored in the parent group. This attribute should be used to specify an order of categories and the category names must match the names of the corresponding DynamicTable in the group."}],"groups":[{"data_type_inc":"DynamicTable","doc":"A DynamicTable representing a particular category for columns in the AlignedDynamicTable parent container. The table MUST be aligned with (i.e., have the same number of rows) as all other DynamicTables stored in the AlignedDynamicTable parent container. The name of the category is given by the name of the DynamicTable and its description by the description attribute of the DynamicTable.","quantity":"*"}]}]})delimiter"; + +const std::string sparse = R"delimiter( +{"groups":[{"data_type_def":"CSRMatrix","data_type_inc":"Container","doc":"A compressed sparse row matrix. Data are stored in the standard CSR format, where column indices for row i are stored in indices[indptr[i]:indptr[i+1]] and their corresponding values are stored in data[indptr[i]:indptr[i+1]].","attributes":[{"name":"shape","dtype":"uint","dims":["number of rows, number of columns"],"shape":[2],"doc":"The shape (number of rows, number of columns) of this sparse matrix."}],"datasets":[{"name":"indices","dtype":"uint","dims":["number of non-zero values"],"shape":[null],"doc":"The column indices."},{"name":"indptr","dtype":"uint","dims":["number of rows in the matrix + 1"],"shape":[null],"doc":"The row index pointer."},{"name":"data","dims":["number of non-zero values"],"shape":[null],"doc":"The non-zero values in the matrix."}]}]})delimiter"; + +const std::string namespaces = R"delimiter( +{"namespaces":[{"name":"hdmf-common","doc":"Common data structures provided by HDMF","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov"],"full_name":"HDMF Common","schema":[{"source":"base"},{"source":"table"},{"source":"sparse"}],"version":"1.8.0"}]})delimiter"; + +void registerVariables(std::map& registry) { + registry["base"] = &base; + registry["table"] = &table; + registry["sparse"] = &sparse; + registry["namespace"] = &namespaces; +}; +} // namespace AQNWB::spec::hdmf_common diff --git a/Source/aqnwb/spec/hdmf_experimental.hpp b/Source/aqnwb/spec/hdmf_experimental.hpp new file mode 100644 index 0000000..ef20ca5 --- /dev/null +++ b/Source/aqnwb/spec/hdmf_experimental.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace AQNWB::spec::hdmf_experimental +{ + +const std::string version = "0.5.0"; + +const std::string experimental = R"delimiter( +{"groups":[],"datasets":[{"data_type_def":"EnumData","data_type_inc":"VectorData","dtype":"uint8","doc":"Data that come from a fixed set of values. A data value of i corresponds to the i-th value in the VectorData referenced by the 'elements' attribute.","attributes":[{"name":"elements","dtype":{"target_type":"VectorData","reftype":"object"},"doc":"Reference to the VectorData object that contains the enumerable elements"}]}]})delimiter"; + +const std::string resources = R"delimiter( +{"groups":[{"data_type_def":"HERD","data_type_inc":"Container","doc":"HDMF External Resources Data Structure. A set of six tables for tracking external resource references in a file or across multiple files.","datasets":[{"data_type_inc":"Data","name":"keys","doc":"A table for storing user terms that are used to refer to external resources.","dtype":[{"name":"key","dtype":"text","doc":"The user term that maps to one or more resources in the `resources` table, e.g., \"human\"."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"files","doc":"A table for storing object ids of files used in external resources.","dtype":[{"name":"file_object_id","dtype":"text","doc":"The object id (UUID) of a file that contains objects that refers to external resources."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"entities","doc":"A table for mapping user terms (i.e., keys) to resource entities.","dtype":[{"name":"entity_id","dtype":"text","doc":"The compact uniform resource identifier (CURIE) of the entity, in the form [prefix]:[unique local identifier], e.g., 'NCBI_TAXON:9606'."},{"name":"entity_uri","dtype":"text","doc":"The URI for the entity this reference applies to. This can be an empty string. e.g., https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=info&id=9606"}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"objects","doc":"A table for identifying which objects in a file contain references to external resources.","dtype":[{"name":"files_idx","dtype":"uint","doc":"The row index to the file in the `files` table containing the object."},{"name":"object_id","dtype":"text","doc":"The object id (UUID) of the object."},{"name":"object_type","dtype":"text","doc":"The data type of the object."},{"name":"relative_path","dtype":"text","doc":"The relative path from the data object with the `object_id` to the dataset or attribute with the value(s) that is associated with an external resource. This can be an empty string if the object is a dataset that contains the value(s) that is associated with an external resource."},{"name":"field","dtype":"text","doc":"The field within the compound data type using an external resource. This is used only if the dataset or attribute is a compound data type; otherwise this should be an empty string."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"object_keys","doc":"A table for identifying which objects use which keys.","dtype":[{"name":"objects_idx","dtype":"uint","doc":"The row index to the object in the `objects` table that holds the key"},{"name":"keys_idx","dtype":"uint","doc":"The row index to the key in the `keys` table."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"entity_keys","doc":"A table for identifying which keys use which entity.","dtype":[{"name":"entities_idx","dtype":"uint","doc":"The row index to the entity in the `entities` table."},{"name":"keys_idx","dtype":"uint","doc":"The row index to the key in the `keys` table."}],"dims":["num_rows"],"shape":[null]}]}]})delimiter"; + +const std::string namespaces = R"delimiter( +{"namespaces":[{"name":"hdmf-experimental","doc":"Experimental data structures provided by HDMF. These are not guaranteed to be available in the future.","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter","Matthew Avaylon"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov","mavaylon@lbl.gov"],"full_name":"HDMF Experimental","schema":[{"namespace":"hdmf-common"},{"source":"experimental"},{"source":"resources"}],"version":"0.5.0"}]})delimiter"; + +void registerVariables(std::map& registry) { + registry["experimental"] = &experimental; + registry["resources"] = &resources; + registry["namespace"] = &namespaces; +}; +} // namespace AQNWB::spec::hdmf_experimental From c314a2548df4b691aaf1094c1d2a800a6193569f Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Fri, 30 Aug 2024 13:18:48 -0500 Subject: [PATCH 11/32] update cmake file to include aqnwb lib --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index f3a0de2..7b825f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,8 @@ set(SOURCE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/Source) file(GLOB_RECURSE SRC_FILES LIST_DIRECTORIES false "${SOURCE_PATH}/*.cpp" "${SOURCE_PATH}/*.h" "${SOURCE_PATH}/*.hpp") set(GUI_COMMONLIB_DIR ${GUI_BASE_DIR}/installed_libs) +include_directories(${SOURCE_PATH}/aqnwb) + set(CONFIGURATION_FOLDER $<$:Debug>$<$>:Release>) list(APPEND CMAKE_PREFIX_PATH ${GUI_COMMONLIB_DIR} ${GUI_COMMONLIB_DIR}/${CONFIGURATION_FOLDER}) From 05d5c3b027f6bdaf56a32d349dcd0ce2a8a383f3 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:53:19 -0500 Subject: [PATCH 12/32] update aqnwb package --- Source/aqnwb/Utils.hpp | 4 +- Source/aqnwb/aqnwb.hpp | 3 - Source/aqnwb/hdf5/HDF5IO.cpp | 4 +- Source/aqnwb/hdf5/HDF5IO.hpp | 4 +- Source/aqnwb/nwb/NWBRecording.cpp | 68 ---------------- Source/aqnwb/nwb/NWBRecording.hpp | 80 ------------------- Source/aqnwb/nwb/base/TimeSeries.cpp | 2 +- Source/aqnwb/nwb/base/TimeSeries.hpp | 4 +- Source/aqnwb/nwb/device/Device.cpp | 2 +- Source/aqnwb/nwb/device/Device.hpp | 4 +- Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp | 4 +- Source/aqnwb/nwb/ecephys/ElectricalSeries.hpp | 6 +- Source/aqnwb/nwb/file/ElectrodeGroup.cpp | 2 +- Source/aqnwb/nwb/file/ElectrodeGroup.hpp | 6 +- Source/aqnwb/nwb/file/ElectrodeTable.cpp | 5 +- Source/aqnwb/nwb/file/ElectrodeTable.hpp | 8 +- Source/aqnwb/nwb/hdmf/base/Container.cpp | 2 +- Source/aqnwb/nwb/hdmf/base/Container.hpp | 2 +- Source/aqnwb/nwb/hdmf/base/Data.hpp | 2 +- Source/aqnwb/nwb/hdmf/table/DynamicTable.cpp | 2 +- Source/aqnwb/nwb/hdmf/table/DynamicTable.hpp | 8 +- Source/aqnwb/nwb/hdmf/table/VectorData.cpp | 2 +- Source/aqnwb/nwb/hdmf/table/VectorData.hpp | 2 +- Source/aqnwb/spec/core.hpp | 61 +++++++------- Source/aqnwb/spec/hdmf_common.hpp | 25 +++--- Source/aqnwb/spec/hdmf_experimental.hpp | 21 ++--- 26 files changed, 92 insertions(+), 241 deletions(-) delete mode 100644 Source/aqnwb/aqnwb.hpp delete mode 100644 Source/aqnwb/nwb/NWBRecording.cpp delete mode 100644 Source/aqnwb/nwb/NWBRecording.hpp diff --git a/Source/aqnwb/Utils.hpp b/Source/aqnwb/Utils.hpp index b1a8581..f98d5b3 100644 --- a/Source/aqnwb/Utils.hpp +++ b/Source/aqnwb/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/Source/aqnwb/aqnwb.hpp b/Source/aqnwb/aqnwb.hpp deleted file mode 100644 index dad5802..0000000 --- a/Source/aqnwb/aqnwb.hpp +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -#include diff --git a/Source/aqnwb/hdf5/HDF5IO.cpp b/Source/aqnwb/hdf5/HDF5IO.cpp index ef7cc60..bf4b69a 100644 --- a/Source/aqnwb/hdf5/HDF5IO.cpp +++ b/Source/aqnwb/hdf5/HDF5IO.cpp @@ -4,12 +4,12 @@ #include #include -#include "HDF5IO.hpp" +#include "hdf5/HDF5IO.hpp" #include #include -#include "../Utils.hpp" +#include "Utils.hpp" using namespace H5; using namespace AQNWB::HDF5; diff --git a/Source/aqnwb/hdf5/HDF5IO.hpp b/Source/aqnwb/hdf5/HDF5IO.hpp index 8eaca67..2a94aef 100644 --- a/Source/aqnwb/hdf5/HDF5IO.hpp +++ b/Source/aqnwb/hdf5/HDF5IO.hpp @@ -6,8 +6,8 @@ #include -#include "../BaseIO.hpp" -#include "../Types.hpp" +#include "BaseIO.hpp" +#include "Types.hpp" namespace H5 { diff --git a/Source/aqnwb/nwb/NWBRecording.cpp b/Source/aqnwb/nwb/NWBRecording.cpp deleted file mode 100644 index bbcff44..0000000 --- a/Source/aqnwb/nwb/NWBRecording.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include "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) { - nwbfile->finalize(); - } - - // 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/Source/aqnwb/nwb/NWBRecording.hpp b/Source/aqnwb/nwb/NWBRecording.hpp deleted file mode 100644 index c844e37..0000000 --- a/Source/aqnwb/nwb/NWBRecording.hpp +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#include "../Types.hpp" -#include "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/Source/aqnwb/nwb/base/TimeSeries.cpp b/Source/aqnwb/nwb/base/TimeSeries.cpp index 254aa69..a9c008b 100644 --- a/Source/aqnwb/nwb/base/TimeSeries.cpp +++ b/Source/aqnwb/nwb/base/TimeSeries.cpp @@ -1,4 +1,4 @@ -#include "TimeSeries.hpp" +#include "nwb/base/TimeSeries.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/nwb/base/TimeSeries.hpp b/Source/aqnwb/nwb/base/TimeSeries.hpp index fb53024..0853785 100644 --- a/Source/aqnwb/nwb/base/TimeSeries.hpp +++ b/Source/aqnwb/nwb/base/TimeSeries.hpp @@ -2,8 +2,8 @@ #include -#include "../../BaseIO.hpp" -#include "../hdmf/base/Container.hpp" +#include "BaseIO.hpp" +#include "nwb/hdmf/base/Container.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/nwb/device/Device.cpp b/Source/aqnwb/nwb/device/Device.cpp index 98fc90e..262d920 100644 --- a/Source/aqnwb/nwb/device/Device.cpp +++ b/Source/aqnwb/nwb/device/Device.cpp @@ -1,4 +1,4 @@ -#include "Device.hpp" +#include "nwb/device/Device.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/nwb/device/Device.hpp b/Source/aqnwb/nwb/device/Device.hpp index 67eed81..74ae706 100644 --- a/Source/aqnwb/nwb/device/Device.hpp +++ b/Source/aqnwb/nwb/device/Device.hpp @@ -2,8 +2,8 @@ #include -#include "../../BaseIO.hpp" -#include "../hdmf/base/Container.hpp" +#include "BaseIO.hpp" +#include "nwb/hdmf/base/Container.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp b/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp index 9d215c1..ec3a3bb 100644 --- a/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp +++ b/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp @@ -1,7 +1,7 @@ -#include "ElectricalSeries.hpp" +#include "nwb/ecephys/ElectricalSeries.hpp" -#include "../file/ElectrodeTable.hpp" +#include "nwb/file/ElectrodeTable.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/nwb/ecephys/ElectricalSeries.hpp b/Source/aqnwb/nwb/ecephys/ElectricalSeries.hpp index 9e594a4..597669b 100644 --- a/Source/aqnwb/nwb/ecephys/ElectricalSeries.hpp +++ b/Source/aqnwb/nwb/ecephys/ElectricalSeries.hpp @@ -2,9 +2,9 @@ #include -#include "../../BaseIO.hpp" -#include "../../Channel.hpp" -#include "../base/TimeSeries.hpp" +#include "BaseIO.hpp" +#include "Channel.hpp" +#include "nwb/base/TimeSeries.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/nwb/file/ElectrodeGroup.cpp b/Source/aqnwb/nwb/file/ElectrodeGroup.cpp index ffe469a..b5beaa0 100644 --- a/Source/aqnwb/nwb/file/ElectrodeGroup.cpp +++ b/Source/aqnwb/nwb/file/ElectrodeGroup.cpp @@ -1,4 +1,4 @@ -#include "ElectrodeGroup.hpp" +#include "nwb/file/ElectrodeGroup.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/nwb/file/ElectrodeGroup.hpp b/Source/aqnwb/nwb/file/ElectrodeGroup.hpp index 352e481..4f4c55c 100644 --- a/Source/aqnwb/nwb/file/ElectrodeGroup.hpp +++ b/Source/aqnwb/nwb/file/ElectrodeGroup.hpp @@ -2,9 +2,9 @@ #include -#include "../../BaseIO.hpp" -#include "../device/Device.hpp" -#include "../hdmf/base/Container.hpp" +#include "BaseIO.hpp" +#include "nwb/device/Device.hpp" +#include "nwb/hdmf/base/Container.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/nwb/file/ElectrodeTable.cpp b/Source/aqnwb/nwb/file/ElectrodeTable.cpp index 6f5e7e8..027c9f6 100644 --- a/Source/aqnwb/nwb/file/ElectrodeTable.cpp +++ b/Source/aqnwb/nwb/file/ElectrodeTable.cpp @@ -1,7 +1,6 @@ +#include "nwb/file/ElectrodeTable.hpp" -#include "ElectrodeTable.hpp" - -#include "../../Channel.hpp" +#include "Channel.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/nwb/file/ElectrodeTable.hpp b/Source/aqnwb/nwb/file/ElectrodeTable.hpp index 0161698..b8611a3 100644 --- a/Source/aqnwb/nwb/file/ElectrodeTable.hpp +++ b/Source/aqnwb/nwb/file/ElectrodeTable.hpp @@ -2,10 +2,10 @@ #include -#include "../../BaseIO.hpp" -#include "../hdmf/table/DynamicTable.hpp" -#include "../hdmf/table/ElementIdentifiers.hpp" -#include "../hdmf/table/VectorData.hpp" +#include "BaseIO.hpp" +#include "nwb/hdmf/table/DynamicTable.hpp" +#include "nwb/hdmf/table/ElementIdentifiers.hpp" +#include "nwb/hdmf/table/VectorData.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/nwb/hdmf/base/Container.cpp b/Source/aqnwb/nwb/hdmf/base/Container.cpp index e47d3ee..525d82e 100644 --- a/Source/aqnwb/nwb/hdmf/base/Container.cpp +++ b/Source/aqnwb/nwb/hdmf/base/Container.cpp @@ -1,4 +1,4 @@ -#include "Container.hpp" +#include "nwb/hdmf/base/Container.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/nwb/hdmf/base/Container.hpp b/Source/aqnwb/nwb/hdmf/base/Container.hpp index c9528b5..1d89c87 100644 --- a/Source/aqnwb/nwb/hdmf/base/Container.hpp +++ b/Source/aqnwb/nwb/hdmf/base/Container.hpp @@ -3,7 +3,7 @@ #include #include -#include "../../../BaseIO.hpp" +#include "BaseIO.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/nwb/hdmf/base/Data.hpp b/Source/aqnwb/nwb/hdmf/base/Data.hpp index 6b3f855..ef14648 100644 --- a/Source/aqnwb/nwb/hdmf/base/Data.hpp +++ b/Source/aqnwb/nwb/hdmf/base/Data.hpp @@ -2,7 +2,7 @@ #include -#include "../../../BaseIO.hpp" +#include "BaseIO.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/nwb/hdmf/table/DynamicTable.cpp b/Source/aqnwb/nwb/hdmf/table/DynamicTable.cpp index c61fae8..d8b8c98 100644 --- a/Source/aqnwb/nwb/hdmf/table/DynamicTable.cpp +++ b/Source/aqnwb/nwb/hdmf/table/DynamicTable.cpp @@ -1,4 +1,4 @@ -#include "DynamicTable.hpp" +#include "nwb/hdmf/table/DynamicTable.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/nwb/hdmf/table/DynamicTable.hpp b/Source/aqnwb/nwb/hdmf/table/DynamicTable.hpp index defe156..6cd8c2a 100644 --- a/Source/aqnwb/nwb/hdmf/table/DynamicTable.hpp +++ b/Source/aqnwb/nwb/hdmf/table/DynamicTable.hpp @@ -2,10 +2,10 @@ #include -#include "../../../BaseIO.hpp" -#include "../base/Container.hpp" -#include "ElementIdentifiers.hpp" -#include "VectorData.hpp" +#include "BaseIO.hpp" +#include "nwb/hdmf/base/Container.hpp" +#include "nwb/hdmf/table/ElementIdentifiers.hpp" +#include "nwb/hdmf/table/VectorData.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/nwb/hdmf/table/VectorData.cpp b/Source/aqnwb/nwb/hdmf/table/VectorData.cpp index ef4a40b..1438387 100644 --- a/Source/aqnwb/nwb/hdmf/table/VectorData.cpp +++ b/Source/aqnwb/nwb/hdmf/table/VectorData.cpp @@ -1,4 +1,4 @@ -#include "VectorData.hpp" +#include "nwb/hdmf/table/VectorData.hpp" using namespace AQNWB::NWB; diff --git a/Source/aqnwb/nwb/hdmf/table/VectorData.hpp b/Source/aqnwb/nwb/hdmf/table/VectorData.hpp index 870696a..7ee93f0 100644 --- a/Source/aqnwb/nwb/hdmf/table/VectorData.hpp +++ b/Source/aqnwb/nwb/hdmf/table/VectorData.hpp @@ -2,7 +2,7 @@ #include -#include "../base/Data.hpp" +#include "nwb/hdmf/base/Data.hpp" namespace AQNWB::NWB { diff --git a/Source/aqnwb/spec/core.hpp b/Source/aqnwb/spec/core.hpp index 3bb5f31..0a67f80 100644 --- a/Source/aqnwb/spec/core.hpp +++ b/Source/aqnwb/spec/core.hpp @@ -1,64 +1,65 @@ #pragma once +#include #include +#include -namespace AQNWB::spec::core +namespace AQNWB::SPEC::CORE { const std::string version = "2.7.0"; -const std::string nwb_base = R"delimiter( +constexpr std::string_view nwb_base = R"delimiter( {"datasets":[{"neurodata_type_def":"NWBData","neurodata_type_inc":"Data","doc":"An abstract data type for a dataset."},{"neurodata_type_def":"TimeSeriesReferenceVectorData","neurodata_type_inc":"VectorData","default_name":"timeseries","dtype":[{"name":"idx_start","dtype":"int32","doc":"Start index into the TimeSeries 'data' and 'timestamp' datasets of the referenced TimeSeries. The first dimension of those arrays is always time."},{"name":"count","dtype":"int32","doc":"Number of data samples available in this time series, during this epoch"},{"name":"timeseries","dtype":{"target_type":"TimeSeries","reftype":"object"},"doc":"The TimeSeries that this index applies to"}],"doc":"Column storing references to a TimeSeries (rows). For each TimeSeries this VectorData column stores the start_index and count to indicate the range in time to be selected as well as an object reference to the TimeSeries."},{"neurodata_type_def":"Image","neurodata_type_inc":"NWBData","dtype":"numeric","dims":[["x","y"],["x","y","r, g, b"],["x","y","r, g, b, a"]],"shape":[[null,null],[null,null,3],[null,null,4]],"doc":"An abstract data type for an image. Shape can be 2-D (x, y), or 3-D where the third dimension can have three or four elements, e.g. (x, y, (r, g, b)) or (x, y, (r, g, b, a)).","attributes":[{"name":"resolution","dtype":"float32","doc":"Pixel resolution of the image, in pixels per centimeter.","required":false},{"name":"description","dtype":"text","doc":"Description of the image.","required":false}]},{"neurodata_type_def":"ImageReferences","neurodata_type_inc":"NWBData","dtype":{"target_type":"Image","reftype":"object"},"dims":["num_images"],"shape":[null],"doc":"Ordered dataset of references to Image objects."}],"groups":[{"neurodata_type_def":"NWBContainer","neurodata_type_inc":"Container","doc":"An abstract data type for a generic container storing collections of data and metadata. Base type for all data and metadata containers."},{"neurodata_type_def":"NWBDataInterface","neurodata_type_inc":"NWBContainer","doc":"An abstract data type for a generic container storing collections of data, as opposed to metadata."},{"neurodata_type_def":"TimeSeries","neurodata_type_inc":"NWBDataInterface","doc":"General purpose time series.","attributes":[{"name":"description","dtype":"text","default_value":"no description","doc":"Description of the time series.","required":false},{"name":"comments","dtype":"text","default_value":"no comments","doc":"Human-readable comments about the TimeSeries. This second descriptive field can be used to store additional information, or descriptive information if the primary description field is populated with a computer-readable string.","required":false}],"datasets":[{"name":"data","dims":[["num_times"],["num_times","num_DIM2"],["num_times","num_DIM2","num_DIM3"],["num_times","num_DIM2","num_DIM3","num_DIM4"]],"shape":[[null],[null,null],[null,null,null],[null,null,null,null]],"doc":"Data values. Data can be in 1-D, 2-D, 3-D, or 4-D. The first dimension should always represent time. This can also be used to store binary data (e.g., image frames). This can also be a link to data stored in an external file.","attributes":[{"name":"conversion","dtype":"float32","default_value":1.0,"doc":"Scalar to multiply each element in data to convert it to the specified 'unit'. If the data are stored in acquisition system units or other units that require a conversion to be interpretable, multiply the data by 'conversion' to convert the data to the specified 'unit'. e.g. if the data acquisition system stores values in this object as signed 16-bit integers (int16 range -32,768 to 32,767) that correspond to a 5V range (-2.5V to 2.5V), and the data acquisition system gain is 8000X, then the 'conversion' multiplier to get from raw data acquisition values to recorded volts is 2.5/32768/8000 = 9.5367e-9.","required":false},{"name":"offset","dtype":"float32","default_value":0.0,"doc":"Scalar to add to the data after scaling by 'conversion' to finalize its coercion to the specified 'unit'. Two common examples of this include (a) data stored in an unsigned type that requires a shift after scaling to re-center the data, and (b) specialized recording devices that naturally cause a scalar offset with respect to the true units.","required":false},{"name":"resolution","dtype":"float32","default_value":-1.0,"doc":"Smallest meaningful difference between values in data, stored in the specified by unit, e.g., the change in value of the least significant bit, or a larger number if signal noise is known to be present. If unknown, use -1.0.","required":false},{"name":"unit","dtype":"text","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."},{"name":"continuity","dtype":"text","doc":"Optionally describe the continuity of the data. Can be \"continuous\", \"instantaneous\", or \"step\". For example, a voltage trace would be \"continuous\", because samples are recorded from a continuous process. An array of lick times would be \"instantaneous\", because the data represents distinct moments in time. Times of image presentations would be \"step\" because the picture remains the same until the next timepoint. This field is optional, but is useful in providing information about the underlying data. It may inform the way this data is interpreted, the way it is visualized, and what analysis methods are applicable.","required":false}]},{"name":"starting_time","dtype":"float64","doc":"Timestamp of the first sample in seconds. When timestamps are uniformly spaced, the timestamp of the first sample can be specified and all subsequent ones calculated from the sampling rate attribute.","quantity":"?","attributes":[{"name":"rate","dtype":"float32","doc":"Sampling rate, in Hz."},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for time, which is fixed to 'seconds'."}]},{"name":"timestamps","dtype":"float64","dims":["num_times"],"shape":[null],"doc":"Timestamps for samples stored in data, in seconds, relative to the common experiment master-clock stored in NWBFile.timestamps_reference_time.","quantity":"?","attributes":[{"name":"interval","dtype":"int32","value":1,"doc":"Value is '1'"},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for timestamps, which is fixed to 'seconds'."}]},{"name":"control","dtype":"uint8","dims":["num_times"],"shape":[null],"doc":"Numerical labels that apply to each time point in data for the purpose of querying and slicing data by these values. If present, the length of this array should be the same size as the first dimension of data.","quantity":"?"},{"name":"control_description","dtype":"text","dims":["num_control_values"],"shape":[null],"doc":"Description of each control value. Must be present if control is present. If present, control_description[0] should describe time points where control == 0.","quantity":"?"}],"groups":[{"name":"sync","doc":"Lab-specific time and sync information as provided directly from hardware devices and that is necessary for aligning all acquired time information to a common timebase. The timestamp array stores time in the common timebase. This group will usually only be populated in TimeSeries that are stored external to the NWB file, in files storing raw data. Once timestamp data is calculated, the contents of 'sync' are mostly for archival purposes.","quantity":"?"}]},{"neurodata_type_def":"ProcessingModule","neurodata_type_inc":"NWBContainer","doc":"A collection of processed data.","attributes":[{"name":"description","dtype":"text","doc":"Description of this collection of processed data."}],"groups":[{"neurodata_type_inc":"NWBDataInterface","doc":"Data objects stored in this collection.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tables stored in this collection.","quantity":"*"}]},{"neurodata_type_def":"Images","neurodata_type_inc":"NWBDataInterface","default_name":"Images","doc":"A collection of images with an optional way to specify the order of the images using the \"order_of_images\" dataset. An order must be specified if the images are referenced by index, e.g., from an IndexSeries.","attributes":[{"name":"description","dtype":"text","doc":"Description of this collection of images."}],"datasets":[{"neurodata_type_inc":"Image","doc":"Images stored in this collection.","quantity":"+"},{"name":"order_of_images","neurodata_type_inc":"ImageReferences","doc":"Ordered dataset of references to Image objects stored in the parent group. Each Image object in the Images group should be stored once and only once, so the dataset should have the same length as the number of images.","quantity":"?"}]}]})delimiter"; -const std::string nwb_device = R"delimiter( +constexpr std::string_view nwb_device = R"delimiter( {"groups":[{"neurodata_type_def":"Device","neurodata_type_inc":"NWBContainer","doc":"Metadata about a data acquisition device, e.g., recording system, electrode, microscope.","attributes":[{"name":"description","dtype":"text","doc":"Description of the device (e.g., model, firmware version, processing software version, etc.) as free-form text.","required":false},{"name":"manufacturer","dtype":"text","doc":"The name of the manufacturer of the device.","required":false}]}]})delimiter"; -const std::string nwb_epoch = R"delimiter( +constexpr std::string_view nwb_epoch = R"delimiter( {"groups":[{"neurodata_type_def":"TimeIntervals","neurodata_type_inc":"DynamicTable","doc":"A container for aggregating epoch data and the TimeSeries that each epoch applies to.","datasets":[{"name":"start_time","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Start time of epoch, in seconds."},{"name":"stop_time","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Stop time of epoch, in seconds."},{"name":"tags","neurodata_type_inc":"VectorData","dtype":"text","doc":"User-defined tags that identify or categorize events.","quantity":"?"},{"name":"tags_index","neurodata_type_inc":"VectorIndex","doc":"Index for tags.","quantity":"?"},{"name":"timeseries","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"An index into a TimeSeries object.","quantity":"?"},{"name":"timeseries_index","neurodata_type_inc":"VectorIndex","doc":"Index for timeseries.","quantity":"?"}]}]})delimiter"; -const std::string nwb_image = R"delimiter( +constexpr std::string_view nwb_image = R"delimiter( {"datasets":[{"neurodata_type_def":"GrayscaleImage","neurodata_type_inc":"Image","dims":["x","y"],"shape":[null,null],"doc":"A grayscale image.","dtype":"numeric"},{"neurodata_type_def":"RGBImage","neurodata_type_inc":"Image","dims":["x","y","r, g, b"],"shape":[null,null,3],"doc":"A color image.","dtype":"numeric"},{"neurodata_type_def":"RGBAImage","neurodata_type_inc":"Image","dims":["x","y","r, g, b, a"],"shape":[null,null,4],"doc":"A color image with transparency.","dtype":"numeric"}],"groups":[{"neurodata_type_def":"ImageSeries","neurodata_type_inc":"TimeSeries","doc":"General image data that is common between acquisition and stimulus time series. Sometimes the image data is stored in the file in a raw format while other times it will be stored as a series of external image files in the host file system. The data field will either be binary data, if the data is stored in the NWB file, or empty, if the data is stored in an external image stack. [frame][x][y] or [frame][x][y][z].","datasets":[{"name":"data","dtype":"numeric","dims":[["frame","x","y"],["frame","x","y","z"]],"shape":[[null,null,null],[null,null,null,null]],"doc":"Binary data representing images across frames. If data are stored in an external file, this should be an empty 3D array."},{"name":"dimension","dtype":"int32","dims":["rank"],"shape":[null],"doc":"Number of pixels on x, y, (and z) axes.","quantity":"?"},{"name":"external_file","dtype":"text","dims":["num_files"],"shape":[null],"doc":"Paths to one or more external file(s). The field is only present if format='external'. This is only relevant if the image series is stored in the file system as one or more image file(s). This field should NOT be used if the image is stored in another NWB file and that file is linked to this file.","quantity":"?","attributes":[{"name":"starting_frame","dtype":"int32","dims":["num_files"],"shape":[null],"doc":"Each external image may contain one or more consecutive frames of the full ImageSeries. This attribute serves as an index to indicate which frames each file contains, to facilitate random access. The 'starting_frame' attribute, hence, contains a list of frame numbers within the full ImageSeries of the first frame of each file listed in the parent 'external_file' dataset. Zero-based indexing is used (hence, the first element will always be zero). For example, if the 'external_file' dataset has three paths to files and the first file has 5 frames, the second file has 10 frames, and the third file has 20 frames, then this attribute will have values [0, 5, 15]. If there is a single external file that holds all of the frames of the ImageSeries (and so there is a single element in the 'external_file' dataset), then this attribute should have value [0]."}]},{"name":"format","dtype":"text","default_value":"raw","doc":"Format of image. If this is 'external', then the attribute 'external_file' contains the path information to the image files. If this is 'raw', then the raw (single-channel) binary data is stored in the 'data' dataset. If this attribute is not present, then the default format='raw' case is assumed.","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the Device object that was used to capture these images.","quantity":"?"}]},{"neurodata_type_def":"ImageMaskSeries","neurodata_type_inc":"ImageSeries","doc":"An alpha mask that is applied to a presented visual stimulus. The 'data' array contains an array of mask values that are applied to the displayed image. Mask values are stored as RGBA. Mask can vary with time. The timestamps array indicates the starting time of a mask, and that mask pattern continues until it's explicitly changed.","links":[{"name":"masked_imageseries","target_type":"ImageSeries","doc":"Link to ImageSeries object that this image mask is applied to."}]},{"neurodata_type_def":"OpticalSeries","neurodata_type_inc":"ImageSeries","doc":"Image data that is presented or recorded. A stimulus template movie will be stored only as an image. When the image is presented as stimulus, additional data is required, such as field of view (e.g., how much of the visual field the image covers, or how what is the area of the target being imaged). If the OpticalSeries represents acquired imaging data, orientation is also important.","datasets":[{"name":"distance","dtype":"float32","doc":"Distance from camera/monitor to target/eye.","quantity":"?"},{"name":"field_of_view","dtype":"float32","dims":[["width, height"],["width, height, depth"]],"shape":[[2],[3]],"doc":"Width, height and depth of image, or imaged area, in meters.","quantity":"?"},{"name":"data","dtype":"numeric","dims":[["frame","x","y"],["frame","x","y","r, g, b"]],"shape":[[null,null,null],[null,null,null,3]],"doc":"Images presented to subject, either grayscale or RGB"},{"name":"orientation","dtype":"text","doc":"Description of image relative to some reference frame (e.g., which way is up). Must also specify frame of reference.","quantity":"?"}]},{"neurodata_type_def":"IndexSeries","neurodata_type_inc":"TimeSeries","doc":"Stores indices to image frames stored in an ImageSeries. The purpose of the IndexSeries is to allow a static image stack to be stored in an Images object, and the images in the stack to be referenced out-of-order. This can be for the display of individual images, or of movie segments (as a movie is simply a series of images). The data field stores the index of the frame in the referenced Images object, and the timestamps array indicates when that image was displayed.","datasets":[{"name":"data","dtype":"uint32","dims":["num_times"],"shape":[null],"doc":"Index of the image (using zero-indexing) in the linked Images object.","attributes":[{"name":"conversion","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"resolution","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"offset","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"unit","dtype":"text","value":"N/A","doc":"This field is unused by IndexSeries and has the value N/A."}]}],"links":[{"name":"indexed_timeseries","target_type":"ImageSeries","doc":"Link to ImageSeries object containing images that are indexed. Use of this link is discouraged and will be deprecated. Link to an Images type instead.","quantity":"?"},{"name":"indexed_images","target_type":"Images","doc":"Link to Images object containing an ordered set of images that are indexed. The Images object must contain a 'ordered_images' dataset specifying the order of the images in the Images type.","quantity":"?"}]}]})delimiter"; -const std::string nwb_file = R"delimiter( +constexpr std::string_view nwb_file = R"delimiter( {"groups":[{"neurodata_type_def":"NWBFile","neurodata_type_inc":"NWBContainer","name":"root","doc":"An NWB file storing cellular-based neurophysiology data from a single experimental session.","attributes":[{"name":"nwb_version","dtype":"text","value":"2.7.0-alpha","doc":"File version string. Use semantic versioning, e.g. 1.2.1. This will be the name of the format with trailing major, minor and patch numbers."}],"datasets":[{"name":"file_create_date","dtype":"isodatetime","dims":["num_modifications"],"shape":[null],"doc":"A record of the date the file was created and of subsequent modifications. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted strings: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds. The file can be created after the experiment was run, so this may differ from the experiment start time. Each modification to the nwb file adds a new entry to the array."},{"name":"identifier","dtype":"text","doc":"A unique text identifier for the file. For example, concatenated lab name, file creation date/time and experimentalist, or a hash of these and/or other values. The goal is that the string should be unique to all other files."},{"name":"session_description","dtype":"text","doc":"A description of the experimental session and data in the file."},{"name":"session_start_time","dtype":"isodatetime","doc":"Date and time of the experiment/session start. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted string: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds."},{"name":"timestamps_reference_time","dtype":"isodatetime","doc":"Date and time corresponding to time zero of all timestamps. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted string: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds. All times stored in the file use this time as reference (i.e., time zero)."}],"groups":[{"name":"acquisition","doc":"Data streams recorded from the system, including ephys, ophys, tracking, etc. This group should be read-only after the experiment is completed and timestamps are corrected to a common timebase. The data stored here may be links to raw data stored in external NWB files. This will allow keeping bulky raw data out of the file while preserving the option of keeping some/all in the file. Acquired data includes tracking and experimental data streams (i.e., everything measured from the system). If bulky data is stored in the /acquisition group, the data can exist in a separate NWB file that is linked to by the file being used for processing and analysis.","groups":[{"neurodata_type_inc":"NWBDataInterface","doc":"Acquired, raw data.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tabular data that is relevant to acquisition","quantity":"*"}]},{"name":"analysis","doc":"Lab-specific and custom scientific analysis of data. There is no defined format for the content of this group - the format is up to the individual user/lab. To facilitate sharing analysis data between labs, the contents here should be stored in standard types (e.g., neurodata_types) and appropriately documented. The file can store lab-specific and custom data analysis without restriction on its form or schema, reducing data formatting restrictions on end users. Such data should be placed in the analysis group. The analysis data should be documented so that it could be shared with other labs.","groups":[{"neurodata_type_inc":"NWBContainer","doc":"Custom analysis results.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tabular data that is relevant to data stored in analysis","quantity":"*"}]},{"name":"scratch","doc":"A place to store one-off analysis results. Data placed here is not intended for sharing. By placing data here, users acknowledge that there is no guarantee that their data meets any standard.","quantity":"?","groups":[{"neurodata_type_inc":"NWBContainer","doc":"Any one-off containers","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Any one-off tables","quantity":"*"}],"datasets":[{"neurodata_type_inc":"ScratchData","doc":"Any one-off datasets","quantity":"*"}]},{"name":"processing","doc":"The home for ProcessingModules. These modules perform intermediate analysis of data that is necessary to perform before scientific analysis. Examples include spike clustering, extracting position from tracking data, stitching together image slices. ProcessingModules can be large and express many data sets from relatively complex analysis (e.g., spike detection and clustering) or small, representing extraction of position information from tracking video, or even binary lick/no-lick decisions. Common software tools (e.g., klustakwik, MClust) are expected to read/write data here. 'Processing' refers to intermediate analysis of the acquired data to make it more amenable to scientific analysis.","groups":[{"neurodata_type_inc":"ProcessingModule","doc":"Intermediate analysis of acquired data.","quantity":"*"}]},{"name":"stimulus","doc":"Data pushed into the system (eg, video stimulus, sound, voltage, etc) and secondary representations of that data (eg, measurements of something used as a stimulus). This group should be made read-only after experiment complete and timestamps are corrected to common timebase. Stores both presented stimuli and stimulus templates, the latter in case the same stimulus is presented multiple times, or is pulled from an external stimulus library. Stimuli are here defined as any signal that is pushed into the system as part of the experiment (eg, sound, video, voltage, etc). Many different experiments can use the same stimuli, and stimuli can be re-used during an experiment. The stimulus group is organized so that one version of template stimuli can be stored and these be used multiple times. These templates can exist in the present file or can be linked to a remote library file.","groups":[{"name":"presentation","doc":"Stimuli presented during the experiment.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries objects containing data of presented stimuli.","quantity":"*"}]},{"name":"templates","doc":"Template stimuli. Timestamps in templates are based on stimulus design and are relative to the beginning of the stimulus. When templates are used, the stimulus instances must convert presentation times to the experiment`s time reference frame.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries objects containing template data of presented stimuli.","quantity":"*"},{"neurodata_type_inc":"Images","doc":"Images objects containing images of presented stimuli.","quantity":"*"}]}]},{"name":"general","doc":"Experimental metadata, including protocol, notes and description of hardware device(s). The metadata stored in this section should be used to describe the experiment. Metadata necessary for interpreting the data is stored with the data. General experimental metadata, including animal strain, experimental protocols, experimenter, devices, etc, are stored under 'general'. Core metadata (e.g., that required to interpret data fields) is stored with the data itself, and implicitly defined by the file specification (e.g., time is in seconds). The strategy used here for storing non-core metadata is to use free-form text fields, such as would appear in sentences or paragraphs from a Methods section. Metadata fields are text to enable them to be more general, for example to represent ranges instead of numerical values. Machine-readable metadata is stored as attributes to these free-form datasets. All entries in the below table are to be included when data is present. Unused groups (e.g., intracellular_ephys in an optophysiology experiment) should not be created unless there is data to store within them.","datasets":[{"name":"data_collection","dtype":"text","doc":"Notes about data collection and analysis.","quantity":"?"},{"name":"experiment_description","dtype":"text","doc":"General description of the experiment.","quantity":"?"},{"name":"experimenter","dtype":"text","doc":"Name of person(s) who performed the experiment. Can also specify roles of different people involved.","quantity":"?","dims":["num_experimenters"],"shape":[null]},{"name":"institution","dtype":"text","doc":"Institution(s) where experiment was performed.","quantity":"?"},{"name":"keywords","dtype":"text","dims":["num_keywords"],"shape":[null],"doc":"Terms to search over.","quantity":"?"},{"name":"lab","dtype":"text","doc":"Laboratory where experiment was performed.","quantity":"?"},{"name":"notes","dtype":"text","doc":"Notes about the experiment.","quantity":"?"},{"name":"pharmacology","dtype":"text","doc":"Description of drugs used, including how and when they were administered. Anesthesia(s), painkiller(s), etc., plus dosage, concentration, etc.","quantity":"?"},{"name":"protocol","dtype":"text","doc":"Experimental protocol, if applicable. e.g., include IACUC protocol number.","quantity":"?"},{"name":"related_publications","dtype":"text","doc":"Publication information. PMID, DOI, URL, etc.","dims":["num_publications"],"shape":[null],"quantity":"?"},{"name":"session_id","dtype":"text","doc":"Lab-specific ID for the session.","quantity":"?"},{"name":"slices","dtype":"text","doc":"Description of slices, including information about preparation thickness, orientation, temperature, and bath solution.","quantity":"?"},{"name":"source_script","dtype":"text","doc":"Script file or link to public source code used to create this NWB file.","quantity":"?","attributes":[{"name":"file_name","dtype":"text","doc":"Name of script file."}]},{"name":"stimulus","dtype":"text","doc":"Notes about stimuli, such as how and where they were presented.","quantity":"?"},{"name":"surgery","dtype":"text","doc":"Narrative description about surgery/surgeries, including date(s) and who performed surgery.","quantity":"?"},{"name":"virus","dtype":"text","doc":"Information about virus(es) used in experiments, including virus ID, source, date made, injection location, volume, etc.","quantity":"?"}],"groups":[{"neurodata_type_inc":"LabMetaData","doc":"Place-holder than can be extended so that lab-specific meta-data can be placed in /general.","quantity":"*"},{"name":"devices","doc":"Description of hardware devices used during experiment, e.g., monitors, ADC boards, microscopes, etc.","quantity":"?","groups":[{"neurodata_type_inc":"Device","doc":"Data acquisition devices.","quantity":"*"}]},{"name":"subject","neurodata_type_inc":"Subject","doc":"Information about the animal or person from which the data was measured.","quantity":"?"},{"name":"extracellular_ephys","doc":"Metadata related to extracellular electrophysiology.","quantity":"?","groups":[{"neurodata_type_inc":"ElectrodeGroup","doc":"Physical group of electrodes.","quantity":"*"},{"name":"electrodes","neurodata_type_inc":"DynamicTable","doc":"A table of all electrodes (i.e. channels) used for recording.","quantity":"?","datasets":[{"name":"x","neurodata_type_inc":"VectorData","dtype":"float32","doc":"x coordinate of the channel location in the brain (+x is posterior).","quantity":"?"},{"name":"y","neurodata_type_inc":"VectorData","dtype":"float32","doc":"y coordinate of the channel location in the brain (+y is inferior).","quantity":"?"},{"name":"z","neurodata_type_inc":"VectorData","dtype":"float32","doc":"z coordinate of the channel location in the brain (+z is right).","quantity":"?"},{"name":"imp","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Impedance of the channel, in ohms.","quantity":"?"},{"name":"location","neurodata_type_inc":"VectorData","dtype":"text","doc":"Location of the electrode (channel). Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."},{"name":"filtering","neurodata_type_inc":"VectorData","dtype":"text","doc":"Description of hardware filtering, including the filter name and frequency cutoffs.","quantity":"?"},{"name":"group","neurodata_type_inc":"VectorData","dtype":{"target_type":"ElectrodeGroup","reftype":"object"},"doc":"Reference to the ElectrodeGroup this electrode is a part of."},{"name":"group_name","neurodata_type_inc":"VectorData","dtype":"text","doc":"Name of the ElectrodeGroup this electrode is a part of."},{"name":"rel_x","neurodata_type_inc":"VectorData","dtype":"float32","doc":"x coordinate in electrode group","quantity":"?"},{"name":"rel_y","neurodata_type_inc":"VectorData","dtype":"float32","doc":"y coordinate in electrode group","quantity":"?"},{"name":"rel_z","neurodata_type_inc":"VectorData","dtype":"float32","doc":"z coordinate in electrode group","quantity":"?"},{"name":"reference","neurodata_type_inc":"VectorData","dtype":"text","doc":"Description of the reference electrode and/or reference scheme used for this electrode, e.g., \"stainless steel skull screw\" or \"online common average referencing\".","quantity":"?"}]}]},{"name":"intracellular_ephys","doc":"Metadata related to intracellular electrophysiology.","quantity":"?","datasets":[{"name":"filtering","dtype":"text","doc":"[DEPRECATED] Use IntracellularElectrode.filtering instead. Description of filtering used. Includes filtering type and parameters, frequency fall-off, etc. If this changes between TimeSeries, filter description should be stored as a text attribute for each TimeSeries.","quantity":"?"}],"groups":[{"neurodata_type_inc":"IntracellularElectrode","doc":"An intracellular electrode.","quantity":"*"},{"name":"sweep_table","neurodata_type_inc":"SweepTable","doc":"[DEPRECATED] Table used to group different PatchClampSeries. SweepTable is being replaced by IntracellularRecordingsTable and SimultaneousRecordingsTable tables. Additional SequentialRecordingsTable, RepetitionsTable and ExperimentalConditions tables provide enhanced support for experiment metadata.","quantity":"?"},{"name":"intracellular_recordings","neurodata_type_inc":"IntracellularRecordingsTable","doc":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording. Each row in the table represents a single recording consisting typically of a stimulus and a corresponding response. In some cases, however, only a stimulus or a response are recorded as as part of an experiment. In this case both, the stimulus and response will point to the same TimeSeries while the idx_start and count of the invalid column will be set to -1, thus, indicating that no values have been recorded for the stimulus or response, respectively. Note, a recording MUST contain at least a stimulus or a response. Typically the stimulus and response are PatchClampSeries. However, the use of AD/DA channels that are not associated to an electrode is also common in intracellular electrophysiology, in which case other TimeSeries may be used.","quantity":"?"},{"name":"simultaneous_recordings","neurodata_type_inc":"SimultaneousRecordingsTable","doc":"A table for grouping different intracellular recordings from the IntracellularRecordingsTable table together that were recorded simultaneously from different electrodes","quantity":"?"},{"name":"sequential_recordings","neurodata_type_inc":"SequentialRecordingsTable","doc":"A table for grouping different sequential recordings from the SimultaneousRecordingsTable table together. This is typically used to group together sequential recordings where the a sequence of stimuli of the same type with varying parameters have been presented in a sequence.","quantity":"?"},{"name":"repetitions","neurodata_type_inc":"RepetitionsTable","doc":"A table for grouping different sequential intracellular recordings together. With each SequentialRecording typically representing a particular type of stimulus, the RepetitionsTable table is typically used to group sets of stimuli applied in sequence.","quantity":"?"},{"name":"experimental_conditions","neurodata_type_inc":"ExperimentalConditionsTable","doc":"A table for grouping different intracellular recording repetitions together that belong to the same experimental experimental_conditions.","quantity":"?"}]},{"name":"optogenetics","doc":"Metadata describing optogenetic stimuluation.","quantity":"?","groups":[{"neurodata_type_inc":"OptogeneticStimulusSite","doc":"An optogenetic stimulation site.","quantity":"*"}]},{"name":"optophysiology","doc":"Metadata related to optophysiology.","quantity":"?","groups":[{"neurodata_type_inc":"ImagingPlane","doc":"An imaging plane.","quantity":"*"}]}]},{"name":"intervals","doc":"Experimental intervals, whether that be logically distinct sub-experiments having a particular scientific goal, trials (see trials subgroup) during an experiment, or epochs (see epochs subgroup) deriving from analysis of data.","quantity":"?","groups":[{"name":"epochs","neurodata_type_inc":"TimeIntervals","doc":"Divisions in time marking experimental stages or sub-divisions of a single recording session.","quantity":"?"},{"name":"trials","neurodata_type_inc":"TimeIntervals","doc":"Repeated experimental events that have a logical grouping.","quantity":"?"},{"name":"invalid_times","neurodata_type_inc":"TimeIntervals","doc":"Time intervals that should be removed from analysis.","quantity":"?"},{"neurodata_type_inc":"TimeIntervals","doc":"Optional additional table(s) for describing other experimental time intervals.","quantity":"*"}]},{"name":"units","neurodata_type_inc":"Units","doc":"Data about sorted spike units.","quantity":"?"}]},{"neurodata_type_def":"LabMetaData","neurodata_type_inc":"NWBContainer","doc":"Lab-specific meta-data."},{"neurodata_type_def":"Subject","neurodata_type_inc":"NWBContainer","doc":"Information about the animal or person from which the data was measured.","datasets":[{"name":"age","dtype":"text","doc":"Age of subject. Can be supplied instead of 'date_of_birth'.","quantity":"?","attributes":[{"name":"reference","doc":"Age is with reference to this event. Can be 'birth' or 'gestational'. If reference is omitted, 'birth' is implied.","dtype":"text","required":false,"default_value":"birth"}]},{"name":"date_of_birth","dtype":"isodatetime","doc":"Date of birth of subject. Can be supplied instead of 'age'.","quantity":"?"},{"name":"description","dtype":"text","doc":"Description of subject and where subject came from (e.g., breeder, if animal).","quantity":"?"},{"name":"genotype","dtype":"text","doc":"Genetic strain. If absent, assume Wild Type (WT).","quantity":"?"},{"name":"sex","dtype":"text","doc":"Gender of subject.","quantity":"?"},{"name":"species","dtype":"text","doc":"Species of subject.","quantity":"?"},{"name":"strain","dtype":"text","doc":"Strain of subject.","quantity":"?"},{"name":"subject_id","dtype":"text","doc":"ID of animal/person used/participating in experiment (lab convention).","quantity":"?"},{"name":"weight","dtype":"text","doc":"Weight at time of experiment, at time of surgery and at other important times.","quantity":"?"}]}],"datasets":[{"neurodata_type_def":"ScratchData","neurodata_type_inc":"NWBData","doc":"Any one-off datasets","attributes":[{"name":"notes","doc":"Any notes the user has about the dataset being stored","dtype":"text"}]}]})delimiter"; -const std::string nwb_misc = R"delimiter( +constexpr std::string_view nwb_misc = R"delimiter( {"groups":[{"neurodata_type_def":"AbstractFeatureSeries","neurodata_type_inc":"TimeSeries","doc":"Abstract features, such as quantitative descriptions of sensory stimuli. The TimeSeries::data field is a 2D array, storing those features (e.g., for visual grating stimulus this might be orientation, spatial frequency and contrast). Null stimuli (eg, uniform gray) can be marked as being an independent feature (eg, 1.0 for gray, 0.0 for actual stimulus) or by storing NaNs for feature values, or through use of the TimeSeries::control fields. A set of features is considered to persist until the next set of features is defined. The final set of features stored should be the null set. This is useful when storing the raw stimulus is impractical.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_features"]],"shape":[[null],[null,null]],"doc":"Values of each feature at each time.","attributes":[{"name":"unit","dtype":"text","default_value":"see 'feature_units'","doc":"Since there can be different units for different features, store the units in 'feature_units'. The default value for this attribute is \"see 'feature_units'\".","required":false}]},{"name":"feature_units","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Units of each feature.","quantity":"?"},{"name":"features","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Description of the features represented in TimeSeries::data."}]},{"neurodata_type_def":"AnnotationSeries","neurodata_type_inc":"TimeSeries","doc":"Stores user annotations made during an experiment. The data[] field stores a text array, and timestamps are stored for each annotation (ie, interval=1). This is largely an alias to a standard TimeSeries storing a text array but that is identifiable as storing annotations in a machine-readable way.","datasets":[{"name":"data","dtype":"text","dims":["num_times"],"shape":[null],"doc":"Annotations made during an experiment.","attributes":[{"name":"resolution","dtype":"float32","value":-1.0,"doc":"Smallest meaningful difference between values in data. Annotations have no units, so the value is fixed to -1.0."},{"name":"unit","dtype":"text","value":"n/a","doc":"Base unit of measurement for working with the data. Annotations have no units, so the value is fixed to 'n/a'."}]}]},{"neurodata_type_def":"IntervalSeries","neurodata_type_inc":"TimeSeries","doc":"Stores intervals of data. The timestamps field stores the beginning and end of intervals. The data field stores whether the interval just started (>0 value) or ended (<0 value). Different interval types can be represented in the same series by using multiple key values (eg, 1 for feature A, 2 for feature B, 3 for feature C, etc). The field data stores an 8-bit integer. This is largely an alias of a standard TimeSeries but that is identifiable as representing time intervals in a machine-readable way.","datasets":[{"name":"data","dtype":"int8","dims":["num_times"],"shape":[null],"doc":"Use values >0 if interval started, <0 if interval ended.","attributes":[{"name":"resolution","dtype":"float32","value":-1.0,"doc":"Smallest meaningful difference between values in data. Annotations have no units, so the value is fixed to -1.0."},{"name":"unit","dtype":"text","value":"n/a","doc":"Base unit of measurement for working with the data. Annotations have no units, so the value is fixed to 'n/a'."}]}]},{"neurodata_type_def":"DecompositionSeries","neurodata_type_inc":"TimeSeries","doc":"Spectral analysis of a time series, e.g. of an LFP or a speech signal.","datasets":[{"name":"data","dtype":"numeric","dims":["num_times","num_channels","num_bands"],"shape":[null,null,null],"doc":"Data decomposed into frequency bands.","attributes":[{"name":"unit","dtype":"text","default_value":"no unit","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion'."}]},{"name":"metric","dtype":"text","doc":"The metric used, e.g. phase, amplitude, power."},{"name":"source_channels","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the channels that this decomposition series was generated from.","quantity":"?"}],"groups":[{"name":"bands","neurodata_type_inc":"DynamicTable","doc":"Table for describing the bands that this series was generated from. There should be one row in this table for each band.","datasets":[{"name":"band_name","neurodata_type_inc":"VectorData","dtype":"text","doc":"Name of the band, e.g. theta."},{"name":"band_limits","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands","low, high"],"shape":[null,2],"doc":"Low and high limit of each band in Hz. If it is a Gaussian filter, use 2 SD on either side of the center."},{"name":"band_mean","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands"],"shape":[null],"doc":"The mean Gaussian filters, in Hz."},{"name":"band_stdev","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands"],"shape":[null],"doc":"The standard deviation of Gaussian filters, in Hz."}]}],"links":[{"name":"source_timeseries","target_type":"TimeSeries","doc":"Link to TimeSeries object that this data was calculated from. Metadata about electrodes and their position can be read from that ElectricalSeries so it is not necessary to store that information here.","quantity":"?"}]},{"neurodata_type_def":"Units","neurodata_type_inc":"DynamicTable","default_name":"Units","doc":"Data about spiking units. Event times of observed units (e.g. cell, synapse, etc.) should be concatenated and stored in spike_times.","datasets":[{"name":"spike_times_index","neurodata_type_inc":"VectorIndex","doc":"Index into the spike_times dataset.","quantity":"?"},{"name":"spike_times","neurodata_type_inc":"VectorData","dtype":"float64","doc":"Spike times for each unit in seconds.","quantity":"?","attributes":[{"name":"resolution","dtype":"float64","doc":"The smallest possible difference between two spike times. Usually 1 divided by the acquisition sampling rate from which spike times were extracted, but could be larger if the acquisition time series was downsampled or smaller if the acquisition time series was smoothed/interpolated and it is possible for the spike time to be between samples.","required":false}]},{"name":"obs_intervals_index","neurodata_type_inc":"VectorIndex","doc":"Index into the obs_intervals dataset.","quantity":"?"},{"name":"obs_intervals","neurodata_type_inc":"VectorData","dtype":"float64","dims":["num_intervals","start|end"],"shape":[null,2],"doc":"Observation intervals for each unit.","quantity":"?"},{"name":"electrodes_index","neurodata_type_inc":"VectorIndex","doc":"Index into electrodes.","quantity":"?"},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"Electrode that each spike unit came from, specified using a DynamicTableRegion.","quantity":"?"},{"name":"electrode_group","neurodata_type_inc":"VectorData","dtype":{"target_type":"ElectrodeGroup","reftype":"object"},"doc":"Electrode group that each spike unit came from.","quantity":"?"},{"name":"waveform_mean","neurodata_type_inc":"VectorData","dtype":"float32","dims":[["num_units","num_samples"],["num_units","num_samples","num_electrodes"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveform mean for each spike unit.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveform_sd","neurodata_type_inc":"VectorData","dtype":"float32","dims":[["num_units","num_samples"],["num_units","num_samples","num_electrodes"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveform standard deviation for each spike unit.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveforms","neurodata_type_inc":"VectorData","dtype":"numeric","dims":["num_waveforms","num_samples"],"shape":[null,null],"doc":"Individual waveforms for each spike on each electrode. This is a doubly indexed column. The 'waveforms_index' column indexes which waveforms in this column belong to the same spike event for a given unit, where each waveform was recorded from a different electrode. The 'waveforms_index_index' column indexes the 'waveforms_index' column to indicate which spike events belong to a given unit. For example, if the 'waveforms_index_index' column has values [2, 5, 6], then the first 2 elements of the 'waveforms_index' column correspond to the 2 spike events of the first unit, the next 3 elements of the 'waveforms_index' column correspond to the 3 spike events of the second unit, and the next 1 element of the 'waveforms_index' column corresponds to the 1 spike event of the third unit. If the 'waveforms_index' column has values [3, 6, 8, 10, 12, 13], then the first 3 elements of the 'waveforms' column contain the 3 spike waveforms that were recorded from 3 different electrodes for the first spike time of the first unit. See https://nwb-schema.readthedocs.io/en/stable/format_description.html#doubly-ragged-arrays for a graphical representation of this example. When there is only one electrode for each unit (i.e., each spike time is associated with a single waveform), then the 'waveforms_index' column will have values 1, 2, ..., N, where N is the number of spike events. The number of electrodes for each spike event should be the same within a given unit. The 'electrodes' column should be used to indicate which electrodes are associated with each unit, and the order of the waveforms within a given unit x spike event should be in the same order as the electrodes referenced in the 'electrodes' column of this table. The number of samples for each waveform must be the same.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveforms_index","neurodata_type_inc":"VectorIndex","doc":"Index into the waveforms dataset. One value for every spike event. See 'waveforms' for more detail.","quantity":"?"},{"name":"waveforms_index_index","neurodata_type_inc":"VectorIndex","doc":"Index into the waveforms_index dataset. One value for every unit (row in the table). See 'waveforms' for more detail.","quantity":"?"}]}]})delimiter"; -const std::string nwb_behavior = R"delimiter( +constexpr std::string_view nwb_behavior = R"delimiter( {"groups":[{"neurodata_type_def":"SpatialSeries","neurodata_type_inc":"TimeSeries","doc":"Direction, e.g., of gaze or travel, or position. The TimeSeries::data field is a 2D array storing position or direction relative to some reference frame. Array structure: [num measurements] [num dimensions]. Each SpatialSeries has a text dataset reference_frame that indicates the zero-position, or the zero-axes for direction. For example, if representing gaze direction, 'straight-ahead' might be a specific pixel on the monitor, or some other point in space. For position data, the 0,0 point might be the top-left corner of an enclosure, as viewed from the tracking camera. The unit of data will indicate how to interpret SpatialSeries values.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","x"],["num_times","x,y"],["num_times","x,y,z"]],"shape":[[null],[null,1],[null,2],[null,3]],"doc":"1-D or 2-D array storing position or direction relative to some reference frame.","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Base unit of measurement for working with the data. The default value is 'meters'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'.","required":false}]},{"name":"reference_frame","dtype":"text","doc":"Description defining what exactly 'straight-ahead' means.","quantity":"?"}]},{"neurodata_type_def":"BehavioralEpochs","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralEpochs","doc":"TimeSeries for storing behavioral epochs. The objective of this and the other two Behavioral interfaces (e.g. BehavioralEvents and BehavioralTimeSeries) is to provide generic hooks for software tools/scripts. This allows a tool/script to take the output one specific interface (e.g., UnitTimes) and plot that data relative to another data modality (e.g., behavioral events) without having to define all possible modalities in advance. Declaring one of these interfaces means that one or more TimeSeries of the specified type is published. These TimeSeries should reside in a group having the same name as the interface. For example, if a BehavioralTimeSeries interface is declared, the module will have one or more TimeSeries defined in the module sub-group 'BehavioralTimeSeries'. BehavioralEpochs should use IntervalSeries. BehavioralEvents is used for irregular events. BehavioralTimeSeries is for continuous data.","groups":[{"neurodata_type_inc":"IntervalSeries","doc":"IntervalSeries object containing start and stop times of epochs.","quantity":"*"}]},{"neurodata_type_def":"BehavioralEvents","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralEvents","doc":"TimeSeries for storing behavioral events. See description of BehavioralEpochs for more details.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing behavioral events.","quantity":"*"}]},{"neurodata_type_def":"BehavioralTimeSeries","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralTimeSeries","doc":"TimeSeries for storing Behavoioral time series data. See description of BehavioralEpochs for more details.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing continuous behavioral data.","quantity":"*"}]},{"neurodata_type_def":"PupilTracking","neurodata_type_inc":"NWBDataInterface","default_name":"PupilTracking","doc":"Eye-tracking data, representing pupil size.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing time series data on pupil size.","quantity":"+"}]},{"neurodata_type_def":"EyeTracking","neurodata_type_inc":"NWBDataInterface","default_name":"EyeTracking","doc":"Eye-tracking data, representing direction of gaze.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing data measuring direction of gaze.","quantity":"*"}]},{"neurodata_type_def":"CompassDirection","neurodata_type_inc":"NWBDataInterface","default_name":"CompassDirection","doc":"With a CompassDirection interface, a module publishes a SpatialSeries object representing a floating point value for theta. The SpatialSeries::reference_frame field should indicate what direction corresponds to 0 and which is the direction of rotation (this should be clockwise). The si_unit for the SpatialSeries should be radians or degrees.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing direction of gaze travel.","quantity":"*"}]},{"neurodata_type_def":"Position","neurodata_type_inc":"NWBDataInterface","default_name":"Position","doc":"Position data, whether along the x, x/y or x/y/z axis.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing position data.","quantity":"+"}]}]})delimiter"; -const std::string nwb_ecephys = R"delimiter( +constexpr std::string_view nwb_ecephys = R"delimiter( {"groups":[{"neurodata_type_def":"ElectricalSeries","neurodata_type_inc":"TimeSeries","doc":"A time series of acquired voltage data from extracellular recordings. The data field is an int or float array storing data in volts. The first dimension should always represent time. The second dimension, if present, should represent channels.","attributes":[{"name":"filtering","dtype":"text","doc":"Filtering applied to all channels of the data. For example, if this ElectricalSeries represents high-pass-filtered data (also known as AP Band), then this value could be \"High-pass 4-pole Bessel filter at 500 Hz\". If this ElectricalSeries represents low-pass-filtered LFP data and the type of filter is unknown, then this value could be \"Low-pass filter at 300 Hz\". If a non-standard filter type is used, provide as much detail about the filter properties as possible.","required":false}],"datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_channels"],["num_times","num_channels","num_samples"]],"shape":[[null],[null,null],[null,null,null]],"doc":"Recorded voltage data.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. This value is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion', followed by 'channel_conversion' (if present), and then add 'offset'."}]},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the electrodes that this time series was generated from."},{"name":"channel_conversion","dtype":"float32","dims":["num_channels"],"shape":[null],"doc":"Channel-specific conversion factor. Multiply the data in the 'data' dataset by these values along the channel axis (as indicated by axis attribute) AND by the global conversion factor in the 'conversion' attribute of 'data' to get the data values in Volts, i.e, data in Volts = data * data.conversion * channel_conversion. This approach allows for both global and per-channel data conversion factors needed to support the storage of electrical recordings as native values generated by data acquisition systems. If this dataset is not present, then there is no channel-specific conversion factor, i.e. it is 1 for all channels.","quantity":"?","attributes":[{"name":"axis","dtype":"int32","value":1,"doc":"The zero-indexed axis of the 'data' dataset that the channel-specific conversion factor corresponds to. This value is fixed to 1."}]}]},{"neurodata_type_def":"SpikeEventSeries","neurodata_type_inc":"ElectricalSeries","doc":"Stores snapshots/snippets of recorded spike events (i.e., threshold crossings). This may also be raw data, as reported by ephys hardware. If so, the TimeSeries::description field should describe how events were detected. All SpikeEventSeries should reside in a module (under EventWaveform interface) even if the spikes were reported and stored by hardware. All events span the same recording channels and store snapshots of equal duration. TimeSeries::data array structure: [num events] [num channels] [num samples] (or [num events] [num samples] for single electrode).","datasets":[{"name":"data","dtype":"numeric","dims":[["num_events","num_samples"],["num_events","num_channels","num_samples"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveforms.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement for waveforms, which is fixed to 'volts'."}]},{"name":"timestamps","dtype":"float64","dims":["num_times"],"shape":[null],"doc":"Timestamps for samples stored in data, in seconds, relative to the common experiment master-clock stored in NWBFile.timestamps_reference_time. Timestamps are required for the events. Unlike for TimeSeries, timestamps are required for SpikeEventSeries and are thus re-specified here.","attributes":[{"name":"interval","dtype":"int32","value":1,"doc":"Value is '1'"},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for timestamps, which is fixed to 'seconds'."}]}]},{"neurodata_type_def":"FeatureExtraction","neurodata_type_inc":"NWBDataInterface","default_name":"FeatureExtraction","doc":"Features, such as PC1 and PC2, that are extracted from signals stored in a SpikeEventSeries or other source.","datasets":[{"name":"description","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Description of features (eg, ''PC1'') for each of the extracted features."},{"name":"features","dtype":"float32","dims":["num_events","num_channels","num_features"],"shape":[null,null,null],"doc":"Multi-dimensional array of features extracted from each event."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Times of events that features correspond to (can be a link)."},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the electrodes that this time series was generated from."}]},{"neurodata_type_def":"EventDetection","neurodata_type_inc":"NWBDataInterface","default_name":"EventDetection","doc":"Detected spike events from voltage trace(s).","datasets":[{"name":"detection_method","dtype":"text","doc":"Description of how events were detected, such as voltage threshold, or dV/dT threshold, as well as relevant values."},{"name":"source_idx","dtype":"int32","dims":["num_events"],"shape":[null],"doc":"Indices (zero-based) into source ElectricalSeries::data array corresponding to time of event. ''description'' should define what is meant by time of event (e.g., .25 ms before action potential peak, zero-crossing time, etc). The index points to each event from the raw data."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Timestamps of events, in seconds.","attributes":[{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for event times, which is fixed to 'seconds'."}]}],"links":[{"name":"source_electricalseries","target_type":"ElectricalSeries","doc":"Link to the ElectricalSeries that this data was calculated from. Metadata about electrodes and their position can be read from that ElectricalSeries so it's not necessary to include that information here."}]},{"neurodata_type_def":"EventWaveform","neurodata_type_inc":"NWBDataInterface","default_name":"EventWaveform","doc":"Represents either the waveforms of detected events, as extracted from a raw data trace in /acquisition, or the event waveforms that were stored during experiment acquisition.","groups":[{"neurodata_type_inc":"SpikeEventSeries","doc":"SpikeEventSeries object(s) containing detected spike event waveforms.","quantity":"*"}]},{"neurodata_type_def":"FilteredEphys","neurodata_type_inc":"NWBDataInterface","default_name":"FilteredEphys","doc":"Electrophysiology data from one or more channels that has been subjected to filtering. Examples of filtered data include Theta and Gamma (LFP has its own interface). FilteredEphys modules publish an ElectricalSeries for each filtered channel or set of channels. The name of each ElectricalSeries is arbitrary but should be informative. The source of the filtered data, whether this is from analysis of another time series or as acquired by hardware, should be noted in each's TimeSeries::description field. There is no assumed 1::1 correspondence between filtered ephys signals and electrodes, as a single signal can apply to many nearby electrodes, and one electrode may have different filtered (e.g., theta and/or gamma) signals represented. Filter properties should be noted in the ElectricalSeries 'filtering' attribute.","groups":[{"neurodata_type_inc":"ElectricalSeries","doc":"ElectricalSeries object(s) containing filtered electrophysiology data.","quantity":"+"}]},{"neurodata_type_def":"LFP","neurodata_type_inc":"NWBDataInterface","default_name":"LFP","doc":"LFP data from one or more channels. The electrode map in each published ElectricalSeries will identify which channels are providing LFP data. Filter properties should be noted in the ElectricalSeries 'filtering' attribute.","groups":[{"neurodata_type_inc":"ElectricalSeries","doc":"ElectricalSeries object(s) containing LFP data for one or more channels.","quantity":"+"}]},{"neurodata_type_def":"ElectrodeGroup","neurodata_type_inc":"NWBContainer","doc":"A physical grouping of electrodes, e.g. a shank of an array.","attributes":[{"name":"description","dtype":"text","doc":"Description of this electrode group."},{"name":"location","dtype":"text","doc":"Location of electrode group. Specify the area, layer, comments on estimation of area/layer, etc. Use standard atlas names for anatomical regions when possible."}],"datasets":[{"name":"position","dtype":[{"name":"x","dtype":"float32","doc":"x coordinate"},{"name":"y","dtype":"float32","doc":"y coordinate"},{"name":"z","dtype":"float32","doc":"z coordinate"}],"doc":"stereotaxic or common framework coordinates","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the device that was used to record from this electrode group."}]},{"neurodata_type_def":"ClusterWaveforms","neurodata_type_inc":"NWBDataInterface","default_name":"ClusterWaveforms","doc":"DEPRECATED The mean waveform shape, including standard deviation, of the different clusters. Ideally, the waveform analysis should be performed on data that is only high-pass filtered. This is a separate module because it is expected to require updating. For example, IMEC probes may require different storage requirements to store/display mean waveforms, requiring a new interface or an extension of this one.","datasets":[{"name":"waveform_filtering","dtype":"text","doc":"Filtering applied to data before generating mean/sd"},{"name":"waveform_mean","dtype":"float32","dims":["num_clusters","num_samples"],"shape":[null,null],"doc":"The mean waveform for each cluster, using the same indices for each wave as cluster numbers in the associated Clustering module (i.e, cluster 3 is in array slot [3]). Waveforms corresponding to gaps in cluster sequence should be empty (e.g., zero- filled)"},{"name":"waveform_sd","dtype":"float32","dims":["num_clusters","num_samples"],"shape":[null,null],"doc":"Stdev of waveforms for each cluster, using the same indices as in mean"}],"links":[{"name":"clustering_interface","target_type":"Clustering","doc":"Link to Clustering interface that was the source of the clustered data"}]},{"neurodata_type_def":"Clustering","neurodata_type_inc":"NWBDataInterface","default_name":"Clustering","doc":"DEPRECATED Clustered spike data, whether from automatic clustering tools (e.g., klustakwik) or as a result of manual sorting.","datasets":[{"name":"description","dtype":"text","doc":"Description of clusters or clustering, (e.g. cluster 0 is noise, clusters curated using Klusters, etc)"},{"name":"num","dtype":"int32","dims":["num_events"],"shape":[null],"doc":"Cluster number of each event"},{"name":"peak_over_rms","dtype":"float32","dims":["num_clusters"],"shape":[null],"doc":"Maximum ratio of waveform peak to RMS on any channel in the cluster (provides a basic clustering metric)."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Times of clustered events, in seconds. This may be a link to times field in associated FeatureExtraction module."}]}]})delimiter"; -const std::string nwb_icephys = R"delimiter( +constexpr std::string_view nwb_icephys = R"delimiter( {"groups":[{"neurodata_type_def":"PatchClampSeries","neurodata_type_inc":"TimeSeries","doc":"An abstract base class for patch-clamp data - stimulus or response, current or voltage.","attributes":[{"name":"stimulus_description","dtype":"text","doc":"Protocol/stimulus name for this patch-clamp dataset."},{"name":"sweep_number","dtype":"uint32","doc":"Sweep number, allows to group different PatchClampSeries together.","required":false}],"datasets":[{"name":"data","dtype":"numeric","dims":["num_times"],"shape":[null],"doc":"Recorded voltage or current.","attributes":[{"name":"unit","dtype":"text","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"gain","dtype":"float32","doc":"Gain of the recording, in units Volt/Amp (v-clamp) or Volt/Volt (c-clamp).","quantity":"?"}],"links":[{"name":"electrode","target_type":"IntracellularElectrode","doc":"Link to IntracellularElectrode object that describes the electrode that was used to apply or record this data."}]},{"neurodata_type_def":"CurrentClampSeries","neurodata_type_inc":"PatchClampSeries","doc":"Voltage data from an intracellular current-clamp recording. A corresponding CurrentClampStimulusSeries (stored separately as a stimulus) is used to store the current injected.","datasets":[{"name":"data","doc":"Recorded voltage.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. which is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"bias_current","dtype":"float32","doc":"Bias current, in amps.","quantity":"?"},{"name":"bridge_balance","dtype":"float32","doc":"Bridge balance, in ohms.","quantity":"?"},{"name":"capacitance_compensation","dtype":"float32","doc":"Capacitance compensation, in farads.","quantity":"?"}]},{"neurodata_type_def":"IZeroClampSeries","neurodata_type_inc":"CurrentClampSeries","doc":"Voltage data from an intracellular recording when all current and amplifier settings are off (i.e., CurrentClampSeries fields will be zero). There is no CurrentClampStimulusSeries associated with an IZero series because the amplifier is disconnected and no stimulus can reach the cell.","attributes":[{"name":"stimulus_description","dtype":"text","doc":"An IZeroClampSeries has no stimulus, so this attribute is automatically set to \"N/A\"","value":"N/A"}],"datasets":[{"name":"bias_current","dtype":"float32","value":0.0,"doc":"Bias current, in amps, fixed to 0.0."},{"name":"bridge_balance","dtype":"float32","value":0.0,"doc":"Bridge balance, in ohms, fixed to 0.0."},{"name":"capacitance_compensation","dtype":"float32","value":0.0,"doc":"Capacitance compensation, in farads, fixed to 0.0."}]},{"neurodata_type_def":"CurrentClampStimulusSeries","neurodata_type_inc":"PatchClampSeries","doc":"Stimulus current applied during current clamp recording.","datasets":[{"name":"data","doc":"Stimulus current applied.","attributes":[{"name":"unit","dtype":"text","value":"amperes","doc":"Base unit of measurement for working with the data. which is fixed to 'amperes'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]}]},{"neurodata_type_def":"VoltageClampSeries","neurodata_type_inc":"PatchClampSeries","doc":"Current data from an intracellular voltage-clamp recording. A corresponding VoltageClampStimulusSeries (stored separately as a stimulus) is used to store the voltage injected.","datasets":[{"name":"data","doc":"Recorded current.","attributes":[{"name":"unit","dtype":"text","value":"amperes","doc":"Base unit of measurement for working with the data. which is fixed to 'amperes'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"capacitance_fast","dtype":"float32","doc":"Fast capacitance, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for capacitance_fast, which is fixed to 'farads'."}]},{"name":"capacitance_slow","dtype":"float32","doc":"Slow capacitance, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for capacitance_fast, which is fixed to 'farads'."}]},{"name":"resistance_comp_bandwidth","dtype":"float32","doc":"Resistance compensation bandwidth, in hertz.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"hertz","doc":"Unit of measurement for resistance_comp_bandwidth, which is fixed to 'hertz'."}]},{"name":"resistance_comp_correction","dtype":"float32","doc":"Resistance compensation correction, in percent.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"percent","doc":"Unit of measurement for resistance_comp_correction, which is fixed to 'percent'."}]},{"name":"resistance_comp_prediction","dtype":"float32","doc":"Resistance compensation prediction, in percent.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"percent","doc":"Unit of measurement for resistance_comp_prediction, which is fixed to 'percent'."}]},{"name":"whole_cell_capacitance_comp","dtype":"float32","doc":"Whole cell capacitance compensation, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for whole_cell_capacitance_comp, which is fixed to 'farads'."}]},{"name":"whole_cell_series_resistance_comp","dtype":"float32","doc":"Whole cell series resistance compensation, in ohms.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"ohms","doc":"Unit of measurement for whole_cell_series_resistance_comp, which is fixed to 'ohms'."}]}]},{"neurodata_type_def":"VoltageClampStimulusSeries","neurodata_type_inc":"PatchClampSeries","doc":"Stimulus voltage applied during a voltage clamp recording.","datasets":[{"name":"data","doc":"Stimulus voltage applied.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. which is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]}]},{"neurodata_type_def":"IntracellularElectrode","neurodata_type_inc":"NWBContainer","doc":"An intracellular electrode and its metadata.","datasets":[{"name":"cell_id","dtype":"text","doc":"unique ID of the cell","quantity":"?"},{"name":"description","dtype":"text","doc":"Description of electrode (e.g., whole-cell, sharp, etc.)."},{"name":"filtering","dtype":"text","doc":"Electrode specific filtering.","quantity":"?"},{"name":"initial_access_resistance","dtype":"text","doc":"Initial access resistance.","quantity":"?"},{"name":"location","dtype":"text","doc":"Location of the electrode. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible.","quantity":"?"},{"name":"resistance","dtype":"text","doc":"Electrode resistance, in ohms.","quantity":"?"},{"name":"seal","dtype":"text","doc":"Information about seal used for recording.","quantity":"?"},{"name":"slice","dtype":"text","doc":"Information about slice used for recording.","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Device that was used to record from this electrode."}]},{"neurodata_type_def":"SweepTable","neurodata_type_inc":"DynamicTable","doc":"[DEPRECATED] Table used to group different PatchClampSeries. SweepTable is being replaced by IntracellularRecordingsTable and SimultaneousRecordingsTable tables. Additional SequentialRecordingsTable, RepetitionsTable, and ExperimentalConditions tables provide enhanced support for experiment metadata.","datasets":[{"name":"sweep_number","neurodata_type_inc":"VectorData","dtype":"uint32","doc":"Sweep number of the PatchClampSeries in that row."},{"name":"series","neurodata_type_inc":"VectorData","dtype":{"target_type":"PatchClampSeries","reftype":"object"},"doc":"The PatchClampSeries with the sweep number in that row."},{"name":"series_index","neurodata_type_inc":"VectorIndex","doc":"Index for series."}]},{"neurodata_type_def":"IntracellularElectrodesTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular electrode related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular electrode related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"electrode","neurodata_type_inc":"VectorData","dtype":{"target_type":"IntracellularElectrode","reftype":"object"},"doc":"Column for storing the reference to the intracellular electrode."}]},{"neurodata_type_def":"IntracellularStimuliTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular stimulus related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular stimulus related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"stimulus","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the recorded stimulus for the recording (rows)."},{"name":"stimulus_template","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the stimulus template for the recording (rows).","quantity":"?"}]},{"neurodata_type_def":"IntracellularResponsesTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular response related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular response related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"response","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the recorded response for the recording (rows)"}]},{"neurodata_type_def":"IntracellularRecordingsTable","neurodata_type_inc":"AlignedDynamicTable","name":"intracellular_recordings","doc":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording. Each row in the table represents a single recording consisting typically of a stimulus and a corresponding response. In some cases, however, only a stimulus or a response is recorded as part of an experiment. In this case, both the stimulus and response will point to the same TimeSeries while the idx_start and count of the invalid column will be set to -1, thus, indicating that no values have been recorded for the stimulus or response, respectively. Note, a recording MUST contain at least a stimulus or a response. Typically the stimulus and response are PatchClampSeries. However, the use of AD/DA channels that are not associated to an electrode is also common in intracellular electrophysiology, in which case other TimeSeries may be used.","attributes":[{"name":"description","dtype":"text","value":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording and for storing metadata about the intracellular recording.","doc":"Description of the contents of this table. Inherited from AlignedDynamicTable and overwritten here to fix the value of the attribute."}],"groups":[{"name":"electrodes","neurodata_type_inc":"IntracellularElectrodesTable","doc":"Table for storing intracellular electrode related metadata."},{"name":"stimuli","neurodata_type_inc":"IntracellularStimuliTable","doc":"Table for storing intracellular stimulus related metadata."},{"name":"responses","neurodata_type_inc":"IntracellularResponsesTable","doc":"Table for storing intracellular response related metadata."}]},{"neurodata_type_def":"SimultaneousRecordingsTable","neurodata_type_inc":"DynamicTable","name":"simultaneous_recordings","doc":"A table for grouping different intracellular recordings from the IntracellularRecordingsTable table together that were recorded simultaneously from different electrodes.","datasets":[{"name":"recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the IntracellularRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"IntracellularRecordingsTable","reftype":"object"},"doc":"Reference to the IntracellularRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the recordings column."}]},{"neurodata_type_def":"SequentialRecordingsTable","neurodata_type_inc":"DynamicTable","name":"sequential_recordings","doc":"A table for grouping different sequential recordings from the SimultaneousRecordingsTable table together. This is typically used to group together sequential recordings where a sequence of stimuli of the same type with varying parameters have been presented in a sequence.","datasets":[{"name":"simultaneous_recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the SimultaneousRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"SimultaneousRecordingsTable","reftype":"object"},"doc":"Reference to the SimultaneousRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"simultaneous_recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the simultaneous_recordings column."},{"name":"stimulus_type","neurodata_type_inc":"VectorData","dtype":"text","doc":"The type of stimulus used for the sequential recording."}]},{"neurodata_type_def":"RepetitionsTable","neurodata_type_inc":"DynamicTable","name":"repetitions","doc":"A table for grouping different sequential intracellular recordings together. With each SequentialRecording typically representing a particular type of stimulus, the RepetitionsTable table is typically used to group sets of stimuli applied in sequence.","datasets":[{"name":"sequential_recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the SequentialRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"SequentialRecordingsTable","reftype":"object"},"doc":"Reference to the SequentialRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"sequential_recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the sequential_recordings column."}]},{"neurodata_type_def":"ExperimentalConditionsTable","neurodata_type_inc":"DynamicTable","name":"experimental_conditions","doc":"A table for grouping different intracellular recording repetitions together that belong to the same experimental condition.","datasets":[{"name":"repetitions","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the RepetitionsTable table.","attributes":[{"name":"table","dtype":{"target_type":"RepetitionsTable","reftype":"object"},"doc":"Reference to the RepetitionsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"repetitions_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the repetitions column."}]}]})delimiter"; -const std::string nwb_ogen = R"delimiter( +constexpr std::string_view nwb_ogen = R"delimiter( {"groups":[{"neurodata_type_def":"OptogeneticSeries","neurodata_type_inc":"TimeSeries","doc":"An optogenetic stimulus.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_rois"]],"shape":[[null],[null,null]],"doc":"Applied power for optogenetic stimulus, in watts. Shape can be 1D or 2D. 2D data is meant to be used in an extension of OptogeneticSeries that defines what the second dimension represents.","attributes":[{"name":"unit","dtype":"text","value":"watts","doc":"Unit of measurement for data, which is fixed to 'watts'."}]}],"links":[{"name":"site","target_type":"OptogeneticStimulusSite","doc":"Link to OptogeneticStimulusSite object that describes the site to which this stimulus was applied."}]},{"neurodata_type_def":"OptogeneticStimulusSite","neurodata_type_inc":"NWBContainer","doc":"A site of optogenetic stimulation.","datasets":[{"name":"description","dtype":"text","doc":"Description of stimulation site."},{"name":"excitation_lambda","dtype":"float32","doc":"Excitation wavelength, in nm."},{"name":"location","dtype":"text","doc":"Location of the stimulation site. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."}],"links":[{"name":"device","target_type":"Device","doc":"Device that generated the stimulus."}]}]})delimiter"; -const std::string nwb_ophys = R"delimiter( +constexpr std::string_view nwb_ophys = R"delimiter( {"groups":[{"neurodata_type_def":"OnePhotonSeries","neurodata_type_inc":"ImageSeries","doc":"Image stack recorded over time from 1-photon microscope.","attributes":[{"name":"pmt_gain","dtype":"float32","doc":"Photomultiplier gain.","required":false},{"name":"scan_line_rate","dtype":"float32","doc":"Lines imaged per second. This is also stored in /general/optophysiology but is kept here as it is useful information for analysis, and so good to be stored w/ the actual data.","required":false},{"name":"exposure_time","dtype":"float32","doc":"Exposure time of the sample; often the inverse of the frequency.","required":false},{"name":"binning","dtype":"uint8","doc":"Amount of pixels combined into 'bins'; could be 1, 2, 4, 8, etc.","required":false},{"name":"power","dtype":"float32","doc":"Power of the excitation in mW, if known.","required":false},{"name":"intensity","dtype":"float32","doc":"Intensity of the excitation in mW/mm^2, if known.","required":false}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this TimeSeries data was generated."}]},{"neurodata_type_def":"TwoPhotonSeries","neurodata_type_inc":"ImageSeries","doc":"Image stack recorded over time from 2-photon microscope.","attributes":[{"name":"pmt_gain","dtype":"float32","doc":"Photomultiplier gain.","required":false},{"name":"scan_line_rate","dtype":"float32","doc":"Lines imaged per second. This is also stored in /general/optophysiology but is kept here as it is useful information for analysis, and so good to be stored w/ the actual data.","required":false}],"datasets":[{"name":"field_of_view","dtype":"float32","dims":[["width|height"],["width|height|depth"]],"shape":[[2],[3]],"doc":"Width, height and depth of image, or imaged area, in meters.","quantity":"?"}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this TimeSeries data was generated."}]},{"neurodata_type_def":"RoiResponseSeries","neurodata_type_inc":"TimeSeries","doc":"ROI responses over an imaging plane. The first dimension represents time. The second dimension, if present, represents ROIs.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_ROIs"]],"shape":[[null],[null,null]],"doc":"Signals from ROIs."},{"name":"rois","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion referencing into an ROITable containing information on the ROIs stored in this timeseries."}]},{"neurodata_type_def":"DfOverF","neurodata_type_inc":"NWBDataInterface","default_name":"DfOverF","doc":"dF/F information about a region of interest (ROI). Storage hierarchy of dF/F should be the same as for segmentation (i.e., same names for ROIs and for image planes).","groups":[{"neurodata_type_inc":"RoiResponseSeries","doc":"RoiResponseSeries object(s) containing dF/F for a ROI.","quantity":"+"}]},{"neurodata_type_def":"Fluorescence","neurodata_type_inc":"NWBDataInterface","default_name":"Fluorescence","doc":"Fluorescence information about a region of interest (ROI). Storage hierarchy of fluorescence should be the same as for segmentation (ie, same names for ROIs and for image planes).","groups":[{"neurodata_type_inc":"RoiResponseSeries","doc":"RoiResponseSeries object(s) containing fluorescence data for a ROI.","quantity":"+"}]},{"neurodata_type_def":"ImageSegmentation","neurodata_type_inc":"NWBDataInterface","default_name":"ImageSegmentation","doc":"Stores pixels in an image that represent different regions of interest (ROIs) or masks. All segmentation for a given imaging plane is stored together, with storage for multiple imaging planes (masks) supported. Each ROI is stored in its own subgroup, with the ROI group containing both a 2D mask and a list of pixels that make up this mask. Segments can also be used for masking neuropil. If segmentation is allowed to change with time, a new imaging plane (or module) is required and ROI names should remain consistent between them.","groups":[{"neurodata_type_inc":"PlaneSegmentation","doc":"Results from image segmentation of a specific imaging plane.","quantity":"+"}]},{"neurodata_type_def":"PlaneSegmentation","neurodata_type_inc":"DynamicTable","doc":"Results from image segmentation of a specific imaging plane.","datasets":[{"name":"image_mask","neurodata_type_inc":"VectorData","dims":[["num_roi","num_x","num_y"],["num_roi","num_x","num_y","num_z"]],"shape":[[null,null,null],[null,null,null,null]],"doc":"ROI masks for each ROI. Each image mask is the size of the original imaging plane (or volume) and members of the ROI are finite non-zero.","quantity":"?"},{"name":"pixel_mask_index","neurodata_type_inc":"VectorIndex","doc":"Index into pixel_mask.","quantity":"?"},{"name":"pixel_mask","neurodata_type_inc":"VectorData","dtype":[{"name":"x","dtype":"uint32","doc":"Pixel x-coordinate."},{"name":"y","dtype":"uint32","doc":"Pixel y-coordinate."},{"name":"weight","dtype":"float32","doc":"Weight of the pixel."}],"doc":"Pixel masks for each ROI: a list of indices and weights for the ROI. Pixel masks are concatenated and parsing of this dataset is maintained by the PlaneSegmentation","quantity":"?"},{"name":"voxel_mask_index","neurodata_type_inc":"VectorIndex","doc":"Index into voxel_mask.","quantity":"?"},{"name":"voxel_mask","neurodata_type_inc":"VectorData","dtype":[{"name":"x","dtype":"uint32","doc":"Voxel x-coordinate."},{"name":"y","dtype":"uint32","doc":"Voxel y-coordinate."},{"name":"z","dtype":"uint32","doc":"Voxel z-coordinate."},{"name":"weight","dtype":"float32","doc":"Weight of the voxel."}],"doc":"Voxel masks for each ROI: a list of indices and weights for the ROI. Voxel masks are concatenated and parsing of this dataset is maintained by the PlaneSegmentation","quantity":"?"}],"groups":[{"name":"reference_images","doc":"Image stacks that the segmentation masks apply to.","groups":[{"neurodata_type_inc":"ImageSeries","doc":"One or more image stacks that the masks apply to (can be one-element stack).","quantity":"*"}]}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this data was generated."}]},{"neurodata_type_def":"ImagingPlane","neurodata_type_inc":"NWBContainer","doc":"An imaging plane and its metadata.","datasets":[{"name":"description","dtype":"text","doc":"Description of the imaging plane.","quantity":"?"},{"name":"excitation_lambda","dtype":"float32","doc":"Excitation wavelength, in nm."},{"name":"imaging_rate","dtype":"float32","doc":"Rate that images are acquired, in Hz. If the corresponding TimeSeries is present, the rate should be stored there instead.","quantity":"?"},{"name":"indicator","dtype":"text","doc":"Calcium indicator."},{"name":"location","dtype":"text","doc":"Location of the imaging plane. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."},{"name":"manifold","dtype":"float32","dims":[["height","width","x, y, z"],["height","width","depth","x, y, z"]],"shape":[[null,null,3],[null,null,null,3]],"doc":"DEPRECATED Physical position of each pixel. 'xyz' represents the position of the pixel relative to the defined coordinate space. Deprecated in favor of origin_coords and grid_spacing.","quantity":"?","attributes":[{"name":"conversion","dtype":"float32","default_value":1.0,"doc":"Scalar to multiply each element in data to convert it to the specified 'unit'. If the data are stored in acquisition system units or other units that require a conversion to be interpretable, multiply the data by 'conversion' to convert the data to the specified 'unit'. e.g. if the data acquisition system stores values in this object as pixels from x = -500 to 499, y = -500 to 499 that correspond to a 2 m x 2 m range, then the 'conversion' multiplier to get from raw data acquisition pixel units to meters is 2/1000.","required":false},{"name":"unit","dtype":"text","default_value":"meters","doc":"Base unit of measurement for working with the data. The default value is 'meters'.","required":false}]},{"name":"origin_coords","dtype":"float32","dims":[["x, y"],["x, y, z"]],"shape":[[2],[3]],"doc":"Physical location of the first element of the imaging plane (0, 0) for 2-D data or (0, 0, 0) for 3-D data. See also reference_frame for what the physical location is relative to (e.g., bregma).","quantity":"?","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Measurement units for origin_coords. The default value is 'meters'."}]},{"name":"grid_spacing","dtype":"float32","dims":[["x, y"],["x, y, z"]],"shape":[[2],[3]],"doc":"Space between pixels in (x, y) or voxels in (x, y, z) directions, in the specified unit. Assumes imaging plane is a regular grid. See also reference_frame to interpret the grid.","quantity":"?","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Measurement units for grid_spacing. The default value is 'meters'."}]},{"name":"reference_frame","dtype":"text","doc":"Describes reference frame of origin_coords and grid_spacing. For example, this can be a text description of the anatomical location and orientation of the grid defined by origin_coords and grid_spacing or the vectors needed to transform or rotate the grid to a common anatomical axis (e.g., AP/DV/ML). This field is necessary to interpret origin_coords and grid_spacing. If origin_coords and grid_spacing are not present, then this field is not required. For example, if the microscope takes 10 x 10 x 2 images, where the first value of the data matrix (index (0, 0, 0)) corresponds to (-1.2, -0.6, -2) mm relative to bregma, the spacing between pixels is 0.2 mm in x, 0.2 mm in y and 0.5 mm in z, and larger numbers in x means more anterior, larger numbers in y means more rightward, and larger numbers in z means more ventral, then enter the following -- origin_coords = (-1.2, -0.6, -2) grid_spacing = (0.2, 0.2, 0.5) reference_frame = \"Origin coordinates are relative to bregma. First dimension corresponds to anterior-posterior axis (larger index = more anterior). Second dimension corresponds to medial-lateral axis (larger index = more rightward). Third dimension corresponds to dorsal-ventral axis (larger index = more ventral).\"","quantity":"?"}],"groups":[{"neurodata_type_inc":"OpticalChannel","doc":"An optical channel used to record from an imaging plane.","quantity":"+"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the Device object that was used to record from this electrode."}]},{"neurodata_type_def":"OpticalChannel","neurodata_type_inc":"NWBContainer","doc":"An optical channel used to record from an imaging plane.","datasets":[{"name":"description","dtype":"text","doc":"Description or other notes about the channel."},{"name":"emission_lambda","dtype":"float32","doc":"Emission wavelength for channel, in nm."}]},{"neurodata_type_def":"MotionCorrection","neurodata_type_inc":"NWBDataInterface","default_name":"MotionCorrection","doc":"An image stack where all frames are shifted (registered) to a common coordinate system, to account for movement and drift between frames. Note: each frame at each point in time is assumed to be 2-D (has only x & y dimensions).","groups":[{"neurodata_type_inc":"CorrectedImageStack","doc":"Results from motion correction of an image stack.","quantity":"+"}]},{"neurodata_type_def":"CorrectedImageStack","neurodata_type_inc":"NWBDataInterface","doc":"Results from motion correction of an image stack.","groups":[{"name":"corrected","neurodata_type_inc":"ImageSeries","doc":"Image stack with frames shifted to the common coordinates."},{"name":"xy_translation","neurodata_type_inc":"TimeSeries","doc":"Stores the x,y delta necessary to align each frame to the common coordinates, for example, to align each frame to a reference image."}],"links":[{"name":"original","target_type":"ImageSeries","doc":"Link to ImageSeries object that is being registered."}]}]})delimiter"; -const std::string nwb_retinotopy = R"delimiter( +constexpr std::string_view nwb_retinotopy = R"delimiter( {"groups":[{"neurodata_type_def":"ImagingRetinotopy","neurodata_type_inc":"NWBDataInterface","default_name":"ImagingRetinotopy","doc":"DEPRECATED. Intrinsic signal optical imaging or widefield imaging for measuring retinotopy. Stores orthogonal maps (e.g., altitude/azimuth; radius/theta) of responses to specific stimuli and a combined polarity map from which to identify visual areas. This group does not store the raw responses imaged during retinotopic mapping or the stimuli presented, but rather the resulting phase and power maps after applying a Fourier transform on the averaged responses. Note: for data consistency, all images and arrays are stored in the format [row][column] and [row, col], which equates to [y][x]. Field of view and dimension arrays may appear backward (i.e., y before x).","datasets":[{"name":"axis_1_phase_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Phase response to stimulus on the first measured axis.","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_1_power_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Power response on the first measured axis. Response is scaled so 0.0 is no power in the response and 1.0 is maximum relative power.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_2_phase_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Phase response to stimulus on the second measured axis.","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_2_power_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Power response on the second measured axis. Response is scaled so 0.0 is no power in the response and 1.0 is maximum relative power.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_descriptions","dtype":"text","dims":["axis_1, axis_2"],"shape":[2],"doc":"Two-element array describing the contents of the two response axis fields. Description should be something like ['altitude', 'azimuth'] or '['radius', 'theta']."},{"name":"focal_depth_image","dtype":"uint16","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Gray-scale image taken with same settings/parameters (e.g., focal depth, wavelength) as data collection. Array format: [rows][columns].","quantity":"?","attributes":[{"name":"bits_per_pixel","dtype":"int32","doc":"Number of bits used to represent each value. This is necessary to determine maximum (white) pixel value."},{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"focal_depth","dtype":"float32","doc":"Focal depth offset, in meters."},{"name":"format","dtype":"text","doc":"Format of image. Right now only 'raw' is supported."}]},{"name":"sign_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Sine of the angle between the direction of the gradient in axis_1 and axis_2.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."}]},{"name":"vasculature_image","dtype":"uint16","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Gray-scale anatomical image of cortical surface. Array structure: [rows][columns]","attributes":[{"name":"bits_per_pixel","dtype":"int32","doc":"Number of bits used to represent each value. This is necessary to determine maximum (white) pixel value"},{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"format","dtype":"text","doc":"Format of image. Right now only 'raw' is supported."}]}]}]})delimiter"; -const std::string namespaces = R"delimiter( +constexpr std::string_view namespaces = R"delimiter( {"namespaces":[{"name":"core","doc":"NWB namespace","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter","Keith Godfrey","Jeff Teeters"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov","keithg@alleninstitute.org","jteeters@berkeley.edu"],"full_name":"NWB core","schema":[{"namespace":"hdmf-common"},{"source":"nwb.base"},{"source":"nwb.device"},{"source":"nwb.epoch"},{"source":"nwb.image"},{"source":"nwb.file"},{"source":"nwb.misc"},{"source":"nwb.behavior"},{"source":"nwb.ecephys"},{"source":"nwb.icephys"},{"source":"nwb.ogen"},{"source":"nwb.ophys"},{"source":"nwb.retinotopy"}],"version":"2.7.0"}]})delimiter"; -void registerVariables(std::map& registry) { - registry["nwb.base"] = &nwb_base; - registry["nwb.device"] = &nwb_device; - registry["nwb.epoch"] = &nwb_epoch; - registry["nwb.image"] = &nwb_image; - registry["nwb.file"] = &nwb_file; - registry["nwb.misc"] = &nwb_misc; - registry["nwb.behavior"] = &nwb_behavior; - registry["nwb.ecephys"] = &nwb_ecephys; - registry["nwb.icephys"] = &nwb_icephys; - registry["nwb.ogen"] = &nwb_ogen; - registry["nwb.ophys"] = &nwb_ophys; - registry["nwb.retinotopy"] = &nwb_retinotopy; - registry["namespace"] = &namespaces; -}; -} // namespace AQNWB::spec::core +constexpr std::array, 13> + specVariables {{{"nwb.base", nwb_base}, + {"nwb.device", nwb_device}, + {"nwb.epoch", nwb_epoch}, + {"nwb.image", nwb_image}, + {"nwb.file", nwb_file}, + {"nwb.misc", nwb_misc}, + {"nwb.behavior", nwb_behavior}, + {"nwb.ecephys", nwb_ecephys}, + {"nwb.icephys", nwb_icephys}, + {"nwb.ogen", nwb_ogen}, + {"nwb.ophys", nwb_ophys}, + {"nwb.retinotopy", nwb_retinotopy}, + {"namespace", namespaces}}}; +} // namespace AQNWB::SPEC::CORE diff --git a/Source/aqnwb/spec/hdmf_common.hpp b/Source/aqnwb/spec/hdmf_common.hpp index 90ea926..54e1f1d 100644 --- a/Source/aqnwb/spec/hdmf_common.hpp +++ b/Source/aqnwb/spec/hdmf_common.hpp @@ -1,28 +1,29 @@ #pragma once +#include #include +#include -namespace AQNWB::spec::hdmf_common +namespace AQNWB::SPEC::HDMF_COMMON { const std::string version = "1.8.0"; -const std::string base = R"delimiter( +constexpr std::string_view base = R"delimiter( {"datasets":[{"data_type_def":"Data","doc":"An abstract data type for a dataset."}],"groups":[{"data_type_def":"Container","doc":"An abstract data type for a group storing collections of data and metadata. Base type for all data and metadata containers."},{"data_type_def":"SimpleMultiContainer","data_type_inc":"Container","doc":"A simple Container for holding onto multiple containers.","datasets":[{"data_type_inc":"Data","quantity":"*","doc":"Data objects held within this SimpleMultiContainer."}],"groups":[{"data_type_inc":"Container","quantity":"*","doc":"Container objects held within this SimpleMultiContainer."}]}]})delimiter"; -const std::string table = R"delimiter( +constexpr std::string_view table = R"delimiter( {"datasets":[{"data_type_def":"VectorData","data_type_inc":"Data","doc":"An n-dimensional dataset representing a column of a DynamicTable. If used without an accompanying VectorIndex, first dimension is along the rows of the DynamicTable and each step along the first dimension is a cell of the larger table. VectorData can also be used to represent a ragged array if paired with a VectorIndex. This allows for storing arrays of varying length in a single cell of the DynamicTable by indexing into this VectorData. The first vector is at VectorData[0:VectorIndex[0]]. The second vector is at VectorData[VectorIndex[0]:VectorIndex[1]], and so on.","dims":[["dim0"],["dim0","dim1"],["dim0","dim1","dim2"],["dim0","dim1","dim2","dim3"]],"shape":[[null],[null,null],[null,null,null],[null,null,null,null]],"attributes":[{"name":"description","dtype":"text","doc":"Description of what these vectors represent."}]},{"data_type_def":"VectorIndex","data_type_inc":"VectorData","dtype":"uint8","doc":"Used with VectorData to encode a ragged array. An array of indices into the first dimension of the target VectorData, and forming a map between the rows of a DynamicTable and the indices of the VectorData. The name of the VectorIndex is expected to be the name of the target VectorData object followed by \"_index\".","dims":["num_rows"],"shape":[null],"attributes":[{"name":"target","dtype":{"target_type":"VectorData","reftype":"object"},"doc":"Reference to the target dataset that this index applies to."}]},{"data_type_def":"ElementIdentifiers","data_type_inc":"Data","default_name":"element_id","dtype":"int","dims":["num_elements"],"shape":[null],"doc":"A list of unique identifiers for values within a dataset, e.g. rows of a DynamicTable."},{"data_type_def":"DynamicTableRegion","data_type_inc":"VectorData","dtype":"int","doc":"DynamicTableRegion provides a link from one table to an index or region of another. The `table` attribute is a link to another `DynamicTable`, indicating which table is referenced, and the data is int(s) indicating the row(s) (0-indexed) of the target array. `DynamicTableRegion`s can be used to associate rows with repeated meta-data without data duplication. They can also be used to create hierarchical relationships between multiple `DynamicTable`s. `DynamicTableRegion` objects may be paired with a `VectorIndex` object to create ragged references, so a single cell of a `DynamicTable` can reference many rows of another `DynamicTable`.","dims":["num_rows"],"shape":[null],"attributes":[{"name":"table","dtype":{"target_type":"DynamicTable","reftype":"object"},"doc":"Reference to the DynamicTable object that this region applies to."},{"name":"description","dtype":"text","doc":"Description of what this table region points to."}]}],"groups":[{"data_type_def":"DynamicTable","data_type_inc":"Container","doc":"A group containing multiple datasets that are aligned on the first dimension (Currently, this requirement if left up to APIs to check and enforce). These datasets represent different columns in the table. Apart from a column that contains unique identifiers for each row, there are no other required datasets. Users are free to add any number of custom VectorData objects (columns) here. DynamicTable also supports ragged array columns, where each element can be of a different size. To add a ragged array column, use a VectorIndex type to index the corresponding VectorData type. See documentation for VectorData and VectorIndex for more details. Unlike a compound data type, which is analogous to storing an array-of-structs, a DynamicTable can be thought of as a struct-of-arrays. This provides an alternative structure to choose from when optimizing storage for anticipated access patterns. Additionally, this type provides a way of creating a table without having to define a compound type up front. Although this convenience may be attractive, users should think carefully about how data will be accessed. DynamicTable is more appropriate for column-centric access, whereas a dataset with a compound type would be more appropriate for row-centric access. Finally, data size should also be taken into account. For small tables, performance loss may be an acceptable trade-off for the flexibility of a DynamicTable.","attributes":[{"name":"colnames","dtype":"text","dims":["num_columns"],"shape":[null],"doc":"The names of the columns in this table. This should be used to specify an order to the columns."},{"name":"description","dtype":"text","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"id","data_type_inc":"ElementIdentifiers","dtype":"int","dims":["num_rows"],"shape":[null],"doc":"Array of unique identifiers for the rows of this dynamic table."},{"data_type_inc":"VectorData","doc":"Vector columns, including index columns, of this dynamic table.","quantity":"*"}]},{"data_type_def":"AlignedDynamicTable","data_type_inc":"DynamicTable","doc":"DynamicTable container that supports storing a collection of sub-tables. Each sub-table is a DynamicTable itself that is aligned with the main table by row index. I.e., all DynamicTables stored in this group MUST have the same number of rows. This type effectively defines a 2-level table in which the main data is stored in the main table implemented by this type and additional columns of the table are grouped into categories, with each category being represented by a separate DynamicTable stored within the group.","attributes":[{"name":"categories","dtype":"text","dims":["num_categories"],"shape":[null],"doc":"The names of the categories in this AlignedDynamicTable. Each category is represented by one DynamicTable stored in the parent group. This attribute should be used to specify an order of categories and the category names must match the names of the corresponding DynamicTable in the group."}],"groups":[{"data_type_inc":"DynamicTable","doc":"A DynamicTable representing a particular category for columns in the AlignedDynamicTable parent container. The table MUST be aligned with (i.e., have the same number of rows) as all other DynamicTables stored in the AlignedDynamicTable parent container. The name of the category is given by the name of the DynamicTable and its description by the description attribute of the DynamicTable.","quantity":"*"}]}]})delimiter"; -const std::string sparse = R"delimiter( +constexpr std::string_view sparse = R"delimiter( {"groups":[{"data_type_def":"CSRMatrix","data_type_inc":"Container","doc":"A compressed sparse row matrix. Data are stored in the standard CSR format, where column indices for row i are stored in indices[indptr[i]:indptr[i+1]] and their corresponding values are stored in data[indptr[i]:indptr[i+1]].","attributes":[{"name":"shape","dtype":"uint","dims":["number of rows, number of columns"],"shape":[2],"doc":"The shape (number of rows, number of columns) of this sparse matrix."}],"datasets":[{"name":"indices","dtype":"uint","dims":["number of non-zero values"],"shape":[null],"doc":"The column indices."},{"name":"indptr","dtype":"uint","dims":["number of rows in the matrix + 1"],"shape":[null],"doc":"The row index pointer."},{"name":"data","dims":["number of non-zero values"],"shape":[null],"doc":"The non-zero values in the matrix."}]}]})delimiter"; -const std::string namespaces = R"delimiter( +constexpr std::string_view namespaces = R"delimiter( {"namespaces":[{"name":"hdmf-common","doc":"Common data structures provided by HDMF","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov"],"full_name":"HDMF Common","schema":[{"source":"base"},{"source":"table"},{"source":"sparse"}],"version":"1.8.0"}]})delimiter"; -void registerVariables(std::map& registry) { - registry["base"] = &base; - registry["table"] = &table; - registry["sparse"] = &sparse; - registry["namespace"] = &namespaces; -}; -} // namespace AQNWB::spec::hdmf_common +constexpr std::array, 4> + specVariables {{{"base", base}, + {"table", table}, + {"sparse", sparse}, + {"namespace", namespaces}}}; +} // namespace AQNWB::SPEC::HDMF_COMMON diff --git a/Source/aqnwb/spec/hdmf_experimental.hpp b/Source/aqnwb/spec/hdmf_experimental.hpp index ef20ca5..d671e05 100644 --- a/Source/aqnwb/spec/hdmf_experimental.hpp +++ b/Source/aqnwb/spec/hdmf_experimental.hpp @@ -1,24 +1,25 @@ #pragma once +#include #include +#include -namespace AQNWB::spec::hdmf_experimental +namespace AQNWB::SPEC::HDMF_EXPERIMENTAL { const std::string version = "0.5.0"; -const std::string experimental = R"delimiter( +constexpr std::string_view experimental = R"delimiter( {"groups":[],"datasets":[{"data_type_def":"EnumData","data_type_inc":"VectorData","dtype":"uint8","doc":"Data that come from a fixed set of values. A data value of i corresponds to the i-th value in the VectorData referenced by the 'elements' attribute.","attributes":[{"name":"elements","dtype":{"target_type":"VectorData","reftype":"object"},"doc":"Reference to the VectorData object that contains the enumerable elements"}]}]})delimiter"; -const std::string resources = R"delimiter( +constexpr std::string_view resources = R"delimiter( {"groups":[{"data_type_def":"HERD","data_type_inc":"Container","doc":"HDMF External Resources Data Structure. A set of six tables for tracking external resource references in a file or across multiple files.","datasets":[{"data_type_inc":"Data","name":"keys","doc":"A table for storing user terms that are used to refer to external resources.","dtype":[{"name":"key","dtype":"text","doc":"The user term that maps to one or more resources in the `resources` table, e.g., \"human\"."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"files","doc":"A table for storing object ids of files used in external resources.","dtype":[{"name":"file_object_id","dtype":"text","doc":"The object id (UUID) of a file that contains objects that refers to external resources."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"entities","doc":"A table for mapping user terms (i.e., keys) to resource entities.","dtype":[{"name":"entity_id","dtype":"text","doc":"The compact uniform resource identifier (CURIE) of the entity, in the form [prefix]:[unique local identifier], e.g., 'NCBI_TAXON:9606'."},{"name":"entity_uri","dtype":"text","doc":"The URI for the entity this reference applies to. This can be an empty string. e.g., https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=info&id=9606"}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"objects","doc":"A table for identifying which objects in a file contain references to external resources.","dtype":[{"name":"files_idx","dtype":"uint","doc":"The row index to the file in the `files` table containing the object."},{"name":"object_id","dtype":"text","doc":"The object id (UUID) of the object."},{"name":"object_type","dtype":"text","doc":"The data type of the object."},{"name":"relative_path","dtype":"text","doc":"The relative path from the data object with the `object_id` to the dataset or attribute with the value(s) that is associated with an external resource. This can be an empty string if the object is a dataset that contains the value(s) that is associated with an external resource."},{"name":"field","dtype":"text","doc":"The field within the compound data type using an external resource. This is used only if the dataset or attribute is a compound data type; otherwise this should be an empty string."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"object_keys","doc":"A table for identifying which objects use which keys.","dtype":[{"name":"objects_idx","dtype":"uint","doc":"The row index to the object in the `objects` table that holds the key"},{"name":"keys_idx","dtype":"uint","doc":"The row index to the key in the `keys` table."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"entity_keys","doc":"A table for identifying which keys use which entity.","dtype":[{"name":"entities_idx","dtype":"uint","doc":"The row index to the entity in the `entities` table."},{"name":"keys_idx","dtype":"uint","doc":"The row index to the key in the `keys` table."}],"dims":["num_rows"],"shape":[null]}]}]})delimiter"; -const std::string namespaces = R"delimiter( +constexpr std::string_view namespaces = R"delimiter( {"namespaces":[{"name":"hdmf-experimental","doc":"Experimental data structures provided by HDMF. These are not guaranteed to be available in the future.","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter","Matthew Avaylon"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov","mavaylon@lbl.gov"],"full_name":"HDMF Experimental","schema":[{"namespace":"hdmf-common"},{"source":"experimental"},{"source":"resources"}],"version":"0.5.0"}]})delimiter"; -void registerVariables(std::map& registry) { - registry["experimental"] = &experimental; - registry["resources"] = &resources; - registry["namespace"] = &namespaces; -}; -} // namespace AQNWB::spec::hdmf_experimental +constexpr std::array, 3> + specVariables {{{"experimental", experimental}, + {"resources", resources}, + {"namespace", namespaces}}}; +} // namespace AQNWB::SPEC::HDMF_EXPERIMENTAL From 95c6c41d60dd07d79e8caaeaf6b7284bd128ffe9 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:47:45 -0700 Subject: [PATCH 13/32] update aqnwb src --- Source/aqnwb/nwb/NWBFile.cpp | 101 +++++++++-------------- Source/aqnwb/nwb/NWBFile.hpp | 98 ++++++---------------- Source/aqnwb/nwb/RecordingContainers.cpp | 65 +++++++++++++++ Source/aqnwb/nwb/RecordingContainers.hpp | 96 +++++++++++++++++++++ 4 files changed, 225 insertions(+), 135 deletions(-) create mode 100644 Source/aqnwb/nwb/RecordingContainers.cpp create mode 100644 Source/aqnwb/nwb/RecordingContainers.hpp diff --git a/Source/aqnwb/nwb/NWBFile.cpp b/Source/aqnwb/nwb/NWBFile.cpp index f277a7c..007bee3 100644 --- a/Source/aqnwb/nwb/NWBFile.cpp +++ b/Source/aqnwb/nwb/NWBFile.cpp @@ -6,23 +6,25 @@ #include #include -#include "NWBFile.hpp" - -#include "../BaseIO.hpp" -#include "../Channel.hpp" -#include "../Utils.hpp" -#include "../spec/core.hpp" -#include "../spec/hdmf_common.hpp" -#include "../spec/hdmf_experimental.hpp" -#include "device/Device.hpp" -#include "ecephys/ElectricalSeries.hpp" -#include "file/ElectrodeGroup.hpp" -#include "file/ElectrodeTable.hpp" +#include "nwb/NWBFile.hpp" + +#include "BaseIO.hpp" +#include "Channel.hpp" +#include "Utils.hpp" +#include "nwb/device/Device.hpp" +#include "nwb/ecephys/ElectricalSeries.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; +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(); } @@ -56,7 +57,7 @@ Status NWBFile::createFileStructure() } io->createCommonNWBAttributes("/", "core", "NWBFile", ""); - io->createAttribute(AQNWB::spec::core::version, "/", "nwb_version"); + io->createAttribute(AQNWB::SPEC::CORE::version, "/", "nwb_version"); io->createGroup("/acquisition"); io->createGroup("/analysis"); @@ -70,9 +71,15 @@ Status NWBFile::createFileStructure() io->createGroup("/specifications"); io->createReferenceAttribute("/specifications", "/", ".specloc"); - cacheSpecifications("core", spec::core::version, spec::core::registerVariables); - cacheSpecifications("hdmf-common", spec::hdmf_common::version, spec::hdmf_common::registerVariables); - cacheSpecifications("hdmf-experimental", spec::hdmf_experimental::version, spec::hdmf_experimental::registerVariables); + + cacheSpecifications( + "core", AQNWB::SPEC::CORE::version, AQNWB::SPEC::CORE::specVariables); + cacheSpecifications("hdmf-common", + AQNWB::SPEC::HDMF_COMMON::version, + AQNWB::SPEC::HDMF_COMMON::specVariables); + cacheSpecifications("hdmf-experimental", + AQNWB::SPEC::HDMF_EXPERIMENTAL::version, + AQNWB::SPEC::HDMF_EXPERIMENTAL::specVariables); std::string time = getCurrentTime(); std::vector timeVec = {time}; @@ -87,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; @@ -126,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) @@ -139,28 +149,20 @@ Status NWBFile::createElectricalSeries( return Status::Success; } -Status NWBFile::startRecording() -{ - return io->startRecording(); -} - -void NWBFile::stopRecording() +template +void NWBFile::cacheSpecifications( + const std::string& specPath, + const std::string& versionNumber, + const std::array, N>& + specVariables) { - io->stopRecording(); -} - -void NWBFile::cacheSpecifications(const std::string& specPath, - const std::string& version, - void (*registerFunc)(std::map&)) -{ - std::map registry; - registerFunc(registry); - io->createGroup("/specifications/" + specPath + "/"); - io->createGroup("/specifications/" + specPath + "/" + version); + io->createGroup("/specifications/" + specPath + "/" + versionNumber); - for (const auto& [name, content] : registry) { - io->createStringDataSet("/specifications/" + specPath + "/" + version + "/" + name, *content); + for (const auto& [name, content] : specVariables) { + io->createStringDataSet("/specifications/" + specPath + "/" + versionNumber + + "/" + std::string(name), + std::string(content)); } } @@ -174,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/Source/aqnwb/nwb/NWBFile.hpp b/Source/aqnwb/nwb/NWBFile.hpp index d629504..277f45a 100644 --- a/Source/aqnwb/nwb/NWBFile.hpp +++ b/Source/aqnwb/nwb/NWBFile.hpp @@ -1,13 +1,16 @@ #pragma once +#include #include -#include #include +#include +#include #include -#include "../BaseIO.hpp" -#include "../Types.hpp" -#include "base/TimeSeries.hpp" +#include "BaseIO.hpp" +#include "Types.hpp" +#include "nwb/RecordingContainers.hpp" +#include "nwb/base/TimeSeries.hpp" /*! * \namespace AQNWB::NWB @@ -16,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. @@ -67,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: /** @@ -119,61 +109,21 @@ class NWBFile /** * @brief Saves the specification files for the schema. * @param specPath The location in the file to store the spec information. - * @param version The version number of the specification files. - * @param registry The registry of specification files. * @param versionNumber The version number of the specification files. + * @param specVariables The contents of the specification files. + * These values are generated from the nwb schema by + * `resources/generate_spec_files.py` */ - void cacheSpecifications(const std::string& specPath, - const std::string& version, - void (*registerFunc)(std::map&)); - - /** - * @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"); + template + void cacheSpecifications( + const std::string& specPath, + const std::string& versionNumber, + const std::array, N>& + specVariables); 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/Source/aqnwb/nwb/RecordingContainers.cpp b/Source/aqnwb/nwb/RecordingContainers.cpp new file mode 100644 index 0000000..d7464bc --- /dev/null +++ b/Source/aqnwb/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/Source/aqnwb/nwb/RecordingContainers.hpp b/Source/aqnwb/nwb/RecordingContainers.hpp new file mode 100644 index 0000000..aa00308 --- /dev/null +++ b/Source/aqnwb/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 From e4135440dd9656191c0e6a0fb3e5219ca11d50ab Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:59:11 -0700 Subject: [PATCH 14/32] update nwbrecording to use aqnwb --- Source/RecordEngine/NWBRecording.cpp | 303 ++++++++++----------------- Source/RecordEngine/NWBRecording.h | 38 ++-- 2 files changed, 134 insertions(+), 207 deletions(-) diff --git a/Source/RecordEngine/NWBRecording.cpp b/Source/RecordEngine/NWBRecording.cpp index ec3bead..5b496e9 100644 --- a/Source/RecordEngine/NWBRecording.cpp +++ b/Source/RecordEngine/NWBRecording.cpp @@ -25,8 +25,8 @@ #include #include "NWBRecording.h" -#include "../aqnwb/Channel.hpp" -#include "../aqnwb/Utils.hpp" +#include "Channel.hpp" +#include "Utils.hpp" #include "../../plugin-GUI/Source/Processors/RecordNode/RecordNode.h" @@ -39,20 +39,9 @@ { } - NWBRecordEngine::~NWBRecordEngine() { - // if (nwb != nullptr) - // { - // spikeChannels.clear(); - // eventChannels.clear(); - // continuousChannelGroups.clear(); - // datasetIndexes.clear(); - // writeChannelIndexes.clear(); - - // nwb->close(); - // nwb.reset(); - // } + NWBRecordEngine::reset(); } RecordEngineManager* NWBRecordEngine::getEngineManager() @@ -60,157 +49,52 @@ RecordEngineManager* NWBRecordEngine::getEngineManager() //static factory that instantiates the engine manager, which allows to configure recording options among other things. See OriginalRecording to see how to create options for a record engine RecordEngineManager* man = new RecordEngineManager("NWB2", "NWB2", &(engineFactory)); EngineParameter* param; - // param = new EngineParameter(EngineParameter::STR, 0, "Identifier Text", String()); - // man->addParameter(param); + + param = new EngineParameter(EngineParameter::STR, 0, "Identifier Text", String()); + man->addParameter(param); return man; } - + + void NWBRecordEngine::openFiles(File rootFolder, int experimentNumber, int recordingNumber) { - // setup file paths - char separator = std::filesystem::path::preferred_separator; - std::string separatorStr(1, separator); // Convert char to std::string - std::string filename = rootFolder.getFullPathName().toStdString() + separatorStr + - "experiment_aqnwb" + std::to_string(experimentNumber) + ".nwb"; - // std::string guiVersion = CoreServices::getGUIVersion(); + if (recordingNumber == 0) // new file needed + { + // clear any existing data and nwbfile + NWBRecordEngine::reset(); - // get pointers to all continuous channels for electrode table - std::vector channelVector; - for (int i = 0; i < recordNode->getNumOutputs(); i++) - { - const ContinuousChannel* channelInfo = getContinuousChannel(i); // channel info object - continuousChannels.add(channelInfo); - } + // create the io object + char separator = std::filesystem::path::preferred_separator; + std::string separatorStr(1, separator); // Convert char to std::string + std::string filename = rootFolder.getFullPathName().toStdString() + separatorStr + + "experiment_aqnwb" + std::to_string(experimentNumber) + ".nwb"; - int streamIndex = -1; - uint16 lastStreamId = 0; + this->io = AQNWB::createIO("HDF5", filename); - for (int ch = 0; ch < getNumRecordedContinuousChannels(); ch++) - { - int globalIndex = getGlobalIndex(ch); // the global channel index (across all channels entering the Record Node) - int localIndex = getLocalIndex(ch); // the local channel index (within a stream) - - const ContinuousChannel* channelInfo = getContinuousChannel(globalIndex); // channel info object - if (channelInfo->getStreamId() != lastStreamId) - { - streamIndex++; - ContinuousGroup newGroup; - continuousChannelGroups.add(newGroup); - } + // create recording array mapping for channel information + NWBRecordEngine::createRecordingArrays(); - continuousChannelGroups.getReference(streamIndex).add(channelInfo); - lastStreamId = channelInfo->getStreamId(); + // create the nwbfile + this->nwbfile = std::make_unique(AQNWB::generateUuid(), io); + this->nwbfile->initialize(); // TODO - have option to initialize cache size based on # of channels - // TODO - part I'm adding - std::string name = channelInfo->getName().toStdString(); - std::string groupName = channelInfo->getSourceNodeName().toStdString() + "-" - + std::to_string(channelInfo->getSourceNodeId()) - + "." + channelInfo->getStreamName().toStdString(); + // create recording containers + this->recordingContainers = std::make_unique(); + this->nwbfile->createElectricalSeries( + this->recordingArrays, AQNWB::BaseDataType::I16, this->recordingContainers.get(), this->esContainerIndexes); - float channel_conversion = channelInfo->getBitVolts() * 1e6; - channelVector.push_back(AQNWB::Channel(name, groupName, streamIndex, localIndex, globalIndex, channel_conversion, channelInfo->getSampleRate(), channelInfo->getBitVolts())); + // start recording + this->io->startRecording(); } - - // TODO - I don't think this properly separates different streams / recording arrays right now - this->recordingArrays = {channelVector}; - - // initialize nwbfile object and create base structure - nwbRecording.openFile(filename, recordingArrays); - - - // if (recordingNumber == 0) // new file needed - // { - - // spikeChannels.clear(); - // eventChannels.clear(); - // continuousChannels.clear(); - // continuousChannelGroups.clear(); - // datasetIndexes.clear(); - // writeChannelIndexes.clear(); - - // // New file for each experiment, e.g. experiment1.nwb, epxperiment2.nwb, etc. - // String basepath = rootFolder.getFullPathName() + - // rootFolder.getSeparatorString() + - // "experiment" + String(experimentNumber) + - // ".nwb"; - - // if (nwb != nullptr) - // { - // nwb->close(); - // nwb.reset(); - // } - - // // create a unique identifier for the file if it doesn't exist - // Uuid identifier; - // identifierText = identifier.toString(); - - // nwb = std::make_unique(basepath, CoreServices::getGUIVersion(), identifierText); - - // // get pointers to all continuous channels for electrode table - // for (int i = 0; i < recordNode->getNumOutputs(); i++) - // { - // const ContinuousChannel* channelInfo = getContinuousChannel(i); // channel info object - - // continuousChannels.add(channelInfo); - // } - - // datasetIndexes.insertMultiple(0, 0, getNumRecordedContinuousChannels()); - // writeChannelIndexes.insertMultiple(0, 0, getNumRecordedContinuousChannels()); - // continuousChannelGroups.clear(); - - // int streamIndex = -1; - // uint16 lastStreamId = 0; - // int indexWithinStream = 0; - - // for (int ch = 0; ch < getNumRecordedContinuousChannels(); ch++) - // { - - // int globalIndex = getGlobalIndex(ch); // the global channel index (across all channels entering the Record Node) - // int localIndex = getLocalIndex(ch); // the local channel index (within a stream) - - // const ContinuousChannel* channelInfo = getContinuousChannel(globalIndex); // channel info object - - // int sourceId = channelInfo->getSourceNodeId(); - // int streamId = channelInfo->getStreamId(); - - // if (streamId != lastStreamId) - // { - // streamIndex++; - // indexWithinStream = 0; - - // ContinuousGroup newGroup; - // continuousChannelGroups.add(newGroup); - - // } - - // continuousChannelGroups.getReference(streamIndex).add(channelInfo); - - // datasetIndexes.set(ch, streamIndex); - // writeChannelIndexes.set(ch, indexWithinStream++); - - // lastStreamId = streamId; - // } - - // for (int i = 0; i < getNumRecordedEventChannels(); i++) - // eventChannels.add(getEventChannel(i)); - - // for (int i = 0; i < getNumRecordedSpikeChannels(); i++) - // spikeChannels.add(getSpikeChannel(i)); - - // //open the file - // nwb->open(getNumRecordedContinuousChannels() + continuousChannelGroups.size() + eventChannels.size() + spikeChannels.size()); //total channels + timestamp arrays, to create a big enough buffer - - // //create the recording - // nwb->startNewRecording(recordingNumber, continuousChannelGroups, continuousChannels, eventChannels, spikeChannels); - // } } + void NWBRecordEngine::closeFiles() { - nwbRecording.closeFile(); - // nwb->stopRecording(); + this->io->stopRecording(); + this->nwbfile->finalize(); } void NWBRecordEngine::writeContinuousData(int writeChannel, @@ -222,7 +106,7 @@ void NWBRecordEngine::writeContinuousData(int writeChannel, // get channel info - add this to RecordingArray or ChannelVector when we make it a class AQNWB::Channel* channel = nullptr; AQNWB::Types::SizeType datasetIndex = 0; - for (auto& channelVector : recordingArrays) { + for (auto& channelVector : this->recordingArrays) { for (auto& ch : channelVector) { if (ch.globalIndex == realChannel) { datasetIndex = ch.groupIndex; @@ -231,44 +115,14 @@ void NWBRecordEngine::writeContinuousData(int writeChannel, } } } - - // get data info - std::vector dataShape = {static_cast(size), 1}; - std::vector positionOffset = {static_cast(0), static_cast(writeChannel)}; - // TODO - update positionOffset tracking - - // write data - std::unique_ptr intBuffer = AQNWB::transformToInt16(static_cast(size), channel->getBitVolts() / 1e6, dataBuffer); - nwbRecording.writeTimeseriesData("ElectricalSeries", - datasetIndex, - *channel, // to do, get recording channel information - dataShape, - positionOffset, - intBuffer.get(), - timestampBuffer); - - // nwb->writeData(datasetIndexes[writeChannel], - // writeChannelIndexes[writeChannel], - // size, - // dataBuffer, - // getContinuousChannel(realChannel)->getBitVolts()); - - // /* All channels in a dataset have the same number of samples and share timestamps. - // But since this method is called asynchronously, the timestamps might not be - // in sync during acquisition, so we chose a channel and write the timestamps - // when writing that channel's data */ - // if (writeChannelIndexes[writeChannel] == 0) - // { - // int64 baseTS = getLatestSampleNumber(writeChannel); - - // for (int i = 0; i < size; i++) - // { - // smpBuffer[i] = baseTS + i; - // } - - // nwb->writeTimestamps(datasetIndexes[writeChannel], size, timestampBuffer); - // nwb->writeSampleNumbers(datasetIndexes[writeChannel], size, smpBuffer); - // } + // write data - TODO - need to test this out still + std::unique_ptr intBuffer = AQNWB::transformToInt16(static_cast(size), channel->getBitVolts() / 1e6, dataBuffer); + this->recordingContainers->writeElectricalSeriesData(this->esContainerIndexes[datasetIndex], + *channel, + static_cast(size), + intBuffer.get(), + timestampBuffer); + // TODO - save sample numbers as well for offline syncing } void NWBRecordEngine::writeEvent(int eventIndex, const MidiMessage& event) @@ -292,9 +146,76 @@ void NWBRecordEngine::writeSpike(int electrodeIndex, const Spike* spike) // nwb->writeSpike(electrodeIndex, channel, spike); } +void NWBRecordEngine::setParameter(EngineParameter& parameter) +{ + strParameter(0, identifierText); +} + +void NWBRecordEngine::reset() +{ + if (this->nwbfile != nullptr) + { + this->recordingArrays.clear(); + this->continuousChannels.clear(); + this->continuousChannelGroups.clear(); + this->esContainerIndexes.clear(); + + this->nwbfile->finalize(); + this->nwbfile.reset(); + } +} + +void NWBRecordEngine::createRecordingArrays() +{ + // get pointers to all continuous channels for electrode table + for (int i = 0; i < recordNode->getNumOutputs(); i++) + { + const ContinuousChannel* channelInfo = getContinuousChannel(i); // channel info object + this->continuousChannels.add(channelInfo); + } + + // group channels by stream + int streamIndex = -1; + uint16 lastStreamId = 0; + for (int ch = 0; ch < getNumRecordedContinuousChannels(); ch++) + { + int globalIndex = getGlobalIndex(ch); // the global channel index (across all channels entering the Record Node) + int localIndex = getLocalIndex(ch); // the local channel index (within a stream) + const ContinuousChannel* channelInfo = getContinuousChannel(globalIndex); // channel info object + if (channelInfo->getStreamId() != lastStreamId) + { + streamIndex++; + ContinuousGroup newGroup; + this->continuousChannelGroups.add(newGroup); + } + + this->continuousChannelGroups.getReference(streamIndex).add(channelInfo); + lastStreamId = channelInfo->getStreamId(); + } + + // create recording arrays for nwb file + for (int streamIndex = 0; streamIndex < this->continuousChannelGroups.size(); streamIndex++) + { + std::vector channelVector; + + for (auto& channelInfo : this->continuousChannelGroups[streamIndex]) + { + std::string name = channelInfo->getName().toStdString(); + std::string groupName = channelInfo->getSourceNodeName().toStdString() + "-" + + std::to_string(channelInfo->getSourceNodeId()) + + "." + channelInfo->getStreamName().toStdString(); + + channelVector.push_back(AQNWB::Channel(name, + groupName, + streamIndex, + channelInfo->getLocalIndex(), + channelInfo->getGlobalIndex(), + channelInfo->getBitVolts() * 1e6, + channelInfo->getSampleRate(), + channelInfo->getBitVolts())); + } + this->recordingArrays.push_back(channelVector); + } +} -// void NWBRecordEngine::setParameter(EngineParameter& parameter) -// { -// strParameter(0, identifierText); -// } diff --git a/Source/RecordEngine/NWBRecording.h b/Source/RecordEngine/NWBRecording.h index 2322b18..c6f6cbc 100644 --- a/Source/RecordEngine/NWBRecording.h +++ b/Source/RecordEngine/NWBRecording.h @@ -26,7 +26,9 @@ #include -#include "../aqnwb/nwb/NWBRecording.hpp" +#include "BaseIO.hpp" +#include "nwb/NWBFile.hpp" +#include "nwb/RecordingContainers.hpp" typedef Array ContinuousGroup; @@ -75,21 +77,30 @@ namespace NWBRecording /** Write the timestamp sync text messages to disk*/ void writeTimestampSyncText(uint64 streamId, int64 timestamp, float sourceSampleRate, String text) override; - // /** Allows the file identifier to be set externally*/ - // void setParameter(EngineParameter ¶meter) override; + /** Allows the file identifier to be set externally*/ + void setParameter(EngineParameter ¶meter) override; + + /** Reset the engine */ + void reset(); + + /** Create recording arrays */ + void createRecordingArrays(); private: - /** NWB recording manager */ - AQNWB::NWB::NWBRecording nwbRecording; + /** NWB file */ + std::unique_ptr nwbfile; + + /** NWB recording container manager */ + std::unique_ptr recordingContainers; + + /** NWB I/O object */ + std::shared_ptr io; /** Holds channel information and ids */ std::vector recordingArrays; - // /** For each incoming recorded channel, which dataset (stream) is it associated with? */ - // Array datasetIndexes; - - // /** For each incoming recorded channel, what is the local index within a stream? */ - // Array writeChannelIndexes; + /** Holds the indexes of the ElectricalSeries containers added to recordingContainers */ + std::vector esContainerIndexes; /** Holds pointers to all recorded channels within a stream */ Array continuousChannelGroups; @@ -97,9 +108,6 @@ namespace NWBRecording // /** Holds pointers to all recorded event channels*/ // Array eventChannels; - // /** Holds pointers to all recorded spike channels*/ - // Array spikeChannels; - /** Holds pointers to all incoming continuous channels (used for electrode table)*/ Array continuousChannels; @@ -107,9 +115,7 @@ namespace NWBRecording HeapBlock smpBuffer; // /** The identifier for the current file (can be set externally) */ - // String identifierText; - - // JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(NWBRecordEngine); + String identifierText; }; } From facb582c0e58c64e5d6ca65f95087048bc2d24a7 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:46:56 -0700 Subject: [PATCH 15/32] update electricalseries write --- Source/RecordEngine/NWBRecording.cpp | 4 ++-- Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Source/RecordEngine/NWBRecording.cpp b/Source/RecordEngine/NWBRecording.cpp index 5b496e9..641021d 100644 --- a/Source/RecordEngine/NWBRecording.cpp +++ b/Source/RecordEngine/NWBRecording.cpp @@ -53,7 +53,6 @@ RecordEngineManager* NWBRecordEngine::getEngineManager() param = new EngineParameter(EngineParameter::STR, 0, "Identifier Text", String()); man->addParameter(param); return man; - } @@ -122,6 +121,7 @@ void NWBRecordEngine::writeContinuousData(int writeChannel, static_cast(size), intBuffer.get(), timestampBuffer); + // TODO - save sample numbers as well for offline syncing } @@ -211,7 +211,7 @@ void NWBRecordEngine::createRecordingArrays() streamIndex, channelInfo->getLocalIndex(), channelInfo->getGlobalIndex(), - channelInfo->getBitVolts() * 1e6, + channelInfo->getBitVolts() * 1e6, // TODO - should be / 1e6? channelInfo->getSampleRate(), channelInfo->getBitVolts())); } diff --git a/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp b/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp index ec3a3bb..f46940a 100644 --- a/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp +++ b/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp @@ -42,9 +42,11 @@ void ElectricalSeries::initialize() TimeSeries::initialize(); // setup variables based on number of channels - std::vector electrodeInds(channelVector.size()); + std::vector electrodeInds(channelVector.size()); + std::vector 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); @@ -54,6 +56,10 @@ void ElectricalSeries::initialize() SizeArray {1}, chunkSize, getPath() + "/channel_conversion")); + channelConversion->writeDataBlock( + std::vector(1, channelVector.size()), + BaseDataType::F32, + &channelConversions[0]); io->createCommonNWBAttributes(getPath() + "/channel_conversion", "hdmf-common", "", From a5c3adfed24c18b7fdd96dbada0fbb7539329570 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:16:01 -0700 Subject: [PATCH 16/32] update float to int tranformation --- Source/RecordEngine/NWBRecording.cpp | 6 +++--- Source/aqnwb/Utils.hpp | 26 ++++++++++++++++++++------ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Source/RecordEngine/NWBRecording.cpp b/Source/RecordEngine/NWBRecording.cpp index 641021d..0636d33 100644 --- a/Source/RecordEngine/NWBRecording.cpp +++ b/Source/RecordEngine/NWBRecording.cpp @@ -75,7 +75,7 @@ RecordEngineManager* NWBRecordEngine::getEngineManager() NWBRecordEngine::createRecordingArrays(); // create the nwbfile - this->nwbfile = std::make_unique(AQNWB::generateUuid(), io); + this->nwbfile = std::make_unique(AQNWB::generateUuid(), io, "Recording with the Open Ephys GUI"); this->nwbfile->initialize(); // TODO - have option to initialize cache size based on # of channels // create recording containers @@ -115,7 +115,7 @@ void NWBRecordEngine::writeContinuousData(int writeChannel, } } // write data - TODO - need to test this out still - std::unique_ptr intBuffer = AQNWB::transformToInt16(static_cast(size), channel->getBitVolts() / 1e6, dataBuffer); + std::unique_ptr intBuffer = AQNWB::transformToInt16(static_cast(size), channel->getBitVolts(), dataBuffer); this->recordingContainers->writeElectricalSeriesData(this->esContainerIndexes[datasetIndex], *channel, static_cast(size), @@ -211,7 +211,7 @@ void NWBRecordEngine::createRecordingArrays() streamIndex, channelInfo->getLocalIndex(), channelInfo->getGlobalIndex(), - channelInfo->getBitVolts() * 1e6, // TODO - should be / 1e6? + 1e6, channelInfo->getSampleRate(), channelInfo->getBitVolts())); } diff --git a/Source/aqnwb/Utils.hpp b/Source/aqnwb/Utils.hpp index f98d5b3..5c5588e 100644 --- a/Source/aqnwb/Utils.hpp +++ b/Source/aqnwb/Utils.hpp @@ -2,8 +2,12 @@ #include #include #include +#include +#include +#include #include +#include #include #include #include @@ -68,6 +72,21 @@ inline std::shared_ptr createIO(const std::string& type, } } +inline void convertFloatToInt16LE(const float* source, void* dest, int numSamples) +{ + auto maxVal = static_cast(0x7fff); + auto intData = static_cast(dest); + + for (int i = 0; i < numSamples; ++i) + { + auto clampedValue = std::clamp(maxVal * source[i], -maxVal, maxVal); + auto intValue = static_cast(static_cast(std::round(clampedValue))); + intValue = boost::endian::native_to_little(intValue); + *reinterpret_cast(intData) = intValue; + intData += 2; // destBytesPerSample is always 2 + } +} + inline std::unique_ptr transformToInt16(SizeType numSamples, float conversion_factor, const float* data) @@ -83,12 +102,7 @@ inline std::unique_ptr 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(std::clamp(value, -32768.0f, 32767.0f)); }); + convertFloatToInt16LE(scaledData.get(), intData.get(), numSamples); return intData; } From 6b3ca47258f493de06e259177855e685d8d84a5e Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:16:17 -0700 Subject: [PATCH 17/32] add option to provide session description --- Source/aqnwb/nwb/NWBFile.cpp | 5 +++-- Source/aqnwb/nwb/NWBFile.hpp | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Source/aqnwb/nwb/NWBFile.cpp b/Source/aqnwb/nwb/NWBFile.cpp index 007bee3..9bfeb41 100644 --- a/Source/aqnwb/nwb/NWBFile.cpp +++ b/Source/aqnwb/nwb/NWBFile.cpp @@ -27,9 +27,10 @@ std::vector NWBFile::emptyContainerIndexes = {}; // NWBFile -NWBFile::NWBFile(const std::string& idText, std::shared_ptr io) +NWBFile::NWBFile(const std::string& idText, std::shared_ptr io, std::string description) : identifierText(idText) , io(io) + , description(description) { } @@ -84,7 +85,7 @@ Status NWBFile::createFileStructure() std::string time = getCurrentTime(); std::vector 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); diff --git a/Source/aqnwb/nwb/NWBFile.hpp b/Source/aqnwb/nwb/NWBFile.hpp index 277f45a..9d734e7 100644 --- a/Source/aqnwb/nwb/NWBFile.hpp +++ b/Source/aqnwb/nwb/NWBFile.hpp @@ -30,8 +30,9 @@ class NWBFile * @brief Constructor for NWBFile class. * @param idText The identifier text for the NWBFile. * @param io The shared pointer to the IO object. + * @param description A description of the NWBFile session. */ - NWBFile(const std::string& idText, std::shared_ptr io); + NWBFile(const std::string& idText, std::shared_ptr io, std::string description = "a recording session"); /** * @brief Deleted copy constructor to prevent construction-copying. @@ -124,6 +125,7 @@ class NWBFile const std::string identifierText; std::shared_ptr io; static std::vector emptyContainerIndexes; + std::string description; }; } // namespace AQNWB::NWB \ No newline at end of file From 0c84c81b49679413f7bc32c0b6bfdddfa3bc4c59 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:23:17 -0700 Subject: [PATCH 18/32] update aqnwb src --- CMakeLists.txt | 3 ++ Source/aqnwb/Channel.hpp | 6 ++-- Source/aqnwb/Utils.hpp | 32 ++++++++++--------- Source/aqnwb/nwb/NWBFile.cpp | 12 +++++-- Source/aqnwb/nwb/NWBFile.hpp | 10 ++++-- Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp | 6 ++-- 6 files changed, 42 insertions(+), 27 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b825f5..2eef954 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -110,6 +110,8 @@ if(NOT HDF5_FOUND) #if package finding fails, try manually find_path(HDF5_INCLUDE_DIRS H5Cpp.h) endif() +include_directories(${HDF5_INCLUDE_DIRS}) + find_package(Boost REQUIRED) target_link_libraries(${PLUGIN_NAME} ${HDF5_LIBRARIES} ${HDF5_CXX_LIBRARIES} ${Boost_LIBRARIES}) @@ -117,6 +119,7 @@ target_include_directories(${PLUGIN_NAME} PRIVATE ${HDF5_INCLUDE_DIRS} ${Boost_I #target_include_directories(${PLUGIN_NAME} PUBLIC ../OpenEphysHDF5Lib/Source) + # Open Ephys common libraries include(link_open_ephys_lib.cmake) link_open_ephys_lib(${PLUGIN_NAME} OpenEphysHDF5) diff --git a/Source/aqnwb/Channel.hpp b/Source/aqnwb/Channel.hpp index fccdf22..4166aa8 100644 --- a/Source/aqnwb/Channel.hpp +++ b/Source/aqnwb/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 position = {0.f, 0.f, 0.f}, const std::string comments = "no comments"); diff --git a/Source/aqnwb/Utils.hpp b/Source/aqnwb/Utils.hpp index 5c5588e..43a0041 100644 --- a/Source/aqnwb/Utils.hpp +++ b/Source/aqnwb/Utils.hpp @@ -1,10 +1,10 @@ +#include #include +#include +#include #include #include #include -#include -#include -#include #include #include @@ -72,19 +72,21 @@ inline std::shared_ptr createIO(const std::string& type, } } -inline void convertFloatToInt16LE(const float* source, void* dest, int numSamples) +inline void convertFloatToInt16LE(const float* source, + void* dest, + int numSamples) { - auto maxVal = static_cast(0x7fff); - auto intData = static_cast(dest); - - for (int i = 0; i < numSamples; ++i) - { - auto clampedValue = std::clamp(maxVal * source[i], -maxVal, maxVal); - auto intValue = static_cast(static_cast(std::round(clampedValue))); - intValue = boost::endian::native_to_little(intValue); - *reinterpret_cast(intData) = intValue; - intData += 2; // destBytesPerSample is always 2 - } + auto maxVal = static_cast(0x7fff); + auto intData = static_cast(dest); + + for (int i = 0; i < numSamples; ++i) { + auto clampedValue = std::clamp(maxVal * source[i], -maxVal, maxVal); + auto intValue = + static_cast(static_cast(std::round(clampedValue))); + intValue = boost::endian::native_to_little(intValue); + *reinterpret_cast(intData) = intValue; + intData += 2; // destBytesPerSample is always 2 + } } inline std::unique_ptr transformToInt16(SizeType numSamples, diff --git a/Source/aqnwb/nwb/NWBFile.cpp b/Source/aqnwb/nwb/NWBFile.cpp index 9bfeb41..5ce0ee1 100644 --- a/Source/aqnwb/nwb/NWBFile.cpp +++ b/Source/aqnwb/nwb/NWBFile.cpp @@ -27,17 +27,20 @@ std::vector NWBFile::emptyContainerIndexes = {}; // NWBFile -NWBFile::NWBFile(const std::string& idText, std::shared_ptr io, std::string description) +NWBFile::NWBFile(const std::string& idText, std::shared_ptr io) : identifierText(idText) , io(io) - , description(description) { } NWBFile::~NWBFile() {} -Status NWBFile::initialize() +Status NWBFile::initialize(const std::string description, + const std::string dataCollection) { + this->description = description; + this->dataCollection = dataCollection; + if (std::filesystem::exists(io->getFileName())) { return io->open(false); } else { @@ -69,6 +72,9 @@ Status NWBFile::createFileStructure() 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"); diff --git a/Source/aqnwb/nwb/NWBFile.hpp b/Source/aqnwb/nwb/NWBFile.hpp index 9d734e7..2c13cdf 100644 --- a/Source/aqnwb/nwb/NWBFile.hpp +++ b/Source/aqnwb/nwb/NWBFile.hpp @@ -30,9 +30,8 @@ class NWBFile * @brief Constructor for NWBFile class. * @param idText The identifier text for the NWBFile. * @param io The shared pointer to the IO object. - * @param description A description of the NWBFile session. */ - NWBFile(const std::string& idText, std::shared_ptr io, std::string description = "a recording session"); + NWBFile(const std::string& idText, std::shared_ptr io); /** * @brief Deleted copy constructor to prevent construction-copying. @@ -52,8 +51,11 @@ class NWBFile /** * @brief Initializes the NWB file by opening and setting up the file * structure. + * @param description A description of the NWBFile session. + * @param dataCollection Information about the data collection methods. */ - Status initialize(); + Status initialize(const std::string description = "a recording session", + const std::string dataCollection = ""); /** * @brief Finalizes the NWB file by closing it. @@ -125,7 +127,9 @@ class NWBFile const std::string identifierText; std::shared_ptr io; static std::vector emptyContainerIndexes; + std::string description; + std::string dataCollection; }; } // namespace AQNWB::NWB \ No newline at end of file diff --git a/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp b/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp index f46940a..bf186d2 100644 --- a/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp +++ b/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp @@ -57,9 +57,9 @@ void ElectricalSeries::initialize() chunkSize, getPath() + "/channel_conversion")); channelConversion->writeDataBlock( - std::vector(1, channelVector.size()), - BaseDataType::F32, - &channelConversions[0]); + std::vector(1, channelVector.size()), + BaseDataType::F32, + &channelConversions[0]); io->createCommonNWBAttributes(getPath() + "/channel_conversion", "hdmf-common", "", From cc1aa0abe257727704327fdd092da9bdbcd6420e Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:24:07 -0700 Subject: [PATCH 19/32] add data collection and description inputs --- Source/RecordEngine/NWBRecording.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Source/RecordEngine/NWBRecording.cpp b/Source/RecordEngine/NWBRecording.cpp index 0636d33..45e7dbd 100644 --- a/Source/RecordEngine/NWBRecording.cpp +++ b/Source/RecordEngine/NWBRecording.cpp @@ -49,7 +49,6 @@ RecordEngineManager* NWBRecordEngine::getEngineManager() //static factory that instantiates the engine manager, which allows to configure recording options among other things. See OriginalRecording to see how to create options for a record engine RecordEngineManager* man = new RecordEngineManager("NWB2", "NWB2", &(engineFactory)); EngineParameter* param; - param = new EngineParameter(EngineParameter::STR, 0, "Identifier Text", String()); man->addParameter(param); return man; @@ -75,13 +74,16 @@ RecordEngineManager* NWBRecordEngine::getEngineManager() NWBRecordEngine::createRecordingArrays(); // create the nwbfile - this->nwbfile = std::make_unique(AQNWB::generateUuid(), io, "Recording with the Open Ephys GUI"); - this->nwbfile->initialize(); // TODO - have option to initialize cache size based on # of channels + std::string dataCollection = "Open Ephys GUI Version " + CoreServices::getGUIVersion().toStdString(); + this->nwbfile = std::make_unique(AQNWB::generateUuid(), io); + this->nwbfile->initialize("Recording with the Open Ephys GUI", dataCollection); + // TODO - have option to initialize cache size based on # of channels // create recording containers this->recordingContainers = std::make_unique(); this->nwbfile->createElectricalSeries( - this->recordingArrays, AQNWB::BaseDataType::I16, this->recordingContainers.get(), this->esContainerIndexes); + this->recordingArrays, AQNWB::BaseDataType::I16, this->recordingContainers.get(), this->esContainerIndexes); + // TODO add io_settings to set chunk size for different data types // start recording this->io->startRecording(); @@ -114,7 +116,7 @@ void NWBRecordEngine::writeContinuousData(int writeChannel, } } } - // write data - TODO - need to test this out still + std::unique_ptr intBuffer = AQNWB::transformToInt16(static_cast(size), channel->getBitVolts(), dataBuffer); this->recordingContainers->writeElectricalSeriesData(this->esContainerIndexes[datasetIndex], *channel, @@ -126,7 +128,8 @@ void NWBRecordEngine::writeContinuousData(int writeChannel, } void NWBRecordEngine::writeEvent(int eventIndex, const MidiMessage& event) -{ +{ + // TODO - replacew with AQNWB // const EventChannel* channel = getEventChannel(eventIndex); // EventPtr eventStruct = Event::deserialize(event, channel); @@ -135,12 +138,14 @@ void NWBRecordEngine::writeEvent(int eventIndex, const MidiMessage& event) void NWBRecordEngine::writeTimestampSyncText(uint64 streamId, int64 timestamp, float sourceSampleRate, String text) { + // TODO - replacew with AQNWB // nwb->writeTimestampSyncText(streamId, timestamp, sourceSampleRate, text); } void NWBRecordEngine::writeSpike(int electrodeIndex, const Spike* spike) { + // TODO - replacew with AQNWB // const SpikeChannel* channel = getSpikeChannel(electrodeIndex); // nwb->writeSpike(electrodeIndex, channel, spike); From 132ec099f785d6a7b02c31d2511beba90be49448 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:32:53 -0700 Subject: [PATCH 20/32] add writespike functionality - wip --- Source/RecordEngine/NWBRecording.cpp | 38 +++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/Source/RecordEngine/NWBRecording.cpp b/Source/RecordEngine/NWBRecording.cpp index 45e7dbd..2fe1b29 100644 --- a/Source/RecordEngine/NWBRecording.cpp +++ b/Source/RecordEngine/NWBRecording.cpp @@ -110,7 +110,6 @@ void NWBRecordEngine::writeContinuousData(int writeChannel, for (auto& channelVector : this->recordingArrays) { for (auto& ch : channelVector) { if (ch.globalIndex == realChannel) { - datasetIndex = ch.groupIndex; channel = &ch; break; } @@ -118,7 +117,7 @@ void NWBRecordEngine::writeContinuousData(int writeChannel, } std::unique_ptr intBuffer = AQNWB::transformToInt16(static_cast(size), channel->getBitVolts(), dataBuffer); - this->recordingContainers->writeElectricalSeriesData(this->esContainerIndexes[datasetIndex], + this->recordingContainers->writeElectricalSeriesData(this->esContainerIndexes[channel->groupIndex], *channel, static_cast(size), intBuffer.get(), @@ -145,10 +144,37 @@ void NWBRecordEngine::writeTimestampSyncText(uint64 streamId, int64 timestamp, f void NWBRecordEngine::writeSpike(int electrodeIndex, const Spike* spike) { - // TODO - replacew with AQNWB - // const SpikeChannel* channel = getSpikeChannel(electrodeIndex); - - // nwb->writeSpike(electrodeIndex, channel, spike); + // // extract info from spike channel + // const SpikeChannel* spikeChannel = getSpikeChannel(electrodeIndex); + // int nSamplesPerChannel = spikeChannel->getTotalSamples(); + // int nChannels = spikeChannel->getNumChannels(); + // int nSamples = nSamplesPerChannel * nChannels; + + // AQNWB::Channel* channel = nullptr; + // AQNWB::Types::SizeType datasetIndex = 0; + // for (auto& channelVector : this->recordingArrays) { // TODO - maybe different from recordingArrays? + // for (auto& ch : channelVector) { + // if (ch.globalIndex == realChannel) { + // channel = &ch; + // break; + // } + // } + // } + + // // extract info from spike object + // double timestamps = spike->getTimestampInSeconds(); + // std::unique_ptr intData = AQNWB::transformToInt16(static_cast(nSamples), + // channel->getBitVolts(), + // spike->getDataPointer()); + + // // write spike data + // this->recordingContainers->writeSpikeData(this->spikeContainerIndexes[channel->groupIndex], + // *channel, + // static_cast(nChannels, SamplesPerChannel), + // intData.get(), + // ×tamps); + + // TODO - add writeEventMetadata functionalities } void NWBRecordEngine::setParameter(EngineParameter& parameter) From cfdb7cb8af620bc4a5368dfb41175f2f1d9e5463 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Mon, 9 Sep 2024 22:14:52 -0700 Subject: [PATCH 21/32] update aqnwb src --- Source/aqnwb/BaseIO.hpp | 8 ++ Source/aqnwb/Channel.cpp | 2 + Source/aqnwb/Channel.hpp | 6 + Source/aqnwb/Utils.hpp | 17 +++ Source/aqnwb/hdf5/HDF5IO.cpp | 10 ++ Source/aqnwb/hdf5/HDF5IO.hpp | 7 + Source/aqnwb/nwb/NWBFile.cpp | 136 ++++++++++++++---- Source/aqnwb/nwb/NWBFile.hpp | 33 ++++- Source/aqnwb/nwb/RecordingContainers.cpp | 16 +++ Source/aqnwb/nwb/RecordingContainers.hpp | 18 ++- Source/aqnwb/nwb/ecephys/SpikeEventSeries.cpp | 60 ++++++++ Source/aqnwb/nwb/ecephys/SpikeEventSeries.hpp | 70 +++++++++ Source/aqnwb/nwb/file/ElectrodeTable.cpp | 4 +- 13 files changed, 353 insertions(+), 34 deletions(-) create mode 100644 Source/aqnwb/nwb/ecephys/SpikeEventSeries.cpp create mode 100644 Source/aqnwb/nwb/ecephys/SpikeEventSeries.hpp diff --git a/Source/aqnwb/BaseIO.hpp b/Source/aqnwb/BaseIO.hpp index 3dd375d..737b376 100644 --- a/Source/aqnwb/BaseIO.hpp +++ b/Source/aqnwb/BaseIO.hpp @@ -292,6 +292,13 @@ class BaseIO virtual std::unique_ptr 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. @@ -325,6 +332,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. diff --git a/Source/aqnwb/Channel.cpp b/Source/aqnwb/Channel.cpp index d8f9357..9aeb5eb 100644 --- a/Source/aqnwb/Channel.cpp +++ b/Source/aqnwb/Channel.cpp @@ -6,6 +6,7 @@ using namespace AQNWB; Channel::Channel(const std::string name, const std::string groupName, + const std::string sourceName, const SizeType groupIndex, const SizeType localIndex, const SizeType globalIndex, @@ -16,6 +17,7 @@ Channel::Channel(const std::string name, const std::string comments) : name(name) , groupName(groupName) + , sourceName(sourceName) , groupIndex(groupIndex) , localIndex(localIndex) , globalIndex(globalIndex) diff --git a/Source/aqnwb/Channel.hpp b/Source/aqnwb/Channel.hpp index 4166aa8..335535b 100644 --- a/Source/aqnwb/Channel.hpp +++ b/Source/aqnwb/Channel.hpp @@ -20,6 +20,7 @@ class Channel */ Channel(const std::string name, const std::string groupName, + const std::string sourceName, const SizeType groupIndex, const SizeType localIndex, const SizeType globalIndex, @@ -64,6 +65,11 @@ class Channel */ std::string groupName; + /** + * @brief Name of the data source the channel belongs to. + */ + std::string sourceName; + /** * @brief Index of array group the channel belongs to. */ diff --git a/Source/aqnwb/Utils.hpp b/Source/aqnwb/Utils.hpp index 43a0041..5c75f98 100644 --- a/Source/aqnwb/Utils.hpp +++ b/Source/aqnwb/Utils.hpp @@ -72,10 +72,21 @@ inline std::shared_ptr 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(0x7fff); auto intData = static_cast(dest); @@ -89,6 +100,12 @@ inline void convertFloatToInt16LE(const float* source, } } +/** + * @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 transformToInt16(SizeType numSamples, float conversion_factor, const float* data) diff --git a/Source/aqnwb/hdf5/HDF5IO.cpp b/Source/aqnwb/hdf5/HDF5IO.cpp index bf4b69a..58fe39e 100644 --- a/Source/aqnwb/hdf5/HDF5IO.cpp +++ b/Source/aqnwb/hdf5/HDF5IO.cpp @@ -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 HDF5IO::getDataSet( const std::string& path) { diff --git a/Source/aqnwb/hdf5/HDF5IO.hpp b/Source/aqnwb/hdf5/HDF5IO.hpp index 2a94aef..2ae1c6a 100644 --- a/Source/aqnwb/hdf5/HDF5IO.hpp +++ b/Source/aqnwb/hdf5/HDF5IO.hpp @@ -234,6 +234,13 @@ class HDF5IO : public BaseIO std::unique_ptr 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. diff --git a/Source/aqnwb/nwb/NWBFile.cpp b/Source/aqnwb/nwb/NWBFile.cpp index 5ce0ee1..8981316 100644 --- a/Source/aqnwb/nwb/NWBFile.cpp +++ b/Source/aqnwb/nwb/NWBFile.cpp @@ -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 NWBFile::emptyContainerIndexes = {}; @@ -38,14 +41,11 @@ NWBFile::~NWBFile() {} Status NWBFile::initialize(const std::string description, const std::string dataCollection) { - this->description = description; - this->dataCollection = dataCollection; - if (std::filesystem::exists(io->getFileName())) { return io->open(false); } else { io->open(true); - return createFileStructure(); + return createFileStructure(description, dataCollection); } } @@ -54,7 +54,8 @@ Status NWBFile::finalize() return io->close(); } -Status NWBFile::createFileStructure() +Status NWBFile::createFileStructure(std::string description, + std::string dataCollection) { if (!io->canModifyObjects()) { return Status::Failure; @@ -109,27 +110,32 @@ Status NWBFile::createElectricalSeries( return Status::Failure; } - // store all recorded data in the acquisition group - std::string rootPath = "/acquisition/"; - - // 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(io); + elecTable->initialize(); + } // 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 sourceName = channelVector[0].sourceName; + std::string devicePath = "/general/devices/" + sourceName; + std::string electrodePath = "/general/extracellular_ephys/" + sourceName; + std::string electricalSeriesPath = acquisitionPath + "/" + groupName; - 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 elecGroup = ElectrodeGroup(electrodePath, io, "description", "unknown", device); - elecGroup.initialize(); + elecGroup.initialize(); + } // Setup electrical series datasets auto electricalSeries = std::make_unique( @@ -144,14 +150,92 @@ Status NWBFile::createElectricalSeries( electricalSeries->initialize(); recordingContainers->addContainer(std::move(electricalSeries)); containerIndexes.push_back(recordingContainers->containers.size() - 1); + } + + if (!electrodeTableCreated) { + // Add electrode information to table (does not write to datasets yet) + for (const auto& channelVector : recordingArrays) { + elecTable->addElectrodes(channelVector); + } + + // write electrode information to datasets + elecTable->finalize(); + } + + return Status::Success; +} + +Status NWBFile::createSpikeEventSeries( + std::vector recordingArrays, + const SizeType numSamples, + const BaseDataType& dataType, + RecordingContainers* recordingContainers, + std::vector& containerIndexes) +{ + if (!io->canModifyObjects()) { + return Status::Failure; + } + + // Setup electrode table if it was not yet created + bool electrodeTableCreated = + io->objectExists(ElectrodeTable::electrodeTablePath); + if (!electrodeTableCreated) { + elecTable = std::make_unique(io); + elecTable->initialize(); + } + + // Create continuous datasets + for (const auto& channelVector : recordingArrays) { + // Setup electrodes and devices + std::string groupName = channelVector[0].groupName; + std::string sourceName = channelVector[0].sourceName; + std::string devicePath = "/general/devices/" + sourceName; + std::string electrodePath = "/general/extracellular_ephys/" + sourceName; + std::string spikeEventSeriesPath = acquisitionPath + "/" + groupName; + + // 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(); - // Add electrode information to electrode table (does not write to datasets - // yet) - elecTable.addElectrodes(channelVector); + 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(), numSamples}; + chunkSize = SizeArray {SPIKE_CHUNK_XSIZE, 1, 1}; + } + + auto spikeEventSeries = std::make_unique( + 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(); + if (!electrodeTableCreated) { + // Add electrode information to table (does not write to datasets yet) + for (const auto& channelVector : recordingArrays) { + elecTable->addElectrodes(channelVector); + } + + // write electrode information to datasets + elecTable->finalize(); + } return Status::Success; } @@ -163,7 +247,7 @@ void NWBFile::cacheSpecifications( const std::array, N>& specVariables) { - io->createGroup("/specifications/" + specPath + "/"); + io->createGroup("/specifications/" + specPath); io->createGroup("/specifications/" + specPath + "/" + versionNumber); for (const auto& [name, content] : specVariables) { diff --git a/Source/aqnwb/nwb/NWBFile.hpp b/Source/aqnwb/nwb/NWBFile.hpp index 2c13cdf..83db82d 100644 --- a/Source/aqnwb/nwb/NWBFile.hpp +++ b/Source/aqnwb/nwb/NWBFile.hpp @@ -11,6 +11,7 @@ #include "Types.hpp" #include "nwb/RecordingContainers.hpp" #include "nwb/base/TimeSeries.hpp" +#include "nwb/file/ElectrodeTable.hpp" /*! * \namespace AQNWB::NWB @@ -71,10 +72,10 @@ class NWBFile * @param recordingArrays vector of ChannelVector indicating the electrodes to * record from. A separate ElectricalSeries will be * created for each ChannelVector. + * @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( @@ -83,15 +84,38 @@ class NWBFile RecordingContainers* recordingContainers = nullptr, std::vector& 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 numSamples The number of samples to store for a single event. + * @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 recordingArrays, + const SizeType numSamples, + const BaseDataType& dataType = BaseDataType::I16, + RecordingContainers* recordingContainers = nullptr, + std::vector& containerIndexes = emptyContainerIndexes); + protected: /** * @brief Creates the default file structure. * Note, this function will fail if the file is in a mode where * new objects cannot be added, which can be checked via * nwbfile.io->canModifyObjects() + * @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(); + Status createFileStructure(std::string description, + std::string dataCollection); private: /** @@ -124,12 +148,11 @@ class NWBFile const std::array, N>& specVariables); + std::unique_ptr elecTable; const std::string identifierText; std::shared_ptr io; static std::vector emptyContainerIndexes; - - std::string description; - std::string dataCollection; + inline const static std::string acquisitionPath = "/acquisition"; }; } // namespace AQNWB::NWB \ No newline at end of file diff --git a/Source/aqnwb/nwb/RecordingContainers.cpp b/Source/aqnwb/nwb/RecordingContainers.cpp index d7464bc..4658d55 100644 --- a/Source/aqnwb/nwb/RecordingContainers.cpp +++ b/Source/aqnwb/nwb/RecordingContainers.cpp @@ -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; @@ -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(getContainer(containerInd)); + + if (ses == nullptr) + return Status::Failure; + + ses->writeSpike(numSamples, numChannels, data, timestamps); +} diff --git a/Source/aqnwb/nwb/RecordingContainers.hpp b/Source/aqnwb/nwb/RecordingContainers.hpp index aa00308..f38d84c 100644 --- a/Source/aqnwb/nwb/RecordingContainers.hpp +++ b/Source/aqnwb/nwb/RecordingContainers.hpp @@ -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. @@ -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> containers; std::string name; }; diff --git a/Source/aqnwb/nwb/ecephys/SpikeEventSeries.cpp b/Source/aqnwb/nwb/ecephys/SpikeEventSeries.cpp new file mode 100644 index 0000000..7c0c53c --- /dev/null +++ b/Source/aqnwb/nwb/ecephys/SpikeEventSeries.cpp @@ -0,0 +1,60 @@ +#include "nwb/ecephys/SpikeEventSeries.hpp" + +using namespace AQNWB::NWB; + +// SpikeEventSeries + +/** Constructor */ +SpikeEventSeries::SpikeEventSeries(const std::string& path, + std::shared_ptr io, + const BaseDataType& dataType, + const Types::ChannelVector& channelVector, + const std::string& description, + const SizeArray& dsetSize, + const SizeArray& chunkSize, + const float& conversion, + const float& resolution, + const float& offset) + : ElectricalSeries(path, + io, + dataType, + channelVector, + description, + dsetSize, + chunkSize, + conversion, + resolution, + offset) +{ +} + +/** Destructor */ +SpikeEventSeries::~SpikeEventSeries() {} + +void SpikeEventSeries::initialize() +{ + ElectricalSeries::initialize(); + + this->eventsRecorded = 0; +} + +Status SpikeEventSeries::writeSpike(const SizeType& numSamples, + const SizeType& numChannels, + const void* data, + const void* timestamps) +{ + // get offsets and datashape + std::vector dataShape; + std::vector positionOffset; + if (numChannels == 1) { + dataShape = {1, numSamples}; + positionOffset = {this->eventsRecorded, 0}; + } else { + dataShape = {1, numChannels, numSamples}; + positionOffset = {this->eventsRecorded, 0, 0}; + } + this->eventsRecorded += 1; + + // write channel data + return writeData(dataShape, positionOffset, data, timestamps); +} \ No newline at end of file diff --git a/Source/aqnwb/nwb/ecephys/SpikeEventSeries.hpp b/Source/aqnwb/nwb/ecephys/SpikeEventSeries.hpp new file mode 100644 index 0000000..ad2dd2a --- /dev/null +++ b/Source/aqnwb/nwb/ecephys/SpikeEventSeries.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include + +#include "BaseIO.hpp" +#include "Channel.hpp" +#include "nwb/ecephys/ElectricalSeries.hpp" + +namespace AQNWB::NWB +{ +/** + * @brief Stores snapshots/snippets of recorded spike events (i.e., threshold + * crossings). + */ +class SpikeEventSeries : public ElectricalSeries +{ +public: + /** + * @brief Constructor. + * @param path The location of the SpikeEventSeries in the file. + * @param io A shared pointer to the IO object. + * @param description The description of the SpikeEventSeries, should describe + * how events were detected. + */ + SpikeEventSeries(const std::string& path, + std::shared_ptr io, + const BaseDataType& dataType, + const Types::ChannelVector& channelVector, + const std::string& description, + const SizeArray& dsetSize, + const SizeArray& chunkSize, + const float& conversion = 1.0f, + const float& resolution = -1.0f, + const float& offset = 0.0f); + + /** + * @brief Destructor + */ + ~SpikeEventSeries(); + + /** + * @brief Initializes the Electrical Series + */ + void initialize(); + + /** + * @brief Write a single spike series event + * @param numSamples The number of samples in the event + * @param numChannels The number of channels in the event + * @param data The data of the event + * @param timestamps The timestamps of the event + * @param + */ + Status writeSpike(const SizeType& numSamples, + const SizeType& numChannels, + const void* data, + const void* timestamps); + +private: + /** + * @brief The neurodataType of the SpikeEventSeries. + */ + std::string neurodataType = "SpikeEventSeries"; + + /** + * @brief The number of events already written. + */ + SizeType eventsRecorded; +}; +} // namespace AQNWB::NWB diff --git a/Source/aqnwb/nwb/file/ElectrodeTable.cpp b/Source/aqnwb/nwb/file/ElectrodeTable.cpp index 027c9f6..b512b62 100644 --- a/Source/aqnwb/nwb/file/ElectrodeTable.cpp +++ b/Source/aqnwb/nwb/file/ElectrodeTable.cpp @@ -41,8 +41,8 @@ void ElectrodeTable::addElectrodes(std::vector channels) { // create datasets for (const auto& ch : channels) { - groupReferences.push_back(groupPathBase + ch.groupName); - groupNames.push_back(ch.groupName); + groupReferences.push_back(groupPathBase + ch.sourceName); + groupNames.push_back(ch.sourceName); electrodeNumbers.push_back(ch.globalIndex); locationNames.push_back("unknown"); } From 7d81a1501f04ee0a2c169d64e8bbeef9c72907a4 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:33:30 -0700 Subject: [PATCH 22/32] remove nSamples as input --- Source/aqnwb/nwb/NWBFile.cpp | 3 +-- Source/aqnwb/nwb/NWBFile.hpp | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Source/aqnwb/nwb/NWBFile.cpp b/Source/aqnwb/nwb/NWBFile.cpp index 8981316..a832d5e 100644 --- a/Source/aqnwb/nwb/NWBFile.cpp +++ b/Source/aqnwb/nwb/NWBFile.cpp @@ -167,7 +167,6 @@ Status NWBFile::createElectricalSeries( Status NWBFile::createSpikeEventSeries( std::vector recordingArrays, - const SizeType numSamples, const BaseDataType& dataType, RecordingContainers* recordingContainers, std::vector& containerIndexes) @@ -210,7 +209,7 @@ Status NWBFile::createSpikeEventSeries( dsetSize = SizeArray {0, 0}; chunkSize = SizeArray {SPIKE_CHUNK_XSIZE, 1}; } else { - dsetSize = SizeArray {0, channelVector.size(), numSamples}; + dsetSize = SizeArray {0, channelVector.size(), 0}; chunkSize = SizeArray {SPIKE_CHUNK_XSIZE, 1, 1}; } diff --git a/Source/aqnwb/nwb/NWBFile.hpp b/Source/aqnwb/nwb/NWBFile.hpp index 83db82d..09ad713 100644 --- a/Source/aqnwb/nwb/NWBFile.hpp +++ b/Source/aqnwb/nwb/NWBFile.hpp @@ -90,7 +90,6 @@ class NWBFile * @param recordingArrays vector of ChannelVector indicating the electrodes to * record from. A separate ElectricalSeries will be * created for each ChannelVector. - * @param numSamples The number of samples to store for a single event. * @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 @@ -99,7 +98,6 @@ class NWBFile */ Status createSpikeEventSeries( std::vector recordingArrays, - const SizeType numSamples, const BaseDataType& dataType = BaseDataType::I16, RecordingContainers* recordingContainers = nullptr, std::vector& containerIndexes = emptyContainerIndexes); From 4bbb78ea2f9a291131e961682bf48abfb4a2bc09 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:34:11 -0700 Subject: [PATCH 23/32] add spike event series functions --- Source/RecordEngine/NWBRecording.cpp | 104 +++++++++++++++++---------- Source/RecordEngine/NWBRecording.h | 13 ++-- 2 files changed, 75 insertions(+), 42 deletions(-) diff --git a/Source/RecordEngine/NWBRecording.cpp b/Source/RecordEngine/NWBRecording.cpp index 2fe1b29..7202bcd 100644 --- a/Source/RecordEngine/NWBRecording.cpp +++ b/Source/RecordEngine/NWBRecording.cpp @@ -85,6 +85,9 @@ RecordEngineManager* NWBRecordEngine::getEngineManager() this->recordingArrays, AQNWB::BaseDataType::I16, this->recordingContainers.get(), this->esContainerIndexes); // TODO add io_settings to set chunk size for different data types + this->nwbfile->createSpikeEventSeries( + this->spikeRecordingArrays, AQNWB::BaseDataType::I16, this->recordingContainers.get(), this->spikeContainerIndexes); + // start recording this->io->startRecording(); } @@ -128,7 +131,7 @@ void NWBRecordEngine::writeContinuousData(int writeChannel, void NWBRecordEngine::writeEvent(int eventIndex, const MidiMessage& event) { - // TODO - replacew with AQNWB + // TODO - replace with AQNWB // const EventChannel* channel = getEventChannel(eventIndex); // EventPtr eventStruct = Event::deserialize(event, channel); @@ -144,37 +147,26 @@ void NWBRecordEngine::writeTimestampSyncText(uint64 streamId, int64 timestamp, f void NWBRecordEngine::writeSpike(int electrodeIndex, const Spike* spike) { - // // extract info from spike channel - // const SpikeChannel* spikeChannel = getSpikeChannel(electrodeIndex); - // int nSamplesPerChannel = spikeChannel->getTotalSamples(); - // int nChannels = spikeChannel->getNumChannels(); - // int nSamples = nSamplesPerChannel * nChannels; - - // AQNWB::Channel* channel = nullptr; - // AQNWB::Types::SizeType datasetIndex = 0; - // for (auto& channelVector : this->recordingArrays) { // TODO - maybe different from recordingArrays? - // for (auto& ch : channelVector) { - // if (ch.globalIndex == realChannel) { - // channel = &ch; - // break; - // } - // } - // } - - // // extract info from spike object - // double timestamps = spike->getTimestampInSeconds(); - // std::unique_ptr intData = AQNWB::transformToInt16(static_cast(nSamples), - // channel->getBitVolts(), - // spike->getDataPointer()); - - // // write spike data - // this->recordingContainers->writeSpikeData(this->spikeContainerIndexes[channel->groupIndex], - // *channel, - // static_cast(nChannels, SamplesPerChannel), - // intData.get(), - // ×tamps); - - // TODO - add writeEventMetadata functionalities + // extract info from spike channel + const SpikeChannel* spikeChannel = getSpikeChannel(electrodeIndex); + SizeType numSamplesPerChannel = static_cast(spikeChannel->getTotalSamples()); + SizeType numChannels = static_cast(spikeChannel->getNumChannels()); + SizeType numSamples = numSamplesPerChannel * numChannels; + + // extract info from spike object + double timestamps = spike->getTimestampInSeconds(); + std::unique_ptr intData = AQNWB::transformToInt16(static_cast(numSamples), + spikeChannel->getSourceChannels()[0]->getBitVolts(), + spike->getDataPointer()); + + // write spike data + this->recordingContainers->writeSpikeEventData(this->spikeContainerIndexes[electrodeIndex], + numSamplesPerChannel, + numChannels, + intData.get(), + ×tamps); + + // TODO - add writeEventMetadata functionalities } void NWBRecordEngine::setParameter(EngineParameter& parameter) @@ -186,10 +178,14 @@ void NWBRecordEngine::reset() { if (this->nwbfile != nullptr) { - this->recordingArrays.clear(); + this->continuousChannels.clear(); this->continuousChannelGroups.clear(); + this->spikeChannels.clear(); + + this->recordingArrays.clear(); this->esContainerIndexes.clear(); + this->spikeContainerIndexes.clear(); this->nwbfile->finalize(); this->nwbfile.reset(); @@ -205,7 +201,7 @@ void NWBRecordEngine::createRecordingArrays() this->continuousChannels.add(channelInfo); } - // group channels by stream + // add continuous channels int streamIndex = -1; uint16 lastStreamId = 0; for (int ch = 0; ch < getNumRecordedContinuousChannels(); ch++) @@ -224,8 +220,13 @@ void NWBRecordEngine::createRecordingArrays() this->continuousChannelGroups.getReference(streamIndex).add(channelInfo); lastStreamId = channelInfo->getStreamId(); } - - // create recording arrays for nwb file + + // add spike channels + for (int i = 0; i < getNumRecordedSpikeChannels(); i++) { + spikeChannels.add(getSpikeChannel(i)); + } + + // create recording arrays for continuous groups in nwb file for (int streamIndex = 0; streamIndex < this->continuousChannelGroups.size(); streamIndex++) { std::vector channelVector; @@ -238,6 +239,7 @@ void NWBRecordEngine::createRecordingArrays() + "." + channelInfo->getStreamName().toStdString(); channelVector.push_back(AQNWB::Channel(name, + groupName, groupName, streamIndex, channelInfo->getLocalIndex(), @@ -248,5 +250,33 @@ void NWBRecordEngine::createRecordingArrays() } this->recordingArrays.push_back(channelVector); } -} + // create recording arrays for spike channels in nwb file + for (int i = 0; i < this->spikeChannels.size(); i++) + { + std::vector channelVector; + + const SpikeChannel* spikeChannel = this->spikeChannels[i]; + std::string sourceName = spikeChannel->getSourceNodeName().toStdString() + "-" + + std::to_string(spikeChannel->getSourceNodeId()) + + "." + spikeChannel->getStreamName().toStdString() + '.' + spikeChannel->getName().toStdString(); + + for (int ch = 0; ch < spikeChannel->getNumChannels(); ch++) + { + const ContinuousChannel* schan = spikeChannel->getSourceChannels()[ch]; + std::string continuousSourceName = schan->getSourceNodeName().toStdString() + "-" + + std::to_string(schan->getSourceNodeId()) + + "." + schan->getStreamName().toStdString(); + AQNWB::Channel channel(schan->getName().toStdString(), + sourceName, + continuousSourceName, + schan->getLocalIndex(), + schan->getGlobalIndex(), + 1e6, + schan->getSampleRate(), + schan->getBitVolts()); + channelVector.push_back(channel); + } + this->spikeRecordingArrays.push_back(channelVector); + } +} diff --git a/Source/RecordEngine/NWBRecording.h b/Source/RecordEngine/NWBRecording.h index c6f6cbc..68535df 100644 --- a/Source/RecordEngine/NWBRecording.h +++ b/Source/RecordEngine/NWBRecording.h @@ -99,21 +99,24 @@ namespace NWBRecording /** Holds channel information and ids */ std::vector recordingArrays; + /** Holds channel information and ids */ + std::vector spikeRecordingArrays; + /** Holds the indexes of the ElectricalSeries containers added to recordingContainers */ std::vector esContainerIndexes; + /** Holds the indexes of the ElectricalSeries containers added to recordingContainers */ + std::vector spikeContainerIndexes; + /** Holds pointers to all recorded channels within a stream */ Array continuousChannelGroups; - // /** Holds pointers to all recorded event channels*/ - // Array eventChannels; + /** Holds pointers to all recorded spike channels*/ + Array spikeChannels; /** Holds pointers to all incoming continuous channels (used for electrode table)*/ Array continuousChannels; - /** Holds integer sample numbers for writing */ - HeapBlock smpBuffer; - // /** The identifier for the current file (can be set externally) */ String identifierText; }; From d4f00efde364bc33835c0b63fad16d47016628fa Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:51:07 -0700 Subject: [PATCH 24/32] fix channel input, leak --- Source/RecordEngine/NWBRecording.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/RecordEngine/NWBRecording.cpp b/Source/RecordEngine/NWBRecording.cpp index 7202bcd..46bfe47 100644 --- a/Source/RecordEngine/NWBRecording.cpp +++ b/Source/RecordEngine/NWBRecording.cpp @@ -184,6 +184,7 @@ void NWBRecordEngine::reset() this->spikeChannels.clear(); this->recordingArrays.clear(); + this->spikeRecordingArrays.clear(); this->esContainerIndexes.clear(); this->spikeContainerIndexes.clear(); @@ -270,6 +271,7 @@ void NWBRecordEngine::createRecordingArrays() AQNWB::Channel channel(schan->getName().toStdString(), sourceName, continuousSourceName, + i, schan->getLocalIndex(), schan->getGlobalIndex(), 1e6, From df5e02095a68115b88c9101cb84b9b71e344376c Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:39:45 -0700 Subject: [PATCH 25/32] update aqnwb to use recording names --- Source/aqnwb/BaseIO.hpp | 3 +- Source/aqnwb/Channel.cpp | 2 - Source/aqnwb/Channel.hpp | 6 -- Source/aqnwb/hdf5/HDF5IO.hpp | 3 +- Source/aqnwb/nwb/NWBFile.cpp | 82 +++++++++++++++--------- Source/aqnwb/nwb/NWBFile.hpp | 4 ++ Source/aqnwb/nwb/file/ElectrodeTable.cpp | 4 +- 7 files changed, 60 insertions(+), 44 deletions(-) diff --git a/Source/aqnwb/BaseIO.hpp b/Source/aqnwb/BaseIO.hpp index 737b376..19d47cb 100644 --- a/Source/aqnwb/BaseIO.hpp +++ b/Source/aqnwb/BaseIO.hpp @@ -293,7 +293,8 @@ class BaseIO const std::string& path) = 0; /** - * @brief Checks whether a Dataset, Group, or Link already exists at the location in the file. + * @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. */ diff --git a/Source/aqnwb/Channel.cpp b/Source/aqnwb/Channel.cpp index 9aeb5eb..d8f9357 100644 --- a/Source/aqnwb/Channel.cpp +++ b/Source/aqnwb/Channel.cpp @@ -6,7 +6,6 @@ using namespace AQNWB; Channel::Channel(const std::string name, const std::string groupName, - const std::string sourceName, const SizeType groupIndex, const SizeType localIndex, const SizeType globalIndex, @@ -17,7 +16,6 @@ Channel::Channel(const std::string name, const std::string comments) : name(name) , groupName(groupName) - , sourceName(sourceName) , groupIndex(groupIndex) , localIndex(localIndex) , globalIndex(globalIndex) diff --git a/Source/aqnwb/Channel.hpp b/Source/aqnwb/Channel.hpp index 335535b..4166aa8 100644 --- a/Source/aqnwb/Channel.hpp +++ b/Source/aqnwb/Channel.hpp @@ -20,7 +20,6 @@ class Channel */ Channel(const std::string name, const std::string groupName, - const std::string sourceName, const SizeType groupIndex, const SizeType localIndex, const SizeType globalIndex, @@ -65,11 +64,6 @@ class Channel */ std::string groupName; - /** - * @brief Name of the data source the channel belongs to. - */ - std::string sourceName; - /** * @brief Index of array group the channel belongs to. */ diff --git a/Source/aqnwb/hdf5/HDF5IO.hpp b/Source/aqnwb/hdf5/HDF5IO.hpp index 2ae1c6a..8d2480d 100644 --- a/Source/aqnwb/hdf5/HDF5IO.hpp +++ b/Source/aqnwb/hdf5/HDF5IO.hpp @@ -235,7 +235,8 @@ class HDF5IO : public BaseIO const std::string& path) override; /** - * @brief Checks whether a Dataset, Group, or Link already exists at the location in the file. + * @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. */ diff --git a/Source/aqnwb/nwb/NWBFile.cpp b/Source/aqnwb/nwb/NWBFile.cpp index a832d5e..3ebda2b 100644 --- a/Source/aqnwb/nwb/NWBFile.cpp +++ b/Source/aqnwb/nwb/NWBFile.cpp @@ -102,6 +102,7 @@ Status NWBFile::createFileStructure(std::string description, Status NWBFile::createElectricalSeries( std::vector recordingArrays, + std::vector recordingNames, const BaseDataType& dataType, RecordingContainers* recordingContainers, std::vector& containerIndexes) @@ -110,30 +111,42 @@ Status NWBFile::createElectricalSeries( 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(io); elecTable->initialize(); + + // Add electrode information to table (does not write to datasets yet) + for (const auto& channelVector : recordingArrays) { + elecTable->addElectrodes(channelVector); + } } - // Create continuous datasets - for (const auto& channelVector : recordingArrays) { + // 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 sourceName = channelVector[0].sourceName; - std::string devicePath = "/general/devices/" + sourceName; - std::string electrodePath = "/general/extracellular_ephys/" + sourceName; - std::string electricalSeriesPath = acquisitionPath + "/" + groupName; + std::string devicePath = "/general/devices/" + groupName; + std::string electrodePath = "/general/extracellular_ephys/" + groupName; + std::string electricalSeriesPath = acquisitionPath + "/" + recordingName; - // Check if device exists for groupName, create device and electrode group if not - if (!io->objectExists(devicePath)){ + // 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); + ElectrodeGroup(electrodePath, io, "description", "unknown", device); elecGroup.initialize(); } @@ -152,13 +165,9 @@ Status NWBFile::createElectricalSeries( containerIndexes.push_back(recordingContainers->containers.size() - 1); } + // write electrode information to datasets + // (requires that the ElectrodeGroup have been written) if (!electrodeTableCreated) { - // Add electrode information to table (does not write to datasets yet) - for (const auto& channelVector : recordingArrays) { - elecTable->addElectrodes(channelVector); - } - - // write electrode information to datasets elecTable->finalize(); } @@ -167,6 +176,7 @@ Status NWBFile::createElectricalSeries( Status NWBFile::createSpikeEventSeries( std::vector recordingArrays, + std::vector recordingNames, const BaseDataType& dataType, RecordingContainers* recordingContainers, std::vector& containerIndexes) @@ -175,30 +185,42 @@ Status NWBFile::createSpikeEventSeries( 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(io); elecTable->initialize(); + + // Add electrode information to table (does not write to datasets yet) + for (const auto& channelVector : recordingArrays) { + elecTable->addElectrodes(channelVector); + } } - // Create continuous datasets - for (const auto& channelVector : recordingArrays) { - // Setup electrodes and devices - std::string groupName = channelVector[0].groupName; - std::string sourceName = channelVector[0].sourceName; - std::string devicePath = "/general/devices/" + sourceName; - std::string electrodePath = "/general/extracellular_ephys/" + sourceName; - std::string spikeEventSeriesPath = acquisitionPath + "/" + groupName; + // Create datasets + for (size_t i = 0; i < recordingArrays.size(); ++i) { + const auto& channelVector = recordingArrays[i]; + const std::string& recordingName = recordingNames[i]; - // Check if device exists for groupName, create device and electrode group if not - if (!io->objectExists(devicePath)){ + // 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); + ElectrodeGroup(electrodePath, io, "description", "unknown", device); elecGroup.initialize(); } @@ -226,13 +248,9 @@ Status NWBFile::createSpikeEventSeries( containerIndexes.push_back(recordingContainers->containers.size() - 1); } + // write electrode information to datasets + // (requires that the ElectrodeGroup have been written) if (!electrodeTableCreated) { - // Add electrode information to table (does not write to datasets yet) - for (const auto& channelVector : recordingArrays) { - elecTable->addElectrodes(channelVector); - } - - // write electrode information to datasets elecTable->finalize(); } diff --git a/Source/aqnwb/nwb/NWBFile.hpp b/Source/aqnwb/nwb/NWBFile.hpp index 09ad713..24b446c 100644 --- a/Source/aqnwb/nwb/NWBFile.hpp +++ b/Source/aqnwb/nwb/NWBFile.hpp @@ -72,6 +72,7 @@ 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 @@ -80,6 +81,7 @@ class NWBFile */ Status createElectricalSeries( std::vector recordingArrays, + std::vector recordingNames, const BaseDataType& dataType = BaseDataType::I16, RecordingContainers* recordingContainers = nullptr, std::vector& containerIndexes = emptyContainerIndexes); @@ -90,6 +92,7 @@ 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 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 @@ -98,6 +101,7 @@ class NWBFile */ Status createSpikeEventSeries( std::vector recordingArrays, + std::vector recordingNames, const BaseDataType& dataType = BaseDataType::I16, RecordingContainers* recordingContainers = nullptr, std::vector& containerIndexes = emptyContainerIndexes); diff --git a/Source/aqnwb/nwb/file/ElectrodeTable.cpp b/Source/aqnwb/nwb/file/ElectrodeTable.cpp index b512b62..027c9f6 100644 --- a/Source/aqnwb/nwb/file/ElectrodeTable.cpp +++ b/Source/aqnwb/nwb/file/ElectrodeTable.cpp @@ -41,8 +41,8 @@ void ElectrodeTable::addElectrodes(std::vector channels) { // create datasets for (const auto& ch : channels) { - groupReferences.push_back(groupPathBase + ch.sourceName); - groupNames.push_back(ch.sourceName); + groupReferences.push_back(groupPathBase + ch.groupName); + groupNames.push_back(ch.groupName); electrodeNumbers.push_back(ch.globalIndex); locationNames.push_back("unknown"); } From 8e7011edd08f2fad408299289beb81cd9d8a1d9a Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:40:03 -0700 Subject: [PATCH 26/32] add series names as input for dset creation --- Source/RecordEngine/NWBRecording.cpp | 10 ++++++---- Source/RecordEngine/NWBRecording.h | 6 ++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Source/RecordEngine/NWBRecording.cpp b/Source/RecordEngine/NWBRecording.cpp index 46bfe47..d9b13ff 100644 --- a/Source/RecordEngine/NWBRecording.cpp +++ b/Source/RecordEngine/NWBRecording.cpp @@ -82,11 +82,11 @@ RecordEngineManager* NWBRecordEngine::getEngineManager() // create recording containers this->recordingContainers = std::make_unique(); this->nwbfile->createElectricalSeries( - this->recordingArrays, AQNWB::BaseDataType::I16, this->recordingContainers.get(), this->esContainerIndexes); + this->recordingArrays, this->recordingArraysNames, AQNWB::BaseDataType::I16, this->recordingContainers.get(), this->esContainerIndexes); // TODO add io_settings to set chunk size for different data types this->nwbfile->createSpikeEventSeries( - this->spikeRecordingArrays, AQNWB::BaseDataType::I16, this->recordingContainers.get(), this->spikeContainerIndexes); + this->spikeRecordingArrays, this->spikeRecordingArraysNames, AQNWB::BaseDataType::I16, this->recordingContainers.get(), this->spikeContainerIndexes); // start recording this->io->startRecording(); @@ -184,7 +184,9 @@ void NWBRecordEngine::reset() this->spikeChannels.clear(); this->recordingArrays.clear(); + this->recordingArraysNames.clear(); this->spikeRecordingArrays.clear(); + this->spikeRecordingArraysNames.clear(); this->esContainerIndexes.clear(); this->spikeContainerIndexes.clear(); @@ -240,7 +242,6 @@ void NWBRecordEngine::createRecordingArrays() + "." + channelInfo->getStreamName().toStdString(); channelVector.push_back(AQNWB::Channel(name, - groupName, groupName, streamIndex, channelInfo->getLocalIndex(), @@ -250,6 +251,7 @@ void NWBRecordEngine::createRecordingArrays() channelInfo->getBitVolts())); } this->recordingArrays.push_back(channelVector); + this->recordingArraysNames.push_back(channelVector[0].groupName); } // create recording arrays for spike channels in nwb file @@ -269,7 +271,6 @@ void NWBRecordEngine::createRecordingArrays() + std::to_string(schan->getSourceNodeId()) + "." + schan->getStreamName().toStdString(); AQNWB::Channel channel(schan->getName().toStdString(), - sourceName, continuousSourceName, i, schan->getLocalIndex(), @@ -280,5 +281,6 @@ void NWBRecordEngine::createRecordingArrays() channelVector.push_back(channel); } this->spikeRecordingArrays.push_back(channelVector); + this->spikeRecordingArraysNames.push_back(sourceName); } } diff --git a/Source/RecordEngine/NWBRecording.h b/Source/RecordEngine/NWBRecording.h index 68535df..83c0a00 100644 --- a/Source/RecordEngine/NWBRecording.h +++ b/Source/RecordEngine/NWBRecording.h @@ -98,9 +98,15 @@ namespace NWBRecording /** Holds channel information and ids */ std::vector recordingArrays; + + /** Holds names of the recordingArrays */ + std::vector recordingArraysNames; /** Holds channel information and ids */ std::vector spikeRecordingArrays; + + /** Holds names of the spikeRecordingArrays */ + std::vector spikeRecordingArraysNames; /** Holds the indexes of the ElectricalSeries containers added to recordingContainers */ std::vector esContainerIndexes; From b2085b08cad886837ab352344b16876eefb18e42 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:47:23 -0700 Subject: [PATCH 27/32] remove aqnwb folder --- Source/aqnwb/BaseIO.cpp | 108 --- Source/aqnwb/BaseIO.hpp | 444 ----------- Source/aqnwb/Channel.cpp | 45 -- Source/aqnwb/Channel.hpp | 108 --- Source/aqnwb/Types.hpp | 42 - Source/aqnwb/Utils.hpp | 128 --- Source/aqnwb/hdf5/HDF5IO.cpp | 738 ------------------ Source/aqnwb/hdf5/HDF5IO.hpp | 343 -------- Source/aqnwb/nwb/NWBFile.cpp | 286 ------- Source/aqnwb/nwb/NWBFile.hpp | 160 ---- Source/aqnwb/nwb/RecordingContainers.cpp | 81 -- Source/aqnwb/nwb/RecordingContainers.hpp | 112 --- Source/aqnwb/nwb/base/TimeSeries.cpp | 80 -- Source/aqnwb/nwb/base/TimeSeries.hpp | 148 ---- Source/aqnwb/nwb/device/Device.cpp | 38 - Source/aqnwb/nwb/device/Device.hpp | 63 -- Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp | 101 --- Source/aqnwb/nwb/ecephys/ElectricalSeries.hpp | 98 --- Source/aqnwb/nwb/ecephys/SpikeEventSeries.cpp | 60 -- Source/aqnwb/nwb/ecephys/SpikeEventSeries.hpp | 70 -- Source/aqnwb/nwb/file/ElectrodeGroup.cpp | 48 -- Source/aqnwb/nwb/file/ElectrodeGroup.hpp | 81 -- Source/aqnwb/nwb/file/ElectrodeTable.cpp | 84 -- Source/aqnwb/nwb/file/ElectrodeTable.hpp | 128 --- Source/aqnwb/nwb/hdmf/base/Container.cpp | 27 - Source/aqnwb/nwb/hdmf/base/Container.hpp | 51 -- Source/aqnwb/nwb/hdmf/base/Data.hpp | 30 - Source/aqnwb/nwb/hdmf/table/DynamicTable.cpp | 85 -- Source/aqnwb/nwb/hdmf/table/DynamicTable.hpp | 96 --- .../nwb/hdmf/table/ElementIdentifiers.hpp | 14 - Source/aqnwb/nwb/hdmf/table/VectorData.cpp | 9 - Source/aqnwb/nwb/hdmf/table/VectorData.hpp | 27 - Source/aqnwb/spec/core.hpp | 65 -- Source/aqnwb/spec/hdmf_common.hpp | 29 - Source/aqnwb/spec/hdmf_experimental.hpp | 25 - 35 files changed, 4052 deletions(-) delete mode 100644 Source/aqnwb/BaseIO.cpp delete mode 100644 Source/aqnwb/BaseIO.hpp delete mode 100644 Source/aqnwb/Channel.cpp delete mode 100644 Source/aqnwb/Channel.hpp delete mode 100644 Source/aqnwb/Types.hpp delete mode 100644 Source/aqnwb/Utils.hpp delete mode 100644 Source/aqnwb/hdf5/HDF5IO.cpp delete mode 100644 Source/aqnwb/hdf5/HDF5IO.hpp delete mode 100644 Source/aqnwb/nwb/NWBFile.cpp delete mode 100644 Source/aqnwb/nwb/NWBFile.hpp delete mode 100644 Source/aqnwb/nwb/RecordingContainers.cpp delete mode 100644 Source/aqnwb/nwb/RecordingContainers.hpp delete mode 100644 Source/aqnwb/nwb/base/TimeSeries.cpp delete mode 100644 Source/aqnwb/nwb/base/TimeSeries.hpp delete mode 100644 Source/aqnwb/nwb/device/Device.cpp delete mode 100644 Source/aqnwb/nwb/device/Device.hpp delete mode 100644 Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp delete mode 100644 Source/aqnwb/nwb/ecephys/ElectricalSeries.hpp delete mode 100644 Source/aqnwb/nwb/ecephys/SpikeEventSeries.cpp delete mode 100644 Source/aqnwb/nwb/ecephys/SpikeEventSeries.hpp delete mode 100644 Source/aqnwb/nwb/file/ElectrodeGroup.cpp delete mode 100644 Source/aqnwb/nwb/file/ElectrodeGroup.hpp delete mode 100644 Source/aqnwb/nwb/file/ElectrodeTable.cpp delete mode 100644 Source/aqnwb/nwb/file/ElectrodeTable.hpp delete mode 100644 Source/aqnwb/nwb/hdmf/base/Container.cpp delete mode 100644 Source/aqnwb/nwb/hdmf/base/Container.hpp delete mode 100644 Source/aqnwb/nwb/hdmf/base/Data.hpp delete mode 100644 Source/aqnwb/nwb/hdmf/table/DynamicTable.cpp delete mode 100644 Source/aqnwb/nwb/hdmf/table/DynamicTable.hpp delete mode 100644 Source/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp delete mode 100644 Source/aqnwb/nwb/hdmf/table/VectorData.cpp delete mode 100644 Source/aqnwb/nwb/hdmf/table/VectorData.hpp delete mode 100644 Source/aqnwb/spec/core.hpp delete mode 100644 Source/aqnwb/spec/hdmf_common.hpp delete mode 100644 Source/aqnwb/spec/hdmf_experimental.hpp diff --git a/Source/aqnwb/BaseIO.cpp b/Source/aqnwb/BaseIO.cpp deleted file mode 100644 index 353f709..0000000 --- a/Source/aqnwb/BaseIO.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include "BaseIO.hpp" - -#include "Utils.hpp" - -using namespace AQNWB; - -// BaseDataType - -BaseDataType::BaseDataType(BaseDataType::Type t, SizeType s) - : type(t) - , typeSize(s) -{ -} - -BaseDataType BaseDataType::STR(SizeType size) -{ - return BaseDataType(T_STR, size); -} - -const BaseDataType BaseDataType::U8 = BaseDataType(T_U8, 1); -const BaseDataType BaseDataType::U16 = BaseDataType(T_U16, 1); -const BaseDataType BaseDataType::U32 = BaseDataType(T_U32, 1); -const BaseDataType BaseDataType::U64 = BaseDataType(T_U64, 1); -const BaseDataType BaseDataType::I8 = BaseDataType(T_I8, 1); -const BaseDataType BaseDataType::I16 = BaseDataType(T_I16, 1); -const BaseDataType BaseDataType::I32 = BaseDataType(T_I32, 1); -const BaseDataType BaseDataType::I64 = BaseDataType(T_I64, 1); -const BaseDataType BaseDataType::F32 = BaseDataType(T_F32, 1); -const BaseDataType BaseDataType::F64 = BaseDataType(T_F64, 1); -const BaseDataType BaseDataType::DSTR = BaseDataType(T_STR, DEFAULT_STR_SIZE); - -// BaseIO - -BaseIO::BaseIO() - : readyToOpen(true) - , opened(false) -{ -} - -BaseIO::~BaseIO() {} - -bool BaseIO::isOpen() const -{ - return opened; -} - -bool BaseIO::isReadyToOpen() const -{ - return readyToOpen; -} - -bool BaseIO::canModifyObjects() -{ - return true; -} - -Status BaseIO::createCommonNWBAttributes(const std::string& path, - const std::string& objectNamespace, - const std::string& neurodataType, - const std::string& description) -{ - createAttribute(objectNamespace, path, "namespace"); - createAttribute(generateUuid(), path, "object_id"); - if (neurodataType != "") - createAttribute(neurodataType, path, "neurodata_type"); - if (description != "") - createAttribute(description, path, "description"); - return Status::Success; -} - -Status BaseIO::createDataAttributes(const std::string& path, - const float& conversion, - const float& resolution, - const std::string& unit) -{ - createAttribute(BaseDataType::F32, &conversion, path + "/data", "conversion"); - createAttribute(BaseDataType::F32, &resolution, path + "/data", "resolution"); - createAttribute(unit, path + "/data", "unit"); - - return Status::Success; -} - -Status BaseIO::createTimestampsAttributes(const std::string& path) -{ - int interval = 1; - createAttribute(BaseDataType::I32, - static_cast(&interval), - path + "/timestamps", - "interval"); - createAttribute("seconds", path + "/timestamps", "unit"); - - return Status::Success; -} - -// BaseRecordingData - -BaseRecordingData::BaseRecordingData() {} - -BaseRecordingData::~BaseRecordingData() {} - -// Overload that uses the member variable position (works for simple data -// extension) -Status BaseRecordingData::writeDataBlock(const std::vector& dataShape, - const BaseDataType& type, - const void* data) -{ - return writeDataBlock(dataShape, position, type, data); -} diff --git a/Source/aqnwb/BaseIO.hpp b/Source/aqnwb/BaseIO.hpp deleted file mode 100644 index 19d47cb..0000000 --- a/Source/aqnwb/BaseIO.hpp +++ /dev/null @@ -1,444 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "Types.hpp" - -#define DEFAULT_STR_SIZE 256 -#define DEFAULT_ARRAY_SIZE 1 - -using Status = AQNWB::Types::Status; -using SizeArray = AQNWB::Types::SizeArray; -using SizeType = AQNWB::Types::SizeType; - -/*! - * \namespace AQNWB - * \brief The main namespace for AqNWB - */ -namespace AQNWB -{ - -class BaseRecordingData; - -/** - * @brief Represents a base data type. - * - * This class provides an enumeration of different data types and their - * corresponding sizes. It also includes handy accessors for commonly used data - * types. - */ -class BaseDataType -{ -public: - /** - * @brief Enumeration of different data types. - */ - enum Type - { - T_U8, ///< Unsigned 8-bit integer - T_U16, ///< Unsigned 16-bit integer - T_U32, ///< Unsigned 32-bit integer - T_U64, ///< Unsigned 64-bit integer - T_I8, ///< Signed 8-bit integer - T_I16, ///< Signed 16-bit integer - T_I32, ///< Signed 32-bit integer - T_I64, ///< Signed 64-bit integer - T_F32, ///< 32-bit floating point - T_F64, ///< 64-bit floating point - T_STR, ///< String - V_STR, ///< Variable length string - }; - - /** - * @brief Constructs a BaseDataType object with the specified type and size. - * @param t The data type. - * @param s The size of the data type. - */ - BaseDataType(Type t = T_I32, SizeType s = 1); - - Type type; ///< The data type. - SizeType typeSize; ///< The size of the data type. - - // handy accessors - static const BaseDataType U8; ///< Accessor for unsigned 8-bit integer. - static const BaseDataType U16; ///< Accessor for unsigned 16-bit integer. - static const BaseDataType U32; ///< Accessor for unsigned 32-bit integer. - static const BaseDataType U64; ///< Accessor for unsigned 64-bit integer. - static const BaseDataType I8; ///< Accessor for signed 8-bit integer. - static const BaseDataType I16; ///< Accessor for signed 16-bit integer. - static const BaseDataType I32; ///< Accessor for signed 32-bit integer. - static const BaseDataType I64; ///< Accessor for signed 64-bit integer. - static const BaseDataType F32; ///< Accessor for 32-bit floating point. - static const BaseDataType F64; ///< Accessor for 64-bit floating point. - static const BaseDataType DSTR; ///< Accessor for dynamic string. - static BaseDataType STR( - SizeType size); ///< Accessor for string with specified size. -}; - -/** - * @brief The BaseIO class is an abstract base class that defines the interface - * for input/output (IO) operations on a file. - * - * This class provides pure virtual methods that must be implemented by all IO - * classes. It also includes other methods for common IO operations. - * - * @note This class cannot be instantiated directly as it is an abstract class. - */ -class BaseIO -{ -public: - /** - * @brief Constructor for the BaseIO class. - */ - BaseIO(); - - /** - * @brief Copy constructor is deleted to prevent construction-copying. - */ - BaseIO(const BaseIO&) = delete; - - /** - * @brief Assignment operator is deleted to prevent copying. - */ - BaseIO& operator=(const BaseIO&) = delete; - - /** - * @brief Destructor the BaseIO class. - */ - virtual ~BaseIO(); - - /** - * @brief Returns the full path to the file. - * @return The full path to the file. - */ - virtual std::string getFileName() = 0; - - /** - * @brief Opens the file for writing. - * @return The status of the file opening operation. - */ - virtual Status open() = 0; - - /** - * @brief Opens an existing file or creates a new file for writing. - * @param newfile Flag indicating whether to create a new file. - * @return The status of the file opening operation. - */ - virtual Status open(bool newfile) = 0; - - /** - * @brief Closes the file. - * @return The status of the file closing operation. - */ - virtual Status close() = 0; - - /** - * @brief Flush data to disk - * @return The status of the flush operation. - */ - virtual Status flush() = 0; - - /** - * @brief Creates an attribute at a given location in the file. - * @param type The base data type of the attribute. - * @param data Pointer to the attribute data. - * @param path The location in the file to set the attribute. - * @param name The name of the attribute. - * @param size The size of the attribute (default is 1). - * @return The status of the attribute creation operation. - */ - virtual Status createAttribute(const BaseDataType& type, - const void* data, - const std::string& path, - const std::string& name, - const SizeType& size = 1) = 0; - - /** - * @brief Creates a string attribute at a given location in the file. - * @param data The string attribute data. - * @param path The location in the file to set the attribute. - * @param name The name of the attribute. - * @return The status of the attribute creation operation. - */ - virtual Status createAttribute(const std::string& data, - const std::string& path, - const std::string& name) = 0; - - /** - * @brief Creates a string array attribute at a given location in the file. - * @param data The string array attribute data. - * @param path The location in the file to set the attribute. - * @param name The name of the attribute. - * @return The status of the attribute creation operation. - */ - virtual Status createAttribute(const std::vector& data, - const std::string& path, - const std::string& name) = 0; - - /** - * @brief Creates a string array attribute at a given location in the file. - * @param data The string array attribute data. - * @param path The location in the file to set the attribute. - * @param name The name of the attribute. - * @param maxSize The maximum size of the string. - * @return The status of the attribute creation operation. - */ - virtual Status createAttribute(const std::vector& data, - const std::string& path, - const std::string& name, - const SizeType& maxSize) = 0; - - /** - * @brief Sets an object reference attribute for a given location in the file. - * @param referencePath The full path to the referenced group / dataset. - * @param path The location in the file to set the attribute. - * @param name The name of the attribute. - * @return The status of the attribute creation operation. - */ - virtual Status createReferenceAttribute(const std::string& referencePath, - const std::string& path, - const std::string& name) = 0; - - /** - * @brief Creates a new group in the file. - * @param path The location in the file of the new group. - * @return The status of the group creation operation. - */ - virtual Status createGroup(const std::string& path) = 0; - - /** - * @brief Creates a soft link to another location in the file. - * @param path The location in the file to the new link. - * @param reference The location in the file of the object that is being - * linked to. - * @return The status of the link creation operation. - */ - virtual Status createLink(const std::string& path, - const std::string& reference) = 0; - - /** - * @brief Creates a non-modifiable dataset with a string value. - * @param path The location in the file of the dataset. - * @param value The string value of the dataset. - * @return The status of the dataset creation operation. - */ - virtual Status createStringDataSet(const std::string& path, - const std::string& value) = 0; - - /** - * @brief Creates a dataset that holds an array of string values. - * @param path The location in the file of the dataset. - * @param values The vector of string values of the dataset. - * @return The status of the dataset creation operation. - */ - virtual Status createStringDataSet( - const std::string& path, const std::vector& values) = 0; - - /** - * @brief Creates a dataset that holds an array of references to groups within - * the file. - * @param path The location in the file of the new dataset. - * @param references The array of references. - * @return The status of the dataset creation operation. - */ - virtual Status createReferenceDataSet( - const std::string& path, const std::vector& references) = 0; - - /** - * @brief Starts the recording process. - * @return The status of the operation. - */ - virtual Status startRecording() = 0; - - /** - * @brief Stops the recording process. - * @return The status of the operation. - */ - virtual Status stopRecording() = 0; - - /** - * @brief Returns true if the file is in a mode where objects can - * be added or deleted. Note, this does not apply to the modification - * of raw data on already existing objects. Derived classes should - * override this function to check if objects can be modified. - * @return True if the file is in a modification mode, false otherwise. - */ - virtual bool canModifyObjects(); - - /** - * @brief Creates an extendable dataset with a given base data type, size, - * chunking, and path. - * @param type The base data type of the dataset. - * @param size The size of the dataset. - * @param chunking The chunking size of the dataset. - * @param path The location in the file of the new dataset. - * @return A pointer to the created dataset. - */ - virtual std::unique_ptr createArrayDataSet( - const BaseDataType& type, - const SizeArray& size, - const SizeArray& chunking, - const std::string& path) = 0; - - /** - * @brief Returns a pointer to a dataset at a given path. - * @param path The location in the file of the dataset. - * @return A pointer to the dataset. - */ - virtual std::unique_ptr 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. - * @param objectNamespace The namespace of the object. - * @param neurodataType The neurodata type of the object. - * @param description The description of the object (default is empty). - * @return The status of the operation. - */ - Status createCommonNWBAttributes(const std::string& path, - const std::string& objectNamespace, - const std::string& neurodataType = "", - const std::string& description = ""); - - /** - * @brief Convenience function for creating data related attributes. - * @param path The location of the object in the file. - * @param conversion Scalar to multiply each element in data to convert it to - * the specified ‘unit’. - * @param resolution Smallest meaningful difference between values in data. - * @param unit Base unit of measurement for working with the data. - * @return The status of the operation. - */ - Status createDataAttributes(const std::string& path, - const float& conversion, - const float& resolution, - const std::string& unit); - - /** - * @brief Convenience function for creating timestamp related attributes. - * @param path The location of the object in the file. - * @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. - */ - bool isOpen() const; - - /** - * @brief Returns true if the file is able to be opened. - * @return True if the file is able to be opened, false otherwise. - */ - bool isReadyToOpen() const; - - /** - * @brief The name of the file. - */ - const std::string filename; - -protected: - /** - * @brief Creates a new group if it does not already exist. - * @param path The location of the group in the file. - * @return The status of the operation. - */ - virtual Status createGroupIfDoesNotExist(const std::string& path) = 0; - - /** - * @brief Whether the file is ready to be opened. - */ - bool readyToOpen; - - /** - * @brief Whether the file is currently open. - */ - bool opened; -}; - -/** - * @brief The base class to represent recording data that can be extended. - * - * This class provides functionality for writing 1D and 2D blocks of data. - */ -class BaseRecordingData -{ -public: - /** - * @brief Default constructor. - */ - BaseRecordingData(); - - /** - * @brief Deleted copy constructor to prevent construction-copying. - */ - BaseRecordingData(const BaseRecordingData&) = delete; - - /** - * @brief Deleted copy assignment operator to prevent copying. - */ - BaseRecordingData& operator=(const BaseRecordingData&) = delete; - - /** - * @brief Destructor. - */ - virtual ~BaseRecordingData(); - - /** - * @brief Writes a block of data using the stored position information. - * This is not intended to be overwritten by derived classes, but is a - * convenience function for writing data using the last recorded position. - * @param dataShape The size of the data block. - * @param type The data type of the elements in the data block. - * @param data A pointer to the data block. - * @return The status of the write operation. - */ - Status writeDataBlock(const std::vector& dataShape, - const BaseDataType& type, - const void* data); - - /** - * @brief Writes a block of data (any number of dimensions). - * @param dataShape The size of the data block. - * @param positionOffset The position of the data block to write to. - * @param type The data type of the elements in the data block. - * @param data A pointer to the data block. - * @return The status of the write operation. - */ - virtual Status writeDataBlock(const std::vector& dataShape, - const std::vector& positionOffset, - const BaseDataType& type, - const void* data) = 0; - -protected: - /** - * @brief The size of the dataset in each dimension. - */ - std::vector size; - - /** - * @brief The current position in the dataset. - */ - std::vector position; - - /** - * @brief The number of dimensions in the data block. - */ - SizeType nDimensions; -}; - -} // namespace AQNWB diff --git a/Source/aqnwb/Channel.cpp b/Source/aqnwb/Channel.cpp deleted file mode 100644 index d8f9357..0000000 --- a/Source/aqnwb/Channel.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include - -#include "Channel.hpp" - -using namespace AQNWB; - -Channel::Channel(const std::string name, - const std::string groupName, - const SizeType groupIndex, - const SizeType localIndex, - const SizeType globalIndex, - const float conversion, - const float samplingRate, - const float bitVolts, - const std::array position, - const std::string comments) - : name(name) - , groupName(groupName) - , groupIndex(groupIndex) - , localIndex(localIndex) - , globalIndex(globalIndex) - , position(position) - , conversion(conversion) - , samplingRate(samplingRate) - , bitVolts(bitVolts) - , comments(comments) -{ -} - -Channel::~Channel() {} - -float Channel::getConversion() const -{ - return bitVolts / conversion; -} - -float Channel::getSamplingRate() const -{ - return samplingRate; -} - -float Channel::getBitVolts() const -{ - return bitVolts; -} diff --git a/Source/aqnwb/Channel.hpp b/Source/aqnwb/Channel.hpp deleted file mode 100644 index 4166aa8..0000000 --- a/Source/aqnwb/Channel.hpp +++ /dev/null @@ -1,108 +0,0 @@ -#pragma once - -#include -#include - -#include "Types.hpp" - -using SizeType = AQNWB::Types::SizeType; - -namespace AQNWB -{ -/** - * @brief Class for storing acquisition system channel information. - */ -class Channel -{ -public: - /** - * @brief Constructor. - */ - Channel(const std::string name, - const std::string groupName, - const SizeType groupIndex, - const SizeType localIndex, - const SizeType globalIndex, - const float conversion = 1e6f, // uV to V - const float samplingRate = 30000.f, // placeholder - const float bitVolts = 0.05f, // least significant bit needed to - // convert 16-bit int to volts - // currently a placeholder - const std::array position = {0.f, 0.f, 0.f}, - const std::string comments = "no comments"); - - /** - * @brief Destructor - */ - ~Channel(); - - /** - * @brief Getter for conversion factor - * @return The conversion value. - */ - float getConversion() const; - - /** - * @brief Getter for samplingRate - * @return The samplingRate value. - */ - float getSamplingRate() const; - - /** - * @brief Getter for bitVolts - * @return The bitVolts value. - */ - float getBitVolts() const; - - /** - * @brief Name of the channel. - */ - std::string name; - - /** - * @brief Name of the array group the channel belongs to. - */ - std::string groupName; - - /** - * @brief Index of array group the channel belongs to. - */ - SizeType groupIndex; - - /** - * @brief Index of channel within the recording array. - */ - SizeType localIndex; - - /** - * @brief Index of channel across the recording system. - */ - SizeType globalIndex; - - /** - * @brief Coordinates of channel (x, y, z) within the recording array. - */ - std::array position; - - /** - * @brief Comments about the channel. - */ - std::string comments; - -private: - /** - * @brief Conversion factor. - */ - float conversion; - - /** - * @brief Sampling rate of the channel. - */ - float samplingRate; - - /** - * @brief floating point value of microvolts per bit - */ - float bitVolts; -}; -} // namespace AQNWB diff --git a/Source/aqnwb/Types.hpp b/Source/aqnwb/Types.hpp deleted file mode 100644 index 37bbd8a..0000000 --- a/Source/aqnwb/Types.hpp +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include -#include - -namespace AQNWB -{ - -// Forward declaration of Channel -class Channel; - -/** - * @brief Provides definitions for various types used in the project. - */ -class Types -{ -public: - /** - * @brief Represents the status of an operation. - */ - enum Status - { - Success = 0, - Failure = -1 - }; - - /** - * @brief Alias for the size type used in the project. - */ - using SizeType = size_t; - - /** - * @brief Alias for an array of size types used in the project. - */ - using SizeArray = std::vector; - - /** - * @brief Alias for a vector of channels. - */ - using ChannelVector = std::vector; -}; -} // namespace AQNWB diff --git a/Source/aqnwb/Utils.hpp b/Source/aqnwb/Utils.hpp deleted file mode 100644 index 5c75f98..0000000 --- a/Source/aqnwb/Utils.hpp +++ /dev/null @@ -1,128 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "BaseIO.hpp" -#include "boost/date_time/c_local_time_adjustor.hpp" -#include "hdf5/HDF5IO.hpp" - -namespace AQNWB -{ -/** - * @brief Generates a UUID (Universally Unique Identifier) as a string. - * @return The generated UUID as a string. - */ -inline std::string generateUuid() -{ - boost::uuids::uuid uuid = boost::uuids::random_generator()(); - std::string uuidStr = boost::uuids::to_string(uuid); - - return uuidStr; -} - -/** - * @brief Get the current time in ISO 8601 format with the UTC offset. - * @return The current time as a string in ISO 8601 format. - */ -inline std::string getCurrentTime() -{ - // Set up boost time zone adjustment and time facet - using local_adj = - boost::date_time::c_local_adjustor; - boost::posix_time::time_facet* f = new boost::posix_time::time_facet(); - f->time_duration_format("%+%H:%M"); - - // get local time, utc time, and offset - auto now = boost::posix_time::microsec_clock::universal_time(); - auto utc_now = local_adj::utc_to_local(now); - boost::posix_time::time_duration td = utc_now - now; - - // Format the date and time in ISO 8601 format with the UTC offset - std::ostringstream oss_offset; - oss_offset.imbue(std::locale(oss_offset.getloc(), f)); - oss_offset << td; - - std::string currentTime = to_iso_extended_string(utc_now); - currentTime += oss_offset.str(); - - return currentTime; -} - -/** - * @brief Factory method to create an IO object. - * @return A pointer to a BaseIO object - */ -inline std::shared_ptr createIO(const std::string& type, - const std::string& filename) -{ - if (type == "HDF5") { - return std::make_shared(filename); - } else { - throw std::invalid_argument("Invalid IO 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(0x7fff); - auto intData = static_cast(dest); - - for (int i = 0; i < numSamples; ++i) { - auto clampedValue = std::clamp(maxVal * source[i], -maxVal, maxVal); - auto intValue = - static_cast(static_cast(std::round(clampedValue))); - intValue = boost::endian::native_to_little(intValue); - *reinterpret_cast(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 transformToInt16(SizeType numSamples, - float conversion_factor, - const float* data) -{ - std::unique_ptr scaledData = std::make_unique(numSamples); - std::unique_ptr intData = std::make_unique(numSamples); - - // copy data and multiply by scaling factor - double multFactor = 1 / (32767.0f * conversion_factor); - std::transform(data, - data + numSamples, - scaledData.get(), - [multFactor](float value) { return value * multFactor; }); - - // convert float to int16 - convertFloatToInt16LE(scaledData.get(), intData.get(), numSamples); - - return intData; -} -} // namespace AQNWB diff --git a/Source/aqnwb/hdf5/HDF5IO.cpp b/Source/aqnwb/hdf5/HDF5IO.cpp deleted file mode 100644 index 58fe39e..0000000 --- a/Source/aqnwb/hdf5/HDF5IO.cpp +++ /dev/null @@ -1,738 +0,0 @@ -#include -#include -#include -#include -#include - -#include "hdf5/HDF5IO.hpp" - -#include -#include - -#include "Utils.hpp" - -using namespace H5; -using namespace AQNWB::HDF5; - -// HDF5IO - -HDF5IO::HDF5IO() {} - -HDF5IO::HDF5IO(const std::string& fileName, const bool disableSWMRMode) - : filename(fileName) - , disableSWMRMode(disableSWMRMode) -{ -} - -HDF5IO::~HDF5IO() -{ - close(); -} - -std::string HDF5IO::getFileName() -{ - return filename; -} - -Status HDF5IO::open() -{ - if (std::filesystem::exists(getFileName())) { - return open(false); - } else { - return open(true); - } -} - -Status HDF5IO::open(bool newfile) -{ - int accFlags = 0; - - if (opened) - return Status::Failure; - - FileAccPropList fapl = FileAccPropList::DEFAULT; - H5Pset_libver_bounds(fapl.getId(), H5F_LIBVER_LATEST, H5F_LIBVER_LATEST); - - if (newfile) - accFlags = H5F_ACC_TRUNC; - else - accFlags = H5F_ACC_RDWR; - - file = std::make_unique( - getFileName(), accFlags, FileCreatPropList::DEFAULT, fapl); - opened = true; - - return Status::Success; -} - -Status HDF5IO::close() -{ - if (this->file != nullptr && opened) { - this->file->close(); - this->file = nullptr; - this->opened = false; - } - - return Status::Success; -} - -Status checkStatus(int status) -{ - if (status < 0) - return Status::Failure; - else - return Status::Success; -} - -Status HDF5IO::flush() -{ - int status = H5Fflush(this->file->getId(), H5F_SCOPE_GLOBAL); - return checkStatus(status); -} - -Status HDF5IO::createAttribute(const BaseDataType& type, - const void* data, - const std::string& path, - const std::string& name, - const SizeType& size) -{ - H5Object* loc; - Group gloc; - DataSet dloc; - Attribute attr; - DataType H5type; - DataType origType; - - if (!opened) - return Status::Failure; - - // open the group or dataset - H5O_type_t objectType = getObjectType(path); - switch (objectType) { - case H5O_TYPE_GROUP: - gloc = file->openGroup(path); - loc = &gloc; - break; - case H5O_TYPE_DATASET: - dloc = file->openDataSet(path); - loc = &dloc; - break; - default: - return Status::Failure; // not a valid dataset or group type - } - - H5type = getH5Type(type); - origType = getNativeType(type); - - if (size > 1) { - hsize_t dims = static_cast(size); - H5type = ArrayType(H5type, 1, &dims); - origType = ArrayType(origType, 1, &dims); - } - - if (loc->attrExists(name)) { - attr = loc->openAttribute(name); - } else { - DataSpace attr_dataspace(H5S_SCALAR); - attr = loc->createAttribute(name, H5type, attr_dataspace); - } - - attr.write(origType, data); - - return Status::Success; -} - -Status HDF5IO::createAttribute(const std::string& data, - const std::string& path, - const std::string& name) -{ - std::vector dataPtrs; - dataPtrs.push_back(data.c_str()); - - return createAttribute(dataPtrs, path, name, data.length()); -} - -Status HDF5IO::createAttribute(const std::vector& data, - const std::string& path, - const std::string& name) -{ - std::vector dataPtrs; - SizeType maxLength = 0; - for (const std::string& str : data) { - SizeType length = str.length(); - maxLength = std::max(maxLength, length); - dataPtrs.push_back(str.c_str()); - } - - return createAttribute(dataPtrs, path, name, maxLength + 1); -} - -Status HDF5IO::createAttribute(const std::vector& data, - const std::string& path, - const std::string& name, - const SizeType& maxSize) -{ - H5Object* loc; - Group gloc; - DataSet dloc; - Attribute attr; - hsize_t dims[1]; - - if (!opened) - return Status::Failure; - - StrType H5type(PredType::C_S1, maxSize); - H5type.setSize(H5T_VARIABLE); - - // open the group or dataset - H5O_type_t objectType = getObjectType(path); - switch (objectType) { - case H5O_TYPE_GROUP: - gloc = file->openGroup(path); - loc = &gloc; - break; - case H5O_TYPE_DATASET: - dloc = file->openDataSet(path); - loc = &dloc; - break; - default: - return Status::Failure; // not a valid dataset or group type - } - - try { - if (loc->attrExists(name)) { - return Status::Failure; // don't allow overwriting because string - // attributes cannot change size easily - } else { - DataSpace attr_dataspace; - SizeType nStrings = data.size(); - if (nStrings > 1) { - dims[0] = nStrings; - attr_dataspace = DataSpace(1, dims); - } else - attr_dataspace = DataSpace(H5S_SCALAR); - attr = loc->createAttribute(name, H5type, attr_dataspace); - } - attr.write(H5type, data.data()); - } catch (GroupIException error) { - error.printErrorStack(); - } catch (AttributeIException error) { - error.printErrorStack(); - } catch (FileIException error) { - error.printErrorStack(); - } catch (DataSetIException error) { - error.printErrorStack(); - } - return Status::Success; -} - -Status HDF5IO::createReferenceAttribute(const std::string& referencePath, - const std::string& path, - const std::string& name) -{ - H5Object* loc; - Group gloc; - DataSet dloc; - Attribute attr; - - if (!opened) - return Status::Failure; - - // open the group or dataset - H5O_type_t objectType = getObjectType(path); - switch (objectType) { - case H5O_TYPE_GROUP: - gloc = file->openGroup(path); - loc = &gloc; - break; - case H5O_TYPE_DATASET: - dloc = file->openDataSet(path); - loc = &dloc; - break; - default: - return Status::Failure; // not a valid dataset or group type - } - - try { - if (loc->attrExists(name)) { - attr = loc->openAttribute(name); - } else { - DataSpace attr_space(H5S_SCALAR); - attr = loc->createAttribute(name, H5::PredType::STD_REF_OBJ, attr_space); - } - - hobj_ref_t* rdata = new hobj_ref_t[sizeof(hobj_ref_t)]; - - file->reference(rdata, referencePath.c_str()); - - attr.write(H5::PredType::STD_REF_OBJ, rdata); - delete[] rdata; - - } catch (GroupIException error) { - error.printErrorStack(); - } catch (AttributeIException error) { - error.printErrorStack(); - } catch (FileIException error) { - error.printErrorStack(); - } catch (DataSetIException error) { - error.printErrorStack(); - } - - return Status::Success; -} - -Status HDF5IO::createGroup(const std::string& path) -{ - if (!opened) - return Status::Failure; - try { - file->createGroup(path); - } catch (FileIException error) { - error.printErrorStack(); - } catch (GroupIException error) { - error.printErrorStack(); - } - return Status::Success; -} - -Status HDF5IO::createGroupIfDoesNotExist(const std::string& path) -{ - if (!opened) - return Status::Failure; - try { - file->childObjType(path); - } catch (FileIException) { - return createGroup(path); - } - return Status::Success; -} - -/** Creates a link to another location in the file */ -Status HDF5IO::createLink(const std::string& path, const std::string& reference) -{ - if (!opened) - return Status::Failure; - - herr_t error = H5Lcreate_soft(reference.c_str(), - file->getLocId(), - path.c_str(), - H5P_DEFAULT, - H5P_DEFAULT); - - return checkStatus(error); -} - -Status HDF5IO::createReferenceDataSet( - const std::string& path, const std::vector& references) -{ - if (!opened) - return Status::Failure; - - const hsize_t size = references.size(); - - hobj_ref_t* rdata = new hobj_ref_t[size * sizeof(hobj_ref_t)]; - - for (SizeType i = 0; i < size; i++) { - file->reference(&rdata[i], references[i].c_str()); - } - - hid_t space = H5Screate_simple(1, &size, NULL); - - hid_t dset = H5Dcreate(file->getLocId(), - path.c_str(), - H5T_STD_REF_OBJ, - space, - H5P_DEFAULT, - H5P_DEFAULT, - H5P_DEFAULT); - - herr_t writeStatus = - H5Dwrite(dset, H5T_STD_REF_OBJ, H5S_ALL, H5S_ALL, H5P_DEFAULT, rdata); - - delete[] rdata; - - herr_t dsetStatus = H5Dclose(dset); - herr_t spaceStatus = H5Sclose(space); - - return checkStatus(writeStatus); -} - -Status HDF5IO::createStringDataSet(const std::string& path, - const std::string& value) -{ - if (!opened) - return Status::Failure; - - std::unique_ptr dataset; - DataType H5type = getH5Type(BaseDataType::STR(value.length())); - DataSpace dSpace(H5S_SCALAR); - - dataset = - std::make_unique(file->createDataSet(path, H5type, dSpace)); - dataset->write(value.c_str(), H5type); - - return Status::Success; -} - -Status HDF5IO::createStringDataSet(const std::string& path, - const std::vector& values) -{ - if (!opened) - return Status::Failure; - - std::vector cStrs; - cStrs.reserve(values.size()); - for (const auto& str : values) { - cStrs.push_back(str.c_str()); - } - - std::unique_ptr dataset; - dataset = std::unique_ptr(createArrayDataSet( - BaseDataType::V_STR, SizeArray {values.size()}, SizeArray {1}, path)); - dataset->writeDataBlock( - std::vector(1, 1), BaseDataType::V_STR, cStrs.data()); - - return Status::Success; -} - -Status HDF5IO::startRecording() -{ - if (!opened) - return Status::Failure; - - if (!disableSWMRMode) { - herr_t status = H5Fstart_swmr_write(this->file->getId()); - return checkStatus(status); - } - return Status::Success; -} - -Status HDF5IO::stopRecording() -{ - // if SWMR mode is disabled, stopping the recording will leave the file open - if (!disableSWMRMode) { - close(); // SWMR mode cannot be disabled so close the file - } else { - this->flush(); - } - return Status::Success; -} - -bool HDF5IO::canModifyObjects() -{ - if (!opened) - return false; - - // Check if we are in SWMR mode - bool inSWMRMode = false; - unsigned int intent; - herr_t status = H5Fget_intent(this->file->getId(), &intent); - bool statusOK = (status >= 0); - if (statusOK) { - inSWMRMode = (intent & (H5F_ACC_SWMR_READ | H5F_ACC_SWMR_WRITE)); - } - - // if the file is opened and we are not in swmr mode then we can modify - // objects - 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 HDF5IO::getDataSet( - const std::string& path) -{ - std::unique_ptr data; - - if (!opened) - return nullptr; - - try { - data = std::make_unique(file->openDataSet(path)); - return std::make_unique(std::move(data)); - } catch (DataSetIException error) { - error.printErrorStack(); - return nullptr; - } catch (FileIException error) { - error.printErrorStack(); - return nullptr; - } catch (DataSpaceIException error) { - error.printErrorStack(); - return nullptr; - } -} - -std::unique_ptr HDF5IO::createArrayDataSet( - const BaseDataType& type, - const SizeArray& size, - const SizeArray& chunking, - const std::string& path) -{ - std::unique_ptr data; - DSetCreatPropList prop; - DataType H5type = getH5Type(type); - - if (!opened) - return nullptr; - - SizeType dimension = size.size(); - if (dimension < 1) // Check for at least one dimension - return nullptr; - - // Ensure chunking is properly allocated and has at least 'dimension' elements - assert(chunking.size() >= dimension); - - // Use vectors to support an arbitrary number of dimensions - std::vector dims(dimension), chunk_dims(dimension), - max_dims(dimension); - - for (SizeType i = 0; i < dimension; i++) { - dims[i] = static_cast(size[i]); - if (chunking[i] > 0) { - chunk_dims[i] = static_cast(chunking[i]); - max_dims[i] = H5S_UNLIMITED; - } else { - chunk_dims[i] = static_cast(size[i]); - max_dims[i] = static_cast(size[i]); - } - } - - DataSpace dSpace(static_cast(dimension), dims.data(), max_dims.data()); - prop.setChunk(static_cast(dimension), chunk_dims.data()); - - data = std::make_unique( - file->createDataSet(path, H5type, dSpace, prop)); - - return std::make_unique(std::move(data)); -} - -H5O_type_t HDF5IO::getObjectType(const std::string& path) -{ -#if H5_VERSION_GE(1, 12, 0) - // get whether path is a dataset or group - H5O_info_t objInfo; // Structure to hold information about the object - H5Oget_info_by_name( - this->file->getId(), path.c_str(), &objInfo, H5O_INFO_BASIC, H5P_DEFAULT); -#else - // get whether path is a dataset or group - H5O_info_t objInfo; // Structure to hold information about the object - H5Oget_info_by_name(this->file->getId(), path.c_str(), &objInfo, H5P_DEFAULT); -#endif - H5O_type_t objectType = objInfo.type; - - return objectType; -} - -H5::DataType HDF5IO::getNativeType(BaseDataType type) -{ - H5::DataType baseType; - - switch (type.type) { - case BaseDataType::Type::T_I8: - baseType = PredType::NATIVE_INT8; - break; - case BaseDataType::Type::T_I16: - baseType = PredType::NATIVE_INT16; - break; - case BaseDataType::Type::T_I32: - baseType = PredType::NATIVE_INT32; - break; - case BaseDataType::Type::T_I64: - baseType = PredType::NATIVE_INT64; - break; - case BaseDataType::Type::T_U8: - baseType = PredType::NATIVE_UINT8; - break; - case BaseDataType::Type::T_U16: - baseType = PredType::NATIVE_UINT16; - break; - case BaseDataType::Type::T_U32: - baseType = PredType::NATIVE_UINT32; - break; - case BaseDataType::Type::T_U64: - baseType = PredType::NATIVE_UINT64; - break; - case BaseDataType::Type::T_F32: - baseType = PredType::NATIVE_FLOAT; - break; - case BaseDataType::Type::T_F64: - baseType = PredType::NATIVE_DOUBLE; - break; - case BaseDataType::Type::T_STR: - return StrType(PredType::C_S1, type.typeSize); - break; - case BaseDataType::Type::V_STR: - return StrType(PredType::C_S1, H5T_VARIABLE); - break; - default: - baseType = PredType::NATIVE_INT32; - } - if (type.typeSize > 1) { - hsize_t size = type.typeSize; - return ArrayType(baseType, 1, &size); - } else - return baseType; -} - -H5::DataType HDF5IO::getH5Type(BaseDataType type) -{ - H5::DataType baseType; - - switch (type.type) { - case BaseDataType::Type::T_I8: - baseType = PredType::STD_I8LE; - break; - case BaseDataType::Type::T_I16: - baseType = PredType::STD_I16LE; - break; - case BaseDataType::Type::T_I32: - baseType = PredType::STD_I32LE; - break; - case BaseDataType::Type::T_I64: - baseType = PredType::STD_I64LE; - break; - case BaseDataType::Type::T_U8: - baseType = PredType::STD_U8LE; - break; - case BaseDataType::Type::T_U16: - baseType = PredType::STD_U16LE; - break; - case BaseDataType::Type::T_U32: - baseType = PredType::STD_U32LE; - break; - case BaseDataType::Type::T_U64: - baseType = PredType::STD_U64LE; - break; - case BaseDataType::Type::T_F32: - return PredType::IEEE_F32LE; - break; - case BaseDataType::Type::T_F64: - baseType = PredType::IEEE_F64LE; - break; - case BaseDataType::Type::T_STR: - return StrType(PredType::C_S1, type.typeSize); - break; - case BaseDataType::Type::V_STR: - return StrType(PredType::C_S1, H5T_VARIABLE); - break; - default: - return PredType::STD_I32LE; - } - if (type.typeSize > 1) { - hsize_t size = type.typeSize; - return ArrayType(baseType, 1, &size); - } else - return baseType; -} - -// HDF5RecordingData -HDF5RecordingData::HDF5RecordingData(std::unique_ptr data) -{ - DataSpace dSpace = data->getSpace(); - DSetCreatPropList prop = data->getCreatePlist(); - - int nDimensions = dSpace.getSimpleExtentNdims(); - std::vector dims(nDimensions), chunk(nDimensions); - - nDimensions = dSpace.getSimpleExtentDims( - dims.data()); // TODO -redefine here or use original? - prop.getChunk(static_cast(nDimensions), chunk.data()); - - this->size = std::vector(nDimensions); - for (int i = 0; i < nDimensions; ++i) { - this->size[i] = dims[i]; - } - this->nDimensions = nDimensions; - this->position = std::vector( - nDimensions, 0); // Initialize position with 0 for each dimension - this->dSet = std::make_unique(*data); -} - -// HDF5RecordingData - -HDF5RecordingData::~HDF5RecordingData() -{ - // Safety - dSet->flush(H5F_SCOPE_GLOBAL); -} - -Status HDF5RecordingData::writeDataBlock( - const std::vector& dataShape, - const std::vector& positionOffset, - const BaseDataType& type, - const void* data) -{ - try { - // check dataShape and positionOffset inputs match the dimensions of the - // dataset - if (dataShape.size() != nDimensions || positionOffset.size() != nDimensions) - { - return Status::Failure; - } - - // Ensure that we have enough space to accommodate new data - std::vector dSetDims(nDimensions), offset(nDimensions); - for (int i = 0; i < nDimensions; ++i) { - offset[i] = static_cast(positionOffset[i]); - - if (dataShape[i] + offset[i] > size[i]) // TODO - do I need offset here - dSetDims[i] = dataShape[i] + offset[i]; - else - dSetDims[i] = size[i]; - } - - // Adjust dataset dimensions if necessary - dSet->extend(dSetDims.data()); - - // Set size to new size based on updated dimensionality - DataSpace fSpace = dSet->getSpace(); - fSpace.getSimpleExtentDims(dSetDims.data()); - for (int i = 0; i < nDimensions; ++i) { - size[i] = dSetDims[i]; - } - - // Create memory space with the shape of the data - // DataSpace mSpace(dimension, dSetDim.data()); - std::vector dataDims(nDimensions); - for (int i = 0; i < nDimensions; ++i) { - if (dataShape[i] == 0) { - dataDims[i] = 1; - } else { - dataDims[i] = static_cast(dataShape[i]); - } - } - DataSpace mSpace(static_cast(nDimensions), dataDims.data()); - - // Select hyperslab in the file space - fSpace.selectHyperslab(H5S_SELECT_SET, dataDims.data(), offset.data()); - - // Write the data - DataType nativeType = HDF5IO::getNativeType(type); - dSet->write(data, nativeType, mSpace, fSpace); - - // Update position for simple extension - for (int i = 0; i < dataShape.size(); ++i) { - position[i] += dataShape[i]; - } - } catch (DataSetIException error) { - error.printErrorStack(); - } catch (DataSpaceIException error) { - error.printErrorStack(); - } catch (FileIException error) { - error.printErrorStack(); - } - return Status::Success; -} - -const H5::DataSet* HDF5RecordingData::getDataSet() -{ - return dSet.get(); -}; diff --git a/Source/aqnwb/hdf5/HDF5IO.hpp b/Source/aqnwb/hdf5/HDF5IO.hpp deleted file mode 100644 index 8d2480d..0000000 --- a/Source/aqnwb/hdf5/HDF5IO.hpp +++ /dev/null @@ -1,343 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -#include "BaseIO.hpp" -#include "Types.hpp" - -namespace H5 -{ -class DataSet; -class H5File; -class DataType; -class Exception; -} // namespace H5 - -/*! - * \namespace AQNWB::HDF5 - * \brief Namespace for all components of the HDF5 I/O backend - */ -namespace AQNWB::HDF5 -{ -class HDF5RecordingData; // declare here because gets used in HDF5IO class - -/** - * @brief The HDF5IO class provides an interface for reading and writing data to - * HDF5 files. - */ -class HDF5IO : public BaseIO -{ -public: - /** - * @brief Default constructor for the HDF5IO class. - */ - HDF5IO(); - - /** - * @brief Constructor for the HDF5IO class that takes a file name as input. - * @param fileName The name of the HDF5 file. - * @param disableSWMRMode Disable recording of data in Single Writer - * Multiple Reader (SWMR) mode. Using SWMR ensures that the - * HDF5 file remains valid and readable at all times during - * the recording process (but does not allow for new objects - * (Groups or Datasets) to be created. - */ - HDF5IO(const std::string& fileName, const bool disableSWMRMode = false); - - /** - * @brief Destructor for the HDF5IO class. - */ - ~HDF5IO(); - - /** - * @brief Returns the full path to the HDF5 file. - * @return The full path to the HDF5 file. - */ - std::string getFileName() override; - - /** - * @brief Opens an existing file or creates a new file for writing. - * @return The status of the file opening operation. - */ - Status open() override; - - /** - * @brief Opens an existing file or creates a new file for writing. - * @param newfile Flag indicating whether to create a new file. - * @return The status of the file opening operation. - */ - Status open(bool newfile) override; - - /** - * @brief Closes the file. - * @return The status of the file closing operation. - */ - Status close() override; - - /** - * @brief Flush data to disk - * @return The status of the flush operation. - */ - Status flush() override; - - /** - * @brief Creates an attribute at a given location in the file. - * @param type The base data type of the attribute. - * @param data Pointer to the attribute data. - * @param path The location in the file to set the attribute. - * @param name The name of the attribute. - * @param size The size of the attribute (default is 1). - * @return The status of the attribute creation operation. - */ - Status createAttribute(const BaseDataType& type, - const void* data, - const std::string& path, - const std::string& name, - const SizeType& size = 1) override; - - /** - * @brief Creates a string attribute at a given location in the file. - * @param data The string attribute data. - * @param path The location in the file to set the attribute. - * @param name The name of the attribute. - * @return The status of the attribute creation operation. - */ - Status createAttribute(const std::string& data, - const std::string& path, - const std::string& name) override; - - /** - * @brief Creates a string array attribute at a given location in the file. - * @param data The string array attribute data. - * @param path The location in the file to set the attribute. - * @param name The name of the attribute. - * @return The status of the attribute creation operation. - */ - Status createAttribute(const std::vector& data, - const std::string& path, - const std::string& name) override; - - /** - * @brief Creates a string array attribute at a given location in the file. - * @param data The string array attribute data. - * @param path The location in the file to set the attribute. - * @param name The name of the attribute. - * @param maxSize The maximum size of the string. - * @return The status of the attribute creation operation. - */ - Status createAttribute(const std::vector& data, - const std::string& path, - const std::string& name, - const SizeType& maxSize) override; - - /** - * @brief Sets an object reference attribute for a given location in the file. - * @param referencePath The full path to the referenced group / dataset. - * @param path The location in the file to set the attribute. - * @param name The name of the attribute. - * @return The status of the attribute creation operation. - */ - Status createReferenceAttribute(const std::string& referencePath, - const std::string& path, - const std::string& name) override; - - /** - * @brief Creates a new group in the file. - * @param path The location in the file of the new group. - * @return The status of the group creation operation. - */ - Status createGroup(const std::string& path) override; - - /** - * @brief Creates a soft link to another location in the file. - * @param path The location in the file to the new link. - * @param reference The location in the file of the object that is being - * linked to. - * @return The status of the link creation operation. - */ - Status createLink(const std::string& path, - const std::string& reference) override; - - /** - * @brief Creates a non-modifiable dataset with a string value. - * @param path The location in the file of the dataset. - * @param value The string value of the dataset. - * @return The status of the dataset creation operation. - */ - Status createStringDataSet(const std::string& path, - const std::string& value) override; - - /** - * @brief Creates a dataset that holds an array of string values. - * @param path The location in the file of the dataset. - * @param values The vector of string values of the dataset. - * @return The status of the dataset creation operation. - */ - Status createStringDataSet(const std::string& path, - const std::vector& values) override; - - /** - * @brief Creates a dataset that holds an array of references to groups within - * the file. - * @param path The location in the file of the new dataset. - * @param references The array of references. - * @return The status of the dataset creation operation. - */ - Status createReferenceDataSet( - const std::string& path, - const std::vector& references) override; - - /** - * @brief Start SWMR write to start recording process - * @return The status of the start recording operation. - */ - Status startRecording() override; - - /** - * @brief Stops the recording process. - * @return The status of the stop recording operation. - */ - Status stopRecording() override; - - /** - * @brief Checks whether the file is in a mode where objects - * can be added or deleted. Note, this does not apply to the modification - * of raw data on already existing objects. - * @return Whether objects can be modified. - */ - bool canModifyObjects() override; - - /** - * @brief Creates an extendable dataset with a given base data type, size, - * chunking, and path. - * @param type The base data type of the dataset. - * @param size The size of the dataset. - * @param chunking The chunking size of the dataset. - * @param path The location in the file of the new dataset. - * @return A pointer to the created dataset. - */ - std::unique_ptr createArrayDataSet( - const BaseDataType& type, - const SizeArray& size, - const SizeArray& chunking, - const std::string& path) override; - - /** - * @brief Returns a pointer to a dataset at a given path. - * @param path The location in the file of the dataset. - * @return A pointer to the dataset. - */ - std::unique_ptr 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. - * @return The type of object at the given path. - */ - H5O_type_t getObjectType(const std::string& path); - - /** - * @brief Returns the HDF5 native data type for a given base data type. - * @param type The base data type. - * @return The HDF5 native data type. - */ - static H5::DataType getNativeType(BaseDataType type); - - /** - * @brief Returns the HDF5 data type for a given base data type. - * @param type The base data type. - * @return The HDF5 data type. - */ - static H5::DataType getH5Type(BaseDataType type); - -protected: - std::string filename; - - /** - * @brief Creates a new group if it does not exist. - * @param path The location in the file of the group. - * @return The status of the group creation operation. - */ - Status createGroupIfDoesNotExist(const std::string& path) override; - -private: - std::unique_ptr file; - bool disableSWMRMode; // when set do not use SWMR mode when opening the HDF5 - // file -}; - -/** - * @brief Represents an HDF5 Dataset that can be extended indefinitely - in blocks. -* -* This class provides functionality for reading and writing blocks of data -* to an HDF5 dataset. -*/ -class HDF5RecordingData : public BaseRecordingData -{ -public: - /** - * @brief Constructs an HDF5RecordingData object. - * @param data A pointer to the HDF5 dataset. - */ - HDF5RecordingData(std::unique_ptr data); - - /** - * @brief Deleted copy constructor to prevent construction-copying. - */ - HDF5RecordingData(const HDF5RecordingData&) = delete; - - /** - * @brief Deleted copy assignment operator to prevent copying. - */ - HDF5RecordingData& operator=(const HDF5RecordingData&) = delete; - - /** - * @brief Destroys the HDF5RecordingData object. - */ - ~HDF5RecordingData(); - - /** - * @brief Writes a block of data to the HDF5 dataset. - * @param dataShape The size of the data block. - * @param positionOffset The position of the data block to write to. - * @param type The data type of the elements in the data block. - * @param data A pointer to the data block. - * @return The status of the write operation. - */ - Status writeDataBlock(const std::vector& dataShape, - const std::vector& positionOffset, - const BaseDataType& type, - const void* data); - - /** - * @brief Gets a const pointer to the HDF5 dataset. - * @return A const pointer to the HDF5 dataset. - */ - const H5::DataSet* getDataSet(); - -private: - /** - * @brief Pointer to an extendable HDF5 dataset - */ - std::unique_ptr dSet; - - /** - * @brief Return status of HDF5 operations. - */ - Status checkStatus(int status); -}; -} // namespace AQNWB::HDF5 diff --git a/Source/aqnwb/nwb/NWBFile.cpp b/Source/aqnwb/nwb/NWBFile.cpp deleted file mode 100644 index 3ebda2b..0000000 --- a/Source/aqnwb/nwb/NWBFile.cpp +++ /dev/null @@ -1,286 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -#include "nwb/NWBFile.hpp" - -#include "BaseIO.hpp" -#include "Channel.hpp" -#include "Utils.hpp" -#include "nwb/device/Device.hpp" -#include "nwb/ecephys/ElectricalSeries.hpp" -#include "nwb/ecephys/SpikeEventSeries.hpp" -#include "nwb/file/ElectrodeGroup.hpp" -#include "spec/core.hpp" -#include "spec/hdmf_common.hpp" -#include "spec/hdmf_experimental.hpp" - -using namespace AQNWB::NWB; - -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 NWBFile::emptyContainerIndexes = {}; - -// NWBFile - -NWBFile::NWBFile(const std::string& idText, std::shared_ptr io) - : identifierText(idText) - , io(io) -{ -} - -NWBFile::~NWBFile() {} - -Status NWBFile::initialize(const std::string description, - const std::string dataCollection) -{ - if (std::filesystem::exists(io->getFileName())) { - return io->open(false); - } else { - io->open(true); - return createFileStructure(description, dataCollection); - } -} - -Status NWBFile::finalize() -{ - return io->close(); -} - -Status NWBFile::createFileStructure(std::string description, - std::string dataCollection) -{ - 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"); - io->createGroup("/stimulus"); - io->createGroup("/stimulus/presentation"); - io->createGroup("/stimulus/templates"); - 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"); - - cacheSpecifications( - "core", AQNWB::SPEC::CORE::version, AQNWB::SPEC::CORE::specVariables); - cacheSpecifications("hdmf-common", - AQNWB::SPEC::HDMF_COMMON::version, - AQNWB::SPEC::HDMF_COMMON::specVariables); - cacheSpecifications("hdmf-experimental", - AQNWB::SPEC::HDMF_EXPERIMENTAL::version, - AQNWB::SPEC::HDMF_EXPERIMENTAL::specVariables); - - std::string time = getCurrentTime(); - std::vector timeVec = {time}; - io->createStringDataSet("/file_create_date", timeVec); - io->createStringDataSet("/session_description", description); - io->createStringDataSet("/session_start_time", time); - io->createStringDataSet("/timestamps_reference_time", time); - io->createStringDataSet("/identifier", identifierText); - - return Status::Success; -} - -Status NWBFile::createElectricalSeries( - std::vector recordingArrays, - std::vector recordingNames, - const BaseDataType& dataType, - RecordingContainers* recordingContainers, - std::vector& 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(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 electricalSeriesPath = 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 electrical series datasets - auto electricalSeries = std::make_unique( - electricalSeriesPath, - io, - dataType, - channelVector, - "Stores continuously sampled voltage data from an " - "extracellular ephys recording", - SizeArray {0, channelVector.size()}, - SizeArray {CHUNK_XSIZE, 0}); - electricalSeries->initialize(); - recordingContainers->addContainer(std::move(electricalSeries)); - containerIndexes.push_back(recordingContainers->containers.size() - 1); - } - - // write electrode information to datasets - // (requires that the ElectrodeGroup have been written) - if (!electrodeTableCreated) { - elecTable->finalize(); - } - - return Status::Success; -} - -Status NWBFile::createSpikeEventSeries( - std::vector recordingArrays, - std::vector recordingNames, - const BaseDataType& dataType, - RecordingContainers* recordingContainers, - std::vector& 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(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( - 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 - // (requires that the ElectrodeGroup have been written) - if (!electrodeTableCreated) { - elecTable->finalize(); - } - - return Status::Success; -} - -template -void NWBFile::cacheSpecifications( - const std::string& specPath, - const std::string& versionNumber, - const std::array, N>& - specVariables) -{ - io->createGroup("/specifications/" + specPath); - io->createGroup("/specifications/" + specPath + "/" + versionNumber); - - for (const auto& [name, content] : specVariables) { - io->createStringDataSet("/specifications/" + specPath + "/" + versionNumber - + "/" + std::string(name), - std::string(content)); - } -} - -// recording data factory method / -std::unique_ptr NWBFile::createRecordingData( - BaseDataType type, - const SizeArray& size, - const SizeArray& chunking, - const std::string& path) -{ - return std::unique_ptr( - io->createArrayDataSet(type, size, chunking, path)); -} diff --git a/Source/aqnwb/nwb/NWBFile.hpp b/Source/aqnwb/nwb/NWBFile.hpp deleted file mode 100644 index 24b446c..0000000 --- a/Source/aqnwb/nwb/NWBFile.hpp +++ /dev/null @@ -1,160 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "BaseIO.hpp" -#include "Types.hpp" -#include "nwb/RecordingContainers.hpp" -#include "nwb/base/TimeSeries.hpp" -#include "nwb/file/ElectrodeTable.hpp" - -/*! - * \namespace AQNWB::NWB - * \brief Namespace for all classes related to the NWB data standard - */ -namespace AQNWB::NWB -{ - -/** - * @brief The NWBFile class provides an interface for setting up and managing - * the NWB file. - */ -class NWBFile -{ -public: - /** - * @brief Constructor for NWBFile class. - * @param idText The identifier text for the NWBFile. - * @param io The shared pointer to the IO object. - */ - NWBFile(const std::string& idText, std::shared_ptr io); - - /** - * @brief Deleted copy constructor to prevent construction-copying. - */ - NWBFile(const NWBFile&) = delete; - - /** - * @brief Deleted copy assignment operator to prevent copying. - */ - NWBFile& operator=(const NWBFile&) = delete; - - /** - * @brief Destructor for NWBFile class. - */ - ~NWBFile(); - - /** - * @brief Initializes the NWB file by opening and setting up the file - * structure. - * @param description A description of the NWBFile session. - * @param dataCollection Information about the data collection methods. - */ - Status initialize(const std::string description = "a recording session", - const std::string dataCollection = ""); - - /** - * @brief Finalizes the NWB file by closing it. - */ - Status finalize(); - - /** - * @brief Create ElectricalSeries objects to record data into. - * Created objects are stored in recordingContainers. - * Note, this function will fail if the file is in a mode where - * new objects cannot be added, which can be checked via - * nwbfile.io->canModifyObjects() - * @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 - * @return Status The status of the object creation operation. - */ - Status createElectricalSeries( - std::vector recordingArrays, - std::vector recordingNames, - const BaseDataType& dataType = BaseDataType::I16, - RecordingContainers* recordingContainers = nullptr, - std::vector& 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 recordingArrays, - std::vector recordingNames, - const BaseDataType& dataType = BaseDataType::I16, - RecordingContainers* recordingContainers = nullptr, - std::vector& containerIndexes = emptyContainerIndexes); - -protected: - /** - * @brief Creates the default file structure. - * Note, this function will fail if the file is in a mode where - * new objects cannot be added, which can be checked via - * nwbfile.io->canModifyObjects() - * @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(std::string description, - std::string dataCollection); - -private: - /** - * @brief Factory method for creating recording data. - * @param type The base data type. - * @param size The size of the dataset. - * @param chunking The chunking size of the dataset. - * @param path The location in the file of the new dataset. - * @return std::unique_ptr The unique pointer to the - * created recording data. - */ - std::unique_ptr createRecordingData( - BaseDataType type, - const SizeArray& size, - const SizeArray& chunking, - const std::string& path); - - /** - * @brief Saves the specification files for the schema. - * @param specPath The location in the file to store the spec information. - * @param versionNumber The version number of the specification files. - * @param specVariables The contents of the specification files. - * These values are generated from the nwb schema by - * `resources/generate_spec_files.py` - */ - template - void cacheSpecifications( - const std::string& specPath, - const std::string& versionNumber, - const std::array, N>& - specVariables); - - std::unique_ptr elecTable; - const std::string identifierText; - std::shared_ptr io; - static std::vector emptyContainerIndexes; - inline const static std::string acquisitionPath = "/acquisition"; -}; - -} // namespace AQNWB::NWB \ No newline at end of file diff --git a/Source/aqnwb/nwb/RecordingContainers.cpp b/Source/aqnwb/nwb/RecordingContainers.cpp deleted file mode 100644 index 4658d55..0000000 --- a/Source/aqnwb/nwb/RecordingContainers.cpp +++ /dev/null @@ -1,81 +0,0 @@ - -#include "nwb/RecordingContainers.hpp" - -#include "nwb/ecephys/ElectricalSeries.hpp" -#include "nwb/ecephys/SpikeEventSeries.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); -} - -Status RecordingContainers::writeSpikeEventData(const SizeType& containerInd, - const SizeType& numSamples, - const SizeType& numChannels, - const void* data, - const void* timestamps) -{ - SpikeEventSeries* ses = - dynamic_cast(getContainer(containerInd)); - - if (ses == nullptr) - return Status::Failure; - - ses->writeSpike(numSamples, numChannels, data, timestamps); -} diff --git a/Source/aqnwb/nwb/RecordingContainers.hpp b/Source/aqnwb/nwb/RecordingContainers.hpp deleted file mode 100644 index f38d84c..0000000 --- a/Source/aqnwb/nwb/RecordingContainers.hpp +++ /dev/null @@ -1,112 +0,0 @@ -#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 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. - * @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); - - /** - * @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> containers; - std::string name; -}; - -} // namespace AQNWB::NWB diff --git a/Source/aqnwb/nwb/base/TimeSeries.cpp b/Source/aqnwb/nwb/base/TimeSeries.cpp deleted file mode 100644 index a9c008b..0000000 --- a/Source/aqnwb/nwb/base/TimeSeries.cpp +++ /dev/null @@ -1,80 +0,0 @@ -#include "nwb/base/TimeSeries.hpp" - -using namespace AQNWB::NWB; - -// TimeSeries - -/** Constructor */ -TimeSeries::TimeSeries(const std::string& path, - std::shared_ptr io, - const BaseDataType& dataType, - const std::string& unit, - const std::string& description, - const std::string& comments, - const SizeArray& dsetSize, - const SizeArray& chunkSize, - const float& conversion, - const float& resolution, - const float& offset) - : Container(path, io) - , dataType(dataType) - , unit(unit) - , description(description) - , comments(comments) - , dsetSize(dsetSize) - , chunkSize(chunkSize) - , conversion(conversion) - , resolution(resolution) - , offset(offset) -{ -} - -/** Destructor */ -TimeSeries::~TimeSeries() {} - -void TimeSeries::initialize() -{ - Container::initialize(); - - // setup attributes - io->createCommonNWBAttributes(path, "core", neurodataType, description); - io->createAttribute(comments, path, "comments"); - - // setup datasets - this->data = std::unique_ptr(io->createArrayDataSet( - dataType, dsetSize, chunkSize, getPath() + "/data")); - io->createDataAttributes(getPath(), conversion, resolution, unit); - - SizeArray tsDsetSize = { - dsetSize[0]}; // timestamps match data along first dimension - this->timestamps = std::unique_ptr(io->createArrayDataSet( - this->timestampsType, tsDsetSize, chunkSize, getPath() + "/timestamps")); - io->createTimestampsAttributes(getPath()); -} - -Status TimeSeries::writeData(const std::vector& dataShape, - const std::vector& positionOffset, - const void* data, - const void* timestamps) -{ - Status tsStatus = Status::Success; - if (timestamps != nullptr) { - const std::vector timestampsShape = { - dataShape[0]}; // timestamps should match shape of the first data - // dimension - const std::vector timestampsPositionOffset = {positionOffset[0]}; - tsStatus = this->timestamps->writeDataBlock(timestampsShape, - timestampsPositionOffset, - this->timestampsType, - timestamps); - } - - Status dataStatus = this->data->writeDataBlock( - dataShape, positionOffset, this->dataType, data); - - if ((dataStatus != Status::Success) or (tsStatus != Status::Success)) { - return Status::Failure; - } else { - return Status::Success; - } -} diff --git a/Source/aqnwb/nwb/base/TimeSeries.hpp b/Source/aqnwb/nwb/base/TimeSeries.hpp deleted file mode 100644 index 0853785..0000000 --- a/Source/aqnwb/nwb/base/TimeSeries.hpp +++ /dev/null @@ -1,148 +0,0 @@ -#pragma once - -#include - -#include "BaseIO.hpp" -#include "nwb/hdmf/base/Container.hpp" - -namespace AQNWB::NWB -{ -/** - * @brief General purpose time series. - */ -class TimeSeries : public Container -{ -public: - /** - * @brief Constructor. - * @param path The location of the TimeSeries in the file. - * @param io A shared pointer to the IO object. - * @param dataType The data type to use for storing the recorded signal - * @param unit Unit for the electrical signal. Must be "volts" - * @param description The description of the TimeSeries. - * @param comments Human-readable comments about the TimeSeries - * @param dsetSize Initial size of the main dataset - * @param chunkSize Chunk size to use - * @param conversion Scalar to multiply each element in data to convert it to - * the specified ‘unit’ - * @param resolution Smallest meaningful difference between values in data, - * stored in the specified by unit - * @param offset Scalar to add to the data after scaling by ‘conversion’ to - * finalize its coercion to the specified ‘unit' - */ - TimeSeries(const std::string& path, - std::shared_ptr io, - const BaseDataType& dataType, - const std::string& unit, - const std::string& description = "no description", - const std::string& comments = "no comments", - const SizeArray& dsetSize = SizeArray {0}, - const SizeArray& chunkSize = SizeArray {1}, - const float& conversion = 1.0f, - const float& resolution = -1.0f, - const float& offset = 0.0f); - - /** - * @brief Destructor - */ - ~TimeSeries(); - - /** - * @brief Writes a timeseries data block to the file. - * @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 in separate blocks. - * @return The status of the write operation. - */ - Status writeData(const std::vector& dataShape, - const std::vector& positionOffset, - const void* data, - const void* timestamps = nullptr); - - /** - * @brief Initializes the TimeSeries by creating NWB related attributes and - * writing the description and comment metadata. - */ - void initialize(); - - /** - * @brief Pointer to data values. - */ - std::unique_ptr data; - - /** - * @brief Pointer to timestamp values. - */ - std::unique_ptr timestamps; - - /** - * @brief Data type of the data. - */ - BaseDataType dataType; - - /** - * @brief Data type of the timestamps (float64). - */ - BaseDataType timestampsType = BaseDataType::F64; - - /** - * @brief Base unit of measurement for working with the data. Actual stored - * values are not necessarily stored in these units. To access the data in - * these units, multiply ‘data’ by ‘conversion’ and add ‘offset’. - */ - std::string unit; - - /** - * @brief The description of the TimeSeries. - */ - std::string description; - - /** - * @brief Human-readable comments about the TimeSeries. - */ - std::string comments; - - /** - * @brief Size used in dataset creation. Can be expanded when writing if - * needed. - */ - SizeArray dsetSize; - - /** - * @brief Chunking size used in dataset creation. - */ - SizeArray chunkSize; - - /** - * @brief Scalar to multiply each element in data to convert it to the - * specified ‘unit’. - */ - float conversion; - - /** - * @brief Smallest meaningful difference between values in data, stored in the - * specified by unit. - */ - float resolution; - - /** - * @brief Scalar to add to the data after scaling by ‘conversion’ to finalize - * its coercion to the specified ‘unit’. - */ - float offset; - - /** - * @brief The starting time of the TimeSeries. - */ - float startingTime = 0.0; - -private: - /** - * @brief The neurodataType of the TimeSeries. - */ - std::string neurodataType = "TimeSeries"; -}; -} // namespace AQNWB::NWB diff --git a/Source/aqnwb/nwb/device/Device.cpp b/Source/aqnwb/nwb/device/Device.cpp deleted file mode 100644 index 262d920..0000000 --- a/Source/aqnwb/nwb/device/Device.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "nwb/device/Device.hpp" - -using namespace AQNWB::NWB; - -// Device -/** Constructor */ -Device::Device(const std::string& path, - std::shared_ptr io, - const std::string& description, - const std::string& manufacturer) - : Container(path, io) - , description(description) - , manufacturer(manufacturer) -{ -} - -/** Destructor */ -Device::~Device() {} - -void Device::initialize() -{ - Container::initialize(); - - io->createCommonNWBAttributes(path, "core", "Device", description); - io->createAttribute(manufacturer, path, "manufacturer"); -} - -// Getter for manufacturer -std::string Device::getManufacturer() const -{ - return manufacturer; -} - -// Getter for description -std::string Device::getDescription() const -{ - return description; -} diff --git a/Source/aqnwb/nwb/device/Device.hpp b/Source/aqnwb/nwb/device/Device.hpp deleted file mode 100644 index 74ae706..0000000 --- a/Source/aqnwb/nwb/device/Device.hpp +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include - -#include "BaseIO.hpp" -#include "nwb/hdmf/base/Container.hpp" - -namespace AQNWB::NWB -{ -/** - * @brief Metadata about a data acquisition device, e.g., recording system, - * electrode, microscope. - */ -class Device : public Container -{ -public: - /** - * @brief Constructor. - * @param path The location of the device in the file. - * @param io A shared pointer to the IO object. - * @param description The description of the device. - * @param manufacturer The manufacturer of the device. - */ - Device(const std::string& path, - std::shared_ptr io, - const std::string& description, - const std::string& manufacturer); - - /** - * @brief Destructor - */ - ~Device(); - - /** - * @brief Initializes the device by creating NWB related attributes and - * writing the manufactor and description metadata. - */ - void initialize(); - - /** - * @brief Gets the manufacturer of the device. - * @return The manufacturer of the device. - */ - std::string getManufacturer() const; - - /** - * @brief Gets the description of the device. - * @return The description of the device. - */ - std::string getDescription() const; - -private: - /** - * @brief The description of the device. - */ - std::string description; - - /** - * @brief The manufacturer of the device. - */ - std::string manufacturer; -}; -} // namespace AQNWB::NWB diff --git a/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp b/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp deleted file mode 100644 index bf186d2..0000000 --- a/Source/aqnwb/nwb/ecephys/ElectricalSeries.cpp +++ /dev/null @@ -1,101 +0,0 @@ - -#include "nwb/ecephys/ElectricalSeries.hpp" - -#include "nwb/file/ElectrodeTable.hpp" - -using namespace AQNWB::NWB; - -// ElectricalSeries - -/** Constructor */ -ElectricalSeries::ElectricalSeries(const std::string& path, - std::shared_ptr io, - const BaseDataType& dataType, - const Types::ChannelVector& channelVector, - const std::string& description, - const SizeArray& dsetSize, - const SizeArray& chunkSize, - const float& conversion, - const float& resolution, - const float& offset) - : TimeSeries(path, - io, - dataType, - "volts", // default unit for Electrical Series - description, - channelVector[0].comments, - dsetSize, - chunkSize, - channelVector[0].getConversion(), - resolution, - offset) - , channelVector(channelVector) -{ -} - -/** Destructor */ -ElectricalSeries::~ElectricalSeries() {} - -/** Initialization function*/ -void ElectricalSeries::initialize() -{ - TimeSeries::initialize(); - - // setup variables based on number of channels - std::vector electrodeInds(channelVector.size()); - std::vector 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); - - // make channel conversion dataset - channelConversion = std::unique_ptr( - io->createArrayDataSet(BaseDataType::F32, - SizeArray {1}, - chunkSize, - getPath() + "/channel_conversion")); - channelConversion->writeDataBlock( - std::vector(1, channelVector.size()), - BaseDataType::F32, - &channelConversions[0]); - io->createCommonNWBAttributes(getPath() + "/channel_conversion", - "hdmf-common", - "", - "Bit volts values for all channels"); - - // make electrodes dataset - electrodesDataset = std::unique_ptr(io->createArrayDataSet( - BaseDataType::I32, SizeArray {1}, chunkSize, getPath() + "/electrodes")); - electrodesDataset->writeDataBlock( - std::vector(1, channelVector.size()), - BaseDataType::I32, - &electrodeInds[0]); - io->createCommonNWBAttributes( - getPath() + "/electrodes", "hdmf-common", "DynamicTableRegion", ""); - io->createReferenceAttribute( - ElectrodeTable::electrodeTablePath, getPath() + "/electrodes", "table"); -} - -Status ElectricalSeries::writeChannel(SizeType channelInd, - const SizeType& numSamples, - const void* data, - const void* timestamps) -{ - // get offsets and datashape - std::vector dataShape = { - numSamples, 1}; // Note: schema has 1D and 3D but planning to deprecate - std::vector positionOffset = {samplesRecorded[channelInd], - channelInd}; - - // track samples recorded per channel - samplesRecorded[channelInd] += numSamples; - - // write channel data - if (channelInd == 0) { - return writeData(dataShape, positionOffset, data, timestamps); - } else { - return writeData(dataShape, positionOffset, data); - } -} diff --git a/Source/aqnwb/nwb/ecephys/ElectricalSeries.hpp b/Source/aqnwb/nwb/ecephys/ElectricalSeries.hpp deleted file mode 100644 index 597669b..0000000 --- a/Source/aqnwb/nwb/ecephys/ElectricalSeries.hpp +++ /dev/null @@ -1,98 +0,0 @@ -#pragma once - -#include - -#include "BaseIO.hpp" -#include "Channel.hpp" -#include "nwb/base/TimeSeries.hpp" - -namespace AQNWB::NWB -{ -/** - * @brief General purpose time series. - */ -class ElectricalSeries : public TimeSeries -{ -public: - /** - * @brief Constructor. - * @param path The location of the ElectricalSeries in the file. - * @param io A shared pointer to the IO object. - * @param dataType The data type to use for storing the recorded voltage - * @param channelVector The electrodes to use for recording - * @param description The description of the TimeSeries. - * @param dsetSize Initial size of the main dataset. This must be a vector - * with two elements. The first element specifies the length - * in time and the second element must be equal to the - * length of channelVector - * @param chunkSize Chunk size to use. The number of elements must be two to - * specify the size of a chunk in the time and electrode - * dimension - * @param conversion Scalar to multiply each element in data to convert it to - * the specified ‘unit’ - * @param resolution Smallest meaningful difference between values in data, - * stored in the specified by unit - * @param offset Scalar to add to the data after scaling by ‘conversion’ to - * finalize its coercion to the specified ‘unit' - */ - ElectricalSeries(const std::string& path, - std::shared_ptr io, - const BaseDataType& dataType, - const Types::ChannelVector& channelVector, - const std::string& description, - const SizeArray& dsetSize, - const SizeArray& chunkSize, - const float& conversion = 1.0f, - const float& resolution = -1.0f, - const float& offset = 0.0f); - - /** - * @brief Destructor - */ - ~ElectricalSeries(); - - /** - * @brief Initializes the Electrical Series - */ - void initialize(); - - /** - * @brief Writes a channel to an ElectricalSeries dataset. - * @param channelInd The channel index within the ElectricalSeries - * @param numSamples The number of samples to write (length in time). - * @param data A pointer to the data block. - * @param timestamps A pointer to the timestamps block. - * @return The status of the write operation. - */ - Status writeChannel(SizeType channelInd, - const SizeType& numSamples, - const void* data, - const void* timestamps); - - /** - * @brief Channel group that this time series is associated with. - */ - Types::ChannelVector channelVector; - - /** - * @brief Pointer to channel-specific conversion factor dataset. - */ - std::unique_ptr channelConversion; - - /** - * @brief Pointer to electrodes dataset. - */ - std::unique_ptr electrodesDataset; - -private: - /** - * @brief The neurodataType of the TimeSeries. - */ - std::string neurodataType = "ElectricalSeries"; - - /** - * @brief The number of samples already written per channel. - */ - SizeArray samplesRecorded; -}; -} // namespace AQNWB::NWB diff --git a/Source/aqnwb/nwb/ecephys/SpikeEventSeries.cpp b/Source/aqnwb/nwb/ecephys/SpikeEventSeries.cpp deleted file mode 100644 index 7c0c53c..0000000 --- a/Source/aqnwb/nwb/ecephys/SpikeEventSeries.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#include "nwb/ecephys/SpikeEventSeries.hpp" - -using namespace AQNWB::NWB; - -// SpikeEventSeries - -/** Constructor */ -SpikeEventSeries::SpikeEventSeries(const std::string& path, - std::shared_ptr io, - const BaseDataType& dataType, - const Types::ChannelVector& channelVector, - const std::string& description, - const SizeArray& dsetSize, - const SizeArray& chunkSize, - const float& conversion, - const float& resolution, - const float& offset) - : ElectricalSeries(path, - io, - dataType, - channelVector, - description, - dsetSize, - chunkSize, - conversion, - resolution, - offset) -{ -} - -/** Destructor */ -SpikeEventSeries::~SpikeEventSeries() {} - -void SpikeEventSeries::initialize() -{ - ElectricalSeries::initialize(); - - this->eventsRecorded = 0; -} - -Status SpikeEventSeries::writeSpike(const SizeType& numSamples, - const SizeType& numChannels, - const void* data, - const void* timestamps) -{ - // get offsets and datashape - std::vector dataShape; - std::vector positionOffset; - if (numChannels == 1) { - dataShape = {1, numSamples}; - positionOffset = {this->eventsRecorded, 0}; - } else { - dataShape = {1, numChannels, numSamples}; - positionOffset = {this->eventsRecorded, 0, 0}; - } - this->eventsRecorded += 1; - - // write channel data - return writeData(dataShape, positionOffset, data, timestamps); -} \ No newline at end of file diff --git a/Source/aqnwb/nwb/ecephys/SpikeEventSeries.hpp b/Source/aqnwb/nwb/ecephys/SpikeEventSeries.hpp deleted file mode 100644 index ad2dd2a..0000000 --- a/Source/aqnwb/nwb/ecephys/SpikeEventSeries.hpp +++ /dev/null @@ -1,70 +0,0 @@ -#pragma once - -#include - -#include "BaseIO.hpp" -#include "Channel.hpp" -#include "nwb/ecephys/ElectricalSeries.hpp" - -namespace AQNWB::NWB -{ -/** - * @brief Stores snapshots/snippets of recorded spike events (i.e., threshold - * crossings). - */ -class SpikeEventSeries : public ElectricalSeries -{ -public: - /** - * @brief Constructor. - * @param path The location of the SpikeEventSeries in the file. - * @param io A shared pointer to the IO object. - * @param description The description of the SpikeEventSeries, should describe - * how events were detected. - */ - SpikeEventSeries(const std::string& path, - std::shared_ptr io, - const BaseDataType& dataType, - const Types::ChannelVector& channelVector, - const std::string& description, - const SizeArray& dsetSize, - const SizeArray& chunkSize, - const float& conversion = 1.0f, - const float& resolution = -1.0f, - const float& offset = 0.0f); - - /** - * @brief Destructor - */ - ~SpikeEventSeries(); - - /** - * @brief Initializes the Electrical Series - */ - void initialize(); - - /** - * @brief Write a single spike series event - * @param numSamples The number of samples in the event - * @param numChannels The number of channels in the event - * @param data The data of the event - * @param timestamps The timestamps of the event - * @param - */ - Status writeSpike(const SizeType& numSamples, - const SizeType& numChannels, - const void* data, - const void* timestamps); - -private: - /** - * @brief The neurodataType of the SpikeEventSeries. - */ - std::string neurodataType = "SpikeEventSeries"; - - /** - * @brief The number of events already written. - */ - SizeType eventsRecorded; -}; -} // namespace AQNWB::NWB diff --git a/Source/aqnwb/nwb/file/ElectrodeGroup.cpp b/Source/aqnwb/nwb/file/ElectrodeGroup.cpp deleted file mode 100644 index b5beaa0..0000000 --- a/Source/aqnwb/nwb/file/ElectrodeGroup.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "nwb/file/ElectrodeGroup.hpp" - -using namespace AQNWB::NWB; - -// ElectrodeGroup - -/** Constructor */ -ElectrodeGroup::ElectrodeGroup(const std::string& path, - std::shared_ptr io, - const std::string& description, - const std::string& location, - const Device& device) - : Container(path, io) - , description(description) - , location(location) - , device(device) -{ -} - -/** Destructor */ -ElectrodeGroup::~ElectrodeGroup() {} - -void ElectrodeGroup::initialize() -{ - Container::initialize(); - - io->createCommonNWBAttributes(path, "core", "ElectrodeGroup", description); - io->createAttribute(location, path, "location"); - io->createLink("/" + path + "/device", "/" + device.getPath()); -} - -// Getter for description -std::string ElectrodeGroup::getDescription() const -{ - return description; -} - -// Getter for location -std::string ElectrodeGroup::getLocation() const -{ - return location; -} - -// Getter for device -const Device& ElectrodeGroup::getDevice() const -{ - return device; -} diff --git a/Source/aqnwb/nwb/file/ElectrodeGroup.hpp b/Source/aqnwb/nwb/file/ElectrodeGroup.hpp deleted file mode 100644 index 4f4c55c..0000000 --- a/Source/aqnwb/nwb/file/ElectrodeGroup.hpp +++ /dev/null @@ -1,81 +0,0 @@ -#pragma once - -#include - -#include "BaseIO.hpp" -#include "nwb/device/Device.hpp" -#include "nwb/hdmf/base/Container.hpp" - -namespace AQNWB::NWB -{ -/** - * @brief The ElectrodeGroup class represents a physical grouping of electrodes, - * e.g. a shank of an array. - */ -class ElectrodeGroup : public Container -{ -public: - /** - * @brief Constructor. - * @param path The location in the file of the electrode group. - * @param io A shared pointer to the IO object. - * @param description The description of the electrode group. - * @param location The location of electrode group within the subject e.g. - * brain region. - * @param device The device associated with the electrode group. - */ - ElectrodeGroup(const std::string& path, - std::shared_ptr io, - const std::string& description, - const std::string& location, - const Device& device); - - /** - * @brief Destructor. - */ - ~ElectrodeGroup(); - - /** - * @brief Initializes the ElectrodeGroup object. - * - * Initializes the ElectrodeGroup by creating NWB related attributes and - * linking to the Device object. - */ - void initialize(); - - /** - * @brief Gets the description of the electrode group. - * @return The description of the electrode group. - */ - std::string getDescription() const; - - /** - * @brief Gets the location of the electrode group. - * @return The location of the electrode group. - */ - std::string getLocation() const; - - /** - * @brief Gets the device associated with the electrode group. - * @return The device associated with the electrode group. - */ - const Device& getDevice() const; - -private: - /** - * @brief The description of the electrode group. - */ - std::string description; - - /** - * @brief The location of electrode group within the subject e.g. brain - * region. - */ - std::string location; - - /** - * @brief The device associated with the electrode group. - */ - Device device; -}; -} // namespace AQNWB::NWB diff --git a/Source/aqnwb/nwb/file/ElectrodeTable.cpp b/Source/aqnwb/nwb/file/ElectrodeTable.cpp deleted file mode 100644 index 027c9f6..0000000 --- a/Source/aqnwb/nwb/file/ElectrodeTable.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include "nwb/file/ElectrodeTable.hpp" - -#include "Channel.hpp" - -using namespace AQNWB::NWB; - -// ElectrodeTable - -/** Constructor */ -ElectrodeTable::ElectrodeTable(std::shared_ptr io, - const std::string& description) - : DynamicTable(electrodeTablePath, // use the electrodeTablePath - io, - description) -{ -} - -/** Destructor */ -ElectrodeTable::~ElectrodeTable() {} - -/** Initialization function*/ -void ElectrodeTable::initialize() -{ - // create group - DynamicTable::initialize(); - - electrodeDataset->dataset = - std::unique_ptr(io->createArrayDataSet( - BaseDataType::I32, SizeArray {1}, SizeArray {1}, path + "id")); - groupNamesDataset->dataset = std::unique_ptr( - io->createArrayDataSet(BaseDataType::STR(250), - SizeArray {0}, - SizeArray {1}, - path + "group_name")); - locationsDataset - ->dataset = std::unique_ptr(io->createArrayDataSet( - BaseDataType::STR(250), SizeArray {0}, SizeArray {1}, path + "location")); -} - -void ElectrodeTable::addElectrodes(std::vector channels) -{ - // create datasets - for (const auto& ch : channels) { - groupReferences.push_back(groupPathBase + ch.groupName); - groupNames.push_back(ch.groupName); - electrodeNumbers.push_back(ch.globalIndex); - locationNames.push_back("unknown"); - } -} - -void ElectrodeTable::finalize() -{ - setRowIDs(electrodeDataset, electrodeNumbers); - addColumn("group_name", - "the name of the ElectrodeGroup this electrode is a part of", - groupNamesDataset, - groupNames); - addColumn("location", - "the location of channel within the subject e.g. brain region", - locationsDataset, - locationNames); - addColumn("group", - "a reference to the ElectrodeGroup this electrode is a part of", - groupReferences); -} - -// Getter for colNames -const std::vector& ElectrodeTable::getColNames() -{ - return colNames; -} - -// Setter for colNames -void ElectrodeTable::setColNames(const std::vector& newColNames) -{ - colNames = newColNames; -} - -// Getter for groupPath -std::string ElectrodeTable::getGroupPath() const -{ - return groupReferences[0]; // all channels in ChannelVector should have the - // same groupName -} diff --git a/Source/aqnwb/nwb/file/ElectrodeTable.hpp b/Source/aqnwb/nwb/file/ElectrodeTable.hpp deleted file mode 100644 index b8611a3..0000000 --- a/Source/aqnwb/nwb/file/ElectrodeTable.hpp +++ /dev/null @@ -1,128 +0,0 @@ -#pragma once - -#include - -#include "BaseIO.hpp" -#include "nwb/hdmf/table/DynamicTable.hpp" -#include "nwb/hdmf/table/ElementIdentifiers.hpp" -#include "nwb/hdmf/table/VectorData.hpp" - -namespace AQNWB::NWB -{ -/** - * @brief Represents a table containing electrode metadata. - */ -class ElectrodeTable : public DynamicTable -{ -public: - /** - * @brief Constructor. - * @param io The shared pointer to the BaseIO object. - * @param description The description of the table (default: "metadata about - * extracellular electrodes"). - */ - ElectrodeTable(std::shared_ptr io, - const std::string& description = - "metadata about extracellular electrodes"); - - /** - * @brief Destructor. - */ - ~ElectrodeTable(); - - /** - * @brief Initializes the ElectrodeTable. - * - * Initializes the ElectrodeTable by creating NWB related attributes and - * adding required columns. - */ - void initialize(); - - /** - * @brief Finalizes the ElectrodeTable. - * - * Finalizes the ElectrodeTable by adding the required columns and writing - * the data to the file. - */ - void finalize(); - - /** - * @brief Sets up the ElectrodeTable by adding electrodes and their metadata. - * - */ - void addElectrodes(std::vector channels); - - /** - * @brief Gets the column names of the ElectrodeTable. - * @return The vector of column names. - */ - const std::vector& getColNames() override; - - /** - * @brief Sets the column names of the ElectrodeTable. - * @param newColNames The vector of new column names. - */ - void setColNames(const std::vector& newColNames); - - /** - * @brief Gets the group path of the ElectrodeTable. - * @return The group path. - */ - std::string getGroupPath() const; - - /** - * @brief Sets the group path of the ElectrodeTable. - * @param groupPath The new group path. - */ - void setGroupPath(const std::string& groupPath); - - std::unique_ptr electrodeDataset = - std::make_unique(); /**< The electrode dataset. */ - std::unique_ptr groupNamesDataset = - std::make_unique(); /**< The group names dataset. */ - std::unique_ptr locationsDataset = - std::make_unique(); /**< The locations dataset. */ - - /** - * @brief The path to the ElectrodeTable. - */ - inline const static std::string electrodeTablePath = - "/general/extracellular_ephys/electrodes/"; - -private: - /** - * @brief The channel information from the acquisition system. - */ - std::vector channels; - - /** - * @brief The global indices for each electrode. - */ - std::vector electrodeNumbers; - - /** - * @brief The names of the ElectrodeGroup object for each electrode. - */ - std::vector groupNames; - - /** - * @brief The location names for each electrode. - */ - std::vector locationNames; - - /** - * @brief The references to the ElectrodeGroup object for each electrode. - */ - std::vector groupReferences; - - /** - * @brief The vector of column names for the table. - */ - std::vector colNames = {"group", "group_name", "location"}; - - /** - * @brief The references path to the ElectrodeGroup - */ - std::string groupPathBase = "/general/extracellular_ephys/"; -}; -} // namespace AQNWB::NWB diff --git a/Source/aqnwb/nwb/hdmf/base/Container.cpp b/Source/aqnwb/nwb/hdmf/base/Container.cpp deleted file mode 100644 index 525d82e..0000000 --- a/Source/aqnwb/nwb/hdmf/base/Container.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "nwb/hdmf/base/Container.hpp" - -using namespace AQNWB::NWB; - -// Container - -/** Constructor */ -Container::Container(const std::string& path, std::shared_ptr io) - : path(path) - , io(io) -{ -} - -/** Destructor */ -Container::~Container() {} - -/** Initialize */ -void Container::initialize() -{ - io->createGroup(path); -} - -/** Getter for path */ -std::string Container::getPath() const -{ - return path; -} diff --git a/Source/aqnwb/nwb/hdmf/base/Container.hpp b/Source/aqnwb/nwb/hdmf/base/Container.hpp deleted file mode 100644 index 1d89c87..0000000 --- a/Source/aqnwb/nwb/hdmf/base/Container.hpp +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include -#include - -#include "BaseIO.hpp" - -namespace AQNWB::NWB -{ -/** - * @brief Abstract data type for a group storing collections of data and - * metadata - */ -class Container -{ -public: - /** - * @brief Constructor. - * @param path The path of the container. - * @param io A shared pointer to the IO object. - */ - Container(const std::string& path, std::shared_ptr io); - - /** - * @brief Destructor. - */ - virtual ~Container(); - - /** - * @brief Initialize the container. - */ - void initialize(); - - /** - * @brief Gets the path of the container. - * @return The path of the container. - */ - std::string getPath() const; - -protected: - /** - * @brief The path of the container. - */ - std::string path; - - /** - * @brief A shared pointer to the IO object. - */ - std::shared_ptr io; -}; -} // namespace AQNWB::NWB diff --git a/Source/aqnwb/nwb/hdmf/base/Data.hpp b/Source/aqnwb/nwb/hdmf/base/Data.hpp deleted file mode 100644 index ef14648..0000000 --- a/Source/aqnwb/nwb/hdmf/base/Data.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include - -#include "BaseIO.hpp" - -namespace AQNWB::NWB -{ -/** - * @brief An abstract data type for a dataset. - */ -class Data -{ -public: - /** - * @brief Constructor. - */ - Data() {} - - /** - * @brief Destructor. - */ - ~Data() {} - - /** - * @brief Pointer to dataset. - */ - std::unique_ptr dataset; -}; -} // namespace AQNWB::NWB diff --git a/Source/aqnwb/nwb/hdmf/table/DynamicTable.cpp b/Source/aqnwb/nwb/hdmf/table/DynamicTable.cpp deleted file mode 100644 index d8b8c98..0000000 --- a/Source/aqnwb/nwb/hdmf/table/DynamicTable.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "nwb/hdmf/table/DynamicTable.hpp" - -using namespace AQNWB::NWB; - -// DynamicTable - -/** Constructor */ -DynamicTable::DynamicTable(const std::string& path, - std::shared_ptr io, - const std::string& description) - : Container(path, io) - , description(description) -{ -} - -/** Destructor */ -DynamicTable::~DynamicTable() {} - -/** Initialization function*/ -void DynamicTable::initialize() -{ - Container::initialize(); - - io->createCommonNWBAttributes( - path, "hdmf-common", "DynamicTable", getDescription()); - io->createAttribute(getColNames(), path, "colnames"); -} - -/** Add column to table */ -void DynamicTable::addColumn(const std::string& name, - const std::string& colDescription, - std::unique_ptr& vectorData, - const std::vector& values) -{ - if (vectorData->dataset == nullptr) { - std::cerr << "VectorData dataset is not initialized" << std::endl; - } else { - // write in loop because variable length string - for (SizeType i = 0; i < values.size(); i++) - vectorData->dataset->writeDataBlock( - std::vector(1, 1), - BaseDataType::STR(values[i].size() + 1), - values[i].c_str()); // TODO - add tests for this - io->createCommonNWBAttributes( - path + name, "hdmf-common", "VectorData", colDescription); - } -} - -void DynamicTable::setRowIDs(std::unique_ptr& elementIDs, - const std::vector& values) -{ - if (elementIDs->dataset == nullptr) { - std::cerr << "ElementIdentifiers dataset is not initialized" << std::endl; - } else { - elementIDs->dataset->writeDataBlock( - std::vector(1, values.size()), BaseDataType::I32, &values[0]); - io->createCommonNWBAttributes( - path + "id", "hdmf-common", "ElementIdentifiers"); - } -} - -void DynamicTable::addColumn(const std::string& name, - const std::string& colDescription, - const std::vector& values) -{ - if (values.empty()) { - std::cerr << "Data to add to column is empty" << std::endl; - } else { - io->createReferenceDataSet(path + name, values); - io->createCommonNWBAttributes( - path + name, "hdmf-common", "VectorData", colDescription); - } -} - -// Getter for description -std::string DynamicTable::getDescription() const -{ - return description; -} - -// Getter for colNames -const std::vector& DynamicTable::getColNames() -{ - return colNames; -} diff --git a/Source/aqnwb/nwb/hdmf/table/DynamicTable.hpp b/Source/aqnwb/nwb/hdmf/table/DynamicTable.hpp deleted file mode 100644 index 6cd8c2a..0000000 --- a/Source/aqnwb/nwb/hdmf/table/DynamicTable.hpp +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -#include - -#include "BaseIO.hpp" -#include "nwb/hdmf/base/Container.hpp" -#include "nwb/hdmf/table/ElementIdentifiers.hpp" -#include "nwb/hdmf/table/VectorData.hpp" - -namespace AQNWB::NWB -{ -/** - * @brief Represents a group containing multiple datasets that are aligned on - * the first dimension - * - * This class inherits from the `Container` class and provides methods to add - * columns of different types of data to the table. - */ -class DynamicTable : public Container -{ -public: - /** - * @brief Constructor. - * @param path The location of the table in the file. - * @param io A shared pointer to the IO object. - * @param description The description of the table (optional). - */ - DynamicTable(const std::string& path, - std::shared_ptr io, - const std::string& description); - - /** - * @brief Destructor - */ - virtual ~DynamicTable(); - - /** - * @brief Initializes the `DynamicTable` object by creating NWB attributes and - * column names. - */ - void initialize(); - - /** - * @brief Adds a column of vector string data to the table. - * @param name The name of the column. - * @param colDescription The description of the column. - * @param vectorData A unique pointer to the `VectorData` dataset. - * @param values The vector of string values. - */ - void addColumn(const std::string& name, - const std::string& colDescription, - std::unique_ptr& vectorData, - const std::vector& values); - - /** - * @brief Adds a column of references to the table. - * @param name The name of the column. - * @param colDescription The description of the column. - * @param dataset The vector of string values representing the references. - */ - void addColumn(const std::string& name, - const std::string& colDescription, - const std::vector& dataset); - - /** - * @brief Adds a column of element identifiers to the table. - * @param elementIDs A unique pointer to the `ElementIdentifiers` dataset. - * @param values The vector of id values. - */ - void setRowIDs(std::unique_ptr& elementIDs, - const std::vector& values); - - /** - * @brief Gets the description of the table. - * @return The description of the table. - */ - std::string getDescription() const; - - /** - * @brief Gets the column names of the table. - * @return A vector of column names. - */ - virtual const std::vector& getColNames() = 0; - -private: - /** - * @brief Description of the DynamicTable. - */ - std::string description; - - /** - * @brief Names of the columns in the table. - */ - std::vector colNames; -}; -} // namespace AQNWB::NWB diff --git a/Source/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp b/Source/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp deleted file mode 100644 index 36ab29c..0000000 --- a/Source/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include "../base/Data.hpp" - -namespace AQNWB::NWB -{ -/** - * @brief A list of unique identifiers for values within a dataset, e.g. rows of - * a DynamicTable. - */ -class ElementIdentifiers : public Data -{ -}; -} // namespace AQNWB::NWB diff --git a/Source/aqnwb/nwb/hdmf/table/VectorData.cpp b/Source/aqnwb/nwb/hdmf/table/VectorData.cpp deleted file mode 100644 index 1438387..0000000 --- a/Source/aqnwb/nwb/hdmf/table/VectorData.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "nwb/hdmf/table/VectorData.hpp" - -using namespace AQNWB::NWB; - -// VectorData -std::string VectorData::getDescription() const -{ - return description; -} diff --git a/Source/aqnwb/nwb/hdmf/table/VectorData.hpp b/Source/aqnwb/nwb/hdmf/table/VectorData.hpp deleted file mode 100644 index 7ee93f0..0000000 --- a/Source/aqnwb/nwb/hdmf/table/VectorData.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include - -#include "nwb/hdmf/base/Data.hpp" - -namespace AQNWB::NWB -{ -/** - * @brief An n-dimensional dataset representing a column of a DynamicTable. - */ -class VectorData : public Data -{ -public: - /** - * @brief Gets the description of the table. - * @return The description of the table. - */ - std::string getDescription() const; - -private: - /** - * @brief Description of VectorData. - */ - std::string description; -}; -} // namespace AQNWB::NWB diff --git a/Source/aqnwb/spec/core.hpp b/Source/aqnwb/spec/core.hpp deleted file mode 100644 index 0a67f80..0000000 --- a/Source/aqnwb/spec/core.hpp +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace AQNWB::SPEC::CORE -{ - -const std::string version = "2.7.0"; - -constexpr std::string_view nwb_base = R"delimiter( -{"datasets":[{"neurodata_type_def":"NWBData","neurodata_type_inc":"Data","doc":"An abstract data type for a dataset."},{"neurodata_type_def":"TimeSeriesReferenceVectorData","neurodata_type_inc":"VectorData","default_name":"timeseries","dtype":[{"name":"idx_start","dtype":"int32","doc":"Start index into the TimeSeries 'data' and 'timestamp' datasets of the referenced TimeSeries. The first dimension of those arrays is always time."},{"name":"count","dtype":"int32","doc":"Number of data samples available in this time series, during this epoch"},{"name":"timeseries","dtype":{"target_type":"TimeSeries","reftype":"object"},"doc":"The TimeSeries that this index applies to"}],"doc":"Column storing references to a TimeSeries (rows). For each TimeSeries this VectorData column stores the start_index and count to indicate the range in time to be selected as well as an object reference to the TimeSeries."},{"neurodata_type_def":"Image","neurodata_type_inc":"NWBData","dtype":"numeric","dims":[["x","y"],["x","y","r, g, b"],["x","y","r, g, b, a"]],"shape":[[null,null],[null,null,3],[null,null,4]],"doc":"An abstract data type for an image. Shape can be 2-D (x, y), or 3-D where the third dimension can have three or four elements, e.g. (x, y, (r, g, b)) or (x, y, (r, g, b, a)).","attributes":[{"name":"resolution","dtype":"float32","doc":"Pixel resolution of the image, in pixels per centimeter.","required":false},{"name":"description","dtype":"text","doc":"Description of the image.","required":false}]},{"neurodata_type_def":"ImageReferences","neurodata_type_inc":"NWBData","dtype":{"target_type":"Image","reftype":"object"},"dims":["num_images"],"shape":[null],"doc":"Ordered dataset of references to Image objects."}],"groups":[{"neurodata_type_def":"NWBContainer","neurodata_type_inc":"Container","doc":"An abstract data type for a generic container storing collections of data and metadata. Base type for all data and metadata containers."},{"neurodata_type_def":"NWBDataInterface","neurodata_type_inc":"NWBContainer","doc":"An abstract data type for a generic container storing collections of data, as opposed to metadata."},{"neurodata_type_def":"TimeSeries","neurodata_type_inc":"NWBDataInterface","doc":"General purpose time series.","attributes":[{"name":"description","dtype":"text","default_value":"no description","doc":"Description of the time series.","required":false},{"name":"comments","dtype":"text","default_value":"no comments","doc":"Human-readable comments about the TimeSeries. This second descriptive field can be used to store additional information, or descriptive information if the primary description field is populated with a computer-readable string.","required":false}],"datasets":[{"name":"data","dims":[["num_times"],["num_times","num_DIM2"],["num_times","num_DIM2","num_DIM3"],["num_times","num_DIM2","num_DIM3","num_DIM4"]],"shape":[[null],[null,null],[null,null,null],[null,null,null,null]],"doc":"Data values. Data can be in 1-D, 2-D, 3-D, or 4-D. The first dimension should always represent time. This can also be used to store binary data (e.g., image frames). This can also be a link to data stored in an external file.","attributes":[{"name":"conversion","dtype":"float32","default_value":1.0,"doc":"Scalar to multiply each element in data to convert it to the specified 'unit'. If the data are stored in acquisition system units or other units that require a conversion to be interpretable, multiply the data by 'conversion' to convert the data to the specified 'unit'. e.g. if the data acquisition system stores values in this object as signed 16-bit integers (int16 range -32,768 to 32,767) that correspond to a 5V range (-2.5V to 2.5V), and the data acquisition system gain is 8000X, then the 'conversion' multiplier to get from raw data acquisition values to recorded volts is 2.5/32768/8000 = 9.5367e-9.","required":false},{"name":"offset","dtype":"float32","default_value":0.0,"doc":"Scalar to add to the data after scaling by 'conversion' to finalize its coercion to the specified 'unit'. Two common examples of this include (a) data stored in an unsigned type that requires a shift after scaling to re-center the data, and (b) specialized recording devices that naturally cause a scalar offset with respect to the true units.","required":false},{"name":"resolution","dtype":"float32","default_value":-1.0,"doc":"Smallest meaningful difference between values in data, stored in the specified by unit, e.g., the change in value of the least significant bit, or a larger number if signal noise is known to be present. If unknown, use -1.0.","required":false},{"name":"unit","dtype":"text","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."},{"name":"continuity","dtype":"text","doc":"Optionally describe the continuity of the data. Can be \"continuous\", \"instantaneous\", or \"step\". For example, a voltage trace would be \"continuous\", because samples are recorded from a continuous process. An array of lick times would be \"instantaneous\", because the data represents distinct moments in time. Times of image presentations would be \"step\" because the picture remains the same until the next timepoint. This field is optional, but is useful in providing information about the underlying data. It may inform the way this data is interpreted, the way it is visualized, and what analysis methods are applicable.","required":false}]},{"name":"starting_time","dtype":"float64","doc":"Timestamp of the first sample in seconds. When timestamps are uniformly spaced, the timestamp of the first sample can be specified and all subsequent ones calculated from the sampling rate attribute.","quantity":"?","attributes":[{"name":"rate","dtype":"float32","doc":"Sampling rate, in Hz."},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for time, which is fixed to 'seconds'."}]},{"name":"timestamps","dtype":"float64","dims":["num_times"],"shape":[null],"doc":"Timestamps for samples stored in data, in seconds, relative to the common experiment master-clock stored in NWBFile.timestamps_reference_time.","quantity":"?","attributes":[{"name":"interval","dtype":"int32","value":1,"doc":"Value is '1'"},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for timestamps, which is fixed to 'seconds'."}]},{"name":"control","dtype":"uint8","dims":["num_times"],"shape":[null],"doc":"Numerical labels that apply to each time point in data for the purpose of querying and slicing data by these values. If present, the length of this array should be the same size as the first dimension of data.","quantity":"?"},{"name":"control_description","dtype":"text","dims":["num_control_values"],"shape":[null],"doc":"Description of each control value. Must be present if control is present. If present, control_description[0] should describe time points where control == 0.","quantity":"?"}],"groups":[{"name":"sync","doc":"Lab-specific time and sync information as provided directly from hardware devices and that is necessary for aligning all acquired time information to a common timebase. The timestamp array stores time in the common timebase. This group will usually only be populated in TimeSeries that are stored external to the NWB file, in files storing raw data. Once timestamp data is calculated, the contents of 'sync' are mostly for archival purposes.","quantity":"?"}]},{"neurodata_type_def":"ProcessingModule","neurodata_type_inc":"NWBContainer","doc":"A collection of processed data.","attributes":[{"name":"description","dtype":"text","doc":"Description of this collection of processed data."}],"groups":[{"neurodata_type_inc":"NWBDataInterface","doc":"Data objects stored in this collection.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tables stored in this collection.","quantity":"*"}]},{"neurodata_type_def":"Images","neurodata_type_inc":"NWBDataInterface","default_name":"Images","doc":"A collection of images with an optional way to specify the order of the images using the \"order_of_images\" dataset. An order must be specified if the images are referenced by index, e.g., from an IndexSeries.","attributes":[{"name":"description","dtype":"text","doc":"Description of this collection of images."}],"datasets":[{"neurodata_type_inc":"Image","doc":"Images stored in this collection.","quantity":"+"},{"name":"order_of_images","neurodata_type_inc":"ImageReferences","doc":"Ordered dataset of references to Image objects stored in the parent group. Each Image object in the Images group should be stored once and only once, so the dataset should have the same length as the number of images.","quantity":"?"}]}]})delimiter"; - -constexpr std::string_view nwb_device = R"delimiter( -{"groups":[{"neurodata_type_def":"Device","neurodata_type_inc":"NWBContainer","doc":"Metadata about a data acquisition device, e.g., recording system, electrode, microscope.","attributes":[{"name":"description","dtype":"text","doc":"Description of the device (e.g., model, firmware version, processing software version, etc.) as free-form text.","required":false},{"name":"manufacturer","dtype":"text","doc":"The name of the manufacturer of the device.","required":false}]}]})delimiter"; - -constexpr std::string_view nwb_epoch = R"delimiter( -{"groups":[{"neurodata_type_def":"TimeIntervals","neurodata_type_inc":"DynamicTable","doc":"A container for aggregating epoch data and the TimeSeries that each epoch applies to.","datasets":[{"name":"start_time","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Start time of epoch, in seconds."},{"name":"stop_time","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Stop time of epoch, in seconds."},{"name":"tags","neurodata_type_inc":"VectorData","dtype":"text","doc":"User-defined tags that identify or categorize events.","quantity":"?"},{"name":"tags_index","neurodata_type_inc":"VectorIndex","doc":"Index for tags.","quantity":"?"},{"name":"timeseries","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"An index into a TimeSeries object.","quantity":"?"},{"name":"timeseries_index","neurodata_type_inc":"VectorIndex","doc":"Index for timeseries.","quantity":"?"}]}]})delimiter"; - -constexpr std::string_view nwb_image = R"delimiter( -{"datasets":[{"neurodata_type_def":"GrayscaleImage","neurodata_type_inc":"Image","dims":["x","y"],"shape":[null,null],"doc":"A grayscale image.","dtype":"numeric"},{"neurodata_type_def":"RGBImage","neurodata_type_inc":"Image","dims":["x","y","r, g, b"],"shape":[null,null,3],"doc":"A color image.","dtype":"numeric"},{"neurodata_type_def":"RGBAImage","neurodata_type_inc":"Image","dims":["x","y","r, g, b, a"],"shape":[null,null,4],"doc":"A color image with transparency.","dtype":"numeric"}],"groups":[{"neurodata_type_def":"ImageSeries","neurodata_type_inc":"TimeSeries","doc":"General image data that is common between acquisition and stimulus time series. Sometimes the image data is stored in the file in a raw format while other times it will be stored as a series of external image files in the host file system. The data field will either be binary data, if the data is stored in the NWB file, or empty, if the data is stored in an external image stack. [frame][x][y] or [frame][x][y][z].","datasets":[{"name":"data","dtype":"numeric","dims":[["frame","x","y"],["frame","x","y","z"]],"shape":[[null,null,null],[null,null,null,null]],"doc":"Binary data representing images across frames. If data are stored in an external file, this should be an empty 3D array."},{"name":"dimension","dtype":"int32","dims":["rank"],"shape":[null],"doc":"Number of pixels on x, y, (and z) axes.","quantity":"?"},{"name":"external_file","dtype":"text","dims":["num_files"],"shape":[null],"doc":"Paths to one or more external file(s). The field is only present if format='external'. This is only relevant if the image series is stored in the file system as one or more image file(s). This field should NOT be used if the image is stored in another NWB file and that file is linked to this file.","quantity":"?","attributes":[{"name":"starting_frame","dtype":"int32","dims":["num_files"],"shape":[null],"doc":"Each external image may contain one or more consecutive frames of the full ImageSeries. This attribute serves as an index to indicate which frames each file contains, to facilitate random access. The 'starting_frame' attribute, hence, contains a list of frame numbers within the full ImageSeries of the first frame of each file listed in the parent 'external_file' dataset. Zero-based indexing is used (hence, the first element will always be zero). For example, if the 'external_file' dataset has three paths to files and the first file has 5 frames, the second file has 10 frames, and the third file has 20 frames, then this attribute will have values [0, 5, 15]. If there is a single external file that holds all of the frames of the ImageSeries (and so there is a single element in the 'external_file' dataset), then this attribute should have value [0]."}]},{"name":"format","dtype":"text","default_value":"raw","doc":"Format of image. If this is 'external', then the attribute 'external_file' contains the path information to the image files. If this is 'raw', then the raw (single-channel) binary data is stored in the 'data' dataset. If this attribute is not present, then the default format='raw' case is assumed.","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the Device object that was used to capture these images.","quantity":"?"}]},{"neurodata_type_def":"ImageMaskSeries","neurodata_type_inc":"ImageSeries","doc":"An alpha mask that is applied to a presented visual stimulus. The 'data' array contains an array of mask values that are applied to the displayed image. Mask values are stored as RGBA. Mask can vary with time. The timestamps array indicates the starting time of a mask, and that mask pattern continues until it's explicitly changed.","links":[{"name":"masked_imageseries","target_type":"ImageSeries","doc":"Link to ImageSeries object that this image mask is applied to."}]},{"neurodata_type_def":"OpticalSeries","neurodata_type_inc":"ImageSeries","doc":"Image data that is presented or recorded. A stimulus template movie will be stored only as an image. When the image is presented as stimulus, additional data is required, such as field of view (e.g., how much of the visual field the image covers, or how what is the area of the target being imaged). If the OpticalSeries represents acquired imaging data, orientation is also important.","datasets":[{"name":"distance","dtype":"float32","doc":"Distance from camera/monitor to target/eye.","quantity":"?"},{"name":"field_of_view","dtype":"float32","dims":[["width, height"],["width, height, depth"]],"shape":[[2],[3]],"doc":"Width, height and depth of image, or imaged area, in meters.","quantity":"?"},{"name":"data","dtype":"numeric","dims":[["frame","x","y"],["frame","x","y","r, g, b"]],"shape":[[null,null,null],[null,null,null,3]],"doc":"Images presented to subject, either grayscale or RGB"},{"name":"orientation","dtype":"text","doc":"Description of image relative to some reference frame (e.g., which way is up). Must also specify frame of reference.","quantity":"?"}]},{"neurodata_type_def":"IndexSeries","neurodata_type_inc":"TimeSeries","doc":"Stores indices to image frames stored in an ImageSeries. The purpose of the IndexSeries is to allow a static image stack to be stored in an Images object, and the images in the stack to be referenced out-of-order. This can be for the display of individual images, or of movie segments (as a movie is simply a series of images). The data field stores the index of the frame in the referenced Images object, and the timestamps array indicates when that image was displayed.","datasets":[{"name":"data","dtype":"uint32","dims":["num_times"],"shape":[null],"doc":"Index of the image (using zero-indexing) in the linked Images object.","attributes":[{"name":"conversion","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"resolution","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"offset","dtype":"float32","doc":"This field is unused by IndexSeries.","required":false},{"name":"unit","dtype":"text","value":"N/A","doc":"This field is unused by IndexSeries and has the value N/A."}]}],"links":[{"name":"indexed_timeseries","target_type":"ImageSeries","doc":"Link to ImageSeries object containing images that are indexed. Use of this link is discouraged and will be deprecated. Link to an Images type instead.","quantity":"?"},{"name":"indexed_images","target_type":"Images","doc":"Link to Images object containing an ordered set of images that are indexed. The Images object must contain a 'ordered_images' dataset specifying the order of the images in the Images type.","quantity":"?"}]}]})delimiter"; - -constexpr std::string_view nwb_file = R"delimiter( -{"groups":[{"neurodata_type_def":"NWBFile","neurodata_type_inc":"NWBContainer","name":"root","doc":"An NWB file storing cellular-based neurophysiology data from a single experimental session.","attributes":[{"name":"nwb_version","dtype":"text","value":"2.7.0-alpha","doc":"File version string. Use semantic versioning, e.g. 1.2.1. This will be the name of the format with trailing major, minor and patch numbers."}],"datasets":[{"name":"file_create_date","dtype":"isodatetime","dims":["num_modifications"],"shape":[null],"doc":"A record of the date the file was created and of subsequent modifications. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted strings: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds. The file can be created after the experiment was run, so this may differ from the experiment start time. Each modification to the nwb file adds a new entry to the array."},{"name":"identifier","dtype":"text","doc":"A unique text identifier for the file. For example, concatenated lab name, file creation date/time and experimentalist, or a hash of these and/or other values. The goal is that the string should be unique to all other files."},{"name":"session_description","dtype":"text","doc":"A description of the experimental session and data in the file."},{"name":"session_start_time","dtype":"isodatetime","doc":"Date and time of the experiment/session start. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted string: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds."},{"name":"timestamps_reference_time","dtype":"isodatetime","doc":"Date and time corresponding to time zero of all timestamps. The date is stored in UTC with local timezone offset as ISO 8601 extended formatted string: 2018-09-28T14:43:54.123+02:00. Dates stored in UTC end in \"Z\" with no timezone offset. Date accuracy is up to milliseconds. All times stored in the file use this time as reference (i.e., time zero)."}],"groups":[{"name":"acquisition","doc":"Data streams recorded from the system, including ephys, ophys, tracking, etc. This group should be read-only after the experiment is completed and timestamps are corrected to a common timebase. The data stored here may be links to raw data stored in external NWB files. This will allow keeping bulky raw data out of the file while preserving the option of keeping some/all in the file. Acquired data includes tracking and experimental data streams (i.e., everything measured from the system). If bulky data is stored in the /acquisition group, the data can exist in a separate NWB file that is linked to by the file being used for processing and analysis.","groups":[{"neurodata_type_inc":"NWBDataInterface","doc":"Acquired, raw data.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tabular data that is relevant to acquisition","quantity":"*"}]},{"name":"analysis","doc":"Lab-specific and custom scientific analysis of data. There is no defined format for the content of this group - the format is up to the individual user/lab. To facilitate sharing analysis data between labs, the contents here should be stored in standard types (e.g., neurodata_types) and appropriately documented. The file can store lab-specific and custom data analysis without restriction on its form or schema, reducing data formatting restrictions on end users. Such data should be placed in the analysis group. The analysis data should be documented so that it could be shared with other labs.","groups":[{"neurodata_type_inc":"NWBContainer","doc":"Custom analysis results.","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Tabular data that is relevant to data stored in analysis","quantity":"*"}]},{"name":"scratch","doc":"A place to store one-off analysis results. Data placed here is not intended for sharing. By placing data here, users acknowledge that there is no guarantee that their data meets any standard.","quantity":"?","groups":[{"neurodata_type_inc":"NWBContainer","doc":"Any one-off containers","quantity":"*"},{"neurodata_type_inc":"DynamicTable","doc":"Any one-off tables","quantity":"*"}],"datasets":[{"neurodata_type_inc":"ScratchData","doc":"Any one-off datasets","quantity":"*"}]},{"name":"processing","doc":"The home for ProcessingModules. These modules perform intermediate analysis of data that is necessary to perform before scientific analysis. Examples include spike clustering, extracting position from tracking data, stitching together image slices. ProcessingModules can be large and express many data sets from relatively complex analysis (e.g., spike detection and clustering) or small, representing extraction of position information from tracking video, or even binary lick/no-lick decisions. Common software tools (e.g., klustakwik, MClust) are expected to read/write data here. 'Processing' refers to intermediate analysis of the acquired data to make it more amenable to scientific analysis.","groups":[{"neurodata_type_inc":"ProcessingModule","doc":"Intermediate analysis of acquired data.","quantity":"*"}]},{"name":"stimulus","doc":"Data pushed into the system (eg, video stimulus, sound, voltage, etc) and secondary representations of that data (eg, measurements of something used as a stimulus). This group should be made read-only after experiment complete and timestamps are corrected to common timebase. Stores both presented stimuli and stimulus templates, the latter in case the same stimulus is presented multiple times, or is pulled from an external stimulus library. Stimuli are here defined as any signal that is pushed into the system as part of the experiment (eg, sound, video, voltage, etc). Many different experiments can use the same stimuli, and stimuli can be re-used during an experiment. The stimulus group is organized so that one version of template stimuli can be stored and these be used multiple times. These templates can exist in the present file or can be linked to a remote library file.","groups":[{"name":"presentation","doc":"Stimuli presented during the experiment.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries objects containing data of presented stimuli.","quantity":"*"}]},{"name":"templates","doc":"Template stimuli. Timestamps in templates are based on stimulus design and are relative to the beginning of the stimulus. When templates are used, the stimulus instances must convert presentation times to the experiment`s time reference frame.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries objects containing template data of presented stimuli.","quantity":"*"},{"neurodata_type_inc":"Images","doc":"Images objects containing images of presented stimuli.","quantity":"*"}]}]},{"name":"general","doc":"Experimental metadata, including protocol, notes and description of hardware device(s). The metadata stored in this section should be used to describe the experiment. Metadata necessary for interpreting the data is stored with the data. General experimental metadata, including animal strain, experimental protocols, experimenter, devices, etc, are stored under 'general'. Core metadata (e.g., that required to interpret data fields) is stored with the data itself, and implicitly defined by the file specification (e.g., time is in seconds). The strategy used here for storing non-core metadata is to use free-form text fields, such as would appear in sentences or paragraphs from a Methods section. Metadata fields are text to enable them to be more general, for example to represent ranges instead of numerical values. Machine-readable metadata is stored as attributes to these free-form datasets. All entries in the below table are to be included when data is present. Unused groups (e.g., intracellular_ephys in an optophysiology experiment) should not be created unless there is data to store within them.","datasets":[{"name":"data_collection","dtype":"text","doc":"Notes about data collection and analysis.","quantity":"?"},{"name":"experiment_description","dtype":"text","doc":"General description of the experiment.","quantity":"?"},{"name":"experimenter","dtype":"text","doc":"Name of person(s) who performed the experiment. Can also specify roles of different people involved.","quantity":"?","dims":["num_experimenters"],"shape":[null]},{"name":"institution","dtype":"text","doc":"Institution(s) where experiment was performed.","quantity":"?"},{"name":"keywords","dtype":"text","dims":["num_keywords"],"shape":[null],"doc":"Terms to search over.","quantity":"?"},{"name":"lab","dtype":"text","doc":"Laboratory where experiment was performed.","quantity":"?"},{"name":"notes","dtype":"text","doc":"Notes about the experiment.","quantity":"?"},{"name":"pharmacology","dtype":"text","doc":"Description of drugs used, including how and when they were administered. Anesthesia(s), painkiller(s), etc., plus dosage, concentration, etc.","quantity":"?"},{"name":"protocol","dtype":"text","doc":"Experimental protocol, if applicable. e.g., include IACUC protocol number.","quantity":"?"},{"name":"related_publications","dtype":"text","doc":"Publication information. PMID, DOI, URL, etc.","dims":["num_publications"],"shape":[null],"quantity":"?"},{"name":"session_id","dtype":"text","doc":"Lab-specific ID for the session.","quantity":"?"},{"name":"slices","dtype":"text","doc":"Description of slices, including information about preparation thickness, orientation, temperature, and bath solution.","quantity":"?"},{"name":"source_script","dtype":"text","doc":"Script file or link to public source code used to create this NWB file.","quantity":"?","attributes":[{"name":"file_name","dtype":"text","doc":"Name of script file."}]},{"name":"stimulus","dtype":"text","doc":"Notes about stimuli, such as how and where they were presented.","quantity":"?"},{"name":"surgery","dtype":"text","doc":"Narrative description about surgery/surgeries, including date(s) and who performed surgery.","quantity":"?"},{"name":"virus","dtype":"text","doc":"Information about virus(es) used in experiments, including virus ID, source, date made, injection location, volume, etc.","quantity":"?"}],"groups":[{"neurodata_type_inc":"LabMetaData","doc":"Place-holder than can be extended so that lab-specific meta-data can be placed in /general.","quantity":"*"},{"name":"devices","doc":"Description of hardware devices used during experiment, e.g., monitors, ADC boards, microscopes, etc.","quantity":"?","groups":[{"neurodata_type_inc":"Device","doc":"Data acquisition devices.","quantity":"*"}]},{"name":"subject","neurodata_type_inc":"Subject","doc":"Information about the animal or person from which the data was measured.","quantity":"?"},{"name":"extracellular_ephys","doc":"Metadata related to extracellular electrophysiology.","quantity":"?","groups":[{"neurodata_type_inc":"ElectrodeGroup","doc":"Physical group of electrodes.","quantity":"*"},{"name":"electrodes","neurodata_type_inc":"DynamicTable","doc":"A table of all electrodes (i.e. channels) used for recording.","quantity":"?","datasets":[{"name":"x","neurodata_type_inc":"VectorData","dtype":"float32","doc":"x coordinate of the channel location in the brain (+x is posterior).","quantity":"?"},{"name":"y","neurodata_type_inc":"VectorData","dtype":"float32","doc":"y coordinate of the channel location in the brain (+y is inferior).","quantity":"?"},{"name":"z","neurodata_type_inc":"VectorData","dtype":"float32","doc":"z coordinate of the channel location in the brain (+z is right).","quantity":"?"},{"name":"imp","neurodata_type_inc":"VectorData","dtype":"float32","doc":"Impedance of the channel, in ohms.","quantity":"?"},{"name":"location","neurodata_type_inc":"VectorData","dtype":"text","doc":"Location of the electrode (channel). Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."},{"name":"filtering","neurodata_type_inc":"VectorData","dtype":"text","doc":"Description of hardware filtering, including the filter name and frequency cutoffs.","quantity":"?"},{"name":"group","neurodata_type_inc":"VectorData","dtype":{"target_type":"ElectrodeGroup","reftype":"object"},"doc":"Reference to the ElectrodeGroup this electrode is a part of."},{"name":"group_name","neurodata_type_inc":"VectorData","dtype":"text","doc":"Name of the ElectrodeGroup this electrode is a part of."},{"name":"rel_x","neurodata_type_inc":"VectorData","dtype":"float32","doc":"x coordinate in electrode group","quantity":"?"},{"name":"rel_y","neurodata_type_inc":"VectorData","dtype":"float32","doc":"y coordinate in electrode group","quantity":"?"},{"name":"rel_z","neurodata_type_inc":"VectorData","dtype":"float32","doc":"z coordinate in electrode group","quantity":"?"},{"name":"reference","neurodata_type_inc":"VectorData","dtype":"text","doc":"Description of the reference electrode and/or reference scheme used for this electrode, e.g., \"stainless steel skull screw\" or \"online common average referencing\".","quantity":"?"}]}]},{"name":"intracellular_ephys","doc":"Metadata related to intracellular electrophysiology.","quantity":"?","datasets":[{"name":"filtering","dtype":"text","doc":"[DEPRECATED] Use IntracellularElectrode.filtering instead. Description of filtering used. Includes filtering type and parameters, frequency fall-off, etc. If this changes between TimeSeries, filter description should be stored as a text attribute for each TimeSeries.","quantity":"?"}],"groups":[{"neurodata_type_inc":"IntracellularElectrode","doc":"An intracellular electrode.","quantity":"*"},{"name":"sweep_table","neurodata_type_inc":"SweepTable","doc":"[DEPRECATED] Table used to group different PatchClampSeries. SweepTable is being replaced by IntracellularRecordingsTable and SimultaneousRecordingsTable tables. Additional SequentialRecordingsTable, RepetitionsTable and ExperimentalConditions tables provide enhanced support for experiment metadata.","quantity":"?"},{"name":"intracellular_recordings","neurodata_type_inc":"IntracellularRecordingsTable","doc":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording. Each row in the table represents a single recording consisting typically of a stimulus and a corresponding response. In some cases, however, only a stimulus or a response are recorded as as part of an experiment. In this case both, the stimulus and response will point to the same TimeSeries while the idx_start and count of the invalid column will be set to -1, thus, indicating that no values have been recorded for the stimulus or response, respectively. Note, a recording MUST contain at least a stimulus or a response. Typically the stimulus and response are PatchClampSeries. However, the use of AD/DA channels that are not associated to an electrode is also common in intracellular electrophysiology, in which case other TimeSeries may be used.","quantity":"?"},{"name":"simultaneous_recordings","neurodata_type_inc":"SimultaneousRecordingsTable","doc":"A table for grouping different intracellular recordings from the IntracellularRecordingsTable table together that were recorded simultaneously from different electrodes","quantity":"?"},{"name":"sequential_recordings","neurodata_type_inc":"SequentialRecordingsTable","doc":"A table for grouping different sequential recordings from the SimultaneousRecordingsTable table together. This is typically used to group together sequential recordings where the a sequence of stimuli of the same type with varying parameters have been presented in a sequence.","quantity":"?"},{"name":"repetitions","neurodata_type_inc":"RepetitionsTable","doc":"A table for grouping different sequential intracellular recordings together. With each SequentialRecording typically representing a particular type of stimulus, the RepetitionsTable table is typically used to group sets of stimuli applied in sequence.","quantity":"?"},{"name":"experimental_conditions","neurodata_type_inc":"ExperimentalConditionsTable","doc":"A table for grouping different intracellular recording repetitions together that belong to the same experimental experimental_conditions.","quantity":"?"}]},{"name":"optogenetics","doc":"Metadata describing optogenetic stimuluation.","quantity":"?","groups":[{"neurodata_type_inc":"OptogeneticStimulusSite","doc":"An optogenetic stimulation site.","quantity":"*"}]},{"name":"optophysiology","doc":"Metadata related to optophysiology.","quantity":"?","groups":[{"neurodata_type_inc":"ImagingPlane","doc":"An imaging plane.","quantity":"*"}]}]},{"name":"intervals","doc":"Experimental intervals, whether that be logically distinct sub-experiments having a particular scientific goal, trials (see trials subgroup) during an experiment, or epochs (see epochs subgroup) deriving from analysis of data.","quantity":"?","groups":[{"name":"epochs","neurodata_type_inc":"TimeIntervals","doc":"Divisions in time marking experimental stages or sub-divisions of a single recording session.","quantity":"?"},{"name":"trials","neurodata_type_inc":"TimeIntervals","doc":"Repeated experimental events that have a logical grouping.","quantity":"?"},{"name":"invalid_times","neurodata_type_inc":"TimeIntervals","doc":"Time intervals that should be removed from analysis.","quantity":"?"},{"neurodata_type_inc":"TimeIntervals","doc":"Optional additional table(s) for describing other experimental time intervals.","quantity":"*"}]},{"name":"units","neurodata_type_inc":"Units","doc":"Data about sorted spike units.","quantity":"?"}]},{"neurodata_type_def":"LabMetaData","neurodata_type_inc":"NWBContainer","doc":"Lab-specific meta-data."},{"neurodata_type_def":"Subject","neurodata_type_inc":"NWBContainer","doc":"Information about the animal or person from which the data was measured.","datasets":[{"name":"age","dtype":"text","doc":"Age of subject. Can be supplied instead of 'date_of_birth'.","quantity":"?","attributes":[{"name":"reference","doc":"Age is with reference to this event. Can be 'birth' or 'gestational'. If reference is omitted, 'birth' is implied.","dtype":"text","required":false,"default_value":"birth"}]},{"name":"date_of_birth","dtype":"isodatetime","doc":"Date of birth of subject. Can be supplied instead of 'age'.","quantity":"?"},{"name":"description","dtype":"text","doc":"Description of subject and where subject came from (e.g., breeder, if animal).","quantity":"?"},{"name":"genotype","dtype":"text","doc":"Genetic strain. If absent, assume Wild Type (WT).","quantity":"?"},{"name":"sex","dtype":"text","doc":"Gender of subject.","quantity":"?"},{"name":"species","dtype":"text","doc":"Species of subject.","quantity":"?"},{"name":"strain","dtype":"text","doc":"Strain of subject.","quantity":"?"},{"name":"subject_id","dtype":"text","doc":"ID of animal/person used/participating in experiment (lab convention).","quantity":"?"},{"name":"weight","dtype":"text","doc":"Weight at time of experiment, at time of surgery and at other important times.","quantity":"?"}]}],"datasets":[{"neurodata_type_def":"ScratchData","neurodata_type_inc":"NWBData","doc":"Any one-off datasets","attributes":[{"name":"notes","doc":"Any notes the user has about the dataset being stored","dtype":"text"}]}]})delimiter"; - -constexpr std::string_view nwb_misc = R"delimiter( -{"groups":[{"neurodata_type_def":"AbstractFeatureSeries","neurodata_type_inc":"TimeSeries","doc":"Abstract features, such as quantitative descriptions of sensory stimuli. The TimeSeries::data field is a 2D array, storing those features (e.g., for visual grating stimulus this might be orientation, spatial frequency and contrast). Null stimuli (eg, uniform gray) can be marked as being an independent feature (eg, 1.0 for gray, 0.0 for actual stimulus) or by storing NaNs for feature values, or through use of the TimeSeries::control fields. A set of features is considered to persist until the next set of features is defined. The final set of features stored should be the null set. This is useful when storing the raw stimulus is impractical.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_features"]],"shape":[[null],[null,null]],"doc":"Values of each feature at each time.","attributes":[{"name":"unit","dtype":"text","default_value":"see 'feature_units'","doc":"Since there can be different units for different features, store the units in 'feature_units'. The default value for this attribute is \"see 'feature_units'\".","required":false}]},{"name":"feature_units","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Units of each feature.","quantity":"?"},{"name":"features","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Description of the features represented in TimeSeries::data."}]},{"neurodata_type_def":"AnnotationSeries","neurodata_type_inc":"TimeSeries","doc":"Stores user annotations made during an experiment. The data[] field stores a text array, and timestamps are stored for each annotation (ie, interval=1). This is largely an alias to a standard TimeSeries storing a text array but that is identifiable as storing annotations in a machine-readable way.","datasets":[{"name":"data","dtype":"text","dims":["num_times"],"shape":[null],"doc":"Annotations made during an experiment.","attributes":[{"name":"resolution","dtype":"float32","value":-1.0,"doc":"Smallest meaningful difference between values in data. Annotations have no units, so the value is fixed to -1.0."},{"name":"unit","dtype":"text","value":"n/a","doc":"Base unit of measurement for working with the data. Annotations have no units, so the value is fixed to 'n/a'."}]}]},{"neurodata_type_def":"IntervalSeries","neurodata_type_inc":"TimeSeries","doc":"Stores intervals of data. The timestamps field stores the beginning and end of intervals. The data field stores whether the interval just started (>0 value) or ended (<0 value). Different interval types can be represented in the same series by using multiple key values (eg, 1 for feature A, 2 for feature B, 3 for feature C, etc). The field data stores an 8-bit integer. This is largely an alias of a standard TimeSeries but that is identifiable as representing time intervals in a machine-readable way.","datasets":[{"name":"data","dtype":"int8","dims":["num_times"],"shape":[null],"doc":"Use values >0 if interval started, <0 if interval ended.","attributes":[{"name":"resolution","dtype":"float32","value":-1.0,"doc":"Smallest meaningful difference between values in data. Annotations have no units, so the value is fixed to -1.0."},{"name":"unit","dtype":"text","value":"n/a","doc":"Base unit of measurement for working with the data. Annotations have no units, so the value is fixed to 'n/a'."}]}]},{"neurodata_type_def":"DecompositionSeries","neurodata_type_inc":"TimeSeries","doc":"Spectral analysis of a time series, e.g. of an LFP or a speech signal.","datasets":[{"name":"data","dtype":"numeric","dims":["num_times","num_channels","num_bands"],"shape":[null,null,null],"doc":"Data decomposed into frequency bands.","attributes":[{"name":"unit","dtype":"text","default_value":"no unit","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion'."}]},{"name":"metric","dtype":"text","doc":"The metric used, e.g. phase, amplitude, power."},{"name":"source_channels","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the channels that this decomposition series was generated from.","quantity":"?"}],"groups":[{"name":"bands","neurodata_type_inc":"DynamicTable","doc":"Table for describing the bands that this series was generated from. There should be one row in this table for each band.","datasets":[{"name":"band_name","neurodata_type_inc":"VectorData","dtype":"text","doc":"Name of the band, e.g. theta."},{"name":"band_limits","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands","low, high"],"shape":[null,2],"doc":"Low and high limit of each band in Hz. If it is a Gaussian filter, use 2 SD on either side of the center."},{"name":"band_mean","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands"],"shape":[null],"doc":"The mean Gaussian filters, in Hz."},{"name":"band_stdev","neurodata_type_inc":"VectorData","dtype":"float32","dims":["num_bands"],"shape":[null],"doc":"The standard deviation of Gaussian filters, in Hz."}]}],"links":[{"name":"source_timeseries","target_type":"TimeSeries","doc":"Link to TimeSeries object that this data was calculated from. Metadata about electrodes and their position can be read from that ElectricalSeries so it is not necessary to store that information here.","quantity":"?"}]},{"neurodata_type_def":"Units","neurodata_type_inc":"DynamicTable","default_name":"Units","doc":"Data about spiking units. Event times of observed units (e.g. cell, synapse, etc.) should be concatenated and stored in spike_times.","datasets":[{"name":"spike_times_index","neurodata_type_inc":"VectorIndex","doc":"Index into the spike_times dataset.","quantity":"?"},{"name":"spike_times","neurodata_type_inc":"VectorData","dtype":"float64","doc":"Spike times for each unit in seconds.","quantity":"?","attributes":[{"name":"resolution","dtype":"float64","doc":"The smallest possible difference between two spike times. Usually 1 divided by the acquisition sampling rate from which spike times were extracted, but could be larger if the acquisition time series was downsampled or smaller if the acquisition time series was smoothed/interpolated and it is possible for the spike time to be between samples.","required":false}]},{"name":"obs_intervals_index","neurodata_type_inc":"VectorIndex","doc":"Index into the obs_intervals dataset.","quantity":"?"},{"name":"obs_intervals","neurodata_type_inc":"VectorData","dtype":"float64","dims":["num_intervals","start|end"],"shape":[null,2],"doc":"Observation intervals for each unit.","quantity":"?"},{"name":"electrodes_index","neurodata_type_inc":"VectorIndex","doc":"Index into electrodes.","quantity":"?"},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"Electrode that each spike unit came from, specified using a DynamicTableRegion.","quantity":"?"},{"name":"electrode_group","neurodata_type_inc":"VectorData","dtype":{"target_type":"ElectrodeGroup","reftype":"object"},"doc":"Electrode group that each spike unit came from.","quantity":"?"},{"name":"waveform_mean","neurodata_type_inc":"VectorData","dtype":"float32","dims":[["num_units","num_samples"],["num_units","num_samples","num_electrodes"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveform mean for each spike unit.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveform_sd","neurodata_type_inc":"VectorData","dtype":"float32","dims":[["num_units","num_samples"],["num_units","num_samples","num_electrodes"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveform standard deviation for each spike unit.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveforms","neurodata_type_inc":"VectorData","dtype":"numeric","dims":["num_waveforms","num_samples"],"shape":[null,null],"doc":"Individual waveforms for each spike on each electrode. This is a doubly indexed column. The 'waveforms_index' column indexes which waveforms in this column belong to the same spike event for a given unit, where each waveform was recorded from a different electrode. The 'waveforms_index_index' column indexes the 'waveforms_index' column to indicate which spike events belong to a given unit. For example, if the 'waveforms_index_index' column has values [2, 5, 6], then the first 2 elements of the 'waveforms_index' column correspond to the 2 spike events of the first unit, the next 3 elements of the 'waveforms_index' column correspond to the 3 spike events of the second unit, and the next 1 element of the 'waveforms_index' column corresponds to the 1 spike event of the third unit. If the 'waveforms_index' column has values [3, 6, 8, 10, 12, 13], then the first 3 elements of the 'waveforms' column contain the 3 spike waveforms that were recorded from 3 different electrodes for the first spike time of the first unit. See https://nwb-schema.readthedocs.io/en/stable/format_description.html#doubly-ragged-arrays for a graphical representation of this example. When there is only one electrode for each unit (i.e., each spike time is associated with a single waveform), then the 'waveforms_index' column will have values 1, 2, ..., N, where N is the number of spike events. The number of electrodes for each spike event should be the same within a given unit. The 'electrodes' column should be used to indicate which electrodes are associated with each unit, and the order of the waveforms within a given unit x spike event should be in the same order as the electrodes referenced in the 'electrodes' column of this table. The number of samples for each waveform must be the same.","quantity":"?","attributes":[{"name":"sampling_rate","dtype":"float32","doc":"Sampling rate, in hertz.","required":false},{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement. This value is fixed to 'volts'.","required":false}]},{"name":"waveforms_index","neurodata_type_inc":"VectorIndex","doc":"Index into the waveforms dataset. One value for every spike event. See 'waveforms' for more detail.","quantity":"?"},{"name":"waveforms_index_index","neurodata_type_inc":"VectorIndex","doc":"Index into the waveforms_index dataset. One value for every unit (row in the table). See 'waveforms' for more detail.","quantity":"?"}]}]})delimiter"; - -constexpr std::string_view nwb_behavior = R"delimiter( -{"groups":[{"neurodata_type_def":"SpatialSeries","neurodata_type_inc":"TimeSeries","doc":"Direction, e.g., of gaze or travel, or position. The TimeSeries::data field is a 2D array storing position or direction relative to some reference frame. Array structure: [num measurements] [num dimensions]. Each SpatialSeries has a text dataset reference_frame that indicates the zero-position, or the zero-axes for direction. For example, if representing gaze direction, 'straight-ahead' might be a specific pixel on the monitor, or some other point in space. For position data, the 0,0 point might be the top-left corner of an enclosure, as viewed from the tracking camera. The unit of data will indicate how to interpret SpatialSeries values.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","x"],["num_times","x,y"],["num_times","x,y,z"]],"shape":[[null],[null,1],[null,2],[null,3]],"doc":"1-D or 2-D array storing position or direction relative to some reference frame.","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Base unit of measurement for working with the data. The default value is 'meters'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'.","required":false}]},{"name":"reference_frame","dtype":"text","doc":"Description defining what exactly 'straight-ahead' means.","quantity":"?"}]},{"neurodata_type_def":"BehavioralEpochs","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralEpochs","doc":"TimeSeries for storing behavioral epochs. The objective of this and the other two Behavioral interfaces (e.g. BehavioralEvents and BehavioralTimeSeries) is to provide generic hooks for software tools/scripts. This allows a tool/script to take the output one specific interface (e.g., UnitTimes) and plot that data relative to another data modality (e.g., behavioral events) without having to define all possible modalities in advance. Declaring one of these interfaces means that one or more TimeSeries of the specified type is published. These TimeSeries should reside in a group having the same name as the interface. For example, if a BehavioralTimeSeries interface is declared, the module will have one or more TimeSeries defined in the module sub-group 'BehavioralTimeSeries'. BehavioralEpochs should use IntervalSeries. BehavioralEvents is used for irregular events. BehavioralTimeSeries is for continuous data.","groups":[{"neurodata_type_inc":"IntervalSeries","doc":"IntervalSeries object containing start and stop times of epochs.","quantity":"*"}]},{"neurodata_type_def":"BehavioralEvents","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralEvents","doc":"TimeSeries for storing behavioral events. See description of BehavioralEpochs for more details.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing behavioral events.","quantity":"*"}]},{"neurodata_type_def":"BehavioralTimeSeries","neurodata_type_inc":"NWBDataInterface","default_name":"BehavioralTimeSeries","doc":"TimeSeries for storing Behavoioral time series data. See description of BehavioralEpochs for more details.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing continuous behavioral data.","quantity":"*"}]},{"neurodata_type_def":"PupilTracking","neurodata_type_inc":"NWBDataInterface","default_name":"PupilTracking","doc":"Eye-tracking data, representing pupil size.","groups":[{"neurodata_type_inc":"TimeSeries","doc":"TimeSeries object containing time series data on pupil size.","quantity":"+"}]},{"neurodata_type_def":"EyeTracking","neurodata_type_inc":"NWBDataInterface","default_name":"EyeTracking","doc":"Eye-tracking data, representing direction of gaze.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing data measuring direction of gaze.","quantity":"*"}]},{"neurodata_type_def":"CompassDirection","neurodata_type_inc":"NWBDataInterface","default_name":"CompassDirection","doc":"With a CompassDirection interface, a module publishes a SpatialSeries object representing a floating point value for theta. The SpatialSeries::reference_frame field should indicate what direction corresponds to 0 and which is the direction of rotation (this should be clockwise). The si_unit for the SpatialSeries should be radians or degrees.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing direction of gaze travel.","quantity":"*"}]},{"neurodata_type_def":"Position","neurodata_type_inc":"NWBDataInterface","default_name":"Position","doc":"Position data, whether along the x, x/y or x/y/z axis.","groups":[{"neurodata_type_inc":"SpatialSeries","doc":"SpatialSeries object containing position data.","quantity":"+"}]}]})delimiter"; - -constexpr std::string_view nwb_ecephys = R"delimiter( -{"groups":[{"neurodata_type_def":"ElectricalSeries","neurodata_type_inc":"TimeSeries","doc":"A time series of acquired voltage data from extracellular recordings. The data field is an int or float array storing data in volts. The first dimension should always represent time. The second dimension, if present, should represent channels.","attributes":[{"name":"filtering","dtype":"text","doc":"Filtering applied to all channels of the data. For example, if this ElectricalSeries represents high-pass-filtered data (also known as AP Band), then this value could be \"High-pass 4-pole Bessel filter at 500 Hz\". If this ElectricalSeries represents low-pass-filtered LFP data and the type of filter is unknown, then this value could be \"Low-pass filter at 300 Hz\". If a non-standard filter type is used, provide as much detail about the filter properties as possible.","required":false}],"datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_channels"],["num_times","num_channels","num_samples"]],"shape":[[null],[null,null],[null,null,null]],"doc":"Recorded voltage data.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. This value is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion', followed by 'channel_conversion' (if present), and then add 'offset'."}]},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the electrodes that this time series was generated from."},{"name":"channel_conversion","dtype":"float32","dims":["num_channels"],"shape":[null],"doc":"Channel-specific conversion factor. Multiply the data in the 'data' dataset by these values along the channel axis (as indicated by axis attribute) AND by the global conversion factor in the 'conversion' attribute of 'data' to get the data values in Volts, i.e, data in Volts = data * data.conversion * channel_conversion. This approach allows for both global and per-channel data conversion factors needed to support the storage of electrical recordings as native values generated by data acquisition systems. If this dataset is not present, then there is no channel-specific conversion factor, i.e. it is 1 for all channels.","quantity":"?","attributes":[{"name":"axis","dtype":"int32","value":1,"doc":"The zero-indexed axis of the 'data' dataset that the channel-specific conversion factor corresponds to. This value is fixed to 1."}]}]},{"neurodata_type_def":"SpikeEventSeries","neurodata_type_inc":"ElectricalSeries","doc":"Stores snapshots/snippets of recorded spike events (i.e., threshold crossings). This may also be raw data, as reported by ephys hardware. If so, the TimeSeries::description field should describe how events were detected. All SpikeEventSeries should reside in a module (under EventWaveform interface) even if the spikes were reported and stored by hardware. All events span the same recording channels and store snapshots of equal duration. TimeSeries::data array structure: [num events] [num channels] [num samples] (or [num events] [num samples] for single electrode).","datasets":[{"name":"data","dtype":"numeric","dims":[["num_events","num_samples"],["num_events","num_channels","num_samples"]],"shape":[[null,null],[null,null,null]],"doc":"Spike waveforms.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Unit of measurement for waveforms, which is fixed to 'volts'."}]},{"name":"timestamps","dtype":"float64","dims":["num_times"],"shape":[null],"doc":"Timestamps for samples stored in data, in seconds, relative to the common experiment master-clock stored in NWBFile.timestamps_reference_time. Timestamps are required for the events. Unlike for TimeSeries, timestamps are required for SpikeEventSeries and are thus re-specified here.","attributes":[{"name":"interval","dtype":"int32","value":1,"doc":"Value is '1'"},{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for timestamps, which is fixed to 'seconds'."}]}]},{"neurodata_type_def":"FeatureExtraction","neurodata_type_inc":"NWBDataInterface","default_name":"FeatureExtraction","doc":"Features, such as PC1 and PC2, that are extracted from signals stored in a SpikeEventSeries or other source.","datasets":[{"name":"description","dtype":"text","dims":["num_features"],"shape":[null],"doc":"Description of features (eg, ''PC1'') for each of the extracted features."},{"name":"features","dtype":"float32","dims":["num_events","num_channels","num_features"],"shape":[null,null,null],"doc":"Multi-dimensional array of features extracted from each event."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Times of events that features correspond to (can be a link)."},{"name":"electrodes","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion pointer to the electrodes that this time series was generated from."}]},{"neurodata_type_def":"EventDetection","neurodata_type_inc":"NWBDataInterface","default_name":"EventDetection","doc":"Detected spike events from voltage trace(s).","datasets":[{"name":"detection_method","dtype":"text","doc":"Description of how events were detected, such as voltage threshold, or dV/dT threshold, as well as relevant values."},{"name":"source_idx","dtype":"int32","dims":["num_events"],"shape":[null],"doc":"Indices (zero-based) into source ElectricalSeries::data array corresponding to time of event. ''description'' should define what is meant by time of event (e.g., .25 ms before action potential peak, zero-crossing time, etc). The index points to each event from the raw data."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Timestamps of events, in seconds.","attributes":[{"name":"unit","dtype":"text","value":"seconds","doc":"Unit of measurement for event times, which is fixed to 'seconds'."}]}],"links":[{"name":"source_electricalseries","target_type":"ElectricalSeries","doc":"Link to the ElectricalSeries that this data was calculated from. Metadata about electrodes and their position can be read from that ElectricalSeries so it's not necessary to include that information here."}]},{"neurodata_type_def":"EventWaveform","neurodata_type_inc":"NWBDataInterface","default_name":"EventWaveform","doc":"Represents either the waveforms of detected events, as extracted from a raw data trace in /acquisition, or the event waveforms that were stored during experiment acquisition.","groups":[{"neurodata_type_inc":"SpikeEventSeries","doc":"SpikeEventSeries object(s) containing detected spike event waveforms.","quantity":"*"}]},{"neurodata_type_def":"FilteredEphys","neurodata_type_inc":"NWBDataInterface","default_name":"FilteredEphys","doc":"Electrophysiology data from one or more channels that has been subjected to filtering. Examples of filtered data include Theta and Gamma (LFP has its own interface). FilteredEphys modules publish an ElectricalSeries for each filtered channel or set of channels. The name of each ElectricalSeries is arbitrary but should be informative. The source of the filtered data, whether this is from analysis of another time series or as acquired by hardware, should be noted in each's TimeSeries::description field. There is no assumed 1::1 correspondence between filtered ephys signals and electrodes, as a single signal can apply to many nearby electrodes, and one electrode may have different filtered (e.g., theta and/or gamma) signals represented. Filter properties should be noted in the ElectricalSeries 'filtering' attribute.","groups":[{"neurodata_type_inc":"ElectricalSeries","doc":"ElectricalSeries object(s) containing filtered electrophysiology data.","quantity":"+"}]},{"neurodata_type_def":"LFP","neurodata_type_inc":"NWBDataInterface","default_name":"LFP","doc":"LFP data from one or more channels. The electrode map in each published ElectricalSeries will identify which channels are providing LFP data. Filter properties should be noted in the ElectricalSeries 'filtering' attribute.","groups":[{"neurodata_type_inc":"ElectricalSeries","doc":"ElectricalSeries object(s) containing LFP data for one or more channels.","quantity":"+"}]},{"neurodata_type_def":"ElectrodeGroup","neurodata_type_inc":"NWBContainer","doc":"A physical grouping of electrodes, e.g. a shank of an array.","attributes":[{"name":"description","dtype":"text","doc":"Description of this electrode group."},{"name":"location","dtype":"text","doc":"Location of electrode group. Specify the area, layer, comments on estimation of area/layer, etc. Use standard atlas names for anatomical regions when possible."}],"datasets":[{"name":"position","dtype":[{"name":"x","dtype":"float32","doc":"x coordinate"},{"name":"y","dtype":"float32","doc":"y coordinate"},{"name":"z","dtype":"float32","doc":"z coordinate"}],"doc":"stereotaxic or common framework coordinates","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the device that was used to record from this electrode group."}]},{"neurodata_type_def":"ClusterWaveforms","neurodata_type_inc":"NWBDataInterface","default_name":"ClusterWaveforms","doc":"DEPRECATED The mean waveform shape, including standard deviation, of the different clusters. Ideally, the waveform analysis should be performed on data that is only high-pass filtered. This is a separate module because it is expected to require updating. For example, IMEC probes may require different storage requirements to store/display mean waveforms, requiring a new interface or an extension of this one.","datasets":[{"name":"waveform_filtering","dtype":"text","doc":"Filtering applied to data before generating mean/sd"},{"name":"waveform_mean","dtype":"float32","dims":["num_clusters","num_samples"],"shape":[null,null],"doc":"The mean waveform for each cluster, using the same indices for each wave as cluster numbers in the associated Clustering module (i.e, cluster 3 is in array slot [3]). Waveforms corresponding to gaps in cluster sequence should be empty (e.g., zero- filled)"},{"name":"waveform_sd","dtype":"float32","dims":["num_clusters","num_samples"],"shape":[null,null],"doc":"Stdev of waveforms for each cluster, using the same indices as in mean"}],"links":[{"name":"clustering_interface","target_type":"Clustering","doc":"Link to Clustering interface that was the source of the clustered data"}]},{"neurodata_type_def":"Clustering","neurodata_type_inc":"NWBDataInterface","default_name":"Clustering","doc":"DEPRECATED Clustered spike data, whether from automatic clustering tools (e.g., klustakwik) or as a result of manual sorting.","datasets":[{"name":"description","dtype":"text","doc":"Description of clusters or clustering, (e.g. cluster 0 is noise, clusters curated using Klusters, etc)"},{"name":"num","dtype":"int32","dims":["num_events"],"shape":[null],"doc":"Cluster number of each event"},{"name":"peak_over_rms","dtype":"float32","dims":["num_clusters"],"shape":[null],"doc":"Maximum ratio of waveform peak to RMS on any channel in the cluster (provides a basic clustering metric)."},{"name":"times","dtype":"float64","dims":["num_events"],"shape":[null],"doc":"Times of clustered events, in seconds. This may be a link to times field in associated FeatureExtraction module."}]}]})delimiter"; - -constexpr std::string_view nwb_icephys = R"delimiter( -{"groups":[{"neurodata_type_def":"PatchClampSeries","neurodata_type_inc":"TimeSeries","doc":"An abstract base class for patch-clamp data - stimulus or response, current or voltage.","attributes":[{"name":"stimulus_description","dtype":"text","doc":"Protocol/stimulus name for this patch-clamp dataset."},{"name":"sweep_number","dtype":"uint32","doc":"Sweep number, allows to group different PatchClampSeries together.","required":false}],"datasets":[{"name":"data","dtype":"numeric","dims":["num_times"],"shape":[null],"doc":"Recorded voltage or current.","attributes":[{"name":"unit","dtype":"text","doc":"Base unit of measurement for working with the data. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"gain","dtype":"float32","doc":"Gain of the recording, in units Volt/Amp (v-clamp) or Volt/Volt (c-clamp).","quantity":"?"}],"links":[{"name":"electrode","target_type":"IntracellularElectrode","doc":"Link to IntracellularElectrode object that describes the electrode that was used to apply or record this data."}]},{"neurodata_type_def":"CurrentClampSeries","neurodata_type_inc":"PatchClampSeries","doc":"Voltage data from an intracellular current-clamp recording. A corresponding CurrentClampStimulusSeries (stored separately as a stimulus) is used to store the current injected.","datasets":[{"name":"data","doc":"Recorded voltage.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. which is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"bias_current","dtype":"float32","doc":"Bias current, in amps.","quantity":"?"},{"name":"bridge_balance","dtype":"float32","doc":"Bridge balance, in ohms.","quantity":"?"},{"name":"capacitance_compensation","dtype":"float32","doc":"Capacitance compensation, in farads.","quantity":"?"}]},{"neurodata_type_def":"IZeroClampSeries","neurodata_type_inc":"CurrentClampSeries","doc":"Voltage data from an intracellular recording when all current and amplifier settings are off (i.e., CurrentClampSeries fields will be zero). There is no CurrentClampStimulusSeries associated with an IZero series because the amplifier is disconnected and no stimulus can reach the cell.","attributes":[{"name":"stimulus_description","dtype":"text","doc":"An IZeroClampSeries has no stimulus, so this attribute is automatically set to \"N/A\"","value":"N/A"}],"datasets":[{"name":"bias_current","dtype":"float32","value":0.0,"doc":"Bias current, in amps, fixed to 0.0."},{"name":"bridge_balance","dtype":"float32","value":0.0,"doc":"Bridge balance, in ohms, fixed to 0.0."},{"name":"capacitance_compensation","dtype":"float32","value":0.0,"doc":"Capacitance compensation, in farads, fixed to 0.0."}]},{"neurodata_type_def":"CurrentClampStimulusSeries","neurodata_type_inc":"PatchClampSeries","doc":"Stimulus current applied during current clamp recording.","datasets":[{"name":"data","doc":"Stimulus current applied.","attributes":[{"name":"unit","dtype":"text","value":"amperes","doc":"Base unit of measurement for working with the data. which is fixed to 'amperes'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]}]},{"neurodata_type_def":"VoltageClampSeries","neurodata_type_inc":"PatchClampSeries","doc":"Current data from an intracellular voltage-clamp recording. A corresponding VoltageClampStimulusSeries (stored separately as a stimulus) is used to store the voltage injected.","datasets":[{"name":"data","doc":"Recorded current.","attributes":[{"name":"unit","dtype":"text","value":"amperes","doc":"Base unit of measurement for working with the data. which is fixed to 'amperes'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]},{"name":"capacitance_fast","dtype":"float32","doc":"Fast capacitance, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for capacitance_fast, which is fixed to 'farads'."}]},{"name":"capacitance_slow","dtype":"float32","doc":"Slow capacitance, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for capacitance_fast, which is fixed to 'farads'."}]},{"name":"resistance_comp_bandwidth","dtype":"float32","doc":"Resistance compensation bandwidth, in hertz.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"hertz","doc":"Unit of measurement for resistance_comp_bandwidth, which is fixed to 'hertz'."}]},{"name":"resistance_comp_correction","dtype":"float32","doc":"Resistance compensation correction, in percent.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"percent","doc":"Unit of measurement for resistance_comp_correction, which is fixed to 'percent'."}]},{"name":"resistance_comp_prediction","dtype":"float32","doc":"Resistance compensation prediction, in percent.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"percent","doc":"Unit of measurement for resistance_comp_prediction, which is fixed to 'percent'."}]},{"name":"whole_cell_capacitance_comp","dtype":"float32","doc":"Whole cell capacitance compensation, in farads.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"farads","doc":"Unit of measurement for whole_cell_capacitance_comp, which is fixed to 'farads'."}]},{"name":"whole_cell_series_resistance_comp","dtype":"float32","doc":"Whole cell series resistance compensation, in ohms.","quantity":"?","attributes":[{"name":"unit","dtype":"text","value":"ohms","doc":"Unit of measurement for whole_cell_series_resistance_comp, which is fixed to 'ohms'."}]}]},{"neurodata_type_def":"VoltageClampStimulusSeries","neurodata_type_inc":"PatchClampSeries","doc":"Stimulus voltage applied during a voltage clamp recording.","datasets":[{"name":"data","doc":"Stimulus voltage applied.","attributes":[{"name":"unit","dtype":"text","value":"volts","doc":"Base unit of measurement for working with the data. which is fixed to 'volts'. Actual stored values are not necessarily stored in these units. To access the data in these units, multiply 'data' by 'conversion' and add 'offset'."}]}]},{"neurodata_type_def":"IntracellularElectrode","neurodata_type_inc":"NWBContainer","doc":"An intracellular electrode and its metadata.","datasets":[{"name":"cell_id","dtype":"text","doc":"unique ID of the cell","quantity":"?"},{"name":"description","dtype":"text","doc":"Description of electrode (e.g., whole-cell, sharp, etc.)."},{"name":"filtering","dtype":"text","doc":"Electrode specific filtering.","quantity":"?"},{"name":"initial_access_resistance","dtype":"text","doc":"Initial access resistance.","quantity":"?"},{"name":"location","dtype":"text","doc":"Location of the electrode. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible.","quantity":"?"},{"name":"resistance","dtype":"text","doc":"Electrode resistance, in ohms.","quantity":"?"},{"name":"seal","dtype":"text","doc":"Information about seal used for recording.","quantity":"?"},{"name":"slice","dtype":"text","doc":"Information about slice used for recording.","quantity":"?"}],"links":[{"name":"device","target_type":"Device","doc":"Device that was used to record from this electrode."}]},{"neurodata_type_def":"SweepTable","neurodata_type_inc":"DynamicTable","doc":"[DEPRECATED] Table used to group different PatchClampSeries. SweepTable is being replaced by IntracellularRecordingsTable and SimultaneousRecordingsTable tables. Additional SequentialRecordingsTable, RepetitionsTable, and ExperimentalConditions tables provide enhanced support for experiment metadata.","datasets":[{"name":"sweep_number","neurodata_type_inc":"VectorData","dtype":"uint32","doc":"Sweep number of the PatchClampSeries in that row."},{"name":"series","neurodata_type_inc":"VectorData","dtype":{"target_type":"PatchClampSeries","reftype":"object"},"doc":"The PatchClampSeries with the sweep number in that row."},{"name":"series_index","neurodata_type_inc":"VectorIndex","doc":"Index for series."}]},{"neurodata_type_def":"IntracellularElectrodesTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular electrode related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular electrode related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"electrode","neurodata_type_inc":"VectorData","dtype":{"target_type":"IntracellularElectrode","reftype":"object"},"doc":"Column for storing the reference to the intracellular electrode."}]},{"neurodata_type_def":"IntracellularStimuliTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular stimulus related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular stimulus related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"stimulus","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the recorded stimulus for the recording (rows)."},{"name":"stimulus_template","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the stimulus template for the recording (rows).","quantity":"?"}]},{"neurodata_type_def":"IntracellularResponsesTable","neurodata_type_inc":"DynamicTable","doc":"Table for storing intracellular response related metadata.","attributes":[{"name":"description","dtype":"text","value":"Table for storing intracellular response related metadata.","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"response","neurodata_type_inc":"TimeSeriesReferenceVectorData","doc":"Column storing the reference to the recorded response for the recording (rows)"}]},{"neurodata_type_def":"IntracellularRecordingsTable","neurodata_type_inc":"AlignedDynamicTable","name":"intracellular_recordings","doc":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording. Each row in the table represents a single recording consisting typically of a stimulus and a corresponding response. In some cases, however, only a stimulus or a response is recorded as part of an experiment. In this case, both the stimulus and response will point to the same TimeSeries while the idx_start and count of the invalid column will be set to -1, thus, indicating that no values have been recorded for the stimulus or response, respectively. Note, a recording MUST contain at least a stimulus or a response. Typically the stimulus and response are PatchClampSeries. However, the use of AD/DA channels that are not associated to an electrode is also common in intracellular electrophysiology, in which case other TimeSeries may be used.","attributes":[{"name":"description","dtype":"text","value":"A table to group together a stimulus and response from a single electrode and a single simultaneous recording and for storing metadata about the intracellular recording.","doc":"Description of the contents of this table. Inherited from AlignedDynamicTable and overwritten here to fix the value of the attribute."}],"groups":[{"name":"electrodes","neurodata_type_inc":"IntracellularElectrodesTable","doc":"Table for storing intracellular electrode related metadata."},{"name":"stimuli","neurodata_type_inc":"IntracellularStimuliTable","doc":"Table for storing intracellular stimulus related metadata."},{"name":"responses","neurodata_type_inc":"IntracellularResponsesTable","doc":"Table for storing intracellular response related metadata."}]},{"neurodata_type_def":"SimultaneousRecordingsTable","neurodata_type_inc":"DynamicTable","name":"simultaneous_recordings","doc":"A table for grouping different intracellular recordings from the IntracellularRecordingsTable table together that were recorded simultaneously from different electrodes.","datasets":[{"name":"recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the IntracellularRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"IntracellularRecordingsTable","reftype":"object"},"doc":"Reference to the IntracellularRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the recordings column."}]},{"neurodata_type_def":"SequentialRecordingsTable","neurodata_type_inc":"DynamicTable","name":"sequential_recordings","doc":"A table for grouping different sequential recordings from the SimultaneousRecordingsTable table together. This is typically used to group together sequential recordings where a sequence of stimuli of the same type with varying parameters have been presented in a sequence.","datasets":[{"name":"simultaneous_recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the SimultaneousRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"SimultaneousRecordingsTable","reftype":"object"},"doc":"Reference to the SimultaneousRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"simultaneous_recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the simultaneous_recordings column."},{"name":"stimulus_type","neurodata_type_inc":"VectorData","dtype":"text","doc":"The type of stimulus used for the sequential recording."}]},{"neurodata_type_def":"RepetitionsTable","neurodata_type_inc":"DynamicTable","name":"repetitions","doc":"A table for grouping different sequential intracellular recordings together. With each SequentialRecording typically representing a particular type of stimulus, the RepetitionsTable table is typically used to group sets of stimuli applied in sequence.","datasets":[{"name":"sequential_recordings","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the SequentialRecordingsTable table.","attributes":[{"name":"table","dtype":{"target_type":"SequentialRecordingsTable","reftype":"object"},"doc":"Reference to the SequentialRecordingsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"sequential_recordings_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the sequential_recordings column."}]},{"neurodata_type_def":"ExperimentalConditionsTable","neurodata_type_inc":"DynamicTable","name":"experimental_conditions","doc":"A table for grouping different intracellular recording repetitions together that belong to the same experimental condition.","datasets":[{"name":"repetitions","neurodata_type_inc":"DynamicTableRegion","doc":"A reference to one or more rows in the RepetitionsTable table.","attributes":[{"name":"table","dtype":{"target_type":"RepetitionsTable","reftype":"object"},"doc":"Reference to the RepetitionsTable table that this table region applies to. This specializes the attribute inherited from DynamicTableRegion to fix the type of table that can be referenced here."}]},{"name":"repetitions_index","neurodata_type_inc":"VectorIndex","doc":"Index dataset for the repetitions column."}]}]})delimiter"; - -constexpr std::string_view nwb_ogen = R"delimiter( -{"groups":[{"neurodata_type_def":"OptogeneticSeries","neurodata_type_inc":"TimeSeries","doc":"An optogenetic stimulus.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_rois"]],"shape":[[null],[null,null]],"doc":"Applied power for optogenetic stimulus, in watts. Shape can be 1D or 2D. 2D data is meant to be used in an extension of OptogeneticSeries that defines what the second dimension represents.","attributes":[{"name":"unit","dtype":"text","value":"watts","doc":"Unit of measurement for data, which is fixed to 'watts'."}]}],"links":[{"name":"site","target_type":"OptogeneticStimulusSite","doc":"Link to OptogeneticStimulusSite object that describes the site to which this stimulus was applied."}]},{"neurodata_type_def":"OptogeneticStimulusSite","neurodata_type_inc":"NWBContainer","doc":"A site of optogenetic stimulation.","datasets":[{"name":"description","dtype":"text","doc":"Description of stimulation site."},{"name":"excitation_lambda","dtype":"float32","doc":"Excitation wavelength, in nm."},{"name":"location","dtype":"text","doc":"Location of the stimulation site. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."}],"links":[{"name":"device","target_type":"Device","doc":"Device that generated the stimulus."}]}]})delimiter"; - -constexpr std::string_view nwb_ophys = R"delimiter( -{"groups":[{"neurodata_type_def":"OnePhotonSeries","neurodata_type_inc":"ImageSeries","doc":"Image stack recorded over time from 1-photon microscope.","attributes":[{"name":"pmt_gain","dtype":"float32","doc":"Photomultiplier gain.","required":false},{"name":"scan_line_rate","dtype":"float32","doc":"Lines imaged per second. This is also stored in /general/optophysiology but is kept here as it is useful information for analysis, and so good to be stored w/ the actual data.","required":false},{"name":"exposure_time","dtype":"float32","doc":"Exposure time of the sample; often the inverse of the frequency.","required":false},{"name":"binning","dtype":"uint8","doc":"Amount of pixels combined into 'bins'; could be 1, 2, 4, 8, etc.","required":false},{"name":"power","dtype":"float32","doc":"Power of the excitation in mW, if known.","required":false},{"name":"intensity","dtype":"float32","doc":"Intensity of the excitation in mW/mm^2, if known.","required":false}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this TimeSeries data was generated."}]},{"neurodata_type_def":"TwoPhotonSeries","neurodata_type_inc":"ImageSeries","doc":"Image stack recorded over time from 2-photon microscope.","attributes":[{"name":"pmt_gain","dtype":"float32","doc":"Photomultiplier gain.","required":false},{"name":"scan_line_rate","dtype":"float32","doc":"Lines imaged per second. This is also stored in /general/optophysiology but is kept here as it is useful information for analysis, and so good to be stored w/ the actual data.","required":false}],"datasets":[{"name":"field_of_view","dtype":"float32","dims":[["width|height"],["width|height|depth"]],"shape":[[2],[3]],"doc":"Width, height and depth of image, or imaged area, in meters.","quantity":"?"}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this TimeSeries data was generated."}]},{"neurodata_type_def":"RoiResponseSeries","neurodata_type_inc":"TimeSeries","doc":"ROI responses over an imaging plane. The first dimension represents time. The second dimension, if present, represents ROIs.","datasets":[{"name":"data","dtype":"numeric","dims":[["num_times"],["num_times","num_ROIs"]],"shape":[[null],[null,null]],"doc":"Signals from ROIs."},{"name":"rois","neurodata_type_inc":"DynamicTableRegion","doc":"DynamicTableRegion referencing into an ROITable containing information on the ROIs stored in this timeseries."}]},{"neurodata_type_def":"DfOverF","neurodata_type_inc":"NWBDataInterface","default_name":"DfOverF","doc":"dF/F information about a region of interest (ROI). Storage hierarchy of dF/F should be the same as for segmentation (i.e., same names for ROIs and for image planes).","groups":[{"neurodata_type_inc":"RoiResponseSeries","doc":"RoiResponseSeries object(s) containing dF/F for a ROI.","quantity":"+"}]},{"neurodata_type_def":"Fluorescence","neurodata_type_inc":"NWBDataInterface","default_name":"Fluorescence","doc":"Fluorescence information about a region of interest (ROI). Storage hierarchy of fluorescence should be the same as for segmentation (ie, same names for ROIs and for image planes).","groups":[{"neurodata_type_inc":"RoiResponseSeries","doc":"RoiResponseSeries object(s) containing fluorescence data for a ROI.","quantity":"+"}]},{"neurodata_type_def":"ImageSegmentation","neurodata_type_inc":"NWBDataInterface","default_name":"ImageSegmentation","doc":"Stores pixels in an image that represent different regions of interest (ROIs) or masks. All segmentation for a given imaging plane is stored together, with storage for multiple imaging planes (masks) supported. Each ROI is stored in its own subgroup, with the ROI group containing both a 2D mask and a list of pixels that make up this mask. Segments can also be used for masking neuropil. If segmentation is allowed to change with time, a new imaging plane (or module) is required and ROI names should remain consistent between them.","groups":[{"neurodata_type_inc":"PlaneSegmentation","doc":"Results from image segmentation of a specific imaging plane.","quantity":"+"}]},{"neurodata_type_def":"PlaneSegmentation","neurodata_type_inc":"DynamicTable","doc":"Results from image segmentation of a specific imaging plane.","datasets":[{"name":"image_mask","neurodata_type_inc":"VectorData","dims":[["num_roi","num_x","num_y"],["num_roi","num_x","num_y","num_z"]],"shape":[[null,null,null],[null,null,null,null]],"doc":"ROI masks for each ROI. Each image mask is the size of the original imaging plane (or volume) and members of the ROI are finite non-zero.","quantity":"?"},{"name":"pixel_mask_index","neurodata_type_inc":"VectorIndex","doc":"Index into pixel_mask.","quantity":"?"},{"name":"pixel_mask","neurodata_type_inc":"VectorData","dtype":[{"name":"x","dtype":"uint32","doc":"Pixel x-coordinate."},{"name":"y","dtype":"uint32","doc":"Pixel y-coordinate."},{"name":"weight","dtype":"float32","doc":"Weight of the pixel."}],"doc":"Pixel masks for each ROI: a list of indices and weights for the ROI. Pixel masks are concatenated and parsing of this dataset is maintained by the PlaneSegmentation","quantity":"?"},{"name":"voxel_mask_index","neurodata_type_inc":"VectorIndex","doc":"Index into voxel_mask.","quantity":"?"},{"name":"voxel_mask","neurodata_type_inc":"VectorData","dtype":[{"name":"x","dtype":"uint32","doc":"Voxel x-coordinate."},{"name":"y","dtype":"uint32","doc":"Voxel y-coordinate."},{"name":"z","dtype":"uint32","doc":"Voxel z-coordinate."},{"name":"weight","dtype":"float32","doc":"Weight of the voxel."}],"doc":"Voxel masks for each ROI: a list of indices and weights for the ROI. Voxel masks are concatenated and parsing of this dataset is maintained by the PlaneSegmentation","quantity":"?"}],"groups":[{"name":"reference_images","doc":"Image stacks that the segmentation masks apply to.","groups":[{"neurodata_type_inc":"ImageSeries","doc":"One or more image stacks that the masks apply to (can be one-element stack).","quantity":"*"}]}],"links":[{"name":"imaging_plane","target_type":"ImagingPlane","doc":"Link to ImagingPlane object from which this data was generated."}]},{"neurodata_type_def":"ImagingPlane","neurodata_type_inc":"NWBContainer","doc":"An imaging plane and its metadata.","datasets":[{"name":"description","dtype":"text","doc":"Description of the imaging plane.","quantity":"?"},{"name":"excitation_lambda","dtype":"float32","doc":"Excitation wavelength, in nm."},{"name":"imaging_rate","dtype":"float32","doc":"Rate that images are acquired, in Hz. If the corresponding TimeSeries is present, the rate should be stored there instead.","quantity":"?"},{"name":"indicator","dtype":"text","doc":"Calcium indicator."},{"name":"location","dtype":"text","doc":"Location of the imaging plane. Specify the area, layer, comments on estimation of area/layer, stereotaxic coordinates if in vivo, etc. Use standard atlas names for anatomical regions when possible."},{"name":"manifold","dtype":"float32","dims":[["height","width","x, y, z"],["height","width","depth","x, y, z"]],"shape":[[null,null,3],[null,null,null,3]],"doc":"DEPRECATED Physical position of each pixel. 'xyz' represents the position of the pixel relative to the defined coordinate space. Deprecated in favor of origin_coords and grid_spacing.","quantity":"?","attributes":[{"name":"conversion","dtype":"float32","default_value":1.0,"doc":"Scalar to multiply each element in data to convert it to the specified 'unit'. If the data are stored in acquisition system units or other units that require a conversion to be interpretable, multiply the data by 'conversion' to convert the data to the specified 'unit'. e.g. if the data acquisition system stores values in this object as pixels from x = -500 to 499, y = -500 to 499 that correspond to a 2 m x 2 m range, then the 'conversion' multiplier to get from raw data acquisition pixel units to meters is 2/1000.","required":false},{"name":"unit","dtype":"text","default_value":"meters","doc":"Base unit of measurement for working with the data. The default value is 'meters'.","required":false}]},{"name":"origin_coords","dtype":"float32","dims":[["x, y"],["x, y, z"]],"shape":[[2],[3]],"doc":"Physical location of the first element of the imaging plane (0, 0) for 2-D data or (0, 0, 0) for 3-D data. See also reference_frame for what the physical location is relative to (e.g., bregma).","quantity":"?","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Measurement units for origin_coords. The default value is 'meters'."}]},{"name":"grid_spacing","dtype":"float32","dims":[["x, y"],["x, y, z"]],"shape":[[2],[3]],"doc":"Space between pixels in (x, y) or voxels in (x, y, z) directions, in the specified unit. Assumes imaging plane is a regular grid. See also reference_frame to interpret the grid.","quantity":"?","attributes":[{"name":"unit","dtype":"text","default_value":"meters","doc":"Measurement units for grid_spacing. The default value is 'meters'."}]},{"name":"reference_frame","dtype":"text","doc":"Describes reference frame of origin_coords and grid_spacing. For example, this can be a text description of the anatomical location and orientation of the grid defined by origin_coords and grid_spacing or the vectors needed to transform or rotate the grid to a common anatomical axis (e.g., AP/DV/ML). This field is necessary to interpret origin_coords and grid_spacing. If origin_coords and grid_spacing are not present, then this field is not required. For example, if the microscope takes 10 x 10 x 2 images, where the first value of the data matrix (index (0, 0, 0)) corresponds to (-1.2, -0.6, -2) mm relative to bregma, the spacing between pixels is 0.2 mm in x, 0.2 mm in y and 0.5 mm in z, and larger numbers in x means more anterior, larger numbers in y means more rightward, and larger numbers in z means more ventral, then enter the following -- origin_coords = (-1.2, -0.6, -2) grid_spacing = (0.2, 0.2, 0.5) reference_frame = \"Origin coordinates are relative to bregma. First dimension corresponds to anterior-posterior axis (larger index = more anterior). Second dimension corresponds to medial-lateral axis (larger index = more rightward). Third dimension corresponds to dorsal-ventral axis (larger index = more ventral).\"","quantity":"?"}],"groups":[{"neurodata_type_inc":"OpticalChannel","doc":"An optical channel used to record from an imaging plane.","quantity":"+"}],"links":[{"name":"device","target_type":"Device","doc":"Link to the Device object that was used to record from this electrode."}]},{"neurodata_type_def":"OpticalChannel","neurodata_type_inc":"NWBContainer","doc":"An optical channel used to record from an imaging plane.","datasets":[{"name":"description","dtype":"text","doc":"Description or other notes about the channel."},{"name":"emission_lambda","dtype":"float32","doc":"Emission wavelength for channel, in nm."}]},{"neurodata_type_def":"MotionCorrection","neurodata_type_inc":"NWBDataInterface","default_name":"MotionCorrection","doc":"An image stack where all frames are shifted (registered) to a common coordinate system, to account for movement and drift between frames. Note: each frame at each point in time is assumed to be 2-D (has only x & y dimensions).","groups":[{"neurodata_type_inc":"CorrectedImageStack","doc":"Results from motion correction of an image stack.","quantity":"+"}]},{"neurodata_type_def":"CorrectedImageStack","neurodata_type_inc":"NWBDataInterface","doc":"Results from motion correction of an image stack.","groups":[{"name":"corrected","neurodata_type_inc":"ImageSeries","doc":"Image stack with frames shifted to the common coordinates."},{"name":"xy_translation","neurodata_type_inc":"TimeSeries","doc":"Stores the x,y delta necessary to align each frame to the common coordinates, for example, to align each frame to a reference image."}],"links":[{"name":"original","target_type":"ImageSeries","doc":"Link to ImageSeries object that is being registered."}]}]})delimiter"; - -constexpr std::string_view nwb_retinotopy = R"delimiter( -{"groups":[{"neurodata_type_def":"ImagingRetinotopy","neurodata_type_inc":"NWBDataInterface","default_name":"ImagingRetinotopy","doc":"DEPRECATED. Intrinsic signal optical imaging or widefield imaging for measuring retinotopy. Stores orthogonal maps (e.g., altitude/azimuth; radius/theta) of responses to specific stimuli and a combined polarity map from which to identify visual areas. This group does not store the raw responses imaged during retinotopic mapping or the stimuli presented, but rather the resulting phase and power maps after applying a Fourier transform on the averaged responses. Note: for data consistency, all images and arrays are stored in the format [row][column] and [row, col], which equates to [y][x]. Field of view and dimension arrays may appear backward (i.e., y before x).","datasets":[{"name":"axis_1_phase_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Phase response to stimulus on the first measured axis.","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_1_power_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Power response on the first measured axis. Response is scaled so 0.0 is no power in the response and 1.0 is maximum relative power.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_2_phase_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Phase response to stimulus on the second measured axis.","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_2_power_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Power response on the second measured axis. Response is scaled so 0.0 is no power in the response and 1.0 is maximum relative power.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"unit","dtype":"text","doc":"Unit that axis data is stored in (e.g., degrees)."}]},{"name":"axis_descriptions","dtype":"text","dims":["axis_1, axis_2"],"shape":[2],"doc":"Two-element array describing the contents of the two response axis fields. Description should be something like ['altitude', 'azimuth'] or '['radius', 'theta']."},{"name":"focal_depth_image","dtype":"uint16","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Gray-scale image taken with same settings/parameters (e.g., focal depth, wavelength) as data collection. Array format: [rows][columns].","quantity":"?","attributes":[{"name":"bits_per_pixel","dtype":"int32","doc":"Number of bits used to represent each value. This is necessary to determine maximum (white) pixel value."},{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"focal_depth","dtype":"float32","doc":"Focal depth offset, in meters."},{"name":"format","dtype":"text","doc":"Format of image. Right now only 'raw' is supported."}]},{"name":"sign_map","dtype":"float32","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Sine of the angle between the direction of the gradient in axis_1 and axis_2.","quantity":"?","attributes":[{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."}]},{"name":"vasculature_image","dtype":"uint16","dims":["num_rows","num_cols"],"shape":[null,null],"doc":"Gray-scale anatomical image of cortical surface. Array structure: [rows][columns]","attributes":[{"name":"bits_per_pixel","dtype":"int32","doc":"Number of bits used to represent each value. This is necessary to determine maximum (white) pixel value"},{"name":"dimension","dtype":"int32","dims":["num_rows, num_cols"],"shape":[2],"doc":"Number of rows and columns in the image. NOTE: row, column representation is equivalent to height, width."},{"name":"field_of_view","dtype":"float32","dims":["height, width"],"shape":[2],"doc":"Size of viewing area, in meters."},{"name":"format","dtype":"text","doc":"Format of image. Right now only 'raw' is supported."}]}]}]})delimiter"; - -constexpr std::string_view namespaces = R"delimiter( -{"namespaces":[{"name":"core","doc":"NWB namespace","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter","Keith Godfrey","Jeff Teeters"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov","keithg@alleninstitute.org","jteeters@berkeley.edu"],"full_name":"NWB core","schema":[{"namespace":"hdmf-common"},{"source":"nwb.base"},{"source":"nwb.device"},{"source":"nwb.epoch"},{"source":"nwb.image"},{"source":"nwb.file"},{"source":"nwb.misc"},{"source":"nwb.behavior"},{"source":"nwb.ecephys"},{"source":"nwb.icephys"},{"source":"nwb.ogen"},{"source":"nwb.ophys"},{"source":"nwb.retinotopy"}],"version":"2.7.0"}]})delimiter"; - -constexpr std::array, 13> - specVariables {{{"nwb.base", nwb_base}, - {"nwb.device", nwb_device}, - {"nwb.epoch", nwb_epoch}, - {"nwb.image", nwb_image}, - {"nwb.file", nwb_file}, - {"nwb.misc", nwb_misc}, - {"nwb.behavior", nwb_behavior}, - {"nwb.ecephys", nwb_ecephys}, - {"nwb.icephys", nwb_icephys}, - {"nwb.ogen", nwb_ogen}, - {"nwb.ophys", nwb_ophys}, - {"nwb.retinotopy", nwb_retinotopy}, - {"namespace", namespaces}}}; -} // namespace AQNWB::SPEC::CORE diff --git a/Source/aqnwb/spec/hdmf_common.hpp b/Source/aqnwb/spec/hdmf_common.hpp deleted file mode 100644 index 54e1f1d..0000000 --- a/Source/aqnwb/spec/hdmf_common.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace AQNWB::SPEC::HDMF_COMMON -{ - -const std::string version = "1.8.0"; - -constexpr std::string_view base = R"delimiter( -{"datasets":[{"data_type_def":"Data","doc":"An abstract data type for a dataset."}],"groups":[{"data_type_def":"Container","doc":"An abstract data type for a group storing collections of data and metadata. Base type for all data and metadata containers."},{"data_type_def":"SimpleMultiContainer","data_type_inc":"Container","doc":"A simple Container for holding onto multiple containers.","datasets":[{"data_type_inc":"Data","quantity":"*","doc":"Data objects held within this SimpleMultiContainer."}],"groups":[{"data_type_inc":"Container","quantity":"*","doc":"Container objects held within this SimpleMultiContainer."}]}]})delimiter"; - -constexpr std::string_view table = R"delimiter( -{"datasets":[{"data_type_def":"VectorData","data_type_inc":"Data","doc":"An n-dimensional dataset representing a column of a DynamicTable. If used without an accompanying VectorIndex, first dimension is along the rows of the DynamicTable and each step along the first dimension is a cell of the larger table. VectorData can also be used to represent a ragged array if paired with a VectorIndex. This allows for storing arrays of varying length in a single cell of the DynamicTable by indexing into this VectorData. The first vector is at VectorData[0:VectorIndex[0]]. The second vector is at VectorData[VectorIndex[0]:VectorIndex[1]], and so on.","dims":[["dim0"],["dim0","dim1"],["dim0","dim1","dim2"],["dim0","dim1","dim2","dim3"]],"shape":[[null],[null,null],[null,null,null],[null,null,null,null]],"attributes":[{"name":"description","dtype":"text","doc":"Description of what these vectors represent."}]},{"data_type_def":"VectorIndex","data_type_inc":"VectorData","dtype":"uint8","doc":"Used with VectorData to encode a ragged array. An array of indices into the first dimension of the target VectorData, and forming a map between the rows of a DynamicTable and the indices of the VectorData. The name of the VectorIndex is expected to be the name of the target VectorData object followed by \"_index\".","dims":["num_rows"],"shape":[null],"attributes":[{"name":"target","dtype":{"target_type":"VectorData","reftype":"object"},"doc":"Reference to the target dataset that this index applies to."}]},{"data_type_def":"ElementIdentifiers","data_type_inc":"Data","default_name":"element_id","dtype":"int","dims":["num_elements"],"shape":[null],"doc":"A list of unique identifiers for values within a dataset, e.g. rows of a DynamicTable."},{"data_type_def":"DynamicTableRegion","data_type_inc":"VectorData","dtype":"int","doc":"DynamicTableRegion provides a link from one table to an index or region of another. The `table` attribute is a link to another `DynamicTable`, indicating which table is referenced, and the data is int(s) indicating the row(s) (0-indexed) of the target array. `DynamicTableRegion`s can be used to associate rows with repeated meta-data without data duplication. They can also be used to create hierarchical relationships between multiple `DynamicTable`s. `DynamicTableRegion` objects may be paired with a `VectorIndex` object to create ragged references, so a single cell of a `DynamicTable` can reference many rows of another `DynamicTable`.","dims":["num_rows"],"shape":[null],"attributes":[{"name":"table","dtype":{"target_type":"DynamicTable","reftype":"object"},"doc":"Reference to the DynamicTable object that this region applies to."},{"name":"description","dtype":"text","doc":"Description of what this table region points to."}]}],"groups":[{"data_type_def":"DynamicTable","data_type_inc":"Container","doc":"A group containing multiple datasets that are aligned on the first dimension (Currently, this requirement if left up to APIs to check and enforce). These datasets represent different columns in the table. Apart from a column that contains unique identifiers for each row, there are no other required datasets. Users are free to add any number of custom VectorData objects (columns) here. DynamicTable also supports ragged array columns, where each element can be of a different size. To add a ragged array column, use a VectorIndex type to index the corresponding VectorData type. See documentation for VectorData and VectorIndex for more details. Unlike a compound data type, which is analogous to storing an array-of-structs, a DynamicTable can be thought of as a struct-of-arrays. This provides an alternative structure to choose from when optimizing storage for anticipated access patterns. Additionally, this type provides a way of creating a table without having to define a compound type up front. Although this convenience may be attractive, users should think carefully about how data will be accessed. DynamicTable is more appropriate for column-centric access, whereas a dataset with a compound type would be more appropriate for row-centric access. Finally, data size should also be taken into account. For small tables, performance loss may be an acceptable trade-off for the flexibility of a DynamicTable.","attributes":[{"name":"colnames","dtype":"text","dims":["num_columns"],"shape":[null],"doc":"The names of the columns in this table. This should be used to specify an order to the columns."},{"name":"description","dtype":"text","doc":"Description of what is in this dynamic table."}],"datasets":[{"name":"id","data_type_inc":"ElementIdentifiers","dtype":"int","dims":["num_rows"],"shape":[null],"doc":"Array of unique identifiers for the rows of this dynamic table."},{"data_type_inc":"VectorData","doc":"Vector columns, including index columns, of this dynamic table.","quantity":"*"}]},{"data_type_def":"AlignedDynamicTable","data_type_inc":"DynamicTable","doc":"DynamicTable container that supports storing a collection of sub-tables. Each sub-table is a DynamicTable itself that is aligned with the main table by row index. I.e., all DynamicTables stored in this group MUST have the same number of rows. This type effectively defines a 2-level table in which the main data is stored in the main table implemented by this type and additional columns of the table are grouped into categories, with each category being represented by a separate DynamicTable stored within the group.","attributes":[{"name":"categories","dtype":"text","dims":["num_categories"],"shape":[null],"doc":"The names of the categories in this AlignedDynamicTable. Each category is represented by one DynamicTable stored in the parent group. This attribute should be used to specify an order of categories and the category names must match the names of the corresponding DynamicTable in the group."}],"groups":[{"data_type_inc":"DynamicTable","doc":"A DynamicTable representing a particular category for columns in the AlignedDynamicTable parent container. The table MUST be aligned with (i.e., have the same number of rows) as all other DynamicTables stored in the AlignedDynamicTable parent container. The name of the category is given by the name of the DynamicTable and its description by the description attribute of the DynamicTable.","quantity":"*"}]}]})delimiter"; - -constexpr std::string_view sparse = R"delimiter( -{"groups":[{"data_type_def":"CSRMatrix","data_type_inc":"Container","doc":"A compressed sparse row matrix. Data are stored in the standard CSR format, where column indices for row i are stored in indices[indptr[i]:indptr[i+1]] and their corresponding values are stored in data[indptr[i]:indptr[i+1]].","attributes":[{"name":"shape","dtype":"uint","dims":["number of rows, number of columns"],"shape":[2],"doc":"The shape (number of rows, number of columns) of this sparse matrix."}],"datasets":[{"name":"indices","dtype":"uint","dims":["number of non-zero values"],"shape":[null],"doc":"The column indices."},{"name":"indptr","dtype":"uint","dims":["number of rows in the matrix + 1"],"shape":[null],"doc":"The row index pointer."},{"name":"data","dims":["number of non-zero values"],"shape":[null],"doc":"The non-zero values in the matrix."}]}]})delimiter"; - -constexpr std::string_view namespaces = R"delimiter( -{"namespaces":[{"name":"hdmf-common","doc":"Common data structures provided by HDMF","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov"],"full_name":"HDMF Common","schema":[{"source":"base"},{"source":"table"},{"source":"sparse"}],"version":"1.8.0"}]})delimiter"; - -constexpr std::array, 4> - specVariables {{{"base", base}, - {"table", table}, - {"sparse", sparse}, - {"namespace", namespaces}}}; -} // namespace AQNWB::SPEC::HDMF_COMMON diff --git a/Source/aqnwb/spec/hdmf_experimental.hpp b/Source/aqnwb/spec/hdmf_experimental.hpp deleted file mode 100644 index d671e05..0000000 --- a/Source/aqnwb/spec/hdmf_experimental.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace AQNWB::SPEC::HDMF_EXPERIMENTAL -{ - -const std::string version = "0.5.0"; - -constexpr std::string_view experimental = R"delimiter( -{"groups":[],"datasets":[{"data_type_def":"EnumData","data_type_inc":"VectorData","dtype":"uint8","doc":"Data that come from a fixed set of values. A data value of i corresponds to the i-th value in the VectorData referenced by the 'elements' attribute.","attributes":[{"name":"elements","dtype":{"target_type":"VectorData","reftype":"object"},"doc":"Reference to the VectorData object that contains the enumerable elements"}]}]})delimiter"; - -constexpr std::string_view resources = R"delimiter( -{"groups":[{"data_type_def":"HERD","data_type_inc":"Container","doc":"HDMF External Resources Data Structure. A set of six tables for tracking external resource references in a file or across multiple files.","datasets":[{"data_type_inc":"Data","name":"keys","doc":"A table for storing user terms that are used to refer to external resources.","dtype":[{"name":"key","dtype":"text","doc":"The user term that maps to one or more resources in the `resources` table, e.g., \"human\"."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"files","doc":"A table for storing object ids of files used in external resources.","dtype":[{"name":"file_object_id","dtype":"text","doc":"The object id (UUID) of a file that contains objects that refers to external resources."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"entities","doc":"A table for mapping user terms (i.e., keys) to resource entities.","dtype":[{"name":"entity_id","dtype":"text","doc":"The compact uniform resource identifier (CURIE) of the entity, in the form [prefix]:[unique local identifier], e.g., 'NCBI_TAXON:9606'."},{"name":"entity_uri","dtype":"text","doc":"The URI for the entity this reference applies to. This can be an empty string. e.g., https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=info&id=9606"}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"objects","doc":"A table for identifying which objects in a file contain references to external resources.","dtype":[{"name":"files_idx","dtype":"uint","doc":"The row index to the file in the `files` table containing the object."},{"name":"object_id","dtype":"text","doc":"The object id (UUID) of the object."},{"name":"object_type","dtype":"text","doc":"The data type of the object."},{"name":"relative_path","dtype":"text","doc":"The relative path from the data object with the `object_id` to the dataset or attribute with the value(s) that is associated with an external resource. This can be an empty string if the object is a dataset that contains the value(s) that is associated with an external resource."},{"name":"field","dtype":"text","doc":"The field within the compound data type using an external resource. This is used only if the dataset or attribute is a compound data type; otherwise this should be an empty string."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"object_keys","doc":"A table for identifying which objects use which keys.","dtype":[{"name":"objects_idx","dtype":"uint","doc":"The row index to the object in the `objects` table that holds the key"},{"name":"keys_idx","dtype":"uint","doc":"The row index to the key in the `keys` table."}],"dims":["num_rows"],"shape":[null]},{"data_type_inc":"Data","name":"entity_keys","doc":"A table for identifying which keys use which entity.","dtype":[{"name":"entities_idx","dtype":"uint","doc":"The row index to the entity in the `entities` table."},{"name":"keys_idx","dtype":"uint","doc":"The row index to the key in the `keys` table."}],"dims":["num_rows"],"shape":[null]}]}]})delimiter"; - -constexpr std::string_view namespaces = R"delimiter( -{"namespaces":[{"name":"hdmf-experimental","doc":"Experimental data structures provided by HDMF. These are not guaranteed to be available in the future.","author":["Andrew Tritt","Oliver Ruebel","Ryan Ly","Ben Dichter","Matthew Avaylon"],"contact":["ajtritt@lbl.gov","oruebel@lbl.gov","rly@lbl.gov","bdichter@lbl.gov","mavaylon@lbl.gov"],"full_name":"HDMF Experimental","schema":[{"namespace":"hdmf-common"},{"source":"experimental"},{"source":"resources"}],"version":"0.5.0"}]})delimiter"; - -constexpr std::array, 3> - specVariables {{{"experimental", experimental}, - {"resources", resources}, - {"namespace", namespaces}}}; -} // namespace AQNWB::SPEC::HDMF_EXPERIMENTAL From 19d19d58f43f1b2c97669db063d6f9a22459d2ea Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:57:06 -0700 Subject: [PATCH 28/32] remove modules --- .gitmodules | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 From 28ea47bff65882ad280b0d608e4fd67782172fd5 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:01:19 -0700 Subject: [PATCH 29/32] add aqnwb submod with sparse checkout --- .gitmodules | 3 +++ Source/aqnwb | 1 + 2 files changed, 4 insertions(+) create mode 160000 Source/aqnwb diff --git a/.gitmodules b/.gitmodules index e69de29..d507b6f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Source/aqnwb"] + path = Source/aqnwb + url = https://github.com/NeurodataWithoutBorders/aqnwb diff --git a/Source/aqnwb b/Source/aqnwb new file mode 160000 index 0000000..3a12145 --- /dev/null +++ b/Source/aqnwb @@ -0,0 +1 @@ +Subproject commit 3a121450dbb6c0ce1d37327a13c587b4d44072a4 From 53baa1cbd652d7281d3c850b30e0aa7ca989849f Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:12:33 -0700 Subject: [PATCH 30/32] update cmake for submodule --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2eef954..f5e81db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,7 +36,7 @@ set(SOURCE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/Source) file(GLOB_RECURSE SRC_FILES LIST_DIRECTORIES false "${SOURCE_PATH}/*.cpp" "${SOURCE_PATH}/*.h" "${SOURCE_PATH}/*.hpp") set(GUI_COMMONLIB_DIR ${GUI_BASE_DIR}/installed_libs) -include_directories(${SOURCE_PATH}/aqnwb) +include_directories(${SOURCE_PATH}/aqnwb/src) set(CONFIGURATION_FOLDER $<$:Debug>$<$>:Release>) @@ -50,6 +50,7 @@ endif() target_compile_features(${PLUGIN_NAME} PUBLIC cxx_auto_type cxx_generalized_initializers cxx_std_17) target_include_directories(${PLUGIN_NAME} PUBLIC ${GUI_BASE_DIR}/JuceLibraryCode ${GUI_BASE_DIR}/JuceLibraryCode/modules ${GUI_BASE_DIR}/Plugins/Headers ${GUI_COMMONLIB_DIR}/include) +target_compile_definitions(${PLUGIN_NAME} PUBLIC BOOST_NO_CXX98_FUNCTION_BASE) set(GUI_BIN_DIR ${GUI_BASE_DIR}/Build/${CONFIGURATION_FOLDER}) From 02f7d9c3935327551c0aaeb01b602b83bd661fd5 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:05:44 -0700 Subject: [PATCH 31/32] remove NWBFormat files --- Source/RecordEngine/NWBFormat.cpp | 946 ------------------------------ Source/RecordEngine/NWBFormat.h | 274 --------- 2 files changed, 1220 deletions(-) delete mode 100644 Source/RecordEngine/NWBFormat.cpp delete mode 100644 Source/RecordEngine/NWBFormat.h diff --git a/Source/RecordEngine/NWBFormat.cpp b/Source/RecordEngine/NWBFormat.cpp deleted file mode 100644 index 32bfcc8..0000000 --- a/Source/RecordEngine/NWBFormat.cpp +++ /dev/null @@ -1,946 +0,0 @@ -/* - ------------------------------------------------------------------ - - This file is part of the Open Ephys GUI - Copyright (C) 2014 Open Ephys - - ------------------------------------------------------------------ - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - */ - - #include "NWBFormat.h" - - using namespace NWBRecording; - -#ifndef EVENT_CHUNK_SIZE -#define EVENT_CHUNK_SIZE 8 -#endif - -#ifndef SPIKE_CHUNK_XSIZE -#define SPIKE_CHUNK_XSIZE 8 -#endif - -#ifndef SPIKE_CHUNK_YSIZE -#define SPIKE_CHUNK_YSIZE 40 -#endif - - #define MAX_BUFFER_SIZE 40960 - -NWBFile::NWBFile(String fName, String ver, String idText) : - HDF5FileBase(), - filename(fName), - identifierText(idText), - GUIVersion(ver) -{ - readyToOpen = true; //In KWIK this is in initFile, but the new recordEngine methods make it safe for it to be here - - scaledBuffer.malloc(MAX_BUFFER_SIZE); - intBuffer.malloc(MAX_BUFFER_SIZE); - bufferSize = MAX_BUFFER_SIZE; -} - -NWBFile::~NWBFile() -{ - continuousDataSets.clear(); - spikeDataSets.clear(); - eventDataSets.clear(); - syncMsgDataSet.reset(); -} - -int NWBFile::createFileStructure() -{ - - setAttributeStr("core", "/", "namespace"); - setAttributeStr("NWBFile", "/", "neurodata_type"); - setAttributeStr("2.5.0", "/", "nwb_version"); - setAttributeStr(identifierText, "/", "object_id"); - - if (createGroup("/acquisition")) return -1; - - if (createGroup("/analysis")) return -1; - - String time = Time::getCurrentTime().formatted("%Y-%m-%dT%H:%M:%S") + Time::getCurrentTime().getUTCOffsetString(true); - - createTextDataSet("", "file_create_date", time); - - if (createGroup("/general")) return -1; - if (createGroup("general/devices")) return -1; - if (createGroup("general/extracellular_ephys")) return -1; - if (createGroup("general/extracellular_ephys/electrodes")) return -1; - - StringArray colnames; - colnames.add("group"); - colnames.add("group_name"); - colnames.add("location"); - setAttributeStrArray(colnames, "general/extracellular_ephys/electrodes", "colnames"); - setAttributeStr("metadata about extracellular electrodes", "general/extracellular_ephys/electrodes", "description"); - setAttributeStr("hdmf-common", "general/extracellular_ephys/electrodes", "namespace"); - setAttributeStr("DynamicTable", "general/extracellular_ephys/electrodes", "neurodata_type"); - setAttributeStr(generateUuid(), "general/extracellular_ephys/electrodes", "object_id"); - - if (createGroup("/processing")) return -1; - - if (createGroup("/stimulus")) return -1; - if (createGroup("/stimulus/presentation")) return -1; - if (createGroup("/stimulus/templates")) return -1; - - createStringDataSet("/session_description", "Recording with the Open Ephys GUI"); - createStringDataSet("/session_start_time", time); - createStringDataSet("/timestamps_reference_time", time); - createStringDataSet("/identifier", "test-identifier"); - - return 0; - -} - -TimeSeries::TimeSeries(String rootPath, String name, String description_) - : basePath(rootPath + name), description(description_) -{ - -} - -ecephys::ElectricalSeries::ElectricalSeries(String rootPath, String name, String description_, - int channel_count_, Array channel_conversion_) - : TimeSeries(rootPath, name, description_), - channel_conversion(channel_conversion_), - channel_count(channel_count_) -{ - -} - -ecephys::SpikeEventSeries::SpikeEventSeries(String rootPath, String name, String description_, - int channel_count, Array channel_conversion_) - : ecephys::ElectricalSeries(rootPath, name, description_, channel_count, channel_conversion_) -{ - -} - -TTLEventSeries::TTLEventSeries(String rootPath, String name, String description_) - : TimeSeries(rootPath, name, description_) -{ - -} - -AnnotationSeries::AnnotationSeries(String rootPath, String name, String description_) - : TimeSeries(rootPath, name, description_) -{ - -} - - -bool NWBFile::startNewRecording( - int recordingNumber, - const Array& continuousArray, - const Array& continuousChannels, - const Array& eventArray, - const Array& electrodeArray) -{ - - // all recorded data is stored in the "acquisition" group - String rootPath = "/acquisition/"; - - continuousDataSets.clearQuick(true); - spikeDataSets.clearQuick(true); - eventDataSets.clearQuick(true); - - Array all_electrode_inds; - StringArray groupNames; - StringArray groupReferences; - - // 0. put global inds into electrode table - for (auto ch : continuousChannels) - { - all_electrode_inds.add(ch->getGlobalIndex()); - - String groupName = ch->getSourceNodeName() + "-" - + String(ch->getSourceNodeId()) - + "." + ch->getStreamName(); - - groupNames.add(groupName); - groupReferences.add("/general/extracellular_ephys/" + groupName); - } - - // 1. Create continuous datasets - for (int i = 0; i < continuousArray.size(); i++) - { - - // Get the scaling info for each channel - ContinuousGroup group = continuousArray.getReference(i); - - Array channel_conversion; - for (int ch = 0; ch < group.size(); ch++) - { - channel_conversion.add(group[ch]->getBitVolts() / 1e6); - } - - String groupName = group[0]->getSourceNodeName() + "-" - + String(group[0]->getSourceNodeId()) - + "." + group[0]->getStreamName(); - - String fullPath = "general/extracellular_ephys/" + groupName; - createGroup(fullPath); - setAttributeStr("description", fullPath, "description"); - setAttributeStr("unknown", fullPath, "location"); - setAttributeStr("core", fullPath, "namespace"); - setAttributeStr("ElectrodeGroup", fullPath, "neurodata_type"); - setAttributeStr(generateUuid(), fullPath, "object_id"); - - createGroup("general/devices/" + groupName); - - setAttributeStr("description", "general/devices/" + groupName, "description"); - setAttributeStr("unknown", "general/devices/" + groupName, "manufacturer"); - setAttributeStr("core", "general/devices/" + groupName, "namespace"); - setAttributeStr("Device", "general/devices/" + groupName, "neurodata_type"); - setAttributeStr(generateUuid(), "general/devices/" + groupName, "object_id"); - - createReference("/" + fullPath + "/device", "/general/devices/" + groupName); - - Array electrode_inds; - for (int ch = 0; ch < group.size(); ch++) - { - int index = group[ch]->getGlobalIndex(); - electrode_inds.add(index); - } - - ecephys::ElectricalSeries* electricalSeries = - new ecephys::ElectricalSeries(rootPath, - groupName, - "Stores continuously sampled voltage data from an extracellular ephys recording", - group.size(), - channel_conversion - ); - - if (recordingNumber == 0) - if (!createTimeSeriesBase(electricalSeries)) - return false; - - electricalSeries->baseDataSet = createDataSet(BaseDataType::I16, - 0, - electricalSeries->channel_count, - CHUNK_XSIZE, - electricalSeries->basePath + "/data"); - - if (electricalSeries->baseDataSet == nullptr) - { - std::cerr << "Error creating dataset for " << groupName << std::endl; - return false; - } else { - createDataAttributes(electricalSeries->basePath, channel_conversion[0], -1.0f, "volts"); - } - - electricalSeries->timestampDataSet = - createTimestampDataSet(electricalSeries->basePath + "/timestamps", CHUNK_XSIZE, 1/group[0]->getSampleRate()); - if (electricalSeries->timestampDataSet == nullptr) return false; - - electricalSeries->sampleNumberDataSet = - createSampleNumberDataSet(electricalSeries->basePath + "/sync", CHUNK_XSIZE); - if (electricalSeries->sampleNumberDataSet == nullptr) return false; - - electricalSeries->channelConversionDataSet = createChannelConversionDataSet(electricalSeries->basePath + "/channel_conversion", "Bit volts values for all channels", CHUNK_XSIZE); - - if (electricalSeries->channelConversionDataSet == nullptr) return false; - writeChannelConversions(electricalSeries); - - electricalSeries->electrodeDataSet = createElectrodeDataSet(electricalSeries->basePath + "/electrodes", "Electrode index for each channel", CHUNK_XSIZE); - - if (electricalSeries->electrodeDataSet == nullptr) return false; - writeElectrodes(electricalSeries, electrode_inds); - - continuousDataSets.add(electricalSeries); - } - - // 2. create spike datasets - for (int i = 0; i < electrodeArray.size(); i++) - { - const SpikeChannel* sourceInfo = electrodeArray[i]; - - String sourceName = sourceInfo->getSourceNodeName() + "-" + String(sourceInfo->getSourceNodeId()); - sourceName += "." + sourceInfo->getStreamName(); - sourceName += "." + sourceInfo->getName(); - - Array channel_conversion; - - for (int ch = 0; ch < sourceInfo->getNumChannels(); ch++) - { - channel_conversion.add(sourceInfo->getSourceChannels()[0]->getBitVolts() / 1e6); - } - - Array electrode_inds; - - for (int ch = 0; ch < sourceInfo->getNumChannels(); ch++) - { - int globalIndex = sourceInfo->getSourceChannels()[ch]->getGlobalIndex(); - - electrode_inds.add(globalIndex); - } - - ecephys::SpikeEventSeries* spikeEventSeries = - new ecephys::SpikeEventSeries(rootPath, sourceName, - "Stores spike waveforms from an extracellular ephys recording", - sourceInfo->getNumChannels(), - channel_conversion); - - if (recordingNumber == 0) - if (!createTimeSeriesBase(spikeEventSeries)) - return false; - - spikeEventSeries->baseDataSet = createDataSet(BaseDataType::I16, 0, sourceInfo->getNumChannels(), sourceInfo->getTotalSamples(), SPIKE_CHUNK_XSIZE, spikeEventSeries->basePath + "/data"); - - if (spikeEventSeries->baseDataSet == nullptr) - { - std::cerr << "Error creating dataset for electrode " << i << std::endl; - return false; - } else { - createDataAttributes(spikeEventSeries->basePath, channel_conversion[0], -1.0f, "volts"); - } - - spikeEventSeries->timestampDataSet = - createTimestampDataSet(spikeEventSeries->basePath + "/timestamps", CHUNK_XSIZE, 1/sourceInfo->getSourceChannels()[0]->getSampleRate()); - if (spikeEventSeries->timestampDataSet == nullptr) return false; - - spikeEventSeries->sampleNumberDataSet = - createSampleNumberDataSet(spikeEventSeries->basePath + "/sync", CHUNK_XSIZE); - if (spikeEventSeries->sampleNumberDataSet == nullptr) return false; - - spikeEventSeries->channelConversionDataSet = createChannelConversionDataSet(spikeEventSeries->basePath + "/channel_conversion", "Bit volts values for all channels", CHUNK_XSIZE); - - if (spikeEventSeries->channelConversionDataSet == nullptr) return false; - writeChannelConversions(spikeEventSeries); - - spikeEventSeries->electrodeDataSet = createElectrodeDataSet(spikeEventSeries->basePath + "/electrodes", "Electrode index for each channel", CHUNK_XSIZE); - - if (spikeEventSeries->electrodeDataSet == nullptr) return false; - writeElectrodes(spikeEventSeries, electrode_inds); - - spikeDataSets.add(spikeEventSeries); - - } - - // 3. Create event channel datasets - for (int i = 0; i < eventArray.size(); i++) - { - - const EventChannel* info = eventArray[i]; - - String sourceName = info->getSourceNodeName() + "-" + String(info->getSourceNodeId()); - sourceName += "." + info->getStreamName(); - - String typeString, description; - - if (info->getType() == EventChannel::TTL) - { - TTLEventSeries* ttlEventSeries = - new TTLEventSeries(rootPath, sourceName + ".TTL", "Stores the times and lines of TTL events"); - - if (recordingNumber == 0) - if (!createTimeSeriesBase(ttlEventSeries)) return false; - - ttlEventSeries->baseDataSet = createDataSet(getEventH5Type(info->getType(), info->getLength()), 0, EVENT_CHUNK_SIZE, ttlEventSeries->basePath + "/data"); - - if (ttlEventSeries->baseDataSet == nullptr) - { - std::cerr << "Error creating dataset for event " << info->getName() << std::endl; - return false; - } - - ttlEventSeries->timestampDataSet = - createTimestampDataSet(ttlEventSeries->basePath + "/timestamps", EVENT_CHUNK_SIZE, 1/info->getSampleRate()); - if (ttlEventSeries->timestampDataSet == nullptr) return false; - - ttlEventSeries->sampleNumberDataSet = createSampleNumberDataSet(ttlEventSeries->basePath + "/sync", EVENT_CHUNK_SIZE); - if (ttlEventSeries->sampleNumberDataSet == nullptr) return false; - - ttlEventSeries->ttlWordDataSet = createDataSet(BaseDataType::U64, 0, info->getDataSize(), EVENT_CHUNK_SIZE, ttlEventSeries->basePath + "/full_word"); - if (ttlEventSeries->ttlWordDataSet == nullptr) return false; - - eventDataSets.add(ttlEventSeries); - - } - else if (info->getType() == EventChannel::TEXT) - { - AnnotationSeries* annotationSeries = new AnnotationSeries(rootPath, "messages", "Stores timestamped messages generated during an experiment"); - - if (recordingNumber == 0) - if (!createTimeSeriesBase(annotationSeries)) return false; - - annotationSeries->baseDataSet = createDataSet(getEventH5Type(info->getType(), info->getLength()), 0, EVENT_CHUNK_SIZE, annotationSeries->basePath + "/data"); - - if (annotationSeries->baseDataSet == nullptr) - { - std::cerr << "Error creating dataset for event " << info->getName() << std::endl; - return false; - } - - annotationSeries->timestampDataSet = createTimestampDataSet(annotationSeries->basePath + "/timestamps", EVENT_CHUNK_SIZE, 1/info->getSampleRate()); - if (annotationSeries->timestampDataSet == nullptr) return false; - - annotationSeries->sampleNumberDataSet = createSampleNumberDataSet(annotationSeries->basePath + "/sync", EVENT_CHUNK_SIZE); - if (annotationSeries->sampleNumberDataSet == nullptr) return false; - - messagesDataSet.reset(annotationSeries); - } - - } - - //4. Create sync messages dataset - String desc = "Stores recording start timestamps for each processor in text format"; - - AnnotationSeries* annotationSeries = new AnnotationSeries(rootPath, "sync_messages", desc); - - if (recordingNumber == 0) - { - if (!createTimeSeriesBase(annotationSeries)) return false; - - annotationSeries->baseDataSet = createDataSet(BaseDataType::STR(100), 0, 1, annotationSeries->basePath + "/data"); - - if (annotationSeries->baseDataSet == nullptr) - { - std::cerr << "Error creating dataset for sync messages" << std::endl; - return false; - } - } - else { - annotationSeries->baseDataSet = getDataSet(annotationSeries->basePath + "/data"); - } - - if (recordingNumber == 0) - { - annotationSeries->sampleNumberDataSet = createSampleNumberDataSet(annotationSeries->basePath + "/sync", 1); - if (annotationSeries->sampleNumberDataSet == nullptr) return false; - } - else { - annotationSeries->sampleNumberDataSet = getDataSet(annotationSeries->basePath + "/sync"); - } - - if (recordingNumber == 0) - { - annotationSeries->timestampDataSet = createTimestampDataSet(annotationSeries->basePath + "/timestamps", 1, 1); - if (annotationSeries->timestampDataSet == nullptr) return false; - } - else { - annotationSeries->timestampDataSet = getDataSet(annotationSeries->basePath + "/timestamps"); - } - - syncMsgDataSet.reset(annotationSeries); - - // 5. Create electrode table - ScopedPointer elSet = createDataSet(BaseDataType::I32, 1, 1, "general/extracellular_ephys/electrodes/id"); - - std::vector electrodeNumbers; - for (auto i : all_electrode_inds) - electrodeNumbers.push_back(i); - - CHECK_ERROR(elSet->writeDataBlock(electrodeNumbers.size(), BaseDataType::I32, &electrodeNumbers[0])); - - setAttributeStr("hdmf-common", "general/extracellular_ephys/electrodes/id", "namespace"); - setAttributeStr("ElementIdentifiers", "general/extracellular_ephys/electrodes/id", "neurodata_type"); - setAttributeStr(generateUuid(), "general/extracellular_ephys/electrodes/id", "object_id"); - - ScopedPointer groupNamesDataset = createDataSet(BaseDataType::STR(250), 0, 1, "general/extracellular_ephys/electrodes/group_name"); - - for (int i = 0; i < groupNames.size(); i++) - groupNamesDataset->writeDataBlock(1, BaseDataType::STR(groupNames[i].length()), groupNames[i].toUTF8()); - - setAttributeStr("the name of the ElectrodeGroup this electrode is a part of", "general/extracellular_ephys/electrodes/group_name", "description"); - setAttributeStr("hdmf-common", "general/extracellular_ephys/electrodes/group_name", "namespace"); - setAttributeStr("VectorData", "general/extracellular_ephys/electrodes/group_name", "neurodata_type"); - setAttributeStr(generateUuid(), "general/extracellular_ephys/electrodes/group_name", "object_id"); - - ScopedPointer locationsDataset = createDataSet(BaseDataType::STR(250), 0, 1, "general/extracellular_ephys/electrodes/location"); - - for (int i = 0; i < groupNames.size(); i++) - locationsDataset->writeDataBlock(1, BaseDataType::STR(7), String("unknown").toUTF8()); - - setAttributeStr("the location of channel within the subject e.g. brain region", "general/extracellular_ephys/electrodes/location", "description"); - setAttributeStr("hdmf-common", "general/extracellular_ephys/electrodes/location", "namespace"); - setAttributeStr("VectorData", "general/extracellular_ephys/electrodes/location", "neurodata_type"); - setAttributeStr(generateUuid(), "general/extracellular_ephys/electrodes/location", "object_id"); - - createReferenceDataSet("general/extracellular_ephys/electrodes/group", groupReferences); - - setAttributeStr("a reference to the ElectrodeGroup this electrode is a part of", "general/extracellular_ephys/electrodes/group", "description"); - setAttributeStr("hdmf-common", "general/extracellular_ephys/electrodes/group", "namespace"); - setAttributeStr("VectorData", "general/extracellular_ephys/electrodes/group", "neurodata_type"); - setAttributeStr(generateUuid(), "general/extracellular_ephys/electrodes/group", "object_id"); - - return true; - - } - - void NWBFile::stopRecording() - { - - const TimeSeries* tsStruct; - - for (int i = 0; i < continuousDataSets.size(); i++) - { - tsStruct = continuousDataSets[i]; - CHECK_ERROR(setAttribute(BaseDataType::U64, &(tsStruct->numSamples), tsStruct->basePath, "num_samples")); - } - - for (int i = 0; i < spikeDataSets.size(); i++) - { - tsStruct = spikeDataSets[i]; - CHECK_ERROR(setAttribute(BaseDataType::U64, &(tsStruct->numSamples), tsStruct->basePath, "num_samples")); - } - - for (int i = 0; i < eventDataSets.size(); i++) - { - tsStruct = eventDataSets[i]; - CHECK_ERROR(setAttribute(BaseDataType::U64, &(tsStruct->numSamples), tsStruct->basePath, "num_samples")); - } - - CHECK_ERROR(setAttribute(BaseDataType::U64, &(syncMsgDataSet->numSamples), syncMsgDataSet->basePath, "num_samples")); - } - - void NWBFile::writeData(int datasetID, int channel, int nSamples, const float* data, float bitVolts) - { - if (!continuousDataSets[datasetID]) - return; - - if (nSamples > bufferSize) //Shouldn't happen, and if it happens it'll be slow, but better this than crashing. Will be reset on file close and reset. - { - std::cerr << "Write buffer overrun, resizing to" << nSamples << std::endl; - bufferSize = nSamples; - scaledBuffer.malloc(nSamples); - intBuffer.malloc(nSamples); - } - - double multFactor = 1 / (float(0x7fff) * bitVolts); - FloatVectorOperations::copyWithMultiply(scaledBuffer.getData(), data, multFactor, nSamples); - AudioDataConverters::convertFloatToInt16LE(scaledBuffer.getData(), intBuffer.getData(), nSamples); - - continuousDataSets[datasetID]->baseDataSet->writeDataRow(channel, nSamples, BaseDataType::I16, intBuffer); - //CHECK_ERROR(); - - /* Since channels are filled asynchronouysly by the Record Thread, there is no guarantee - that at a any point in time all channels in a dataset have the same number of filled samples. - However, since each dataset is filled from a single source, all channels must have the - same number of samples at acquisition stop. To keep track of the written samples we must chose - an arbitrary channel, and at the end all channels will be the same. */ - - if (channel == 0) //there will always be a first channel or there wouldn't be dataset - continuousDataSets[datasetID]->numSamples += nSamples; - } - - void NWBFile::writeSampleNumbers(int datasetID, int nSamples, const int64* data) - { - if (!continuousDataSets[datasetID]) - return; - - CHECK_ERROR(continuousDataSets[datasetID]->sampleNumberDataSet->writeDataBlock(nSamples, BaseDataType::I64, data)); - } - - void NWBFile::writeTimestamps(int datasetID, int nSamples, const double* data) - { - if (!continuousDataSets[datasetID]) - return; - - CHECK_ERROR(continuousDataSets[datasetID]->timestampDataSet->writeDataBlock(nSamples, BaseDataType::F64, data)); - } - -void NWBFile::writeChannelConversions(ecephys::ElectricalSeries* electricalSeries) -{ - std::vector conversions; - for (auto c : electricalSeries->channel_conversion) - conversions.push_back(c); - - CHECK_ERROR(electricalSeries->channelConversionDataSet->writeDataBlock(conversions.size(), BaseDataType::F32, &conversions[0])); -} - - void NWBFile::writeElectrodes(ecephys::ElectricalSeries* electricalSeries, Array electrodeInds) - { - std::vector electrodeNumbers; - for (auto i : electrodeInds) - electrodeNumbers.push_back(i); - - CHECK_ERROR(electricalSeries->electrodeDataSet->writeDataBlock(electricalSeries->channel_count, BaseDataType::I32, &electrodeNumbers[0])); - } - - void NWBFile::writeSpike(int electrodeId, const SpikeChannel* channel, const Spike* event) - { - if (!spikeDataSets[electrodeId]) - return; - int nSamples = channel->getTotalSamples() * channel->getNumChannels(); - - if (nSamples > bufferSize) //Shouldn't happen, and if it happens it'll be slow, but better this than crashing. Will be reset on file close and reset. - { - std::cerr << "Write buffer overrun, resizing to" << nSamples << std::endl; - bufferSize = nSamples; - scaledBuffer.malloc(nSamples); - intBuffer.malloc(nSamples); - } - - double multFactor = 1 / (float(0x7fff) * channel->getChannelBitVolts(0)); - FloatVectorOperations::copyWithMultiply(scaledBuffer.getData(), event->getDataPointer(), multFactor, nSamples); - AudioDataConverters::convertFloatToInt16LE(scaledBuffer.getData(), intBuffer.getData(), nSamples); - - double timestampSec = event->getTimestampInSeconds(); - - CHECK_ERROR(spikeDataSets[electrodeId]->baseDataSet->writeDataBlock(1, BaseDataType::I16, intBuffer)); - CHECK_ERROR(spikeDataSets[electrodeId]->timestampDataSet->writeDataBlock(1, BaseDataType::F64, ×tampSec)); - writeEventMetadata(spikeDataSets[electrodeId], channel, event); - - const int64 sampleNumber = event->getSampleNumber(); - - CHECK_ERROR(spikeDataSets[electrodeId]->sampleNumberDataSet->writeDataBlock(1, BaseDataType::I64, &sampleNumber)); - - - spikeDataSets[electrodeId]->numSamples += 1; - - } - - void NWBFile::writeEvent(int eventID, const EventChannel* channel, const Event* event) - { - - const void* dataSrc; - BaseDataType type; - int8 ttlVal; - String text; - - switch (event->getEventType()) - { - case EventChannel::TTL: - ttlVal = (static_cast(event)->getState() ? 1 : -1) * (static_cast(event)->getLine() + 1); - dataSrc = &ttlVal; - type = BaseDataType::I8; - break; - case EventChannel::TEXT: - text = static_cast(event)->getText(); - dataSrc = text.toUTF8().getAddress(); - type = BaseDataType::STR(text.length()); - break; - default: - dataSrc = static_cast(event)->getBinaryDataPointer(); - type = getEventH5Type(event->getEventType()); - break; - } - - if (eventID == eventDataSets.size()) //MessageCenter event - { - CHECK_ERROR(messagesDataSet->baseDataSet->writeDataBlock(1, BaseDataType::STR(text.length()), text.toUTF8())); - - const int64 sampleNumber = event->getSampleNumber(); - - CHECK_ERROR(messagesDataSet->sampleNumberDataSet->writeDataBlock(1, BaseDataType::I64, &sampleNumber)); - - const double timeSec = event->getTimestampInSeconds(); - - CHECK_ERROR(messagesDataSet->timestampDataSet->writeDataBlock(1, BaseDataType::F64, &timeSec)); - - messagesDataSet->numSamples += 1; - - } - else if (eventDataSets[eventID]) - { - CHECK_ERROR(eventDataSets[eventID]->baseDataSet->writeDataBlock(1, type, dataSrc)); - - const double timeSec = event->getTimestampInSeconds(); - - CHECK_ERROR(eventDataSets[eventID]->timestampDataSet->writeDataBlock(1, BaseDataType::F64, &timeSec)); - - const int64 sampleNumber = event->getSampleNumber(); - - CHECK_ERROR(eventDataSets[eventID]->sampleNumberDataSet->writeDataBlock(1, BaseDataType::I64, &sampleNumber)); - - if (event->getEventType() == EventChannel::TTL) - { - const uint64 ttlWord = static_cast(event)->getWord(); - CHECK_ERROR(eventDataSets[eventID]->ttlWordDataSet->writeDataBlock(1, BaseDataType::U64, &ttlWord)); - } - - eventDataSets[eventID]->numSamples += 1; - } - else - { - //Attempted to write an event to disk from unknown event source - } - - } - - void NWBFile::writeTimestampSyncText(uint16 sourceID, int64 sampleNumber, float sourceSampleRate, String text) - { - CHECK_ERROR(syncMsgDataSet->baseDataSet->writeDataBlock(1, BaseDataType::STR(text.length()), text.toUTF8())); - - CHECK_ERROR(syncMsgDataSet->sampleNumberDataSet->writeDataBlock(1, BaseDataType::I64, &sampleNumber)); - - double timestamp = (double)sampleNumber; - - CHECK_ERROR(syncMsgDataSet->timestampDataSet->writeDataBlock(1, BaseDataType::F64, ×tamp)); - - syncMsgDataSet->numSamples += 1; - } - - - String NWBFile::getFileName() - { - return filename; - } - - bool NWBFile::createTimeSeriesBase(TimeSeries* timeSeries) - { - if (createGroup(timeSeries->basePath)) return false; - CHECK_ERROR(setAttributeStr(" ", timeSeries->basePath, "comments")); - CHECK_ERROR(setAttributeStr(timeSeries->description, timeSeries->basePath, "description")); - CHECK_ERROR(setAttributeStr("core", timeSeries->basePath, "namespace")); - CHECK_ERROR(setAttributeStr(generateUuid(), timeSeries->basePath, "object_id")); - CHECK_ERROR(setAttributeStr(timeSeries->getNeurodataType(), timeSeries->basePath, "neurodata_type")); - return true; - } - - void NWBFile::createDataAttributes(String basePath, float conversion, float resolution, String unit) - { - CHECK_ERROR(setAttribute(BaseDataType::F32, &conversion, basePath + "/data", "conversion")); - CHECK_ERROR(setAttribute(BaseDataType::F32, &resolution, basePath + "/data", "resolution")); - CHECK_ERROR(setAttributeStr(unit, basePath + "/data", "unit")); - } - - HDF5RecordingData* NWBFile::createTimestampDataSet(String path, int chunk_size, float interval) - { - - HDF5RecordingData* tsSet = createDataSet(BaseDataType::F64, 0, chunk_size, path); - - if (!tsSet) - std::cerr << "Error creating timestamp dataset in " << path << std::endl; - else - { - CHECK_ERROR(setAttribute(BaseDataType::F32, &interval, path, "interval")); - CHECK_ERROR(setAttributeStr("seconds", path, "unit")); - } - - return tsSet; - } - - HDF5RecordingData* NWBFile::createSampleNumberDataSet(String path, int chunk_size) - { - HDF5RecordingData* tsSet = createDataSet(BaseDataType::I64, 0, chunk_size, path); - if (!tsSet) - std::cerr << "Error creating sample number dataset in " << path << std::endl; - else - { - const int32 one = 1; - CHECK_ERROR(setAttribute(BaseDataType::I32, &one, path, "interval")); - CHECK_ERROR(setAttributeStr("samples", path, "unit")); - } - return tsSet; - } - -HDF5RecordingData *NWBFile::createChannelConversionDataSet(String path, String description, int chunk_size) -{ - HDF5RecordingData *elSet = createDataSet(BaseDataType::F32, 1, chunk_size, path); - - if (!elSet) - std::cerr << "Error creating electrode dataset in " << path << std::endl; - else - { - CHECK_ERROR(setAttributeStr(description, path, "description")); - CHECK_ERROR(setAttributeStr("hdmf-common", path, "namespace")); - CHECK_ERROR(setAttributeStr(generateUuid(), path, "object_id")); - } - return elSet; -} - -HDF5RecordingData *NWBFile::createElectrodeDataSet(String path, String description, int chunk_size) -{ - HDF5RecordingData *elSet = createDataSet(BaseDataType::I32, 1, chunk_size, path); - if (!elSet) - std::cerr << "Error creating electrode dataset in " << path << std::endl; - else - { - CHECK_ERROR(setAttributeStr(description, path, "description")); - CHECK_ERROR(setAttributeStr("hdmf-common", path, "namespace")); - CHECK_ERROR(setAttributeStr("DynamicTableRegion", path, "neurodata_type")); - CHECK_ERROR(setAttributeStr(generateUuid(), path, "object_id")); - CHECK_ERROR(setAttributeRef("general/extracellular_ephys/electrodes", path, "table")); - } - return elSet; -} - - bool NWBFile::createExtraInfo(String basePath, String name, String desc, String id, uint16 index, uint16 typeIndex) - { - if (createGroup(basePath)) return false; - CHECK_ERROR(setAttributeStr("openephys:/", basePath, "schema_id")); - CHECK_ERROR(setAttributeStr(name, basePath, "name")); - CHECK_ERROR(setAttributeStr(desc, basePath, "description")); - CHECK_ERROR(setAttributeStr(id, basePath, "identifier")); - CHECK_ERROR(setAttribute(BaseDataType::U16, &index, basePath, "source_index")); - CHECK_ERROR(setAttribute(BaseDataType::U16, &typeIndex, basePath, "source_type_index")); - return true; - } - - bool NWBFile::createChannelMetadataSets(String basePath, const MetadataObject* info) - { - if (!info) return false; - if (createGroup(basePath)) return false; - CHECK_ERROR(setAttributeStr("openephys:/", basePath, "schema_id")); - int nMetadata = info->getMetadataCount(); - - for (int i - = 0; i < nMetadata; i++) - { - const MetadataDescriptor* desc = info->getMetadataDescriptor(i); - String fieldName = "Field_" + String(i+1); - String name = desc->getName(); - String description = desc->getDescription(); - String identifier = desc->getIdentifier(); - BaseDataType type = getMetadataH5Type(desc->getType(), desc->getLength()); //only string types use length, for others is always set to 1. If array types are implemented, change this - int length = desc->getType() == MetadataDescriptor::CHAR ? 1 : desc->getLength(); //strings are a single element of length set in the type (see above) while other elements are saved a - HeapBlock data(desc->getDataSize()); - info->getMetadataValue(i)->getValue(static_cast(data.getData())); - createBinaryDataSet(basePath, fieldName, type, length, data.getData()); - String fullPath = basePath + "/" + fieldName; - CHECK_ERROR(setAttributeStr("openephys:/", fullPath, "schema_id")); - CHECK_ERROR(setAttributeStr(name, fullPath, "name")); - CHECK_ERROR(setAttributeStr(description, fullPath, "description")); - CHECK_ERROR(setAttributeStr(identifier, fullPath, "identifier")); - } - return true; - } - - - bool NWBFile::createEventMetadataSets(String basePath, TimeSeries* timeSeries, const MetadataEventObject* info) - { - if (!info) return false; - if (createGroup(basePath)) return false; - CHECK_ERROR(setAttributeStr("openephys:/", basePath, "schema_id")); - int nMetadata = info->getEventMetadataCount(); - - timeSeries->metaDataSet.clear(); //just in case - for (int i = 0; i < nMetadata; i++) - { - const MetadataDescriptor* desc = info->getEventMetadataDescriptor(i); - String fieldName = "Field_" + String(i+1); - String name = desc->getName(); - String description = desc->getDescription(); - String identifier = desc->getIdentifier(); - BaseDataType type = getMetadataH5Type(desc->getType(), desc->getLength()); //only string types use length, for others is always set to 1. If array types are implemented, change this - int length = desc->getType() == MetadataDescriptor::CHAR ? 1 : desc->getLength(); //strings are a single element of length set in the type (see above) while other elements are saved as arrays - String fullPath = basePath + "/" + fieldName; - HDF5RecordingData* dSet = createDataSet(type, 0, length, EVENT_CHUNK_SIZE, fullPath); - if (!dSet) return false; - timeSeries->metaDataSet.add(dSet); - - CHECK_ERROR(setAttributeStr("openephys:/", fullPath, "schema_id")); - CHECK_ERROR(setAttributeStr(name, fullPath, "name")); - CHECK_ERROR(setAttributeStr(description, fullPath, "description")); - CHECK_ERROR(setAttributeStr(identifier, fullPath, "identifier")); - } - return true; - } - - void NWBFile::writeEventMetadata(TimeSeries* timeSeries, const MetadataEventObject* info, const MetadataEvent* event) - { - jassert(timeSeries->metaDataSet.size() == event->getMetadataValueCount()); - jassert(info->getEventMetadataCount() == event->getMetadataValueCount()); - int nMetadata = event->getMetadataValueCount(); - for (int i = 0; i < nMetadata; i++) - { - BaseDataType type = getMetadataH5Type(info->getEventMetadataDescriptor(i)->getType(), info->getEventMetadataDescriptor(i)->getLength()); - timeSeries->metaDataSet[i]->writeDataBlock(1, type, event->getMetadataValue(i)->getRawValuePointer()); - } - - } - - void NWBFile::createTextDataSet(String path, String name, String text) - { - ScopedPointer dSet; - - if (text.isEmpty()) text = " "; //to avoid 0-length strings, which cause errors - BaseDataType type = BaseDataType::STR(text.length()); - - dSet = createDataSet(type, 1, 0, path + "/" + name); - if (!dSet) return; - dSet->writeDataBlock(1, type, text.toUTF8()); - } - - void NWBFile::createBinaryDataSet(String path, String name, BaseDataType type, int length, void* data) - { - ScopedPointer dSet; - if ((length < 1) || !data) return; - - dSet = createDataSet(type, 1, length, 1, path + "/" + name); - if (!dSet) return; - dSet->writeDataBlock(1, type, data); - } - -String NWBFile::generateUuid() -{ - Uuid id; - return id.toDashedString(); -} - - //These two methods whould be easy to adapt to support array types for all base types, for now - //length is only used for string types. - NWBFile::BaseDataType NWBFile::getEventH5Type(EventChannel::Type type, int length) - { - switch (type) - { - case EventChannel::INT8_ARRAY: - return BaseDataType::I8; - case EventChannel::UINT8_ARRAY: - return BaseDataType::U8; - case EventChannel::INT16_ARRAY: - return BaseDataType::I16; - case EventChannel::UINT16_ARRAY: - return BaseDataType::U16; - case EventChannel::INT32_ARRAY: - return BaseDataType::I32; - case EventChannel::UINT32_ARRAY: - return BaseDataType::U32; - case EventChannel::INT64_ARRAY: - return BaseDataType::I64; - case EventChannel::UINT64_ARRAY: - return BaseDataType::U64; - case EventChannel::FLOAT_ARRAY: - return BaseDataType::F32; - case EventChannel::DOUBLE_ARRAY: - return BaseDataType::F64; - case EventChannel::TEXT: - return BaseDataType::STR(length); - default: - return BaseDataType::I8; - } - } - NWBFile::BaseDataType NWBFile::getMetadataH5Type(MetadataDescriptor::MetadataType type, int length) - { - switch (type) - { - case MetadataDescriptor::INT8: - return BaseDataType::I8; - case MetadataDescriptor::UINT8: - return BaseDataType::U8; - case MetadataDescriptor::INT16: - return BaseDataType::I16; - case MetadataDescriptor::UINT16: - return BaseDataType::U16; - case MetadataDescriptor::INT32: - return BaseDataType::I32; - case MetadataDescriptor::UINT32: - return BaseDataType::U32; - case MetadataDescriptor::INT64: - return BaseDataType::I64; - case MetadataDescriptor::UINT64: - return BaseDataType::U64; - case MetadataDescriptor::FLOAT: - return BaseDataType::F32; - case MetadataDescriptor::DOUBLE: - return BaseDataType::F64; - case MetadataDescriptor::CHAR: - return BaseDataType::STR(length); - default: - return BaseDataType::I8; - } - } diff --git a/Source/RecordEngine/NWBFormat.h b/Source/RecordEngine/NWBFormat.h deleted file mode 100644 index e85f6d8..0000000 --- a/Source/RecordEngine/NWBFormat.h +++ /dev/null @@ -1,274 +0,0 @@ -/* - ------------------------------------------------------------------ - - This file is part of the Open Ephys GUI - Copyright (C) 2014 Open Ephys - - ------------------------------------------------------------------ - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - */ - -#ifndef NWBFORMAT_H -#define NWBFORMAT_H - -#include -#include -#include - -using namespace OpenEphysHDF5; - -namespace NWBRecording -{ - - typedef Array ContinuousGroup; - - /** - Represents a generic NWB TimeSeries dataset - */ - class TimeSeries - { - public: - - /** Constructor */ - TimeSeries(String rootPath, String name, String description); - - /** Holds the sample data */ - ScopedPointer baseDataSet; - - /** Holds the timestamps (in seconds) for each sample */ - ScopedPointer timestampDataSet; - - /** Holds the sample number for each sample (relative to the start of acquisition) */ - ScopedPointer sampleNumberDataSet; - - /** Holds metadata for this time series */ - OwnedArray metaDataSet; - - /** The path to this dataset within the NWB file */ - String basePath; - - /** The description of this dataset*/ - String description; - - /** Total number of samples written */ - uint64 numSamples = 0; - - /** Get neurodata_type */ - virtual String getNeurodataType() { return "TimeSeries";} - - }; - - namespace ecephys { - /** - Represents an NWB ElectricalSeries dataset - */ - class ElectricalSeries : public TimeSeries - { - public: - /** Constructor */ - ElectricalSeries(String rootPath, String name, String description, - int channel_count, Array channel_conversion); - - /** Holds the sample number for each sample (relative to the start of acquisition) */ - ScopedPointer channelConversionDataSet; - - /** Holds the DynamicTableRegion index of each electrode */ - ScopedPointer electrodeDataSet; - - /** Channel conversion values */ - Array channel_conversion; - - /** Number of channels to write */ - int channel_count; - - /** Get neurodata_type */ - virtual String getNeurodataType() override { return "ElectricalSeries";} - }; - - /** - Represents a sequence of spike events - */ - class SpikeEventSeries : public ElectricalSeries - { - - public: - /** Constructor */ - SpikeEventSeries(String rootPath, String name, String description, - int channel_count, Array channel_conversion); - - /** Get neurodata_type */ - virtual String getNeurodataType() override { return "SpikeEventSeries";} - }; - } - - /** - Represents a TTL event series (not a core NWB data type) - */ - class TTLEventSeries : public TimeSeries - { - public: - /** Constructor */ - TTLEventSeries(String rootPath, String name, String description); - - /** Holds the TTL word for each sample */ - ScopedPointer ttlWordDataSet; - - /** Get neurodata_type */ - virtual String getNeurodataType() override { return "TimeSeries";} - }; - - /** - Represents a sequence of string annotations - */ - class AnnotationSeries : public TimeSeries - { - public: - /** Constructor */ - AnnotationSeries(String rootPath, String name, String description); - - /** Get neurodata_type */ - virtual String getNeurodataType() override { return "AnnotationSeries";} - }; - - /** - - Represents an NWB 2.0 File (a specific type of HDF5 file) - - */ - class NWBFile : public HDF5FileBase - { - public: - - /** Constructor */ - NWBFile(String fName, String ver, String identifier); - - /** Destructor */ - ~NWBFile(); - - /** Creates the groups required for a new recording, given an array of continuous channels, event channels, and spike channels*/ - bool startNewRecording(int recordingNumber, - const Array& continuousArray, - const Array& continuousChannels, - const Array& eventArray, - const Array& electrodeArray); - - /** Writes the num_samples value and closes the relevent datasets */ - void stopRecording(); - - /** Writes continuous data for a particular channel */ - void writeData(int datasetID, int channel, int nSamples, const float* data, float bitVolts); - - /** Writes synchronized timestamps for a particular continuous dataset */ - void writeTimestamps(int datasetID, int nSamples, const double* data); - - /** Writes sample numbers for a particular continuous dataset */ - void writeSampleNumbers(int datasetID, int nSamples, const int64* data); - - /** Writes electrode numbers for a continuous dataset */ - void writeElectrodes(ecephys::ElectricalSeries* electricalSeries, Array electrodeInds); - - /** Writes channel conversion values */ - void writeChannelConversions(ecephys::ElectricalSeries* series); - - /** Writes a spike event*/ - void writeSpike(int electrodeId, const SpikeChannel* channel, const Spike* event); - - /** Writes an event (TEXT or TTL) */ - void writeEvent(int eventID, const EventChannel* channel, const Event* event); - - /** Writes a timestamp sync text event */ - void writeTimestampSyncText(uint16 sourceID, - int64 timestamp, - float sourceSampleRate, - String text); - - /** Returns the name of this NWB file */ - String getFileName() override; - - /** Generate a new uuid string*/ - String generateUuid(); - - protected: - - /** Initializes the default groups */ - int createFileStructure() override; - - private: - - /** Creates a new dataset to hold text data (messages) */ - void createTextDataSet(String path, String name, String text); - - /** Creates a new dataset to hold binary events */ - void createBinaryDataSet(String path, String name, HDF5FileBase::BaseDataType type, int length, void* data); - - /** Returns the HDF5 data type for a given event channel type */ - static HDF5FileBase::BaseDataType getEventH5Type(EventChannel::Type type, int length = 1); - - /** Returns the HDF5 data type for a given metadata type*/ - static HDF5FileBase::BaseDataType getMetadataH5Type(MetadataDescriptor::MetadataType type, int length = 1); - - /** Creates a time series dataset*/ - bool createTimeSeriesBase(TimeSeries* timeSeries); - - /** Creates dataset attributes */ - bool createExtraInfo(String basePath, String name, String desc, String id, uint16 index, uint16 typeIndex); - - /** Creates a dataset of synchronized timestamps (interval = 1/sample_rate) */ - HDF5RecordingData* createTimestampDataSet(String basePath, int chunk_size, float interval); - - /** Creates a dataset of sample numbers */ - HDF5RecordingData* createSampleNumberDataSet(String basePath, int chunk_size); - - /** Creates a dataset for electrode indices */ - HDF5RecordingData* createElectrodeDataSet(String basePath, String description, int chunk_size); - - /** Creates a dataset for electrode indices */ - HDF5RecordingData* createChannelConversionDataSet(String basePath, String description, int chunk_size); - - /** Adds attributes (e.g. conversion, resolution) to a continuous dataset */ - void createDataAttributes(String basePath, float conversion, float resolution, String unit); - - /** Creates a dataset for channel metdata */ - bool createChannelMetadataSets(String basePath, const MetadataObject* info); - - /** Creates a dataset for event metdata */ - bool createEventMetadataSets(String basePath, TimeSeries* timeSeries, const MetadataEventObject* info); - - /** Writes metadata associated with an event*/ - void writeEventMetadata(TimeSeries* timeSeries, const MetadataEventObject* info, const MetadataEvent* event); - - const String filename; - const String GUIVersion; - - OwnedArray continuousDataSets; - OwnedArray spikeDataSets; - OwnedArray eventDataSets; - std::unique_ptr messagesDataSet; - std::unique_ptr syncMsgDataSet; - - const String identifierText; - - HeapBlock scaledBuffer; - HeapBlock intBuffer; - size_t bufferSize; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(NWBFile); - - }; - -} - -#endif From 5c7508930a2a116ab027f7286720238d243f965b Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:11:15 -0700 Subject: [PATCH 32/32] update clearing of existing data --- Source/RecordEngine/NWBRecording.cpp | 52 ++++++++++++++++------------ Source/RecordEngine/NWBRecording.h | 3 -- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/Source/RecordEngine/NWBRecording.cpp b/Source/RecordEngine/NWBRecording.cpp index d9b13ff..a38c7c8 100644 --- a/Source/RecordEngine/NWBRecording.cpp +++ b/Source/RecordEngine/NWBRecording.cpp @@ -41,7 +41,21 @@ NWBRecordEngine::~NWBRecordEngine() { - NWBRecordEngine::reset(); + if (this->nwbfile != nullptr) + { + + this->continuousChannels.clear(); + this->continuousChannelGroups.clear(); + this->spikeChannels.clear(); + + this->recordingArrays.clear(); + this->spikeRecordingArrays.clear(); + this->esContainerIndexes.clear(); + this->spikeContainerIndexes.clear(); + + this->nwbfile->finalize(); + this->nwbfile.reset(); + } } RecordEngineManager* NWBRecordEngine::getEngineManager() @@ -60,7 +74,20 @@ RecordEngineManager* NWBRecordEngine::getEngineManager() if (recordingNumber == 0) // new file needed { // clear any existing data and nwbfile - NWBRecordEngine::reset(); + this->continuousChannels.clear(); + this->continuousChannelGroups.clear(); + this->spikeChannels.clear(); + + this->recordingArrays.clear(); + this->spikeRecordingArrays.clear(); + this->esContainerIndexes.clear(); + this->spikeContainerIndexes.clear(); + + if (this->nwbfile != nullptr) + { + this->nwbfile->finalize(); + this->nwbfile.reset(); + } // create the io object char separator = std::filesystem::path::preferred_separator; @@ -174,27 +201,6 @@ void NWBRecordEngine::setParameter(EngineParameter& parameter) strParameter(0, identifierText); } -void NWBRecordEngine::reset() -{ - if (this->nwbfile != nullptr) - { - - this->continuousChannels.clear(); - this->continuousChannelGroups.clear(); - this->spikeChannels.clear(); - - this->recordingArrays.clear(); - this->recordingArraysNames.clear(); - this->spikeRecordingArrays.clear(); - this->spikeRecordingArraysNames.clear(); - this->esContainerIndexes.clear(); - this->spikeContainerIndexes.clear(); - - this->nwbfile->finalize(); - this->nwbfile.reset(); - } -} - void NWBRecordEngine::createRecordingArrays() { // get pointers to all continuous channels for electrode table diff --git a/Source/RecordEngine/NWBRecording.h b/Source/RecordEngine/NWBRecording.h index 83c0a00..c841f6a 100644 --- a/Source/RecordEngine/NWBRecording.h +++ b/Source/RecordEngine/NWBRecording.h @@ -80,9 +80,6 @@ namespace NWBRecording /** Allows the file identifier to be set externally*/ void setParameter(EngineParameter ¶meter) override; - /** Reset the engine */ - void reset(); - /** Create recording arrays */ void createRecordingArrays();

P# zd>B~|^zZtF!%7eOqm^Se@R@H9tWbaMWq>=^2;wvb<$Ppgi%W;`Gp$Tw5kQ7^&`#lCu_C!KBomaeLhrwAYEWV4i8s z%$9YG=pSNJFpR`a7Y9BEiLm@E5PZuX!#tR|v+|CDs3?zMqWCQE+YbUaKp0(@KzT0q zb{A*w_$@7?;9%>#{0Ql|r>Zv+0i*fYvQg1*&c-i0dJxL}T1HhmtbK}P#0n`tPgoWE nH7uwk*$gEPJ4JgV(wKvONt|NDjlwy;y88 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 z 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 z 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 VL8ZI}uBiQSJg@@*00000004!0Npt`J diff --git a/libs/macos/include/aqnwb/BaseIO.hpp b/libs/macos/include/aqnwb/BaseIO.hpp index 0f897e4..6e571a4 100644 --- a/libs/macos/include/aqnwb/BaseIO.hpp +++ b/libs/macos/include/aqnwb/BaseIO.hpp @@ -15,6 +15,10 @@ using Status = AQNWB::Types::Status; using SizeArray = AQNWB::Types::SizeArray; using SizeType = AQNWB::Types::SizeType; +/*! + * \namespace AQNWB + * \brief The main namespace for AqNWB + */ namespace AQNWB { @@ -132,6 +136,12 @@ class BaseIO */ virtual Status close() = 0; + /** + * @brief Flush data to disk + * @return The status of the flush operation. + */ + virtual Status flush() = 0; + /** * @brief Creates an attribute at a given location in the file. * @param type The base data type of the attribute. @@ -238,6 +248,27 @@ class BaseIO virtual Status createReferenceDataSet( const std::string& path, const std::vector& references) = 0; + /** + * @brief Starts the recording process. + * @return The status of the operation. + */ + virtual Status startRecording() = 0; + + /** + * @brief Stops the recording process. + * @return The status of the operation. + */ + virtual Status stopRecording() = 0; + + /** + * @brief Returns true if the file is in a mode where objects can + * be added or deleted. Note, this does not apply to the modification + * of raw data on already existing objects. Derived classes should + * override this function to check if objects can be modified. + * @return True if the file is in a modification mode, false otherwise. + */ + virtual bool canModifyObjects(); + /** * @brief Creates an extendable dataset with a given base data type, size, * chunking, and path. diff --git a/libs/macos/include/aqnwb/hdf5/HDF5IO.hpp b/libs/macos/include/aqnwb/hdf5/HDF5IO.hpp index 30e2137..f98408c 100644 --- a/libs/macos/include/aqnwb/hdf5/HDF5IO.hpp +++ b/libs/macos/include/aqnwb/hdf5/HDF5IO.hpp @@ -17,6 +17,10 @@ class DataType; class Exception; } // namespace H5 +/*! + * \namespace AQNWB::HDF5 + * \brief Namespace for all components of the HDF5 I/O backend + */ namespace AQNWB::HDF5 { class HDF5RecordingData; // declare here because gets used in HDF5IO class @@ -36,8 +40,13 @@ class HDF5IO : public BaseIO /** * @brief Constructor for the HDF5IO class that takes a file name as input. * @param fileName The name of the HDF5 file. + * @param disableSWMRMode Disable recording of data in Single Writer + * Multiple Reader (SWMR) mode. Using SWMR ensures that the + * HDF5 file remains valid and readable at all times during + * the recording process (but does not allow for new objects + * (Groups or Datasets) to be created. */ - HDF5IO(const std::string& fileName); + HDF5IO(const std::string& fileName, const bool disableSWMRMode = false); /** * @brief Destructor for the HDF5IO class. @@ -69,6 +78,12 @@ class HDF5IO : public BaseIO */ Status close() override; + /** + * @brief Flush data to disk + * @return The status of the flush operation. + */ + Status flush() override; + /** * @brief Creates an attribute at a given location in the file. * @param type The base data type of the attribute. @@ -176,6 +191,26 @@ class HDF5IO : public BaseIO const std::string& path, const std::vector& references) override; + /** + * @brief Start SWMR write to start recording process + * @return The status of the start recording operation. + */ + Status startRecording() override; + + /** + * @brief Stops the recording process. + * @return The status of the stop recording operation. + */ + Status stopRecording() override; + + /** + * @brief Checks whether the file is in a mode where objects + * can be added or deleted. Note, this does not apply to the modification + * of raw data on already existing objects. + * @return Whether objects can be modified. + */ + bool canModifyObjects() override; + /** * @brief Creates an extendable dataset with a given base data type, size, * chunking, and path. @@ -232,6 +267,8 @@ class HDF5IO : public BaseIO private: std::unique_ptr file; + bool disableSWMRMode; // when set do not use SWMR mode when opening the HDF5 + // file }; /** diff --git a/libs/macos/include/aqnwb/nwb/NWBFile.hpp b/libs/macos/include/aqnwb/nwb/NWBFile.hpp index 6c78d07..9d54289 100644 --- a/libs/macos/include/aqnwb/nwb/NWBFile.hpp +++ b/libs/macos/include/aqnwb/nwb/NWBFile.hpp @@ -8,6 +8,10 @@ #include "aqnwb/Types.hpp" #include "aqnwb/nwb/base/TimeSeries.hpp" +/*! + * \namespace AQNWB::NWB + * \brief Namespace for all classes related to the NWB data standard + */ namespace AQNWB::NWB { @@ -46,19 +50,22 @@ class NWBFile * @brief Initializes the NWB file by opening and setting up the file * structure. */ - void initialize(); + Status initialize(); /** * @brief Finalizes the NWB file by closing it. */ - void finalize(); + Status finalize(); /** * @brief Create ElectricalSeries objects to record data into. * Created objects are stored in recordingContainers. + * Note, this function will fail if the file is in a mode where + * new objects cannot be added, which can be checked via + * nwbfile.io->canModifyObjects() * @param recordingArrays vector of ChannelVector indicating the electrodes to * record from. A separate ElectricalSeries will be - * created for each ChannelVector + * created for each ChannelVector. * @param dataType The data type of the elements in the data block. * @return Status The status of the object creation operation. */ @@ -67,7 +74,12 @@ class NWBFile const BaseDataType& dataType = BaseDataType::I16); /** - * @brief Closes the relevant datasets. + * @brief Starts the recording. + */ + Status startRecording(); + + /** + * @brief Stops the recording. */ void stopRecording(); @@ -95,6 +107,9 @@ class NWBFile protected: /** * @brief Creates the default file structure. + * Note, this function will fail if the file is in a mode where + * new objects cannot be added, which can be checked via + * nwbfile.io->canModifyObjects() * @return Status The status of the file structure creation. */ Status createFileStructure(); From 46122fc8480e9d8319509ecb04dee3b8d3b61dca Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Tue, 27 Aug 2024 09:38:21 -0700 Subject: [PATCH 04/32] update to static lib --- Source/aqnwb/aqnwb/BaseIO.cpp | 108 +++ .../include => Source/aqnwb}/aqnwb/BaseIO.hpp | 0 Source/aqnwb/aqnwb/Channel.cpp | 43 ++ .../aqnwb}/aqnwb/Channel.hpp | 0 .../include => Source/aqnwb}/aqnwb/Types.hpp | 0 .../include => Source/aqnwb}/aqnwb/Utils.hpp | 0 Source/aqnwb/aqnwb/aqnwb.hpp | 6 + Source/aqnwb/aqnwb/aqnwb_export.hpp | 42 + Source/aqnwb/aqnwb/hdf5/HDF5IO.cpp | 727 ++++++++++++++++++ .../aqnwb}/aqnwb/hdf5/HDF5IO.hpp | 0 Source/aqnwb/aqnwb/nwb/NWBFile.cpp | 211 +++++ .../aqnwb}/aqnwb/nwb/NWBFile.hpp | 1 + Source/aqnwb/aqnwb/nwb/NWBRecording.cpp | 67 ++ .../aqnwb}/aqnwb/nwb/NWBRecording.hpp | 12 +- Source/aqnwb/aqnwb/nwb/base/TimeSeries.cpp | 80 ++ .../aqnwb}/aqnwb/nwb/base/TimeSeries.hpp | 0 Source/aqnwb/aqnwb/nwb/device/Device.cpp | 38 + .../aqnwb}/aqnwb/nwb/device/Device.hpp | 0 .../aqnwb/nwb/ecephys/ElectricalSeries.cpp | 94 +++ .../aqnwb/nwb/ecephys/ElectricalSeries.hpp | 0 .../aqnwb/aqnwb/nwb/file/ElectrodeGroup.cpp | 48 ++ .../aqnwb}/aqnwb/nwb/file/ElectrodeGroup.hpp | 0 .../aqnwb/aqnwb/nwb/file/ElectrodeTable.cpp | 84 ++ .../aqnwb}/aqnwb/nwb/file/ElectrodeTable.hpp | 0 .../aqnwb/aqnwb/nwb/hdmf/base/Container.cpp | 27 + .../aqnwb}/aqnwb/nwb/hdmf/base/Container.hpp | 0 .../aqnwb}/aqnwb/nwb/hdmf/base/Data.hpp | 0 .../aqnwb/nwb/hdmf/table/DynamicTable.cpp | 84 ++ .../aqnwb/nwb/hdmf/table/DynamicTable.hpp | 0 .../nwb/hdmf/table/ElementIdentifiers.hpp | 0 .../aqnwb/aqnwb/nwb/hdmf/table/VectorData.cpp | 9 + .../aqnwb/nwb/hdmf/table/VectorData.hpp | 0 libs/macos/bin/libaqnwb.0.1.0.dylib | Bin 681336 -> 0 bytes libs/macos/bin/libaqnwb.0.dylib | 1 - libs/macos/include/aqnwb/aqnwb.hpp | 27 - libs/macos/lib/libaqnwb.dylib | 1 - 36 files changed, 1673 insertions(+), 37 deletions(-) create mode 100644 Source/aqnwb/aqnwb/BaseIO.cpp rename {libs/macos/include => Source/aqnwb}/aqnwb/BaseIO.hpp (100%) create mode 100644 Source/aqnwb/aqnwb/Channel.cpp rename {libs/macos/include => Source/aqnwb}/aqnwb/Channel.hpp (100%) rename {libs/macos/include => Source/aqnwb}/aqnwb/Types.hpp (100%) rename {libs/macos/include => Source/aqnwb}/aqnwb/Utils.hpp (100%) create mode 100644 Source/aqnwb/aqnwb/aqnwb.hpp create mode 100644 Source/aqnwb/aqnwb/aqnwb_export.hpp create mode 100644 Source/aqnwb/aqnwb/hdf5/HDF5IO.cpp rename {libs/macos/include => Source/aqnwb}/aqnwb/hdf5/HDF5IO.hpp (100%) create mode 100644 Source/aqnwb/aqnwb/nwb/NWBFile.cpp rename {libs/macos/include => Source/aqnwb}/aqnwb/nwb/NWBFile.hpp (99%) create mode 100644 Source/aqnwb/aqnwb/nwb/NWBRecording.cpp rename {libs/macos/include => Source/aqnwb}/aqnwb/nwb/NWBRecording.hpp (85%) create mode 100644 Source/aqnwb/aqnwb/nwb/base/TimeSeries.cpp rename {libs/macos/include => Source/aqnwb}/aqnwb/nwb/base/TimeSeries.hpp (100%) create mode 100644 Source/aqnwb/aqnwb/nwb/device/Device.cpp rename {libs/macos/include => Source/aqnwb}/aqnwb/nwb/device/Device.hpp (100%) create mode 100644 Source/aqnwb/aqnwb/nwb/ecephys/ElectricalSeries.cpp rename {libs/macos/include => Source/aqnwb}/aqnwb/nwb/ecephys/ElectricalSeries.hpp (100%) create mode 100644 Source/aqnwb/aqnwb/nwb/file/ElectrodeGroup.cpp rename {libs/macos/include => Source/aqnwb}/aqnwb/nwb/file/ElectrodeGroup.hpp (100%) create mode 100644 Source/aqnwb/aqnwb/nwb/file/ElectrodeTable.cpp rename {libs/macos/include => Source/aqnwb}/aqnwb/nwb/file/ElectrodeTable.hpp (100%) create mode 100644 Source/aqnwb/aqnwb/nwb/hdmf/base/Container.cpp rename {libs/macos/include => Source/aqnwb}/aqnwb/nwb/hdmf/base/Container.hpp (100%) rename {libs/macos/include => Source/aqnwb}/aqnwb/nwb/hdmf/base/Data.hpp (100%) create mode 100644 Source/aqnwb/aqnwb/nwb/hdmf/table/DynamicTable.cpp rename {libs/macos/include => Source/aqnwb}/aqnwb/nwb/hdmf/table/DynamicTable.hpp (100%) rename {libs/macos/include => Source/aqnwb}/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp (100%) create mode 100644 Source/aqnwb/aqnwb/nwb/hdmf/table/VectorData.cpp rename {libs/macos/include => Source/aqnwb}/aqnwb/nwb/hdmf/table/VectorData.hpp (100%) delete mode 100755 libs/macos/bin/libaqnwb.0.1.0.dylib delete mode 120000 libs/macos/bin/libaqnwb.0.dylib delete mode 100644 libs/macos/include/aqnwb/aqnwb.hpp delete mode 120000 libs/macos/lib/libaqnwb.dylib diff --git a/Source/aqnwb/aqnwb/BaseIO.cpp b/Source/aqnwb/aqnwb/BaseIO.cpp new file mode 100644 index 0000000..1cd0f45 --- /dev/null +++ b/Source/aqnwb/aqnwb/BaseIO.cpp @@ -0,0 +1,108 @@ +#include "aqnwb/BaseIO.hpp" + +#include "aqnwb/Utils.hpp" + +using namespace AQNWB; + +// BaseDataType + +BaseDataType::BaseDataType(BaseDataType::Type t, SizeType s) + : type(t) + , typeSize(s) +{ +} + +BaseDataType BaseDataType::STR(SizeType size) +{ + return BaseDataType(T_STR, size); +} + +const BaseDataType BaseDataType::U8 = BaseDataType(T_U8, 1); +const BaseDataType BaseDataType::U16 = BaseDataType(T_U16, 1); +const BaseDataType BaseDataType::U32 = BaseDataType(T_U32, 1); +const BaseDataType BaseDataType::U64 = BaseDataType(T_U64, 1); +const BaseDataType BaseDataType::I8 = BaseDataType(T_I8, 1); +const BaseDataType BaseDataType::I16 = BaseDataType(T_I16, 1); +const BaseDataType BaseDataType::I32 = BaseDataType(T_I32, 1); +const BaseDataType BaseDataType::I64 = BaseDataType(T_I64, 1); +const BaseDataType BaseDataType::F32 = BaseDataType(T_F32, 1); +const BaseDataType BaseDataType::F64 = BaseDataType(T_F64, 1); +const BaseDataType BaseDataType::DSTR = BaseDataType(T_STR, DEFAULT_STR_SIZE); + +// BaseIO + +BaseIO::BaseIO() + : readyToOpen(true) + , opened(false) +{ +} + +BaseIO::~BaseIO() {} + +bool BaseIO::isOpen() const +{ + return opened; +} + +bool BaseIO::isReadyToOpen() const +{ + return readyToOpen; +} + +bool BaseIO::canModifyObjects() +{ + return true; +} + +Status BaseIO::createCommonNWBAttributes(const std::string& path, + const std::string& objectNamespace, + const std::string& neurodataType, + const std::string& description) +{ + createAttribute(objectNamespace, path, "namespace"); + createAttribute(generateUuid(), path, "object_id"); + if (neurodataType != "") + createAttribute(neurodataType, path, "neurodata_type"); + if (description != "") + createAttribute(description, path, "description"); + return Status::Success; +} + +Status BaseIO::createDataAttributes(const std::string& path, + const float& conversion, + const float& resolution, + const std::string& unit) +{ + createAttribute(BaseDataType::F32, &conversion, path + "/data", "conversion"); + createAttribute(BaseDataType::F32, &resolution, path + "/data", "resolution"); + createAttribute(unit, path + "/data", "unit"); + + return Status::Success; +} + +Status BaseIO::createTimestampsAttributes(const std::string& path) +{ + int interval = 1; + createAttribute(BaseDataType::I32, + static_cast(&interval), + path + "/timestamps", + "interval"); + createAttribute("seconds", path + "/timestamps", "unit"); + + return Status::Success; +} + +// BaseRecordingData + +BaseRecordingData::BaseRecordingData() {} + +BaseRecordingData::~BaseRecordingData() {} + +// Overload that uses the member variable position (works for simple data +// extension) +Status BaseRecordingData::writeDataBlock(const std::vector& dataShape, + const BaseDataType& type, + const void* data) +{ + return writeDataBlock(dataShape, position, type, data); +} diff --git a/libs/macos/include/aqnwb/BaseIO.hpp b/Source/aqnwb/aqnwb/BaseIO.hpp similarity index 100% rename from libs/macos/include/aqnwb/BaseIO.hpp rename to Source/aqnwb/aqnwb/BaseIO.hpp diff --git a/Source/aqnwb/aqnwb/Channel.cpp b/Source/aqnwb/aqnwb/Channel.cpp new file mode 100644 index 0000000..ccc3f72 --- /dev/null +++ b/Source/aqnwb/aqnwb/Channel.cpp @@ -0,0 +1,43 @@ +#include + +#include "aqnwb/Channel.hpp" + +using namespace AQNWB; + +Channel::Channel(const std::string name, + const std::string groupName, + const SizeType localIndex, + const SizeType globalIndex, + const float conversion, + const float samplingRate, + const float bitVolts, + const std::array position, + const std::string comments) + : name(name) + , groupName(groupName) + , localIndex(localIndex) + , globalIndex(globalIndex) + , position(position) + , conversion(conversion) + , samplingRate(samplingRate) + , bitVolts(bitVolts) + , comments(comments) +{ +} + +Channel::~Channel() {} + +float Channel::getConversion() const +{ + return bitVolts / conversion; +} + +float Channel::getSamplingRate() const +{ + return samplingRate; +} + +float Channel::getBitVolts() const +{ + return bitVolts; +} diff --git a/libs/macos/include/aqnwb/Channel.hpp b/Source/aqnwb/aqnwb/Channel.hpp similarity index 100% rename from libs/macos/include/aqnwb/Channel.hpp rename to Source/aqnwb/aqnwb/Channel.hpp diff --git a/libs/macos/include/aqnwb/Types.hpp b/Source/aqnwb/aqnwb/Types.hpp similarity index 100% rename from libs/macos/include/aqnwb/Types.hpp rename to Source/aqnwb/aqnwb/Types.hpp diff --git a/libs/macos/include/aqnwb/Utils.hpp b/Source/aqnwb/aqnwb/Utils.hpp similarity index 100% rename from libs/macos/include/aqnwb/Utils.hpp rename to Source/aqnwb/aqnwb/Utils.hpp diff --git a/Source/aqnwb/aqnwb/aqnwb.hpp b/Source/aqnwb/aqnwb/aqnwb.hpp new file mode 100644 index 0000000..45c083f --- /dev/null +++ b/Source/aqnwb/aqnwb/aqnwb.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include + +#include "aqnwb/nwb/NWBRecording.hpp" +#include "aqnwb/nwb/NWBFile.hpp" diff --git a/Source/aqnwb/aqnwb/aqnwb_export.hpp b/Source/aqnwb/aqnwb/aqnwb_export.hpp new file mode 100644 index 0000000..1be4353 --- /dev/null +++ b/Source/aqnwb/aqnwb/aqnwb_export.hpp @@ -0,0 +1,42 @@ + +#ifndef AQNWB_EXPORT_H +#define AQNWB_EXPORT_H + +#ifdef AQNWB_STATIC_DEFINE +# define AQNWB_EXPORT +# define AQNWB_NO_EXPORT +#else +# ifndef AQNWB_EXPORT +# ifdef aqnwb_aqnwb_EXPORTS + /* We are building this library */ +# define AQNWB_EXPORT +# else + /* We are using this library */ +# define AQNWB_EXPORT +# endif +# endif + +# ifndef AQNWB_NO_EXPORT +# define AQNWB_NO_EXPORT +# endif +#endif + +#ifndef AQNWB_DEPRECATED +# define AQNWB_DEPRECATED __attribute__ ((__deprecated__)) +#endif + +#ifndef AQNWB_DEPRECATED_EXPORT +# define AQNWB_DEPRECATED_EXPORT AQNWB_EXPORT AQNWB_DEPRECATED +#endif + +#ifndef AQNWB_DEPRECATED_NO_EXPORT +# define AQNWB_DEPRECATED_NO_EXPORT AQNWB_NO_EXPORT AQNWB_DEPRECATED +#endif + +#if 0 /* DEFINE_NO_DEPRECATED */ +# ifndef AQNWB_NO_DEPRECATED +# define AQNWB_NO_DEPRECATED +# endif +#endif + +#endif /* AQNWB_EXPORT_H */ diff --git a/Source/aqnwb/aqnwb/hdf5/HDF5IO.cpp b/Source/aqnwb/aqnwb/hdf5/HDF5IO.cpp new file mode 100644 index 0000000..1228948 --- /dev/null +++ b/Source/aqnwb/aqnwb/hdf5/HDF5IO.cpp @@ -0,0 +1,727 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include "aqnwb/Utils.hpp" +#include "aqnwb/hdf5/HDF5IO.hpp" + +using namespace H5; +using namespace AQNWB::HDF5; + +// HDF5IO + +HDF5IO::HDF5IO() {} + +HDF5IO::HDF5IO(const std::string& fileName, const bool disableSWMRMode) + : filename(fileName) + , disableSWMRMode(disableSWMRMode) +{ +} + +HDF5IO::~HDF5IO() +{ + close(); +} + +std::string HDF5IO::getFileName() +{ + return filename; +} + +Status HDF5IO::open() +{ + if (std::filesystem::exists(getFileName())) { + return open(false); + } else { + return open(true); + } +} + +Status HDF5IO::open(bool newfile) +{ + int accFlags = 0; + + if (opened) + return Status::Failure; + + FileAccPropList fapl = FileAccPropList::DEFAULT; + H5Pset_libver_bounds(fapl.getId(), H5F_LIBVER_LATEST, H5F_LIBVER_LATEST); + + if (newfile) + accFlags = H5F_ACC_TRUNC; + else + accFlags = H5F_ACC_RDWR; + + file = std::make_unique( + getFileName(), accFlags, FileCreatPropList::DEFAULT, fapl); + opened = true; + + return Status::Success; +} + +Status HDF5IO::close() +{ + if (this->file != nullptr && opened) { + this->file->close(); + this->file = nullptr; + this->opened = false; + } + + return Status::Success; +} + +Status checkStatus(int status) +{ + if (status < 0) + return Status::Failure; + else + return Status::Success; +} + +Status HDF5IO::flush() +{ + int status = H5Fflush(this->file->getId(), H5F_SCOPE_GLOBAL); + return checkStatus(status); +} + +Status HDF5IO::createAttribute(const BaseDataType& type, + const void* data, + const std::string& path, + const std::string& name, + const SizeType& size) +{ + H5Object* loc; + Group gloc; + DataSet dloc; + Attribute attr; + DataType H5type; + DataType origType; + + if (!opened) + return Status::Failure; + + // open the group or dataset + H5O_type_t objectType = getObjectType(path); + switch (objectType) { + case H5O_TYPE_GROUP: + gloc = file->openGroup(path); + loc = &gloc; + break; + case H5O_TYPE_DATASET: + dloc = file->openDataSet(path); + loc = &dloc; + break; + default: + return Status::Failure; // not a valid dataset or group type + } + + H5type = getH5Type(type); + origType = getNativeType(type); + + if (size > 1) { + hsize_t dims = static_cast(size); + H5type = ArrayType(H5type, 1, &dims); + origType = ArrayType(origType, 1, &dims); + } + + if (loc->attrExists(name)) { + attr = loc->openAttribute(name); + } else { + DataSpace attr_dataspace(H5S_SCALAR); + attr = loc->createAttribute(name, H5type, attr_dataspace); + } + + attr.write(origType, data); + + return Status::Success; +} + +Status HDF5IO::createAttribute(const std::string& data, + const std::string& path, + const std::string& name) +{ + std::vector dataPtrs; + dataPtrs.push_back(data.c_str()); + + return createAttribute(dataPtrs, path, name, data.length()); +} + +Status HDF5IO::createAttribute(const std::vector& data, + const std::string& path, + const std::string& name) +{ + std::vector dataPtrs; + SizeType maxLength = 0; + for (const std::string& str : data) { + SizeType length = str.length(); + maxLength = std::max(maxLength, length); + dataPtrs.push_back(str.c_str()); + } + + return createAttribute(dataPtrs, path, name, maxLength + 1); +} + +Status HDF5IO::createAttribute(const std::vector& data, + const std::string& path, + const std::string& name, + const SizeType& maxSize) +{ + H5Object* loc; + Group gloc; + DataSet dloc; + Attribute attr; + hsize_t dims[1]; + + if (!opened) + return Status::Failure; + + StrType H5type(PredType::C_S1, maxSize); + H5type.setSize(H5T_VARIABLE); + + // open the group or dataset + H5O_type_t objectType = getObjectType(path); + switch (objectType) { + case H5O_TYPE_GROUP: + gloc = file->openGroup(path); + loc = &gloc; + break; + case H5O_TYPE_DATASET: + dloc = file->openDataSet(path); + loc = &dloc; + break; + default: + return Status::Failure; // not a valid dataset or group type + } + + try { + if (loc->attrExists(name)) { + return Status::Failure; // don't allow overwriting because string + // attributes cannot change size easily + } else { + DataSpace attr_dataspace; + SizeType nStrings = data.size(); + if (nStrings > 1) { + dims[0] = nStrings; + attr_dataspace = DataSpace(1, dims); + } else + attr_dataspace = DataSpace(H5S_SCALAR); + attr = loc->createAttribute(name, H5type, attr_dataspace); + } + attr.write(H5type, data.data()); + } catch (GroupIException error) { + error.printErrorStack(); + } catch (AttributeIException error) { + error.printErrorStack(); + } catch (FileIException error) { + error.printErrorStack(); + } catch (DataSetIException error) { + error.printErrorStack(); + } + return Status::Success; +} + +Status HDF5IO::createReferenceAttribute(const std::string& referencePath, + const std::string& path, + const std::string& name) +{ + H5Object* loc; + Group gloc; + DataSet dloc; + Attribute attr; + + if (!opened) + return Status::Failure; + + // open the group or dataset + H5O_type_t objectType = getObjectType(path); + switch (objectType) { + case H5O_TYPE_GROUP: + gloc = file->openGroup(path); + loc = &gloc; + break; + case H5O_TYPE_DATASET: + dloc = file->openDataSet(path); + loc = &dloc; + break; + default: + return Status::Failure; // not a valid dataset or group type + } + + try { + if (loc->attrExists(name)) { + attr = loc->openAttribute(name); + } else { + DataSpace attr_space(H5S_SCALAR); + attr = loc->createAttribute(name, H5::PredType::STD_REF_OBJ, attr_space); + } + + hobj_ref_t* rdata = new hobj_ref_t[sizeof(hobj_ref_t)]; + + file->reference(rdata, referencePath.c_str()); + + attr.write(H5::PredType::STD_REF_OBJ, rdata); + delete[] rdata; + + } catch (GroupIException error) { + error.printErrorStack(); + } catch (AttributeIException error) { + error.printErrorStack(); + } catch (FileIException error) { + error.printErrorStack(); + } catch (DataSetIException error) { + error.printErrorStack(); + } + + return Status::Success; +} + +Status HDF5IO::createGroup(const std::string& path) +{ + if (!opened) + return Status::Failure; + try { + file->createGroup(path); + } catch (FileIException error) { + error.printErrorStack(); + } catch (GroupIException error) { + error.printErrorStack(); + } + return Status::Success; +} + +Status HDF5IO::createGroupIfDoesNotExist(const std::string& path) +{ + if (!opened) + return Status::Failure; + try { + file->childObjType(path); + } catch (FileIException) { + return createGroup(path); + } + return Status::Success; +} + +/** Creates a link to another location in the file */ +Status HDF5IO::createLink(const std::string& path, const std::string& reference) +{ + if (!opened) + return Status::Failure; + + herr_t error = H5Lcreate_soft(reference.c_str(), + file->getLocId(), + path.c_str(), + H5P_DEFAULT, + H5P_DEFAULT); + + return checkStatus(error); +} + +Status HDF5IO::createReferenceDataSet( + const std::string& path, const std::vector& references) +{ + if (!opened) + return Status::Failure; + + const hsize_t size = references.size(); + + hobj_ref_t* rdata = new hobj_ref_t[size * sizeof(hobj_ref_t)]; + + for (SizeType i = 0; i < size; i++) { + file->reference(&rdata[i], references[i].c_str()); + } + + hid_t space = H5Screate_simple(1, &size, NULL); + + hid_t dset = H5Dcreate(file->getLocId(), + path.c_str(), + H5T_STD_REF_OBJ, + space, + H5P_DEFAULT, + H5P_DEFAULT, + H5P_DEFAULT); + + herr_t writeStatus = + H5Dwrite(dset, H5T_STD_REF_OBJ, H5S_ALL, H5S_ALL, H5P_DEFAULT, rdata); + + delete[] rdata; + + herr_t dsetStatus = H5Dclose(dset); + herr_t spaceStatus = H5Sclose(space); + + return checkStatus(writeStatus); +} + +Status HDF5IO::createStringDataSet(const std::string& path, + const std::string& value) +{ + if (!opened) + return Status::Failure; + + std::unique_ptr dataset; + DataType H5type = getH5Type(BaseDataType::STR(value.length())); + DataSpace dSpace(H5S_SCALAR); + + dataset = + std::make_unique(file->createDataSet(path, H5type, dSpace)); + dataset->write(value.c_str(), H5type); + + return Status::Success; +} + +Status HDF5IO::createStringDataSet(const std::string& path, + const std::vector& values) +{ + if (!opened) + return Status::Failure; + + std::vector cStrs; + cStrs.reserve(values.size()); + for (const auto& str : values) { + cStrs.push_back(str.c_str()); + } + + std::unique_ptr dataset; + dataset = std::unique_ptr(createArrayDataSet( + BaseDataType::V_STR, SizeArray {values.size()}, SizeArray {1}, path)); + dataset->writeDataBlock( + std::vector(1, 1), BaseDataType::V_STR, cStrs.data()); + + return Status::Success; +} + +Status HDF5IO::startRecording() +{ + if (!opened) + return Status::Failure; + + if (!disableSWMRMode) { + herr_t status = H5Fstart_swmr_write(this->file->getId()); + return checkStatus(status); + } + return Status::Success; +} + +Status HDF5IO::stopRecording() +{ + // if SWMR mode is disabled, stopping the recording will leave the file open + if (!disableSWMRMode) { + close(); // SWMR mode cannot be disabled so close the file + } else { + this->flush(); + } + return Status::Success; +} + +bool HDF5IO::canModifyObjects() +{ + if (!opened) + return false; + + // Check if we are in SWMR mode + bool inSWMRMode = false; + unsigned int intent; + herr_t status = H5Fget_intent(this->file->getId(), &intent); + bool statusOK = (status >= 0); + if (statusOK) { + inSWMRMode = (intent & (H5F_ACC_SWMR_READ | H5F_ACC_SWMR_WRITE)); + } + + // if the file is opened and we are not in swmr mode then we can modify + // objects + return statusOK && !inSWMRMode; +} + +std::unique_ptr HDF5IO::getDataSet( + const std::string& path) +{ + std::unique_ptr data; + + if (!opened) + return nullptr; + + try { + data = std::make_unique(file->openDataSet(path)); + return std::make_unique(std::move(data)); + } catch (DataSetIException error) { + error.printErrorStack(); + return nullptr; + } catch (FileIException error) { + error.printErrorStack(); + return nullptr; + } catch (DataSpaceIException error) { + error.printErrorStack(); + return nullptr; + } +} + +std::unique_ptr HDF5IO::createArrayDataSet( + const BaseDataType& type, + const SizeArray& size, + const SizeArray& chunking, + const std::string& path) +{ + std::unique_ptr data; + DSetCreatPropList prop; + DataType H5type = getH5Type(type); + + if (!opened) + return nullptr; + + SizeType dimension = size.size(); + if (dimension < 1) // Check for at least one dimension + return nullptr; + + // Ensure chunking is properly allocated and has at least 'dimension' elements + assert(chunking.size() >= dimension); + + // Use vectors to support an arbitrary number of dimensions + std::vector dims(dimension), chunk_dims(dimension), + max_dims(dimension); + + for (SizeType i = 0; i < dimension; i++) { + dims[i] = static_cast(size[i]); + if (chunking[i] > 0) { + chunk_dims[i] = static_cast(chunking[i]); + max_dims[i] = H5S_UNLIMITED; + } else { + chunk_dims[i] = static_cast(size[i]); + max_dims[i] = static_cast(size[i]); + } + } + + DataSpace dSpace(static_cast(dimension), dims.data(), max_dims.data()); + prop.setChunk(static_cast(dimension), chunk_dims.data()); + + data = std::make_unique( + file->createDataSet(path, H5type, dSpace, prop)); + + return std::make_unique(std::move(data)); +} + +H5O_type_t HDF5IO::getObjectType(const std::string& path) +{ +#if H5_VERSION_GE(1, 12, 0) + // get whether path is a dataset or group + H5O_info_t objInfo; // Structure to hold information about the object + H5Oget_info_by_name( + this->file->getId(), path.c_str(), &objInfo, H5O_INFO_BASIC, H5P_DEFAULT); +#else + // get whether path is a dataset or group + H5O_info_t objInfo; // Structure to hold information about the object + H5Oget_info_by_name(this->file->getId(), path.c_str(), &objInfo, H5P_DEFAULT); +#endif + H5O_type_t objectType = objInfo.type; + + return objectType; +} + +H5::DataType HDF5IO::getNativeType(BaseDataType type) +{ + H5::DataType baseType; + + switch (type.type) { + case BaseDataType::Type::T_I8: + baseType = PredType::NATIVE_INT8; + break; + case BaseDataType::Type::T_I16: + baseType = PredType::NATIVE_INT16; + break; + case BaseDataType::Type::T_I32: + baseType = PredType::NATIVE_INT32; + break; + case BaseDataType::Type::T_I64: + baseType = PredType::NATIVE_INT64; + break; + case BaseDataType::Type::T_U8: + baseType = PredType::NATIVE_UINT8; + break; + case BaseDataType::Type::T_U16: + baseType = PredType::NATIVE_UINT16; + break; + case BaseDataType::Type::T_U32: + baseType = PredType::NATIVE_UINT32; + break; + case BaseDataType::Type::T_U64: + baseType = PredType::NATIVE_UINT64; + break; + case BaseDataType::Type::T_F32: + baseType = PredType::NATIVE_FLOAT; + break; + case BaseDataType::Type::T_F64: + baseType = PredType::NATIVE_DOUBLE; + break; + case BaseDataType::Type::T_STR: + return StrType(PredType::C_S1, type.typeSize); + break; + case BaseDataType::Type::V_STR: + return StrType(PredType::C_S1, H5T_VARIABLE); + break; + default: + baseType = PredType::NATIVE_INT32; + } + if (type.typeSize > 1) { + hsize_t size = type.typeSize; + return ArrayType(baseType, 1, &size); + } else + return baseType; +} + +H5::DataType HDF5IO::getH5Type(BaseDataType type) +{ + H5::DataType baseType; + + switch (type.type) { + case BaseDataType::Type::T_I8: + baseType = PredType::STD_I8LE; + break; + case BaseDataType::Type::T_I16: + baseType = PredType::STD_I16LE; + break; + case BaseDataType::Type::T_I32: + baseType = PredType::STD_I32LE; + break; + case BaseDataType::Type::T_I64: + baseType = PredType::STD_I64LE; + break; + case BaseDataType::Type::T_U8: + baseType = PredType::STD_U8LE; + break; + case BaseDataType::Type::T_U16: + baseType = PredType::STD_U16LE; + break; + case BaseDataType::Type::T_U32: + baseType = PredType::STD_U32LE; + break; + case BaseDataType::Type::T_U64: + baseType = PredType::STD_U64LE; + break; + case BaseDataType::Type::T_F32: + return PredType::IEEE_F32LE; + break; + case BaseDataType::Type::T_F64: + baseType = PredType::IEEE_F64LE; + break; + case BaseDataType::Type::T_STR: + return StrType(PredType::C_S1, type.typeSize); + break; + case BaseDataType::Type::V_STR: + return StrType(PredType::C_S1, H5T_VARIABLE); + break; + default: + return PredType::STD_I32LE; + } + if (type.typeSize > 1) { + hsize_t size = type.typeSize; + return ArrayType(baseType, 1, &size); + } else + return baseType; +} + +// HDF5RecordingData +HDF5RecordingData::HDF5RecordingData(std::unique_ptr data) +{ + DataSpace dSpace = data->getSpace(); + DSetCreatPropList prop = data->getCreatePlist(); + + int nDimensions = dSpace.getSimpleExtentNdims(); + std::vector dims(nDimensions), chunk(nDimensions); + + nDimensions = dSpace.getSimpleExtentDims( + dims.data()); // TODO -redefine here or use original? + prop.getChunk(static_cast(nDimensions), chunk.data()); + + this->size = std::vector(nDimensions); + for (int i = 0; i < nDimensions; ++i) { + this->size[i] = dims[i]; + } + this->nDimensions = nDimensions; + this->position = std::vector( + nDimensions, 0); // Initialize position with 0 for each dimension + this->dSet = std::make_unique(*data); +} + +// HDF5RecordingData + +HDF5RecordingData::~HDF5RecordingData() +{ + // Safety + dSet->flush(H5F_SCOPE_GLOBAL); +} + +Status HDF5RecordingData::writeDataBlock( + const std::vector& dataShape, + const std::vector& positionOffset, + const BaseDataType& type, + const void* data) +{ + try { + // check dataShape and positionOffset inputs match the dimensions of the + // dataset + if (dataShape.size() != nDimensions || positionOffset.size() != nDimensions) + { + return Status::Failure; + } + + // Ensure that we have enough space to accommodate new data + std::vector dSetDims(nDimensions), offset(nDimensions); + for (int i = 0; i < nDimensions; ++i) { + offset[i] = static_cast(positionOffset[i]); + + if (dataShape[i] + offset[i] > size[i]) // TODO - do I need offset here + dSetDims[i] = dataShape[i] + offset[i]; + else + dSetDims[i] = size[i]; + } + + // Adjust dataset dimensions if necessary + dSet->extend(dSetDims.data()); + + // Set size to new size based on updated dimensionality + DataSpace fSpace = dSet->getSpace(); + fSpace.getSimpleExtentDims(dSetDims.data()); + for (int i = 0; i < nDimensions; ++i) { + size[i] = dSetDims[i]; + } + + // Create memory space with the shape of the data + // DataSpace mSpace(dimension, dSetDim.data()); + std::vector dataDims(nDimensions); + for (int i = 0; i < nDimensions; ++i) { + if (dataShape[i] == 0) { + dataDims[i] = 1; + } else { + dataDims[i] = static_cast(dataShape[i]); + } + } + DataSpace mSpace(static_cast(nDimensions), dataDims.data()); + + // Select hyperslab in the file space + fSpace.selectHyperslab(H5S_SELECT_SET, dataDims.data(), offset.data()); + + // Write the data + DataType nativeType = HDF5IO::getNativeType(type); + dSet->write(data, nativeType, mSpace, fSpace); + + // Update position for simple extension + for (int i = 0; i < dataShape.size(); ++i) { + position[i] += dataShape[i]; + } + } catch (DataSetIException error) { + error.printErrorStack(); + } catch (DataSpaceIException error) { + error.printErrorStack(); + } catch (FileIException error) { + error.printErrorStack(); + } + return Status::Success; +} + +const H5::DataSet* HDF5RecordingData::getDataSet() +{ + return dSet.get(); +}; diff --git a/libs/macos/include/aqnwb/hdf5/HDF5IO.hpp b/Source/aqnwb/aqnwb/hdf5/HDF5IO.hpp similarity index 100% rename from libs/macos/include/aqnwb/hdf5/HDF5IO.hpp rename to Source/aqnwb/aqnwb/hdf5/HDF5IO.hpp diff --git a/Source/aqnwb/aqnwb/nwb/NWBFile.cpp b/Source/aqnwb/aqnwb/nwb/NWBFile.cpp new file mode 100644 index 0000000..c29169c --- /dev/null +++ b/Source/aqnwb/aqnwb/nwb/NWBFile.cpp @@ -0,0 +1,211 @@ +#include +#include +#include +#include +#include +#include +#include + + +#include "aqnwb/BaseIO.hpp" +#include "aqnwb/Channel.hpp" +#include "aqnwb/Utils.hpp" +#include "aqnwb/nwb/device/Device.hpp" +#include "aqnwb/nwb/ecephys/ElectricalSeries.hpp" +#include "aqnwb/nwb/file/ElectrodeGroup.hpp" +#include "aqnwb/nwb/file/ElectrodeTable.hpp" +#include "aqnwb/nwb/NWBFile.hpp" + +using namespace AQNWB::NWB; + +constexpr SizeType CHUNK_XSIZE = 2048; + +// NWBFile + +NWBFile::NWBFile(const std::string& idText, std::shared_ptr io) + : identifierText(idText) + , io(io) +{ +} + +NWBFile::~NWBFile() {} + +Status NWBFile::initialize() +{ + if (std::filesystem::exists(io->getFileName())) { + return io->open(false); + } else { + io->open(true); + return createFileStructure(); + } +} + +Status NWBFile::finalize() +{ + recordingContainers.reset(); + return io->close(); +} + +Status NWBFile::createFileStructure() +{ + if (!io->canModifyObjects()) { + return Status::Failure; + } + + io->createCommonNWBAttributes("/", "core", "NWBFile", ""); + io->createAttribute(NWBVersion, "/", "nwb_version"); + + io->createGroup("/acquisition"); + io->createGroup("/analysis"); + io->createGroup("/processing"); + io->createGroup("/stimulus"); + io->createGroup("/stimulus/presentation"); + io->createGroup("/stimulus/templates"); + io->createGroup("/general"); + io->createGroup("/general/devices"); + io->createGroup("/general/extracellular_ephys"); + + io->createGroup("/specifications"); + io->createReferenceAttribute("/specifications", "/", ".specloc"); + cacheSpecifications("core/", NWBVersion); + cacheSpecifications("hdmf-common/", HDMFVersion); + cacheSpecifications("hdmf-experimental/", HDMFExperimentalVersion); + + std::string time = getCurrentTime(); + std::vector timeVec = {time}; + io->createStringDataSet("/file_create_date", timeVec); + io->createStringDataSet("/session_description", "a recording session"); + io->createStringDataSet("/session_start_time", time); + io->createStringDataSet("/timestamps_reference_time", time); + io->createStringDataSet("/identifier", identifierText); + + return Status::Success; +} + +Status NWBFile::createElectricalSeries( + std::vector recordingArrays, + const BaseDataType& dataType) +{ + if (!io->canModifyObjects()) { + return Status::Failure; + } + + // store all recorded data in the acquisition group + std::string rootPath = "/acquisition/"; + + // Setup electrode table + ElectrodeTable elecTable = ElectrodeTable(io); + elecTable.initialize(); + + // 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; + + Device device = Device(devicePath, io, "description", "unknown"); + device.initialize(); + + ElectrodeGroup elecGroup = + ElectrodeGroup(electrodePath, io, "description", "unknown", device); + elecGroup.initialize(); + + // Setup electrical series datasets + auto electricalSeries = std::make_unique( + electricalSeriesPath, + io, + dataType, + channelVector, + "Stores continuously sampled voltage data from an " + "extracellular ephys recording", + SizeArray {0, channelVector.size()}, + SizeArray {CHUNK_XSIZE, 0}); + electricalSeries->initialize(); + recordingContainers->addData(std::move(electricalSeries)); + + // Add electrode information to electrode table (does not write to datasets + // yet) + elecTable.addElectrodes(channelVector); + } + + // write electrode information to datasets + elecTable.finalize(); + + return Status::Success; +} + +Status NWBFile::startRecording() +{ + return io->startRecording(); +} + +void NWBFile::stopRecording() +{ + io->stopRecording(); +} + +void NWBFile::cacheSpecifications(const std::string& specPath, + const std::string& versionNumber) +{ + io->createGroup("/specifications/" + specPath); + io->createGroup("/specifications/" + specPath + versionNumber); + + std::filesystem::path currentFile = __FILE__; + std::filesystem::path schemaDir = + currentFile.parent_path().parent_path().parent_path().parent_path().parent_path() / "Resources/spec" + / specPath / versionNumber; + + for (auto const& entry : std::filesystem::directory_iterator {schemaDir}) + if (std::filesystem::is_regular_file(entry) + && entry.path().extension() == ".json") + { + std::string specName = + entry.path().filename().replace_extension("").string(); + if (specName.find("namespace") != std::string::npos) + specName = "namespace"; + + std::ifstream schemaFile(entry.path()); + std::stringstream buffer; + buffer << schemaFile.rdbuf(); + + io->createStringDataSet( + "/specifications/" + specPath + versionNumber + "/" + specName, + buffer.str()); + } +} + +// recording data factory method / +std::unique_ptr NWBFile::createRecordingData( + BaseDataType type, + const SizeArray& size, + const SizeArray& chunking, + const std::string& path) +{ + 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/libs/macos/include/aqnwb/nwb/NWBFile.hpp b/Source/aqnwb/aqnwb/nwb/NWBFile.hpp similarity index 99% rename from libs/macos/include/aqnwb/nwb/NWBFile.hpp rename to Source/aqnwb/aqnwb/nwb/NWBFile.hpp index 9d54289..d645f5f 100644 --- a/libs/macos/include/aqnwb/nwb/NWBFile.hpp +++ b/Source/aqnwb/aqnwb/nwb/NWBFile.hpp @@ -4,6 +4,7 @@ #include #include +#include "aqnwb/aqnwb_export.hpp" #include "aqnwb/BaseIO.hpp" #include "aqnwb/Types.hpp" #include "aqnwb/nwb/base/TimeSeries.hpp" diff --git a/Source/aqnwb/aqnwb/nwb/NWBRecording.cpp b/Source/aqnwb/aqnwb/nwb/NWBRecording.cpp new file mode 100644 index 0000000..9f4178d --- /dev/null +++ b/Source/aqnwb/aqnwb/nwb/NWBRecording.cpp @@ -0,0 +1,67 @@ +#include "aqnwb/Channel.hpp" +#include "aqnwb/nwb/NWBRecording.hpp" +#include "aqnwb/Utils.hpp" +#include "aqnwb/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){ + nwbfile->finalize(); + } + + // 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/libs/macos/include/aqnwb/nwb/NWBRecording.hpp b/Source/aqnwb/aqnwb/nwb/NWBRecording.hpp similarity index 85% rename from libs/macos/include/aqnwb/nwb/NWBRecording.hpp rename to Source/aqnwb/aqnwb/nwb/NWBRecording.hpp index 6926863..792899f 100644 --- a/libs/macos/include/aqnwb/nwb/NWBRecording.hpp +++ b/Source/aqnwb/aqnwb/nwb/NWBRecording.hpp @@ -1,5 +1,6 @@ #pragma once +#include "aqnwb/aqnwb_export.hpp" #include "aqnwb/Types.hpp" #include "aqnwb/nwb/NWBFile.hpp" @@ -9,7 +10,7 @@ namespace AQNWB::NWB * @brief The NWBRecording class manages the recording process */ -class NWBRecording +class AQNWB_EXPORT NWBRecording { public: /** @@ -34,17 +35,12 @@ class NWBRecording /** * @brief Opens the file for recording. - * @param rootFolder The root folder where the file will be stored. - * @param baseName The base name of the file (will be appended with - * experiment number). - * @param experimentNumber The experiment number. + * @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& rootFolder, - const std::string& baseName, - int experimentNumber, + Status openFile(const std::string& filename, std::vector recordingArrays, const std::string& IOType = "HDF5"); diff --git a/Source/aqnwb/aqnwb/nwb/base/TimeSeries.cpp b/Source/aqnwb/aqnwb/nwb/base/TimeSeries.cpp new file mode 100644 index 0000000..80128ae --- /dev/null +++ b/Source/aqnwb/aqnwb/nwb/base/TimeSeries.cpp @@ -0,0 +1,80 @@ +#include "aqnwb/nwb/base/TimeSeries.hpp" + +using namespace AQNWB::NWB; + +// TimeSeries + +/** Constructor */ +TimeSeries::TimeSeries(const std::string& path, + std::shared_ptr io, + const BaseDataType& dataType, + const std::string& unit, + const std::string& description, + const std::string& comments, + const SizeArray& dsetSize, + const SizeArray& chunkSize, + const float& conversion, + const float& resolution, + const float& offset) + : Container(path, io) + , dataType(dataType) + , unit(unit) + , description(description) + , comments(comments) + , dsetSize(dsetSize) + , chunkSize(chunkSize) + , conversion(conversion) + , resolution(resolution) + , offset(offset) +{ +} + +/** Destructor */ +TimeSeries::~TimeSeries() {} + +void TimeSeries::initialize() +{ + Container::initialize(); + + // setup attributes + io->createCommonNWBAttributes(path, "core", neurodataType, description); + io->createAttribute(comments, path, "comments"); + + // setup datasets + this->data = std::unique_ptr(io->createArrayDataSet( + dataType, dsetSize, chunkSize, getPath() + "/data")); + io->createDataAttributes(getPath(), conversion, resolution, unit); + + SizeArray tsDsetSize = { + dsetSize[0]}; // timestamps match data along first dimension + this->timestamps = std::unique_ptr(io->createArrayDataSet( + this->timestampsType, tsDsetSize, chunkSize, getPath() + "/timestamps")); + io->createTimestampsAttributes(getPath()); +} + +Status TimeSeries::writeData(const std::vector& dataShape, + const std::vector& positionOffset, + const void* data, + const void* timestamps) +{ + Status tsStatus = Status::Success; + if (timestamps != nullptr) { + const std::vector timestampsShape = { + dataShape[0]}; // timestamps should match shape of the first data + // dimension + const std::vector timestampsPositionOffset = {positionOffset[0]}; + tsStatus = this->timestamps->writeDataBlock(timestampsShape, + timestampsPositionOffset, + this->timestampsType, + timestamps); + } + + Status dataStatus = this->data->writeDataBlock( + dataShape, positionOffset, this->dataType, data); + + if ((dataStatus != Status::Success) or (tsStatus != Status::Success)) { + return Status::Failure; + } else { + return Status::Success; + } +} diff --git a/libs/macos/include/aqnwb/nwb/base/TimeSeries.hpp b/Source/aqnwb/aqnwb/nwb/base/TimeSeries.hpp similarity index 100% rename from libs/macos/include/aqnwb/nwb/base/TimeSeries.hpp rename to Source/aqnwb/aqnwb/nwb/base/TimeSeries.hpp diff --git a/Source/aqnwb/aqnwb/nwb/device/Device.cpp b/Source/aqnwb/aqnwb/nwb/device/Device.cpp new file mode 100644 index 0000000..5102f03 --- /dev/null +++ b/Source/aqnwb/aqnwb/nwb/device/Device.cpp @@ -0,0 +1,38 @@ +#include "aqnwb/nwb/device/Device.hpp" + +using namespace AQNWB::NWB; + +// Device +/** Constructor */ +Device::Device(const std::string& path, + std::shared_ptr io, + const std::string& description, + const std::string& manufacturer) + : Container(path, io) + , description(description) + , manufacturer(manufacturer) +{ +} + +/** Destructor */ +Device::~Device() {} + +void Device::initialize() +{ + Container::initialize(); + + io->createCommonNWBAttributes(path, "core", "Device", description); + io->createAttribute(manufacturer, path, "manufacturer"); +} + +// Getter for manufacturer +std::string Device::getManufacturer() const +{ + return manufacturer; +} + +// Getter for description +std::string Device::getDescription() const +{ + return description; +} diff --git a/libs/macos/include/aqnwb/nwb/device/Device.hpp b/Source/aqnwb/aqnwb/nwb/device/Device.hpp similarity index 100% rename from libs/macos/include/aqnwb/nwb/device/Device.hpp rename to Source/aqnwb/aqnwb/nwb/device/Device.hpp diff --git a/Source/aqnwb/aqnwb/nwb/ecephys/ElectricalSeries.cpp b/Source/aqnwb/aqnwb/nwb/ecephys/ElectricalSeries.cpp new file mode 100644 index 0000000..3f1fb2d --- /dev/null +++ b/Source/aqnwb/aqnwb/nwb/ecephys/ElectricalSeries.cpp @@ -0,0 +1,94 @@ +#include "aqnwb/nwb/ecephys/ElectricalSeries.hpp" + +#include "aqnwb/nwb/file/ElectrodeTable.hpp" + +using namespace AQNWB::NWB; + +// ElectricalSeries + +/** Constructor */ +ElectricalSeries::ElectricalSeries(const std::string& path, + std::shared_ptr io, + const BaseDataType& dataType, + const Types::ChannelVector& channelVector, + const std::string& description, + const SizeArray& dsetSize, + const SizeArray& chunkSize, + const float& conversion, + const float& resolution, + const float& offset) + : TimeSeries(path, + io, + dataType, + "volts", // default unit for Electrical Series + description, + channelVector[0].comments, + dsetSize, + chunkSize, + channelVector[0].getConversion(), + resolution, + offset) + , channelVector(channelVector) +{ +} + +/** Destructor */ +ElectricalSeries::~ElectricalSeries() {} + +/** Initialization function*/ +void ElectricalSeries::initialize() +{ + TimeSeries::initialize(); + + // setup variables based on number of channels + std::vector electrodeInds(channelVector.size()); + for (size_t i = 0; i < channelVector.size(); ++i) { + electrodeInds[i] = channelVector[i].globalIndex; + } + samplesRecorded = SizeArray(channelVector.size(), 0); + + // make channel conversion dataset + channelConversion = std::unique_ptr( + io->createArrayDataSet(BaseDataType::F32, + SizeArray {1}, + chunkSize, + getPath() + "/channel_conversion")); + io->createCommonNWBAttributes(getPath() + "/channel_conversion", + "hdmf-common", + "", + "Bit volts values for all channels"); + + // make electrodes dataset + electrodesDataset = std::unique_ptr(io->createArrayDataSet( + BaseDataType::I32, SizeArray {1}, chunkSize, getPath() + "/electrodes")); + electrodesDataset->writeDataBlock( + std::vector(1, channelVector.size()), + BaseDataType::I32, + &electrodeInds[0]); + io->createCommonNWBAttributes( + getPath() + "/electrodes", "hdmf-common", "DynamicTableRegion", ""); + io->createReferenceAttribute( + ElectrodeTable::electrodeTablePath, getPath() + "/electrodes", "table"); +} + +Status ElectricalSeries::writeChannel(SizeType channelInd, + const SizeType& numSamples, + const void* data, + const void* timestamps) +{ + // get offsets and datashape + std::vector dataShape = { + numSamples, 1}; // Note: schema has 1D and 3D but planning to deprecate + std::vector positionOffset = {samplesRecorded[channelInd], + channelInd}; + + // track samples recorded per channel + samplesRecorded[channelInd] += numSamples; + + // write channel data + if (channelInd == 0) { + return writeData(dataShape, positionOffset, data, timestamps); + } else { + return writeData(dataShape, positionOffset, data); + } +} diff --git a/libs/macos/include/aqnwb/nwb/ecephys/ElectricalSeries.hpp b/Source/aqnwb/aqnwb/nwb/ecephys/ElectricalSeries.hpp similarity index 100% rename from libs/macos/include/aqnwb/nwb/ecephys/ElectricalSeries.hpp rename to Source/aqnwb/aqnwb/nwb/ecephys/ElectricalSeries.hpp diff --git a/Source/aqnwb/aqnwb/nwb/file/ElectrodeGroup.cpp b/Source/aqnwb/aqnwb/nwb/file/ElectrodeGroup.cpp new file mode 100644 index 0000000..ac5e017 --- /dev/null +++ b/Source/aqnwb/aqnwb/nwb/file/ElectrodeGroup.cpp @@ -0,0 +1,48 @@ +#include "aqnwb/nwb/file/ElectrodeGroup.hpp" + +using namespace AQNWB::NWB; + +// ElectrodeGroup + +/** Constructor */ +ElectrodeGroup::ElectrodeGroup(const std::string& path, + std::shared_ptr io, + const std::string& description, + const std::string& location, + const Device& device) + : Container(path, io) + , description(description) + , location(location) + , device(device) +{ +} + +/** Destructor */ +ElectrodeGroup::~ElectrodeGroup() {} + +void ElectrodeGroup::initialize() +{ + Container::initialize(); + + io->createCommonNWBAttributes(path, "core", "ElectrodeGroup", description); + io->createAttribute(location, path, "location"); + io->createLink("/" + path + "/device", "/" + device.getPath()); +} + +// Getter for description +std::string ElectrodeGroup::getDescription() const +{ + return description; +} + +// Getter for location +std::string ElectrodeGroup::getLocation() const +{ + return location; +} + +// Getter for device +const Device& ElectrodeGroup::getDevice() const +{ + return device; +} diff --git a/libs/macos/include/aqnwb/nwb/file/ElectrodeGroup.hpp b/Source/aqnwb/aqnwb/nwb/file/ElectrodeGroup.hpp similarity index 100% rename from libs/macos/include/aqnwb/nwb/file/ElectrodeGroup.hpp rename to Source/aqnwb/aqnwb/nwb/file/ElectrodeGroup.hpp diff --git a/Source/aqnwb/aqnwb/nwb/file/ElectrodeTable.cpp b/Source/aqnwb/aqnwb/nwb/file/ElectrodeTable.cpp new file mode 100644 index 0000000..f68231c --- /dev/null +++ b/Source/aqnwb/aqnwb/nwb/file/ElectrodeTable.cpp @@ -0,0 +1,84 @@ +#include "aqnwb/nwb/file/ElectrodeTable.hpp" + +#include "aqnwb/Channel.hpp" + +using namespace AQNWB::NWB; + +// ElectrodeTable + +/** Constructor */ +ElectrodeTable::ElectrodeTable(std::shared_ptr io, + const std::string& description) + : DynamicTable(electrodeTablePath, // use the electrodeTablePath + io, + description) +{ +} + +/** Destructor */ +ElectrodeTable::~ElectrodeTable() {} + +/** Initialization function*/ +void ElectrodeTable::initialize() +{ + // create group + DynamicTable::initialize(); + + electrodeDataset->dataset = + std::unique_ptr(io->createArrayDataSet( + BaseDataType::I32, SizeArray {1}, SizeArray {1}, path + "id")); + groupNamesDataset->dataset = std::unique_ptr( + io->createArrayDataSet(BaseDataType::STR(250), + SizeArray {0}, + SizeArray {1}, + path + "group_name")); + locationsDataset + ->dataset = std::unique_ptr(io->createArrayDataSet( + BaseDataType::STR(250), SizeArray {0}, SizeArray {1}, path + "location")); +} + +void ElectrodeTable::addElectrodes(std::vector channels) +{ + // create datasets + for (const auto& ch : channels) { + groupReferences.push_back(groupPathBase + ch.groupName); + groupNames.push_back(ch.groupName); + electrodeNumbers.push_back(ch.globalIndex); + locationNames.push_back("unknown"); + } +} + +void ElectrodeTable::finalize() +{ + setRowIDs(electrodeDataset, electrodeNumbers); + addColumn("group_name", + "the name of the ElectrodeGroup this electrode is a part of", + groupNamesDataset, + groupNames); + addColumn("location", + "the location of channel within the subject e.g. brain region", + locationsDataset, + locationNames); + addColumn("group", + "a reference to the ElectrodeGroup this electrode is a part of", + groupReferences); +} + +// Getter for colNames +const std::vector& ElectrodeTable::getColNames() +{ + return colNames; +} + +// Setter for colNames +void ElectrodeTable::setColNames(const std::vector& newColNames) +{ + colNames = newColNames; +} + +// Getter for groupPath +std::string ElectrodeTable::getGroupPath() const +{ + return groupReferences[0]; // all channels in ChannelVector should have the + // same groupName +} diff --git a/libs/macos/include/aqnwb/nwb/file/ElectrodeTable.hpp b/Source/aqnwb/aqnwb/nwb/file/ElectrodeTable.hpp similarity index 100% rename from libs/macos/include/aqnwb/nwb/file/ElectrodeTable.hpp rename to Source/aqnwb/aqnwb/nwb/file/ElectrodeTable.hpp diff --git a/Source/aqnwb/aqnwb/nwb/hdmf/base/Container.cpp b/Source/aqnwb/aqnwb/nwb/hdmf/base/Container.cpp new file mode 100644 index 0000000..8cd2865 --- /dev/null +++ b/Source/aqnwb/aqnwb/nwb/hdmf/base/Container.cpp @@ -0,0 +1,27 @@ +#include "aqnwb/nwb/hdmf/base/Container.hpp" + +using namespace AQNWB::NWB; + +// Container + +/** Constructor */ +Container::Container(const std::string& path, std::shared_ptr io) + : path(path) + , io(io) +{ +} + +/** Destructor */ +Container::~Container() {} + +/** Initialize */ +void Container::initialize() +{ + io->createGroup(path); +} + +/** Getter for path */ +std::string Container::getPath() const +{ + return path; +} diff --git a/libs/macos/include/aqnwb/nwb/hdmf/base/Container.hpp b/Source/aqnwb/aqnwb/nwb/hdmf/base/Container.hpp similarity index 100% rename from libs/macos/include/aqnwb/nwb/hdmf/base/Container.hpp rename to Source/aqnwb/aqnwb/nwb/hdmf/base/Container.hpp diff --git a/libs/macos/include/aqnwb/nwb/hdmf/base/Data.hpp b/Source/aqnwb/aqnwb/nwb/hdmf/base/Data.hpp similarity index 100% rename from libs/macos/include/aqnwb/nwb/hdmf/base/Data.hpp rename to Source/aqnwb/aqnwb/nwb/hdmf/base/Data.hpp diff --git a/Source/aqnwb/aqnwb/nwb/hdmf/table/DynamicTable.cpp b/Source/aqnwb/aqnwb/nwb/hdmf/table/DynamicTable.cpp new file mode 100644 index 0000000..8c2dec7 --- /dev/null +++ b/Source/aqnwb/aqnwb/nwb/hdmf/table/DynamicTable.cpp @@ -0,0 +1,84 @@ +#include "aqnwb/nwb/hdmf/table/DynamicTable.hpp" + +using namespace AQNWB::NWB; + +// DynamicTable + +/** Constructor */ +DynamicTable::DynamicTable(const std::string& path, + std::shared_ptr io, + const std::string& description) + : Container(path, io) + , description(description) +{ +} + +/** Destructor */ +DynamicTable::~DynamicTable() {} + +/** Initialization function*/ +void DynamicTable::initialize() +{ + Container::initialize(); + + io->createCommonNWBAttributes( + path, "hdmf-common", "DynamicTable", getDescription()); + io->createAttribute(getColNames(), path, "colnames"); +} + +/** Add column to table */ +void DynamicTable::addColumn(const std::string& name, + const std::string& colDescription, + std::unique_ptr& vectorData, + const std::vector& values) +{ + if (vectorData->dataset == nullptr) { + std::cerr << "VectorData dataset is not initialized" << std::endl; + } else { + // write in loop because variable length string + for (SizeType i = 0; i < values.size(); i++) + vectorData->dataset->writeDataBlock(std::vector(1, 1), + BaseDataType::STR(values[i].size()), + &values[i]); + io->createCommonNWBAttributes( + path + name, "hdmf-common", "VectorData", colDescription); + } +} + +void DynamicTable::setRowIDs(std::unique_ptr& elementIDs, + const std::vector& values) +{ + if (elementIDs->dataset == nullptr) { + std::cerr << "ElementIdentifiers dataset is not initialized" << std::endl; + } else { + elementIDs->dataset->writeDataBlock( + std::vector(1, values.size()), BaseDataType::I32, &values[0]); + io->createCommonNWBAttributes( + path + "id", "hdmf-common", "ElementIdentifiers"); + } +} + +void DynamicTable::addColumn(const std::string& name, + const std::string& colDescription, + const std::vector& values) +{ + if (values.empty()) { + std::cerr << "Data to add to column is empty" << std::endl; + } else { + io->createReferenceDataSet(path + name, values); + io->createCommonNWBAttributes( + path + name, "hdmf-common", "VectorData", colDescription); + } +} + +// Getter for description +std::string DynamicTable::getDescription() const +{ + return description; +} + +// Getter for colNames +const std::vector& DynamicTable::getColNames() +{ + return colNames; +} diff --git a/libs/macos/include/aqnwb/nwb/hdmf/table/DynamicTable.hpp b/Source/aqnwb/aqnwb/nwb/hdmf/table/DynamicTable.hpp similarity index 100% rename from libs/macos/include/aqnwb/nwb/hdmf/table/DynamicTable.hpp rename to Source/aqnwb/aqnwb/nwb/hdmf/table/DynamicTable.hpp diff --git a/libs/macos/include/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp b/Source/aqnwb/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp similarity index 100% rename from libs/macos/include/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp rename to Source/aqnwb/aqnwb/nwb/hdmf/table/ElementIdentifiers.hpp diff --git a/Source/aqnwb/aqnwb/nwb/hdmf/table/VectorData.cpp b/Source/aqnwb/aqnwb/nwb/hdmf/table/VectorData.cpp new file mode 100644 index 0000000..5c71ebb --- /dev/null +++ b/Source/aqnwb/aqnwb/nwb/hdmf/table/VectorData.cpp @@ -0,0 +1,9 @@ +#include "aqnwb/nwb/hdmf/table/VectorData.hpp" + +using namespace AQNWB::NWB; + +// VectorData +std::string VectorData::getDescription() const +{ + return description; +} diff --git a/libs/macos/include/aqnwb/nwb/hdmf/table/VectorData.hpp b/Source/aqnwb/aqnwb/nwb/hdmf/table/VectorData.hpp similarity index 100% rename from libs/macos/include/aqnwb/nwb/hdmf/table/VectorData.hpp rename to Source/aqnwb/aqnwb/nwb/hdmf/table/VectorData.hpp diff --git a/libs/macos/bin/libaqnwb.0.1.0.dylib b/libs/macos/bin/libaqnwb.0.1.0.dylib deleted file mode 100755 index 18b1098a62295af2d10f31de70e5b0d8dc742935..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 681336 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