Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Update handling of colnames #149

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a38eb84
Move DynamicTable::colNames from constructor to initalize
oruebel Jan 30, 2025
ff42a15
Remove unused methods from ElectrodeTable
oruebel Jan 30, 2025
b64c257
Support adding of columns to DynamicTable
oruebel Jan 31, 2025
ae82cce
Fix #150 allow writing of variable-length string array attributes
oruebel Jan 31, 2025
0d02c02
Merge branch 'main' into move_colnames
oruebel Jan 31, 2025
394c693
Fix Windows build issue
oruebel Jan 31, 2025
df9267d
Remove colNames from DynamicTable.initialize
oruebel Jan 31, 2025
d35cd56
Return status for DynamicTable write functions
oruebel Jan 31, 2025
596b908
Support overwrite of string array attributes and setting of colnames
oruebel Feb 3, 2025
b580a23
Fix uninitalized variable warning
oruebel Feb 3, 2025
ca4db11
Merge branch 'move_colnames' into overwrite_colnames
oruebel Feb 3, 2025
47709f8
Read colNames in DynamicTable.initialize to support appending of columns
oruebel Feb 3, 2025
8e8d6d6
Read columns in DynamicTable constructor to avoid repeat call to init…
oruebel Feb 3, 2025
049e296
Fix use and writing of variable length string columns in DynamicTable…
oruebel Feb 3, 2025
e31add9
Add basic unit tests for DynamicTable
oruebel Feb 3, 2025
c80b9be
Fix tests and docs errors
oruebel Feb 3, 2025
a1057b4
Merge pull request #151 from NeurodataWithoutBorders/overwrite_colnames
oruebel Feb 3, 2025
b3d040c
Clarify detail in read.dox
oruebel Feb 3, 2025
03b8e71
Define && and || operator for the Status enum to simplify code
oruebel Feb 3, 2025
d69d55c
Fix linter error
oruebel Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/pages/userdocs/read.dox
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,12 @@
* - \ref AQNWB::IO::BaseIO "BaseIO", \ref AQNWB::IO::HDF5::HDF5IO "HDF5IO" responsible for
* reading data from disk and allocating memory for data on read
* - \ref AQNWB::IO::DataBlockGeneric "DataBlockGeneric" represents a generic, n-dimensional block of data
* loaded from a file, storing the data as a generic ``std::any`` along with the ``shape`` of the data.
* - \ref AQNWB::IO::DataBlock "DataBlock" represents a typed, n-dimensional block of data, derived
* from a \ref AQNWB::IO::DataBlockGeneric "DataBlockGeneric"
* loaded from a file, storing the data as a generic ``std::any`` along with the ``shape`` of the data
* (i.e., the ``std::any`` represents a ``std::vector<DTYPE>`` so that we can cast it to the
* ``std::any_cast<std::vector<DTYPE>>(genericDataBlock.data)`` without having to copy data)
* - \ref AQNWB::IO::DataBlock "DataBlock" represents a typed, n-dimensional block of data, derived
* from a \ref AQNWB::IO::DataBlockGeneric "DataBlockGeneric". I.e., here the data has been
* cast to the correct ``std::vector<DTYPE>``.
* - \ref AQNWB::IO::ReadDataWrapper "ReadDataWrapper", is a simple wrapper class that represents
* a dataset/attribute for read, enabling lazy data read and allowing for transparent use of different I/O backends.
* - \ref AQNWB::NWB::Container "Container" type classes represent Groups with an assigned ``neurodata_type``
Expand Down
22 changes: 22 additions & 0 deletions src/Types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,28 @@ class Types
Failure = -1
};

/**
* @brief Overloaded && operator for Status enum
* @param lhs Left-hand side Status
* @param rhs Right-hand side Status
* @return Success if both statuses are Success, Failure otherwise
*/
friend Status operator&&(Status lhs, Status rhs)
{
return (lhs == Success && rhs == Success) ? Success : Failure;
}

/**
* @brief Overloaded || operator for Status enum
* @param lhs Left-hand side Status
* @param rhs Right-hand side Status
* @return Success if either status is Success, Failure otherwise
*/
friend Status operator||(Status lhs, Status rhs)
{
return (lhs == Success || rhs == Success) ? Success : Failure;
}

