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 rules govern how HED 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.Rule R6: A column named
HED, in aDynamicTableor in aMeaningsTable, must be aHedTags. The name is what marks the column as HED (R3, R4), so aVectorDataor aHedValueVectornamedHEDis an error.Rule R7: The values of a
HedValueVectormust satisfy the value class of the#tag of its template. A#with no value class istextClass. When the template isDef/Name/#, the class is that of the placeholder tag inside the definition.Rule R8: A
MeaningsTableHED string or aHedValueVectortemplate may reference another HED-annotated column of the same table, orHEDfor the table’s per-row column, with the curly-brace notation of the HED specification (section 3.2.9.3); all rules of that section apply. A per-rowHEDcell may not contain braces.
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 |
|
R6 |
|
R7 |
|
R8 |
hedtools |
Notes:
HedValueVectormay have any column name and is identified by its neurodata type, not by name.MEANINGS_VALUE_VECTOR_INVALIDandHED_COLUMN_TYPE_INVALIDare ndx-hed codes, registered with hedtools so the issues have the same shape as hedtools’ own (code,message,severity, and theec_*context keys). R7 errors are hedtools codes:VALUE_INVALID,CHARACTER_INVALID, or a units code from hedtools’validate_unitson the one tag, or a syntax code such asPARENTHESES_MISMATCHorPLACEHOLDER_INVALIDfrom the basic checks on the substituted tag.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. HedNWBValidator.validate_table is the table validator; HedNWBValidator.validate_file finds the file’s HedLabMetaData and calls validate_table for every DynamicTable in the file except a MeaningsTable, whose HED is validated with the table it annotates. Passing a MeaningsTable to validate_table raises ValueError.
The steps of validate_table¶
“Stops” means the issues found so far for that table are returned; the other tables of a file are still validated.
Structural rules R5 and R6 on the table and its MeaningsTables. An issue stops.
A table with no
HedTagscolumn, noHedValueVectorcolumn, and no MeaningsTable with aHEDcolumn has nothing to validate and returns no issues without being read. A file’s large HED-free tables (a Units table) cost nothing.The table’s column metadata becomes a BIDS sidecar dict with
get_json_hed_dict: theHedValueVectortemplates, the categoricalLevelsandHEDof each MeaningsTable, and the definitions of theHedLabMetaDataunder thedefinitionskey. No table data is read.hedtools’ staged tabular validation (
SpreadsheetValidator.validate) runs once on the table, through a column source (hedtools’ColumnSourceprotocol) that answers with the column names and with the distinct values of one column at a time, so a column is read only when a stage needs it and never cell by cell. ATimestampVectorDatacolumn is namedonset, asget_bids_dataframenames it. Each stage stops on an error; warnings do not stop.The sidecar, with hedtools’ sidecar validator. Only this stage performs the brace-structure and column-reference checks (self, nested, invalid, or malformed
{column}references) and validates the HED of every categorical level, used or not. A sidecar-level error would be repeated on every row that uses it, and the row annotations assembled from it cannot be trusted.The column values. Rule R7 on every
HedValueVectorcolumn: each distinct value is checked as the template’s#tag with the value in place, then the substituted tag is parsed for the basic syntax checks. A column whose dtype settles the question is not read: a numeric column passestextClass, an integer column passesnumericClassandnameClass, and a float column undernumericClassis scanned once for infinities, which are not numeric values. Each categorical column’s distinct values are checked against the levels of its MeaningsTable (SIDECAR_KEY_MISSING, a warning).The
HEDcolumn: each distinct string once, with the basic (single-string) checks.The assembly gate:
assemble=True(the default): the whole table is read into a BIDS dataframe withget_bids_dataframe(aTimestampVectorDatacolumn becomesonset; missing cells becomen/a), only now, and hedtools assembles each row’s HED from itsHEDcell, its categorical HED, and its value templates and validates the row as a whole. If the table has anonsetcolumn it is a timeline and HED’s temporal constructs (onset, offset, duration, delay scopes that span rows) are validated over the rows in time order; otherwise each row is validated on its own.assemble=False: the table is never converted to a dataframe. Instead the group-level checks run on each distinctHEDstring by itself (anOnsetwithout itsDef, a tag repeated within the cell, an empty group); the required-tag check, which needs every tag of the row, waits for assembly. Nothing that needs the other tags of a row or the other rows is checked: a tag repeated between theHEDcolumn and a categorical annotation, or anOffsetwithout itsOnset, is reported only with assembly.
Where an issue points¶
Every issue carries the table name in ec_table_name; through validate_file it also carries the file identifier in ec_filename.
A sidecar issue (a
HedValueVectortemplate, a MeaningsTable HED string, a definition) carries the column inec_sidecarColumnNameand, for categorical HED, the value inec_sidecarKeyName. It has no row, because a sidecar error is not a row error.A row issue carries the column in
ec_columnand the row inec_row, which is the table row index in both modes: the first data row is 0. (hedtools itself numbers the lines of a BIDS TSV file, header first;validate_tableconverts.)The values of a
HedValueVectorand the strings of theHEDcolumn are validated by distinct value in both modes: each distinct annotation is validated once and reported at the first row holding it, with the number of rows that hold it inrow_count(hedtools’ROW_COUNT_KEY, re-exported byndx_hed.utils.hed_nwb_validator). The same misspelled tag on ten thousand rows is one issue. Both checks are hedtools’ column stages, run before the assembly gate in both modes. Issues from the assembled rows (group-level and temporal checks) are per row.
get_printable_issue_string renders these as “Errors in table ‘
Using the table validator from nwbinspector¶
An inspector check receives one object. For a DynamicTable it finds the schema through the file and validates the table without assembly:
nwbfile = table.get_ancestor(data_type="NWBFile")
hed_metadata = nwbfile.lab_meta_data.get("hed_schema") if nwbfile is not None else None
if isinstance(hed_metadata, HedLabMetaData):
issues = HedNWBValidator(hed_metadata).validate_table(table, assemble=False)
Constructing a validator per table costs two attribute reads; the schema was loaded when the HedLabMetaData was built.
Single-column helpers¶
validate_vector (a HedTags column) validates one column in isolation: each distinct string once, with the basic and the group-level checks (hedtools validate_hed_column with full_string=True), reported at the first row holding it with row_count. It is for a per-row HED column, which may not contain braces, so a MeaningsTable HED column belongs to validate_table of the table it annotates. validate_value_vector (a HedValueVector column) validates the template on its own with any {column} references removed, then checks each distinct value as the # tag with the value in place through hedtools validate_value_column, exactly as rule R7 in validate_table, reporting the first row and row_count; a column whose dtype settles its value class is not read. Both carry the column in ec_column. Neither helper sees another column or another row, and neither checks a reference; that is validate_table’s sidecar stage.
Public API summary¶
Function |
Direction / purpose |
|---|---|
|
BIDS sidecar to a meanings dictionary. |
|
BIDS dataframe plus meanings to an |
|
Any |
|
Any |
|
Any |
|
A |
|
Metadata check, then |
|
Structural rules, sidecar, value classes, then assembled (row and temporal) validation, or with |
|
One column in isolation. |
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). HedNWBValidator always passes its own HedLabMetaData, so hedtools sees the definitions exactly as a BIDS validator would; for a namespaced schema (ts:8.5.0) the exported Definition tag carries the namespace prefix.
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).