HED annotation and validation in ndx-hed#
This document describes how HED (Hierarchical Event Descriptors) annotations are represented in NWB tables, the rules ndx-hed follows, and how the validator works. It reflects the 1.0.0 design (PyNWB 4.0.0, where EventsTable, MeaningsTable, and related types are part of NWB core).
Representing HED in a table#
There are three ways a column contributes HED to a table, mirroring the two BIDS sidecar forms:
Per-row HED: a
HedTagscolumn stores one HED string per row.Value HED: a
HedValueVectorcolumn stores one HED template containing a single#placeholder; the placeholder is replaced by each row’s value to produce that row’s annotation.Categorical HED: a plain column of category labels is annotated by a
MeaningsTable, which maps each distinct value to a meaning and (optionally) a HED string held in aHedTagscolumn namedHEDinside the MeaningsTable.
MeaningsTable is a core hdmf type. ndx-hed does not define it; ndx-hed carries categorical HED by adding a HedTags column to it.
HED column rules#
The following five rules govern how HED columns may appear in an NWB table.
Rule R1: A
HedTagscolumn must be namedHED.Rule R2: A
DynamicTablehas at most oneHedTagscolumn.Rule R3: A
HedTagscolumn inside aMeaningsTableprovides categorical (per-value) HED for the column that the MeaningsTable annotates.Rule R4: A
HedTagscolumn in any otherDynamicTableprovides per-row HED for that table.Rule R5: A
HedValueVectormust not appear in aMeaningsTable.
How each rule is enforced#
Rule |
How it is enforced |
|---|---|
R1 |
|
R2 |
Follows from R1: column names within a table are unique, so two |
R3 |
The MeaningsTable’s |
R4 |
The table’s |
R5 |
|
Notes:
HedValueVectormay have any column name and is identified by its neurodata type, not by name.The
HedValueVectorschema documentation previously stated “Always has the name HED”; that was incorrect and has been corrected.
How validation works#
HED is not validated only tag-by-tag. Its meaning depends on the other HED in the same row, and for time-anchored tables on the other rows. There are two validation modes.
Two modes: timeline and non-timeline#
Non-timeline: rows are not anchored to time. Each row’s complete HED annotation is assembled from all of that row’s columns and validated independently of the other rows.
Timeline: rows are anchored to time (there is an
onsetcolumn). In addition to per-row assembly, HED’s temporal constructs (for example onset, offset, duration, and delay scopes that span rows) are validated over the events in time order.
The mode is determined automatically. A time-anchored table (for example an EventsTable, whose timestamp column is exported as onset) is validated as a timeline; a table with no onset column is validated as non-timeline.
Assembled validation#
HedNWBValidator.validate_file validates every DynamicTable except MeaningsTable using assembled validation, in two steps:
The table is converted to a BIDS-style dataframe and JSON sidecar with
get_bids_tabular.The sidecar (column metadata: value templates and categorical Levels/HED) is validated first with
Sidecar.validate, then the assembled table is validated withTabularInput.validate.TabularInput.validatecombines each row’s per-row HED, categorical HED, and value-template HED into a single annotation before validating, and applies temporal validation when anonsetcolumn is present.
Both steps are required. TabularInput.validate does NOT re-run the sidecar’s brace-structure and column-reference checks (self, nested, invalid, or malformed {column} references); those are only performed by Sidecar.validate. If the sidecar were validated only through the assembled table, those structural problems would be missed and would instead surface as misleading downstream errors (for example a stray { reported as CHARACTER_INVALID). Therefore the sidecar is validated explicitly first; if it has any error, the sidecar issues are returned and the assembled-table step is skipped. Stopping is deliberate: a sidecar-level error (a bad value template or a bad categorical HED string) would be repeated on every row that uses it, and the row annotations assembled from it cannot be trusted, so continuing would only add noise. Sidecar issues carry the table name (ec_table_name), the column (ec_sidecarColumnName), and for categorical HED the value (ec_sidecarKeyName); they have no row, because a sidecar error is not a row error. Warnings alone do not stop validation.
A MeaningsTable is not validated on its own. Its categorical HED is validated as part of the table whose column it annotates. A MeaningsTable is only checked against rule R5.
Per-column helpers#
validate_table, validate_vector, and validate_value_vector remain available for validating a single table or a single column in isolation (per-column checks). These do not perform row assembly or temporal validation; use validate_file (or validate_events for a single EventsTable) for full assembled validation.
Every issue returned by validate_table and validate_file carries the table name in ec_table_name. The rest of the location depends on the kind of issue:
A row issue (an error in a
HedTagscell, or in aHedValueVectorvalue after substitution) carries the column name inec_columnand the row inec_row.validate_tablereports the 0-based row index; the assembled path invalidate_filereports the 1-based row of the BIDS-style table, counting the header.A sidecar issue (an error in a
HedValueVectortemplate or in a MeaningsTable HED string) carries the column inec_sidecarColumnNameand, for categorical HED, the value inec_sidecarKeyName. It has no row. Invalidate_tablea template error likewise has the column inec_columnand no row.
get_printable_issue_string renders these as “Errors in table ‘
Public API summary#
Function |
Direction / purpose |
|---|---|
|
BIDS sidecar to a meanings dictionary. |
|
BIDS dataframe plus meanings to an |
|
Any |
|
Any |
|
A |
|
Assembled validation of every table in a file. |
|
Assembled validation of a single |
|
Per-column validation of a table or column. |
Definitions in the exported sidecar#
In NWB, HED definitions live in the HedLabMetaData object, which is the only place extra definitions may come from. BIDS instead carries them in the sidecar, in an entry that names no column. get_json_hed_dict (and get_bids_tabular) therefore take an optional HedLabMetaData; when one is supplied, its definitions are exported as
{"definitions": {"HED": {"defList": "(Definition/go,(Sensory-event))"}}}
so the sidecar is self-contained and any Def/ references in the table resolve from it alone. Without the metadata no such entry is written, and Def/ references in the exported sidecar are unresolvable (DEF_INVALID) unless the definitions are supplied separately - which is what HedNWBValidator does internally, passing the DefinitionDict from its own HedLabMetaData as hedtools’ extra_def_dicts.
Relationship to BIDS#
ndx-hed can represent everything BIDS can, plus arrangements BIDS does not (for example a table with no onset). Construction from BIDS builds an EventsTable (get_events_table); serialization to a BIDS-style tabular form works on any table (get_bids_tabular).