Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install nwbinspector
nwbinspector nwb_files --threshold BEST_PRACTICE_VIOLATION --ignore=check_subject_exists --json-file-path out.json
nwbinspector nwb_files --threshold BEST_PRACTICE_VIOLATION --ignore=check_electrodes_location_allen_ccf --json-file-path out.json
if ! grep -q '"messages": \[\]' out.json; then
echo "NWBInspector found issues in the NWB files"
exit 1
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/upgrade_schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
run: |
mkdir -p nwb_files
cp build/tests/data/*.nwb nwb_files/
nwbinspector nwb_files --threshold BEST_PRACTICE_VIOLATION --ignore=check_subject_exists --json-file-path out.json
nwbinspector nwb_files --threshold BEST_PRACTICE_VIOLATION --ignore=check_electrodes_location_allen_ccf --json-file-path out.json
if ! grep -q '"messages": \[\]' out.json; then
echo "NWBInspector found issues in the NWB files"
exit 1
Expand Down
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
* Added `ElectricalSeries::writeAllChannels` method and `IO::writeElectricalSeriesData` overload to simplify zero-copy interleaved multichannel writes. (@copilot, @oruebel, [#293](https://github.com/NeurodataWithoutBorders/aqnwb/pull/293))
* Added `ElectricalSeries::channelsAtSameSampleOffset` method to check if all channels are at the same sample offset, which is a requirement for using `writeAllChannels`. (@copilot, @oruebel, [#293](https://github.com/NeurodataWithoutBorders/aqnwb/pull/293))

* Added `Subject` class to represent the `/general/subject` group in NWB files, with corresponding `SubjectSpec` for initialization. (@copilot, @oruebel, [#320](https://github.com/NeurodataWithoutBorders/aqnwb/pull/320))
* Updated nwbinspector tests to remove `--ignore=check_subject_exists` option to require subject (@oruebel, [#320](https://github.com/NeurodataWithoutBorders/aqnwb/pull/320))
* Updated `NWBFile::initialize` to accept a `SubjectSpec` argument for subject metadata initialization (@oruebel, [#320](https://github.com/NeurodataWithoutBorders/aqnwb/pull/320))

### Changed
* **[BREAKING]** Moved `disableSWMRMode` option from `HDF5IO` constructor to a new `HDF5IO::startRecording(bool disableSWMRMode)` overload. The `BaseIO`-compliant `startRecording()` override is preserved and defaults to SWMR enabled.
* **Migration Note**: Code using `HDF5IO(path, true)` must be updated to `HDF5IO(path)` followed by `startRecording(true)`. When the `HDF5IO` object is held as a `std::shared_ptr<BaseIO>` (e.g., from `createIO`), downcast with `std::dynamic_pointer_cast<HDF5IO>` to access the overload. (@oruebel [#297](https://github.com/NeurodataWithoutBorders/aqnwb/pull/297))

### Fixed
* Updated nwbinspector validation tests to ignore the Allen CCF electrode location check when validating mock electrode locations. (@oruebel, [#320](https://github.com/NeurodataWithoutBorders/aqnwb/pull/320))
* Updated nwbinspector validation tests in the CI to: 1) `--ignore=check_subject_exists` and 2) remove dependency on `sanitizer` tests to speed up CI (@oruebel, [#289](https://github.com/NeurodataWithoutBorders/aqnwb/pull/289))
* Fixed `get_utc_offset_seconds` to correctly account for daylight saving time using platform-specific APIs (`tm_gmtoff` on Unix/macOS; `_get_timezone` + `_get_dstbias` on Windows), preventing `session_start_time` from being written ~1 hour ahead of UTC during DST (@cboulay, [#295](https://github.com/NeurodataWithoutBorders/aqnwb/pull/295))
* Fixed HDF5 string type creation to explicitly use UTF-8 character set for fixed-length and variable-length strings in datasets and attributes, improving compatibility with hdmf/PyNWB string decoding (@copilot, @oruebel [#319](https://github.com/NeurodataWithoutBorders/aqnwb/pull/319))
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ add_library(
src/nwb/ecephys/SpikeEventSeries.cpp
src/nwb/file/ElectrodeGroup.cpp
src/nwb/file/ElectrodesTable.cpp
src/nwb/file/Subject.cpp
src/nwb/misc/AnnotationSeries.cpp
src/nwb/hdmf/base/Container.cpp
src/nwb/hdmf/base/Data.cpp
Expand Down
29 changes: 21 additions & 8 deletions src/nwb/NWBFile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "nwb/ecephys/ElectricalSeries.hpp"
#include "nwb/ecephys/SpikeEventSeries.hpp"
#include "nwb/file/ElectrodeGroup.hpp"
#include "nwb/file/Subject.hpp"
#include "nwb/misc/AnnotationSeries.hpp"
#include "spec/NamespaceRegistry.hpp"
#include "spec/core.hpp"
Expand Down Expand Up @@ -49,11 +50,13 @@ NWBFile::NWBFile(const std::string& path, std::shared_ptr<IO::BaseIO> io)

NWBFile::~NWBFile() {}

Status NWBFile::initialize(const std::string& identifierText,
const std::string& description,
const std::string& dataCollection,
const std::string& sessionStartTime,
const std::string& timestampsReferenceTime)
Status NWBFile::initialize(
const std::string& identifierText,
const std::string& description,
const std::string& dataCollection,
const std::string& sessionStartTime,
const std::string& timestampsReferenceTime,
const std::optional<AQNWB::NWB::Subject::SubjectSpec>& subjectSpec)
{
auto ioPtr = getIO();
if (!ioPtr) {
Expand Down Expand Up @@ -88,16 +91,25 @@ Status NWBFile::initialize(const std::string& identifierText,

// Check that the file is empty and initialize if it is
bool fileInitialized = isInitialized();
Status initStatus = Status::Success;
if (!fileInitialized) {
Status createStatus = createFileStructure(identifierText,
description,
dataCollection,
useSessionStartTime,
useTimestampsReferenceTime);
return createStatus;
} else {
return Status::Success;
initStatus = initStatus && createStatus;
}

// Create subject group and its contents if subject metadata is provided
if (subjectSpec.has_value()) {
const std::string subjectPath =
mergePaths(NWBFile::GENERAL_PATH, "subject");
auto subject = AQNWB::NWB::Subject::create(subjectPath, ioPtr);
Status subjectInitStatus = subject->initialize(subjectSpec.value());
initStatus = initStatus && subjectInitStatus;
}
return initStatus;
Comment thread
oruebel marked this conversation as resolved.
}

bool NWBFile::isInitialized() const
Expand Down Expand Up @@ -193,6 +205,7 @@ Status NWBFile::createFileStructure(const std::string& identifierText,
ioPtr->createStringDataSet("/timestamps_reference_time",
timestampsReferenceTime);
ioPtr->createStringDataSet("/identifier", identifierText);

return Status::Success;
}

Expand Down
9 changes: 8 additions & 1 deletion src/nwb/NWBFile.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <array>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
Expand All @@ -15,6 +16,7 @@
#include "nwb/base/ProcessingModule.hpp"
#include "nwb/base/TimeSeries.hpp"
#include "nwb/file/ElectrodesTable.hpp"
#include "nwb/file/Subject.hpp"
#include "spec/core.hpp"

/*!
Expand Down Expand Up @@ -98,12 +100,17 @@ class NWBFile : public NWBContainer
* time. If empty (default), then the getCurrentTime() will be used.
* @param timestampsReferenceTime ISO formatted time string with the timestamp
* reference time. If empty (default), then the getCurrentTime() will be used.
* @param subjectSpec Optional subject metadata. Pass @c std::nullopt
* (default)to explicitly state that no subject should be created (e.g., when
* the subject is unknown).
Comment thread
Copilot marked this conversation as resolved.
Outdated
*/
Status initialize(const std::string& identifierText,
const std::string& description = "a recording session",
const std::string& dataCollection = "",
const std::string& sessionStartTime = "",
const std::string& timestampsReferenceTime = "");
const std::string& timestampsReferenceTime = "",
const std::optional<AQNWB::NWB::Subject::SubjectSpec>&
subjectSpec = std::nullopt);

