Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
689d747
Initial plan
Copilot Aug 13, 2026
54fd124
Apply remaining changes
Copilot Aug 13, 2026
b5575c7
Make Subject required by default in NWBFile::initialize()
Copilot Aug 13, 2026
b4bd01e
Added Subject type
oruebel Aug 13, 2026
ac90401
Merge branch 'copilot/make-subject-required-by-default' of https://gi…
oruebel Aug 13, 2026
d5a741e
Update unit tests
oruebel Aug 13, 2026
a526662
Update Changelog and fix docstring
oruebel Aug 13, 2026
b732aea
Add missing testSubject.cpp file
oruebel Aug 13, 2026
dabddc7
Fix nwb-inspector validation errors
oruebel Aug 13, 2026
cbd9a1f
Fix validation tests
oruebel Aug 13, 2026
10de7dd
Fix nwb-inspector validation errors
oruebel Aug 13, 2026
bcb223e
Fix nwb-inspector validation errors
oruebel Aug 13, 2026
d3f1893
Fix subject metadata nwb-inspector test
oruebel Aug 13, 2026
49f9ea6
Potential fix for pull request finding
oruebel Aug 13, 2026
b016125
Potential fix for pull request finding
oruebel Aug 13, 2026
7d16947
Potential fix for pull request finding
oruebel Aug 13, 2026
7bb903e
Address review comment
oruebel Aug 13, 2026
e6427bd
Avoid overwriting exiting subject
oruebel Aug 13, 2026
5e39cd2
Prevent invalid Subject data write
oruebel Aug 13, 2026
76be639
Merge branch 'main' into copilot/make-subject-required-by-default
oruebel Aug 16, 2026
597be22
Make Subject constructor protected
oruebel Aug 20, 2026
8cd3833
Merge branch 'copilot/make-subject-required-by-default' of https://gi…
oruebel Aug 20, 2026
88c3ce9
Merge branch 'schema_2_10' into copilot/make-subject-required-by-default
oruebel Aug 20, 2026
0a430ce
Fix missing subject in unit test from parent branch
oruebel Aug 20, 2026
5dab595
Added missing convenience Subject::create to fix the path
oruebel Aug 20, 2026
425ff13
Fix codespell
oruebel Aug 20, 2026
b164a7a
Added missing Subject to unit test to fix nwb-inspector error
oruebel Aug 20, 2026
87af098
Fix missing Subject in Event workflow
oruebel Aug 20, 2026
2efab35
Allow function changing in SubjectSpec to ease initalization with opt…
oruebel Aug 20, 2026
bef3132
Fix codespell
oruebel Aug 20, 2026
19a366a
Merge branch 'add_time_intervals' into copilot/make-subject-required-…
oruebel Aug 20, 2026
b28ea09
Add missing Subject to time intervals tests
oruebel Aug 20, 2026
699e4de
Updated CHANGELOG to group related changes for the upcoming release
oruebel Aug 20, 2026
0ad29bb
Merge branch 'add_time_intervals' into copilot/make-subject-required-…
oruebel Aug 20, 2026
3b0005e
Added missing Subject in new unit test
oruebel Aug 20, 2026
d262d8b
Merge branch 'add_time_intervals' into copilot/make-subject-required-…
oruebel Aug 22, 2026
526e6f5
Fix failing unit test
oruebel Aug 22, 2026
6d04453
Fix NWB scherma version in change log
oruebel Aug 22, 2026
1f64903
Fix column docstrings in EventsTable::createDefaultDataSpecs to match…
oruebel Aug 22, 2026
2d43100
Update MeaningsTable.target doc
oruebel Aug 22, 2026
35d2eaf
Merge branch 'add_time_intervals' into copilot/make-subject-required-…
oruebel Aug 23, 2026
907d4f9
Update changelog to fix formatting
oruebel Aug 23, 2026
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
96 changes: 55 additions & 41 deletions CHANGELOG.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ add_library(
src/nwb/event/DurationVectorData.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
35 changes: 27 additions & 8 deletions src/nwb/NWBFile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "nwb/ecephys/SpikeEventSeries.hpp"
#include "nwb/epoch/TimeIntervals.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 @@ -50,11 +51,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 @@ -89,16 +92,31 @@ 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");
if (!ioPtr->objectExists(subjectPath)) {
auto subject = AQNWB::NWB::Subject::create(subjectPath, ioPtr);
Status subjectInitStatus = subject->initialize(subjectSpec.value());
initStatus = initStatus && subjectInitStatus;
} else {
std::cerr << "Subject group already exists in the file. Skipping "
"subject initialization."
<< std::endl;
}
}
return initStatus;
}

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

return Status::Success;
}

Expand Down
8 changes: 7 additions & 1 deletion src/nwb/NWBFile.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "nwb/epoch/TimeIntervals.hpp"
#include "nwb/event/EventsTable.hpp"
#include "nwb/file/ElectrodesTable.hpp"
#include "nwb/file/Subject.hpp"
#include "spec/core.hpp"

/*!
Expand Down Expand Up @@ -114,12 +115,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. By default, an empty Subject
* is created. Pass @c std::nullopt to explicitly state that no subject should
* be created (e.g., when the subject is unknown).
*/
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
160 changes: 160 additions & 0 deletions src/nwb/file/Subject.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#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;
}
}

std::shared_ptr<Subject> Subject::create(std::shared_ptr<IO::BaseIO> io)
{
return RegisteredType::create<Subject>(
mergePaths(NWBFile::GENERAL_PATH, "subject"), io);
}

// Initialize the object
Status Subject::initialize(const SubjectSpec& subjectSpec)
{
// Get the IO object
auto ioPtr = getIO();
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()) {
if (isISO8601Date(subjectSpec.dateOfBirth.value()) == false) {
std::cerr << "Warning: date_of_birth is not in ISO8601 format: "
<< subjectSpec.dateOfBirth.value() << std::endl;
initStatus = Status::Failure;
} else {
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;
}
}
}
// 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