/**
* @brief Types of object used in the NWB schema
*/
Expand Down
4 changes: 3 additions & 1 deletion src/io/BaseIO.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -329,11 +329,13 @@ class BaseIO
* @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 overwrite Overwrite the attribute if it already exists.
* @return The status of the attribute creation operation.
*/
virtual Status createAttribute(const std::vector<std::string>& data,
const std::string& path,
const std::string& name) = 0;
const std::string& name,
const bool overwrite = false) = 0;

/**
* @brief Creates a string array attribute at a given location in the file.
Expand Down
83 changes: 74 additions & 9 deletions src/io/hdf5/HDF5IO.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Status HDF5IO::open()

Status HDF5IO::open(FileMode mode)
{
int accFlags = 0;
unsigned int accFlags = 0;

if (m_opened) {
return Status::Failure;
Expand Down Expand Up @@ -683,17 +683,82 @@ Status HDF5IO::createAttribute(const std::string& data,

Status HDF5IO::createAttribute(const std::vector<std::string>& data,
const std::string& path,
const std::string& name)
const std::string& name,
const bool overwrite)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks good, I am just wondering if there was a specific reason to add the overwrite functionality on this function instead of the version of the createAttribute function defined below:

Status HDF5IO::createAttribute(const std::vector<const char*>& data,
                               const std::string& path,
                               const std::string& name,
                               const SizeType& maxSize)

Since that version is used during both single string and string array attribute creation and performs similar checks for the attribute already existing?

{
std::vector<const char*> 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());
H5Object* loc;
Group gloc;
DataSet dloc;
Attribute attr;
hsize_t dims[1];

if (!canModifyObjects()) {
return Status::Failure;
}

// Create variable length string type
StrType H5type(PredType::C_S1, H5T_VARIABLE);

// open the group or dataset
H5O_type_t objectType = getH5ObjectType(path);
switch (objectType) {
case H5O_TYPE_GROUP:
gloc = m_file->openGroup(path);
loc = &gloc;
break;
case H5O_TYPE_DATASET:
dloc = m_file->openDataSet(path);
loc = &dloc;
break;
default:
return Status::Failure; // not a valid dataset or group type
}

return createAttribute(dataPtrs, path, name, maxLength + 1);
try {
if (loc->attrExists(name)) {
if (overwrite) {
// Delete the existing attribute
loc->removeAttr(name);
} else {
return Status::Failure; // don't allow overwriting
}
}

// Create dataspace based on number of strings
DataSpace attr_dataspace;
if (data.size() > 1) {
dims[0] = data.size();
attr_dataspace = DataSpace(1, dims);
} else {
attr_dataspace = DataSpace(H5S_SCALAR);
}

// Create the attribute
attr = loc->createAttribute(name, H5type, attr_dataspace);

// Write the data directly from the vector of strings
std::vector<const char*> dataPtrs;
dataPtrs.reserve(data.size());
for (const auto& str : data) {
dataPtrs.push_back(str.c_str());
}
attr.write(H5type, dataPtrs.data());

} catch (GroupIException error) {
error.printErrorStack();
return Status::Failure;
} catch (AttributeIException error) {
error.printErrorStack();
return Status::Failure;
} catch (FileIException error) {
error.printErrorStack();
return Status::Failure;
} catch (DataSetIException error) {
error.printErrorStack();
return Status::Failure;
}

return Status::Success;
}

Status HDF5IO::createAttribute(const std::vector<const char*>& data,
Expand Down
10 changes: 7 additions & 3 deletions src/io/hdf5/HDF5IO.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -152,18 +152,22 @@ class HDF5IO : public BaseIO
const std::string& name) override;

/**
* @brief Creates a string array attribute at a given location in the file.
* @brief Creates a variable-length 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 overwrite Overwrite the attribute if it already exists.
* @return The status of the attribute creation operation.
*/
Status createAttribute(const std::vector<std::string>& data,
const std::string& path,
const std::string& name) override;
const std::string& name,
const bool overwrite = false) override;