/**
* @brief Check if the NWB file is initialized.
Expand Down
152 changes: 152 additions & 0 deletions src/nwb/file/Subject.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#include "nwb/file/Subject.hpp"

#include "Utils.hpp"
#include "nwb/NWBFile.hpp"

using namespace AQNWB::NWB;
using namespace AQNWB::IO;

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

Subject::Subject(std::shared_ptr<IO::BaseIO> io)
: NWBContainer(mergePaths(NWBFile::GENERAL_PATH, "subject"), io)
{
}

// Constructor
Subject::Subject(const std::string& path, std::shared_ptr<AQNWB::IO::BaseIO> io)
: NWBContainer(mergePaths(NWBFile::GENERAL_PATH, "subject"), io)
{
if (path != mergePaths(NWBFile::GENERAL_PATH, "subject")) {
std::cerr << "WARNING: Subject object path must be /general/subject. "
"Ignoring provided path."
<< std::endl;
}
}

// Initialize the object
Status Subject::initialize(const SubjectSpec& subjectSpec)
{
// Get the IO object`
auto ioPtr = getIO();
Comment thread
Copilot marked this conversation as resolved.
Outdated
if (!ioPtr) {
std::cerr << "Subject::initialize IO object has been deleted." << std::endl;
return Status::Failure;
}
if (!ioPtr->canModifyObjects()) {
return Status::Failure;
}

// Call parent initialize method.
Status initStatus = Status::Success;
Status parentInitStatus = NWBContainer::initialize();
initStatus = initStatus && parentInitStatus;

// Initialize attributes, datasets, and groups
// Initialize age dataset and age/reference attribute if age is provided
if (subjectSpec.age.has_value()) {
Status ageStatus = ioPtr->createStringDataSet(
mergePaths(this->m_path, "age"), subjectSpec.age.value());
initStatus = initStatus && ageStatus;
if (!ageStatus) {
std::cerr << "Failed to create age dataset." << std::endl;
} else {
std::string ageReference = subjectSpec.ageReference.value_or("birth");
Status ageRefStatus = ioPtr->createAttribute(
ageReference, mergePaths(this->m_path, "age"), "reference");
initStatus = initStatus && ageRefStatus;
if (!ageRefStatus) {
std::cerr << "Failed to create age reference attribute." << std::endl;
}
}
}

// Initialize date_of_birth dataset if date_of_birth is provided
if (subjectSpec.dateOfBirth.has_value()) {
Status dobStatus =
ioPtr->createStringDataSet(mergePaths(this->m_path, "date_of_birth"),
subjectSpec.dateOfBirth.value());
initStatus = initStatus && dobStatus;
if (!dobStatus) {
std::cerr << "Failed to create date_of_birth dataset." << std::endl;
}
if (isISO8601Date(subjectSpec.dateOfBirth.value()) == false) {
std::cerr << "Warning: date_of_birth is not in ISO8601 format: "
<< subjectSpec.dateOfBirth.value() << std::endl;
}
}
Comment thread
oruebel marked this conversation as resolved.
// Initialize description dataset if description is provided
if (subjectSpec.description.has_value()) {
Status descStatus =
ioPtr->createStringDataSet(mergePaths(this->m_path, "description"),
subjectSpec.description.value());
initStatus = initStatus && descStatus;
if (!descStatus) {
std::cerr << "Failed to create description dataset." << std::endl;
}
}

// Initialize genotype dataset if genotype is provided
if (subjectSpec.genotype.has_value()) {
Status genotypeStatus = ioPtr->createStringDataSet(
mergePaths(this->m_path, "genotype"), subjectSpec.genotype.value());
initStatus = initStatus && genotypeStatus;
if (!genotypeStatus) {
std::cerr << "Failed to create genotype dataset." << std::endl;
}
}

// Initialize sex dataset if sex is provided
if (subjectSpec.sex.has_value()) {
Status sexStatus = ioPtr->createStringDataSet(
mergePaths(this->m_path, "sex"), subjectSpec.sex.value());
initStatus = initStatus && sexStatus;
if (!sexStatus) {
std::cerr << "Failed to create sex dataset." << std::endl;
}
}

// Initialize species dataset if species is provided
if (subjectSpec.species.has_value()) {
Status speciesStatus = ioPtr->createStringDataSet(
mergePaths(this->m_path, "species"), subjectSpec.species.value());
initStatus = initStatus && speciesStatus;
if (!speciesStatus) {
std::cerr << "Failed to create species dataset." << std::endl;
}
}

// Initialize strain dataset if strain is provided
if (subjectSpec.strain.has_value()) {
Status strainStatus = ioPtr->createStringDataSet(
mergePaths(this->m_path, "strain"), subjectSpec.strain.value());
initStatus = initStatus && strainStatus;
if (!strainStatus) {
std::cerr << "Failed to create strain dataset." << std::endl;
}
}

// Initialize subject_id dataset if subject_id is provided
if (subjectSpec.subjectId.has_value()) {
Status subjectIdStatus = ioPtr->createStringDataSet(
mergePaths(this->m_path, "subject_id"), subjectSpec.subjectId.value());
initStatus = initStatus && subjectIdStatus;
if (!subjectIdStatus) {
std::cerr << "Failed to create subject_id dataset." << std::endl;
}
}

// Initialize weight dataset if weight is provided
if (subjectSpec.weight.has_value()) {
Status weightStatus = ioPtr->createStringDataSet(
mergePaths(this->m_path, "weight"), subjectSpec.weight.value());
initStatus = initStatus && weightStatus;
if (!weightStatus) {
std::cerr << "Failed to create weight dataset." << std::endl;
}
}

// Return the overall status of the initialization
return initStatus;
}
Loading
Loading