Add HED validation checks - #719
Conversation
Add check_hed_annotations_valid and check_hed_lab_metadata_exists for HED annotations written with the ndx-hed extension. Each error found by the HED validator becomes its own inspector message, naming the column, the row, and the HED error code, with an error that repeats down a column collapsed into a single message. The check validates each annotated column on its own with validate_table rather than calling validate_file, which converts every DynamicTable in the file to a dataframe and would read whole Units tables into memory. ndx-hed is a new optional dependency, installed with `pip install nwbinspector[hed]`. The import is guarded so that both checks register and do nothing when the package is absent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
I opened hed-standard/ndx-hed#149 upstream, which is relevant to the trade-off described above, and it comes with a measurement that corrects part of what I wrote. I benchmarked a file with a 300-unit Units table (20,000 spikes each) and a 50,000-row HED-annotated events table that also carries a 64-wide float column:
The memory argument I gave for not using The dominant cost is neither entry point. Validation is per row and linear, about 0.48 ms per row in memory and 1.8 ms per row reading from a file, and it is completely insensitive to how many distinct annotations a column contains. A 4,000-row column of four distinct tags takes 1.9 s, while those four strings validated once each take 2 ms. Nothing in this PR needs to change today, but it does affect where this should end up. If ndx-hed both skips tables that carry no HED and stops revalidating repeated annotations, Two smaller notes. The per-row cost applies to this check as it stands, so a file with a 50,000-row annotated column will take about 90 seconds here. I can do the same deduplication on the inspector side, validating the distinct values of a column and mapping each issue back to the rows that hold that value, so that the check does not have to wait on an ndx-hed release. That also feeds the message collapsing this PR already does. Say the word and I will add it. Separately, the ndx-hed changelog shows the next release is 1.0.0 rather than 0.3.0, which is why the new extra pins @VisLab, the upstream change is small and the equivalence checks are in the description there, so that is probably the better place to push back on it. |
|
@bendichter: The performance tests were particularly interesting. I have not examined the code yet in detail, but I would like to come to an overall understanding of the strategy before I dive in. There seem to be a lot of checks missing between simple HED tag validation and assembly. Some of these checks might be included at low cost using a different implementation. I think the timeline validation (which looks for relationships among rows could be skipped completely for now as you have done), but there may be some additional checks that would be useful.
I think other structural checks are built in to the creation of the classes themselves. Could you briefly clarify how the |
The rule that a MeaningsTable must not contain a HedValueVector column lives only in HedNWBValidator.validate_file, so the per-table validation strategy did not carry it over. Implemented directly on MeaningsTable by type name, so it also applies when ndx-hed is not installed. The HED resources site moved from hed-resources.org to hedtags.org/hed-resources, which was failing the external-link CI check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every value that occurs in a categorical column should have an entry in the value column of its MeaningsTable. Implemented as a general table check since the rule comes from MeaningsTable itself, so it fires even without ndx-hed; None, "", and "n/a" mark missing data and need no entry, and unused entries in the MeaningsTable remain legal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks @VisLab — answers point by point, with the code updated where you found gaps (c4eaeec and 534df56). 1 (HedLabMetaData validates on construction): Agreed, and the check leans on that: it retrieves the 3 (data value with no mapping in the MeaningsTable): Added as 4 (HedValueVector): It is covered. 5 (short-circuiting): Within a 7 (no HedValueVector in a MeaningsTable): Good catch — that rule lives only in On 2 and 6: agreed, and the assembly/vectorization discussion probably belongs with hed-standard/ndx-hed#149 — if that lands together with skipping HED-free tables, switching this to assembled validation is worth revisiting. (The same push also fixes the link that was failing the external-link CI check: the HED resources site moved to https://www.hedtags.org/hed-resources/.) |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev #719 +/- ##
==========================================
+ Coverage 79.71% 80.95% +1.23%
==========================================
Files 48 49 +1
Lines 1923 2037 +114
==========================================
+ Hits 1533 1649 +116
+ Misses 390 388 -2
🚀 New features to boost your workflow:
|
|
This clarifies things a lot and we're making progress, but I would like to push back on the short-circuit strategy and suggest a possible modification. On the short-circuit strategy: If I am understanding the inspector strategy correctly, it continues to evaluate rows of a
I propose the alternative (much lower overhead approach): The current The bottom line is -- if any of these table-level HED annotations cause an error, there is no point in continuing HED validation at all because a) each error is likely to trigger errors on many if not all rows of the table and b) the errors it triggers are likely to be wrong anyway because validation assumes some basic semantics are being followed and have already been checked. Summary of proposed steps.
If there are any errors (not just warnings), the Inspector should not continue with validation of HED in the table. This is a very low cost -- the Note: the proposed validation includes the validation of the The hedtools validation and how it integrates into the inspector:
|
Closes #718.
This adds HED validation to the inspector using the validator that
ndx-hedprovides. There are two checks, both atBEST_PRACTICE_VIOLATION:check_hed_lab_metadata_exists(onNWBFile) fires when a file contains HED annotations but noHedLabMetaDatadeclaring the version of the HED schema they use. It finds the annotated columns by neurodata type name rather than by class, so it works even whenndx-hedis not installed, in which case PyNWB builds those classes from the specification cached in the file.check_hed_annotations_valid(onDynamicTable) runs the validator and turns each error it reports into its ownInspectorMessage, naming the column, the row, and the HED error code. An error that repeats down a column, such as the same misspelled tag on every row, is collapsed into a single message with the number of rows it affects.A report on a file with a few deliberate mistakes looks like this:
Per-Column Validation Rather Than validate_file
The natural entry point is
HedNWBValidator.validate_file(nwbfile), but it callsto_dataframe()on everyDynamicTablein the file, including tables that carry no HED at all. For the inspector that is not affordable: a file with a single HED column and a large Units table would have the whole raggedspike_timescolumn read into memory, and the inspector is often run against files streamed over a network.So the check is registered on
DynamicTable, skips tables with no HED columns, and callsvalidate_table, which touches only the annotated columns. The trade-off is that the assembled (BIDS-style) validation is not performed, so errors that only appear when the columns of a row are combined, and timeline validation ofOnset/Offsetpairing, are not caught. Every individual HED string in the file is still validated, including the categorical annotations in aMeaningsTable, which is itself aDynamicTablewith aHedTagscolumn and so is validated in its own right (and attributed to itself, which also avoids reporting the same error twice).I would be glad to be talked out of this if there is a cheaper way to get the assembled validation, for example an entry point that takes one table and does not materialize the columns it does not need.
Packaging and CI
ndx-hedis a new optional dependency, installed withpip install nwbinspector[hed]. It is pinned to>0.2.0because 0.2.0 is the last release that predates thendx-hedsupport for pynwb 4 and it importsndx-events, which does not coexist with theEventsTablein pynwb 4. The import in the check module is guarded bytry/except ImportError, so both checks always register and simply produce nothing when the package is missing, which keeps the check registry and the configuration files the same in every environment. I confirmed that path by blocking the import: the checks stay registered, report nothing, and raise no errors.Because there is not yet an
ndx-hedrelease with pynwb 4 support, I did not addhedto the extras that CI installs, and the new tests will skip there until that release lands. Locally I ran them againstndx-hedfrommainand the full suite passes. Once the release is out, addinghedto thepip install ".[dandi,zarr]"line intesting.ymlis all that is needed.Testing
tests/unit_tests/test_hed.pycovers both checks: the passing and failing cases, the file with annotations but no schema version, the row that is reported, the collapsing of repeated errors, and aHedValueVectortemplate. The module is skipped withimportorskipwhenndx-hedis absent. I also checked the whole path on a file written to disk and read back, and the docs build clean with both checks appearing in the auto-generated listing by importance.@rly @oruebel @VisLab, I would appreciate your read on two things in particular: whether
BEST_PRACTICE_VIOLATIONis the right level for an invalid HED tag (I kept it offCRITICALso that it does not gate DANDI publication on a brand new check), and whether giving up the assembled validation is an acceptable trade for the memory behavior.🤖 Generated with Claude Code