How to Integrate a New Namespace
Integrating a new schema namespace with AqNWB (e.g., to support an extension to NWB) involves generating the necessary specification files and ensuring that the namespace is registered with the NamespaceRegistry. This process is simplified through the use of the generate_spec_files.py script.
- Get the schema files: Download or create the schema for the namespace in YAML format.
- Note
- If you are creating a new extension, please see the NWB Extension Tutorial for more information on how to create data schema for NWB.
- Convert the schema files to C++: Run the resources/utils/generate_spec_files.py script on your schema files to generate the necessary C++ header files. This script processes the schema and creates the appropriate C++ header files that include the namespace definitions and registration.
- Note
- To learn more about how to use the script and its parameters, you can view the help doc by running:
python resources/utils/generate_spec_files.py --help
- Include the Generated Header Files: In your C++ code that uses AqNWB, include the generated header files. The header files will automatically take care of registering the namespace with the NamespaceRegistry by calling the REGISTER_NAMESPACE macro. Once the extension has been registered, NWBFile::initialize will automatically take care of caching your schema in the NWB file when creating a new file.
- Implement appropriate RegisteredType classes: Follow the tutorial on Implementing a Registered Type 🔧 to define appropriate interfaces for the neurodata_types defined in your new namespace. Using the schematype_to_aqnwb.py Utility can also provide additional help by providing a simple utility that can automatically generate skeleton AqNWB C++ classes for neurodata_types directly from JSON/YAML schema files.
How the NamespaceRegistry Works
The NamespaceRegistry is a singleton class that manages the registration and lookup of namespace information.
Registering a Namespace
The REGISTER_NAMESPACE macro is used to register a namespace with the NamespaceRegistry. This macro is typically placed in the generated header file for the namespace schema and ensures that the namespace is registered when the header file is included. For example:
#define REGISTER_NAMESPACE(name, version, specVariables)
Macro to register a namespace with the global registry.
Definition NamespaceRegistry.hpp:64
would register the namespace your_namespace with version 1.0.0 and the specified specVariables.
Looking up Namespaces
To retrieve information about a registered namespace, we can use the getNamespaceInfo method. For example:
const NamespaceInfo* info = NamespaceRegistry::instance().getNamespaceInfo("your_namespace");
if (info) {
}
Alternatively, we can also use getAllNamespaces to get a reference to all registered namespaces. For example:
const auto& allNamespaces = NamespaceRegistry::instance().getAllNamespaces();
for (const auto& [name, info] : allNamespaces) {
}
The returned info object is, in both cases, a NamespaceInfo struct with the name, version, and specVariables.
LabMetaData Extension Demo
The following example illustrated the key steps to implement and use an extension to NWB via AqNWB. The complete sources for this demo are available as part of the AqNWB GitHub repo in the demo/labmetadata_extension_demo folder.
Step 1: Get the Schema of the Extension
The demo uses the schema from ndx-labmetadata-example as an example. For convenience, we copied the relevant schema files already to the demo/labmetadata_extension_demo/spec folder.
Step 2: Convert the Schema to C++
The schema files are converted to C++ using the resources/utils/generate_spec_files.py script:
mkdir demo/labmetadata_extension_demo/src
python resources/utils/generate_spec_files.py demo/labmetadata_extension_demo/spec demo/labmetadata_extension_demo/src
This generates the following new header file in the demo/labmetadata_extension_demo/src folder:
ndx_labmetadata_example.hpp
#pragma once
#include <vector>
#include <string>
#include <string_view>
namespace AQNWB::SPEC::NDX_LABMETADATA_EXAMPLE
{
const std::string namespaceName = "ndx-labmetadata-example";
const std::string version = "0.1.0";
constexpr std::string_view ndx_labmetadata_example_extensions = R"delimiter(
{"groups":[{"neurodata_type_def":"LabMetaDataExtensionExample","neurodata_type_inc":"LabMetaData","name":"custom_lab_metadata","doc":"Example extension type for storing lab metadata","datasets":[{"name":"tissue_preparation","dtype":"text","doc":"Lab-specific description of the preparation of the tissue","quantity":"?"}]}]})delimiter";
constexpr std::string_view namespaces = R"delimiter(
{"namespaces":[{"author":["Oliver Ruebel"],"contact":["oruebel@lbl.gov"],"doc":" Example extension to illustrate how to extend LabMetaData for adding lab-specific metadata","name":"ndx-labmetadata-example","schema":[{"namespace":"core","neurodata_types":["LabMetaData"]},{"source":"ndx-labmetadata-example.extensions.yaml"}],"version":"0.1.0"}]})delimiter";
const std::vector<std::pair<std::string_view, std::string_view>>
specVariables {{
{"ndx-labmetadata-example.extensions", ndx_labmetadata_example_extensions},
{"namespace", namespaces}
}};
}
Step 3: Create RegisteredType Classes for all neurodata_types
Next, a RegisteredType class is implemented for the LabMetaDataExtensionExample neurodata_type defined in the schema. Please see the Implementing a Registered Type 🔧 tutorial for more details.
LabMetaDataExtensionExample.hpp
#pragma once
#include "ndx_labmetadata_example.hpp"
{
public:
LabMetaDataExtensionExample(
const std::string& path,
std::shared_ptr<AQNWB::IO::BaseIO> io);
readTissuePreparation,
std::string,
"tissue_preparation",
Lab-specific description of the preparation of the tissue)
LabMetaDataExtensionExample,
AQNWB::SPEC::NDX_LABMETADATA_EXAMPLE::namespaceName)
private:
const std::string m_nwbBasePath = "/general";
};
AQNWB::Types::Status Status
Definition BaseIO.hpp:22
#define REGISTER_SUBCLASS(T, NAMESPACE)
Macro to register a subclass with the RegisteredType class registry.
Definition RegisteredType.hpp:377
#define DEFINE_FIELD(name, storageObjectType, default_type, fieldPath, description)
Defines a lazy-loaded field accessor function.
Definition RegisteredType.hpp:411
Abstract data type for a group storing collections of data and metadata.
Definition Container.hpp:17
Status initialize()
Initialize the container.
Definition Container.cpp:20
constexpr auto DatasetField
Alias for AQNWB::Types::StorageObjectType::Dataset.
Definition RegisteredType.hpp:28
LabMetaDataExtensionExample.cpp
#include "LabMetaDataExtensionExample.hpp"
#include <iostream>
LabMetaDataExtensionExample::LabMetaDataExtensionExample(
const std::string& path,
std::shared_ptr<AQNWB::IO::BaseIO> io)
{
if (path.find(m_nwbBasePath) != 0) {
std::cerr << "LabMetaData path expected to appear in "
<< m_nwbBasePath << " in the NWB file" << std::endl;
}
}
Status LabMetaDataExtensionExample::initialize(
const std::string& tissuePreparation)
{
Status tissueDataStatus = m_io->createStringDataSet(tissuePrepPath, tissuePreparation);
return containerStatus && tissueDataStatus;
}
#define REGISTER_SUBCLASS_IMPL(T)
Macro to initialize the static member registered_ to trigger registration.
Definition RegisteredType.hpp:389
Namespace for all classes related to the NWB data standard.
Definition TimeSeries.hpp:13
The main namespace for AqNWB.
Definition Channel.hpp:11
static std::string mergePaths(const std::string &path1, const std::string &path2)
Merge two paths into a single path, handling extra trailing and starting "/".
Definition Utils.hpp:112
- Note
- As also discussed in more detail in the Implementing a Registered Type 🔧 tutorial, a few key points to note when implementing our derived RegisteredType class:
- Selecting the base class:
- At the very least our class must inherit from RegisteredType
- When the neurodata_type is a dataset then it is often useful to inherit from Data to inherit its base initialize and read methods.
- When a neurodata_type is a group then it is often useful to inherit from Container to inherit its base initialize and read methods (this is what we use here)
- Always inherit from the closest parent if possible. Note: Technically we also should have here created a class for LabMetaData itself and then inherited from it. However since LabMetaData is only an empty group and does not add any fields or features, we can just use Container as our base class instead. The key purpose of the LabMetaData type is that it has a reserved space in the /general group in the NWB file.
- Ensure the registration macros are called: These macros ensure that the class is registered with the RegisteredType type registry.
- Field Definitions: The DEFINE_FIELD and DEFINE_REGISTERED_FIELD macros simplify reading known fields from the file.
- Schema Registration: The generated header file automatically registers the namespace with the NamespaceRegistry using the REGISTER_NAMESPACE macro. This ensures that as soon as we include the header in our app that the schema will be cached in the NWB file.
Step 4: Using the Extension
Finally, we can use our extension with AqNWB like any other type. The following example app illustrates how we can use our extension for writing and reading data with the new LabMetaDataExtensionExample type:
main.cpp
#include "LabMetaDataExtensionExample.hpp"
#include <iostream>
int main(int argc, char* argv[])
{
std::string filePath = "testLabMetaDataExtensionExample.nwb";
std::cout << std::endl << "Opening NWB file: " << filePath << std::endl;
nwbfile.initialize("test_identifier", "Test NWB File", "Data collection info");
auto labMetaData = LabMetaDataExtensionExample(labMetaDataPath, io);
std::cout << "Writing "<< labMetaData.getPath() << " extension data" << std::endl;
labMetaData.initialize("Tissue preparation details");
io->close();
std::cout << "Finished data write. Starting read." <<std::endl;
auto readLabMetaData = LabMetaDataExtensionExample(labMetaDataPath, readIO);
auto tissuePreparationWrapper = readLabMetaData.readTissuePreparation();
std::string tissuePreparation = tissuePreparationWrapper->values().data[0];
std::cout << "Read Tissue Preparation: " << tissuePreparation << std::endl;
readIO->close();
return 0;
}
The NWBFile class provides an interface for setting up and managing the NWB file.
Definition NWBFile.hpp:32
@ ReadOnly
Opens the file in read only mode.
Definition BaseIO.hpp:188
@ Overwrite
Opens the file and overwrites any existing file.
Definition BaseIO.hpp:172
static std::shared_ptr< IO::BaseIO > createIO(const std::string &type, const std::string &filename)
Factory method to create an IO object.
Definition Utils.hpp:89