Release notes#
Release 0.3.0 October 24, 2026#
The first ndx-hed release for PyNWB 4, which closes issue #194. NWBEP001 (EventsTable, MeaningsTable, TimestampVectorData, DurationVectorData) is now part of PyNWB and HDMF core, so ndx-hed no longer depends on the standalone ndx-events extension. Validation now runs through hedtools’ staged tabular validator, so an NWB table is validated the way hedtools validates a BIDS events file. The HED rules for NWB tables are documented in docs/source/hed_validation.md.
Breaking changes#
Dependencies and core types#
Requires
pynwb>=4.2.0,hdmf>=6.2.0, andhedtools>=1.3.0; thendx-eventsdependency is removed.EventsTable,TimestampVectorData, andDurationVectorDataare imported frompynwb.event, andMeaningsTablefromhdmf.common. Use the standardpynwb.NWBFile;NdxEventsNWBFileno longer exists.There is no
CategoricalVectorData. A categorical column is a plainVectorDataannotated by aMeaningsTable.A
MeaningsTableis bound to the column it annotates:MeaningsTable(target=column, ...), named"{column_name}_meanings", attached withtable.add_meanings_table(...)and read back withtable.get_meanings_for_column(col_name). It can no longer be built on its own.
Validation (HedNWBValidator)#
validate_events()is removed; callvalidate_table(events).validate_table(table, error_handler=None, assemble=True)now validates the whole table. In 0.2.0 it checked eachHedTagsandHedValueVectorcolumn on its own. It now applies the structural rules, validates the table’s HED as a BIDS sidecar (value templates, categorical HED from eachMeaningsTable, and theHedLabMetaDatadefinitions), checks the column values, and withassemble=True(the default) validates each row’s combined per-row, categorical, and value HED, temporally when the table has anonsetcolumn. Each stage stops on an error; warnings alone do not stop it. Sidecar-stage issues name the column and categorical value (ec_sidecarColumnName,ec_sidecarKeyName) and have no row. Withassemble=False(for tools such as nwbinspector) no dataframe is built, and each distinctHEDstring is checked on its own; the checks that need the whole row or other rows (required tags, temporal) do not run. Passing aMeaningsTableraisesValueError; its HED is validated with the table it annotates.validate_file(nwbfile, error_handler=None, assemble=True)checks theHedLabMetaDataand then callsvalidate_tablefor everyDynamicTableexcept aMeaningsTable. It compares schemas byget_formatted_version(), which also works for a schema group or a merged library schema.Issues are reported per distinct value. A
HEDstring,HedValueVectorvalue, or categorical value with a problem is reported once, at the first row holding it, with the number of such rows inrow_count(ROW_COUNT_KEY). That includesvalidate_vector(), which in 0.2.0 reported every row.ec_rowis the table row index (first data row 0) in every path; it used to be hedtools’ BIDS line number in the assembled path. Every issue names its table inec_table_name. Throughvalidate_file,ec_filenameholds the NWB file identifier; it no longer holds the table name.A
HedValueVectorplaced in aMeaningsTableis now reported as the issueMEANINGS_VALUE_VECTOR_INVALIDinstead of raisingValueError.
BIDS conversion (ndx_hed.utils.bids2nwb)#
get_bids_events(events_table)is renamedget_bids_tabular(table, hed_metadata=None), and it accepts anyDynamicTable.get_categorical_meanings()takes the targetVectorDatacolumn instead of a column name.extract_meanings()returns the raw sidecar column-info dict for each categorical column instead of a built table. TheMeaningsTableis built byget_events_table()once the target column exists.get_events_table()builds categorical columns as plainVectorData, each with aMeaningsTable. It turns the BIDSn/aspellings inonsetanddurationinto NaN and raises on any other non-numeric value instead of passing it through.get_events_table()raisesValueErrorfor a sidecar that gives theonsetcolumn HED orLevels, or makes thedurationcolumn categorical. 0.2.0 dropped that HED without a word.
Changes#
HedDurationVector#
New data type
HedDurationVector: the coreDurationVectorData(durations in seconds) with ahedtemplate containing one#, for thedurationcolumn of anEventsTable. Code that reads event durations, including PyNWB without ndx-hed, sees an ordinary duration column. It is validated as aHedValueVectoris: its values against the#tag, and it may not appear in aMeaningsTable. A valid template puts what the duration applies to in a nested group, as in(Duration/# s, (Sensory-event)).get_events_table()builds aHedDurationVectorfor a BIDSdurationcolumn that the sidecar annotates, andget_json_hed_dict()exports its template underduration, so the HED survives the BIDS round trip; 0.2.0 dropped it.
HED rules#
New rule: a column named
HEDmust be aHedTags. AVectorDataorHedValueVectornamedHEDis reported asHED_COLUMN_TYPE_INVALID.New rule: the values of a
HedValueVectormust satisfy the value class of its#tag. A#with no value class istextClass, and forDef/Name/#the class comes from the placeholder in the definition. Each distinct value is checked as the#tag with the value in place, so a value that breaks HED syntax (a),a#b) is rejected.The HED specification 4.0.0 rules for tabular files apply, through hedtools: a column name repeated, padded, or blank is
TABULAR_INVALID(aTimestampVectorDatanamedtimestampcounts asonset); a padded value in a categorical orHedValueVectorcolumn isTABULAR_INVALID; a paddedMeaningsTablevalue that carries HED isSIDECAR_INVALID.{column}references inHedValueVectortemplates are checked by the sidecar stage; a per-rowHEDcolumn may not contain them.The new module
ndx_hed.utils.hed_nwb_errorsregisters the ndx-hed codesHED_COLUMN_TYPE_INVALIDandMEANINGS_VALUE_VECTOR_INVALIDwith hedtools, so their issues have the same shape as hedtools’ own.
Validation helpers#
validate_vector()andvalidate_value_vector()remain as single-column helpers. Each validates every distinct value once, so a long column drawn from a few annotations takes roughly the time of its distinct values. Each also reads the column in one slice: a 20,000-rowHedTagscolumn read from HDF5 validates in about 0.01 s instead of 0.5 s. Their issues carryec_column.validate_value_vector()validates the template with its{column}references removed (they are resolved only byvalidate_table), then the values; a template whose parentheses do not balance is reported.A
HedValueVectorwhose dtype settles its value class (for example a numeric column under anumericClasstag) is not read.Missing values (
None,"",n/a, and any NaN, including numpy float32 and float16) are skipped. An integer too large for a float is a valid value, not anOverflowError.
BIDS conversion#
get_bids_tabular()is now two public halves:get_bids_dataframe(table)andget_json_hed_dict(table, hed_metadata=None).get_json_hed_dictbuilds the sidecar from the table’s columns without reading the data or using pandas.get_levels_and_hed(meanings_table)returns aMeaningsTable’sLevelsandHEDdictionaries.Given a
HedLabMetaData,get_json_hed_dict()andget_bids_tabular()export its definitions under the sidecar’s"definitions"key (DEFINITIONS_KEY), so the sidecar resolves itsDef/references. Anything else raisesValueError.get_bids_dataframe()writes missing cells (NaN, None,"") asn/a, as a BIDS TSV does, so a missingHedValueVectorvalue is no longer aVALUE_INVALID(Delay/nan s).get_bids_dataframe(),get_bids_tabular(), andget_events_table()raiseValueErrorfor a repeated column name.get_categorical_meanings()keeps a value listed only underHED, which used to be dropped with its annotation. A value listed only underLevelsgets an empty HED cell; an explicitn/astays.New
read_bids_tsv(path)reads a BIDS TSV file the way hedtools does: every cell as text, a double quote an ordinary character, only an empty cell andn/amissing, and the header names exactly as written.pd.read_csvdefaults would rename a repeatedHEDtoHED.1and readNAandnullas missing.
HedLabMetaData#
hed_schema_versionaccepts one version string ("8.5.0"), a comma-joined string of versions in one namespace ("8.5.0,score_2.2.0"), or a list or tuple of versions (["8.5.0", "sc:score_2.2.0"]). A list is stored as the JSON array string hedtools reads and writes, so the NWB attribute stays one text value.The exported definitions string carries the schema namespace prefix on the
Definitiontag for a namespaced schema, and no longer writesNonefor a definition without contents.
Format, packaging, and documentation#
The NWB namespace version is 0.3.0. It adds
HedDurationVector; the other data types are unchanged from 0.2.0 apart from their doc strings (theHedValueVectordoc used to say, wrongly, that it is always namedHED). Files written from the developmentmainbranch before this release cache namespace version 1.0.0; they open and validate with 0.3.0, with an hdmf warning that the cached namespace is ignored.setup.py,requirements-dev.txt, andrequirements-min.txtare removed. Development installs use thetest,dev, anddocsextras inpyproject.toml, withconstraints/minimum.txtandconstraints/pinned.txt.The examples are rewritten for PyNWB 4. Each validates what it builds; they show the three HED shapes together in one events table with its assembled row HED (
06_complete_workflow.py), the NWB to BIDS round trip (04_bids_conversion.py), and the ndx-hed error codes (05_hed_validation.py).run_all_examples.pynow runs each example’smain(); before, it only imported them.The examples and documentation use HED 8.5.0 (
score_2.2.0andlang_1.3.0for library schemas).04_bids_conversion.pyalso annotates thedurationcolumn, building its HED from{event_type}and{trial_number}.New documentation page
docs/source/hed_validation.mdgives the HED rules for NWB tables and the validation modes. The release notes page includes this file instead of keeping a copy.New
spec_tests/directory runs the hed-tests validation suite throughvalidate_file(python -m pytest spec_tests; seespec_tests/README.md).
Release 0.2.0 October 18, 2025#
Major rewrite and expansion of the ndx-hed extension with three core classes and comprehensive tooling. Web pages with API docs hosted on Github.
New features#
Core classes#
HedLabMetaData: Required metadata container for storing HED schema version and optional custom definitions
Must be added to
NWBFilebefore using any HED annotationsSupports both standard and library schemas
Includes
DefinitionDictfor custom HED definitionsMethods:
get_hed_schema(),get_definition_dict(),add_definitions(),extract_definitions()
HedTags: VectorData subclass for row-specific HED annotations in DynamicTables
Must be named “HED” (enforced by constructor)
Stores one HED string per row
Works with any NWB DynamicTable (trials, units, epochs, etc.)
HedValueVector: VectorData subclass for column-wide HED templates with value placeholders
Stores numerical/categorical data with associated HED annotation template
Uses
#placeholder for values (e.g., “Duration/# s”)HED annotation applies to entire column
Validation system#
HedNWBValidator: Comprehensive validation class for HED annotations in NWB files
validate_file(): Validates entireNWBFilevalidate_dynamic_table(): Validates specific DynamicTablevalidate_hed_tags(): Validates individual HedTags columnvalidate_hed_value_vector(): Validates HedValueVector columnsSupports both in-memory and file-based validation
Full definition support: All validation methods now support external HED definitions from
HedLabMetaDataValidates definition references (e.g.,
Def/Go-stimulus) across all HED annotation types
BIDS integration#
Bidirectional BIDS-NWB conversion utilities in
utils/bids2nwb.py:extract_meanings(): Converts BIDS JSON sidecars to meanings dictionaryget_categorical_meanings(): Creates MeaningsTable from BIDS categorical columnsget_events_table(): Converts BIDS events.tsv + sidecar to NWB EventsTableget_bids_events(): Converts EventsTable back to BIDS format (DataFrame + sidecar)extract_definitions(): Extracts HED definitions from BIDS sidecars
ndx-events integration#
Full support for EventsTable, MeaningsTable, CategoricalVectorData
Three integration patterns:
Direct HED column for event-specific annotations
HedValueVector columns for shared annotations with values
Categorical columns with HED in MeaningsTable
Examples#
Seven comprehensive runnable examples demonstrating all features:
01_basic_hed_classes.py: Introduction to HedLabMetaData, HedTags, and HedValueVector02_trials_with_hed.py: Adding HED annotations to NWB trials table03_events_table_integration.py: Three patterns for EventsTable integration04_bids_conversion.py: Bidirectional BIDS-NWB conversion workflows05_hed_validation.py: Comprehensive validation examples06_complete_workflow.py: End-to-end workflow with file I/O07_hed_definitions.py: Custom HED definitions and expansion
Dependencies#
Updated to
hedtools>=0.7.1(released to PyPI)pynwb>=2.8.2hdmf>=3.14.1Optional:
ndx-events>=0.4.0for EventsTable support
Testing#
78 comprehensive test cases for HedNWBValidator
116+ total test cases across all modules
Full coverage of all core classes and utilities
File I/O roundtrip testing
Validation testing with valid and invalid HED
BIDS conversion roundtrip testing
Definition handling and persistence testing
Definition reference validation across all validator methods
Breaking changes from 0.1.0#
HedTags constructor changes: Removed
hed_versionparameter (now uses HedLabMetaData)New requirement: HedLabMetaData must be added to
NWBFilebefore using HED annotationsName enforcement: HedTags must be named “HED”, HedLabMetaData must be named “hed_schema”
Documentation#
Updated user guide for 0.2.0 architecture
Comprehensive example suite with runnable code
Updated README with Quick Start guide
Release 0.1.0 July 25, 2024#
Implements a
HedTagsclass that extends NWBVectorData.Validates tags in the constructor and as they are added.
The
HedTagsclass can be used alone or added to anyDynamicTable.The initial release only supports string HED version specifications, not tuples or lists.