/**
* @brief Creates a string array attribute at a given location in the file.
* @brief Creates a fixed-length string array attribute at a given location in
Copy link
Collaborator

Choose a reason for hiding this comment

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

From my understanding this function writes variable length string arrays:

H5type.setSize(H5T_VARIABLE);

However, I think it is maybe unclear since a maximum size is accepted, but this parameter is then overwritten. I'm not sure why that is the case....

* 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.
Expand Down
17 changes: 15 additions & 2 deletions src/nwb/RegisteredType.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <filesystem>
#include <functional>
#include <memory>
#include <string>
Expand Down Expand Up @@ -68,6 +69,18 @@ class RegisteredType
*/
inline std::string getPath() const { return m_path; }

/**
* @brief Get the name of the object
*
* This is the last part of getPath(), much like the filename portion
* of a file system path.
* @return String with the name of the object
*/
inline std::string getName() const
{
return std::filesystem::path(m_path).filename().string();
}

/**
* @brief Get a shared pointer to the IO object.
* @return Shared pointer to the IO object.
Expand Down Expand Up @@ -186,14 +199,14 @@ class RegisteredType
virtual std::string getNamespace() const;

/**
* @brief Get the full name of type, i.e., `namespace::typename`
* @brief Get the full name of the type, i.e., `namespace::typename`
*
* This is just a simple convenience function that uses the getNamespace
* and getTypeName methods.
*
* @return The full name of the type consisting of `namespace::typename`
*/
inline std::string getFullName() const
inline std::string getFullTypeName() const
{
return (getNamespace() + "::" + getTypeName());
}
Expand Down
6 changes: 1 addition & 5 deletions src/nwb/base/TimeSeries.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,5 @@ Status TimeSeries::writeData(const std::vector<SizeType>& dataShape,
controlShape, controlPositionOffset, this->controlType, controlInput);
}

if ((dataStatus != Status::Success) || (tsStatus != Status::Success)) {
return Status::Failure;
} else {
return Status::Success;
}
return dataStatus && tsStatus;
}
38 changes: 21 additions & 17 deletions src/nwb/file/ElectrodeTable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ REGISTER_SUBCLASS_IMPL(ElectrodeTable)
/** Constructor */
ElectrodeTable::ElectrodeTable(std::shared_ptr<IO::BaseIO> io)
: DynamicTable(electrodeTablePath, // use the electrodeTablePath
io,
{"group", "group_name", "location"})
io)
, m_electrodeDataset(std::make_unique<ElementIdentifiers>(
AQNWB::mergePaths(electrodeTablePath, "id"), io))
, m_groupNamesDataset(std::make_unique<VectorData>(
Expand Down Expand Up @@ -42,30 +41,32 @@ ElectrodeTable::ElectrodeTable(const std::string& path,
ElectrodeTable::~ElectrodeTable() {}

/** Initialization function*/
void ElectrodeTable::initialize(const std::string& description)
Status ElectrodeTable::initialize(const std::string& description)
{
// create group
DynamicTable::initialize(description);

m_electrodeDataset->initialize(std::unique_ptr<IO::BaseRecordingData>(
m_io->createArrayDataSet(IO::BaseDataType::I32,
SizeArray {1},
SizeArray {1},
AQNWB::mergePaths(m_path, "id"))));
m_groupNamesDataset->initialize(
Status electrodeStatus =
m_electrodeDataset->initialize(std::unique_ptr<IO::BaseRecordingData>(
m_io->createArrayDataSet(IO::BaseDataType::I32,
SizeArray {1},
SizeArray {1},
AQNWB::mergePaths(m_path, "id"))));
Status groupNameStatus = m_groupNamesDataset->initialize(
std::unique_ptr<IO::BaseRecordingData>(
m_io->createArrayDataSet(IO::BaseDataType::STR(250),
m_io->createArrayDataSet(IO::BaseDataType::V_STR,
SizeArray {0},
SizeArray {1},
AQNWB::mergePaths(m_path, "group_name"))),
"the name of the ElectrodeGroup this electrode is a part of");
m_locationsDataset->initialize(
Status locationStatus = m_locationsDataset->initialize(
std::unique_ptr<IO::BaseRecordingData>(
m_io->createArrayDataSet(IO::BaseDataType::STR(250),
m_io->createArrayDataSet(IO::BaseDataType::V_STR,
SizeArray {0},
SizeArray {1},
AQNWB::mergePaths(m_path, "location"))),
"the location of channel within the subject e.g. brain region");
return electrodeStatus && groupNameStatus && locationStatus;
}

void ElectrodeTable::addElectrodes(std::vector<Channel> channelsInput)
Expand All @@ -80,13 +81,16 @@ void ElectrodeTable::addElectrodes(std::vector<Channel> channelsInput)
}
}

void ElectrodeTable::finalize()
Status ElectrodeTable::finalize()
{
setRowIDs(m_electrodeDataset, m_electrodeNumbers);
addColumn(m_groupNamesDataset, m_groupNames);
addColumn(m_locationsDataset, m_locationNames);
addReferenceColumn(
Status rowIdStatus = setRowIDs(m_electrodeDataset, m_electrodeNumbers);
Status locationColStatus = addColumn(m_locationsDataset, m_locationNames);
Status groupColStatus = addReferenceColumn(
"group",
"a reference to the ElectrodeGroup this electrode is a part of",
m_groupReferences);
Status groupNameColStatus = addColumn(m_groupNamesDataset, m_groupNames);
Status finalizeStatus = DynamicTable::finalize();
return rowIdStatus && locationColStatus && groupColStatus
&& groupNameColStatus && finalizeStatus;
}
26 changes: 6 additions & 20 deletions src/nwb/file/ElectrodeTable.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,41 +40,27 @@ class ElectrodeTable : public DynamicTable
* Initializes the ElectrodeTable by creating NWB related attributes and
* adding required columns.
*
* @param description The description of the table (default: "metadata about
* @param description The description of the table (default: "metadata about
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @param description The description of the table (default: "metadata about
* @param description The description of the table (default: "metadata about extracellular electrodes")

* @return Status::Success if successful, otherwise Status::Failure.
*/
void initialize(const std::string& description =
"metadata about extracellular electrodes");
Status initialize(const std::string& description =
"metadata about extracellular electrodes");

/**
* @brief Finalizes the ElectrodeTable.
*
* Finalizes the ElectrodeTable by adding the required columns and writing
* the data to the file.
* @return Status::Success if successful, otherwise Status::Failure.
*/
void finalize();
Status finalize();

/**
* @brief Sets up the ElectrodeTable by adding electrodes and their metadata.
* @param channelsInput The vector of Channel objects to add to the table.
*/
void addElectrodes(std::vector<Channel> channelsInput);

/**
* @brief Gets the group path of the ElectrodeTable.
* @return The group path.
*/
inline std::string getGroupPath() const
{
// all channels in ChannelVector should have the same groupName
return m_groupReferences[0];
}

/**
* @brief Sets the group path of the ElectrodeTable.
* @param groupPath The new group path.
*/
void setGroupPath(const std::string& groupPath);

/**
* @brief The path to the ElectrodeTable.
*/
Expand Down
7 changes: 4 additions & 3 deletions src/nwb/hdmf/base/Container.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ Container::Container(const std::string& path,
Container::~Container() {}

/** Initialize */
void Container::initialize()
Status Container::initialize()
{
m_io->createGroup(m_path);
auto createGroupStatus = m_io->createGroup(m_path);
// setup common attributes
m_io->createCommonNWBAttributes(
auto createAttrsStatus = m_io->createCommonNWBAttributes(
m_path, this->getNamespace(), this->getTypeName());
return createGroupStatus && createAttrsStatus;
}
3 changes: 2 additions & 1 deletion src/nwb/hdmf/base/Container.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ class Container : public RegisteredType

/**
* @brief Initialize the container.
* @return Status::Success if successful, otherwise Status::Failure.
*/
void initialize();
Status initialize();

// Define the data fields to expose for lazy read access
DEFINE_FIELD(readNeurodataType,
Expand Down
Loading
Loading