feat: add NCATS GARD rare-disease registry as a biolink:Disease source - #980
feat: add NCATS GARD rare-disease registry as a biolink:Disease source#980SkyeAv wants to merge 11 commits into
Conversation
Update: no-
|
…on-rare-disease signal
Reverted: the
|
|
@gaurav this branch successfully rebuilt the Disease Compendia at |
There was a problem hiding this comment.
Pull request overview
Adds GARD as a label-and-synonym-only disease source in Babel’s disease pipeline.
Changes:
- Adds GARD download, parsing, configuration, and pipeline wiring.
- Preserves GARD identifiers through the Biolink prefix escape hatch.
- Adds tests, documentation, and a source-impact report.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
config.yaml |
Configures the GARD download and disease inputs. |
pyproject.toml |
Excludes Markdown from Ruff formatting. |
src/prefixes.py |
Defines the GARD prefix. |
src/datahandlers/gard.py |
Downloads and parses GARD data. |
src/createcompendia/diseasephenotype.py |
Allows GARD during compendium writing. |
src/snakefiles/datacollect.snakefile |
Adds GARD collection rules. |
src/snakefiles/diseasephenotype.snakefile |
Generates typed GARD identifiers. |
tests/data/gard_sample.csv |
Provides representative GARD records. |
tests/datahandlers/test_gard.py |
Tests parsing and prefix handling. |
docs/sources/README.md |
Adds GARD to the source index. |
docs/sources/GARD/README.md |
Documents GARD ingestion. |
docs/sources/GARD/impact-report.md |
Summarizes source impact. |
docs/sources/GARD/impact-report/new-cliques.csv |
Lists newly introduced cliques. |
docs/sources/GARD/impact-report/modified-cliques.csv |
Records modified-clique results. |
docs/sources/GARD/impact-report/new-xrefs.tsv |
Records cross-reference results. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The GARD registry publishes zero-padded seven-digit local ids (GARD:0006038 "Chikungunya fever"), but DOID -- the one other disease source that cross-references GARD -- emits the unpadded form for 2,164 of its 2,187 distinct GARD xrefs (GARD:6038, from DOID:0050012 "chikungunya"), with the remaining 23 padded. Ingesting the registry as published would therefore give 1,886 rare diseases two identifiers in two separate cliques: the registry's padded CURIE as a singleton, and DOID's unpadded one inside the DOID/MONDO clique for the same disease. Standardize on the unpadded form. normalize_gard_curie() strips leading zeros and is applied both when parsing the registry CSV (so labels, synonyms and the ids file agree) and to DOID's xref targets in doid.build_xrefs() (so its 23 padded outliers meet the rest). 1,886 registry terms now join the existing clique DOID already xrefed them into, with a label. This also corrects the extra_prefixes=[GARD] comment: GARD contributes no concord of its own, but DOID's concord already carries 2,186 GARD ids that the Biolink prefix filter is currently the only thing keeping out of Disease.txt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two ways the GARD ingest could quietly drop all ~16k rare diseases from a build that still exits green: * The Salesforce ContentVersion link expires or is repointed and serves an HTML error page with HTTP 200 (urllib only raises on non-2xx), or NCATS renames the ID/DisplayName header. Every row is then skipped, the labels file is written empty, disease_gard_ids' awk produces an empty ids file, and nothing fails. The parse summary was the stated safety net, but a logger.info in a multi-hour build is not a control (AGENTS.md). pull_gard now rejects a response whose Content-Type is not a CSV, and the parser raises on a missing ID/DisplayName header or a parse that yields no terms at all. * gard_download_url was read from config inside the rule's run: block with nothing in params:, so Snakemake's rerun triggers never fired and repointing the URL reused the stale gard.csv forever -- exactly what config.yaml's comment tells you to do when NCATS publishes a new version. Declare it as params and pass it, with the output path, into pull_gard(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README and the committed impact report both said GARD's addition was "purely additive" -- "no other source xrefs GARD, so it cannot bridge or disturb any existing clique", 16,214 new cliques and 0 modified. DOID does xref GARD (2,187 ids sitting in intermediate/disease/concords/DOID), so that was never true; the synthetic diff could not see it because those rows are in both its before and after states. Regenerate against a complete local disease intermediate set, with the DOID concord rebuilt from the current release so it carries the unpadded ids: 14,328 new cliques, 1,886 GARD identifiers landing in 1,644 existing cliques (0 merges, no clique structurally grown), 0 concord rows contributed. Also drops the claim that the URL column is redundant because "the CURIE itself resolves via the Biolink prefix map" -- GARD is absent from the prefix map for the pinned biolink_version, as it is from disease's id_prefixes. The real reason it is not ingested is that Babel has no per-identifier URL attribute file. Documents the unpadded local-id decision and the 300 retired GARD ids DOID still asserts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
build_compendium loops over every typed set, so extra_prefixes=[GARD] was handed to the PhenotypicFeature.txt write too. extra_prefixes is a per-Biolink -class allowlist; granting it for a class GARD has no business in means a future HP/MP mapping onto a GARD CURIE would bypass PhenotypicFeature's own prefix filter instead of being dropped by it. No GARD member is typed PhenotypicFeature in the current build, so this changes no output today -- it keeps the exemption from outliving the reason for it. Raised by Copilot review on PR #980. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two gaps in the ingest guards, both raised by Copilot review on PR #980: * The header check covered ID and DisplayName only, so renaming Synonyms would silently drop every alternative synonym while the rule still succeeded. Require all three. * The module docstring justified emitting raw values into the tab-separated labels/synonyms files by citing a scan of one distribution that found no embedded tabs or newlines. That scan describes the file we happened to download, not the next one, and nothing re-checks it -- the docs/sources convention is to commit a check, not a conclusion. _reject_tsv_control_chars enforces the property at write time instead, so a value that would split one TSV record into two malformed ones fails the rule rather than corrupting the output. It fires on none of the 16,214 rows published today; the labels file is byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test_disease_node_factory_tolerates_extra_prefixes_gard passed an empty identifier list, so create_node() returned None on the len(input_identifiers) == 0 check before prefix handling ran at all -- it would have passed with the escape hatch removed. Feed it the real GARD:6038 "Chikungunya fever" CURIE instead and assert both directions: it survives with extra_prefixes=[GARD], and create_node() returns None without it. The label is passed explicitly because NodeFactory(label_dir=None) has none to read. Also switch the id_prefixes assertion to the mapped Biolink class URI (get_element(DISEASE), not the raw element name "disease"), per AGENTS.md. Raised by Copilot review on PR #980. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reran `uv run source-impact-report --source GARD`. Every detail file is byte-identical to the previous run -- 14,328 new cliques, 1,886 GARD identifiers across 1,644 existing cliques, 0 merges -- so only the Generated timestamp and Babel commit move, from b191bab (pre-review) to 4805fcf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds GARD — the NCATS Genetic and Rare Diseases rare-disease registry — as a
biolink:Diseasesource in the existingdisease(diseasephenotype) pipeline. GARD is a flat CSV of rare-disease terms (CURIE, preferred label, pipe-separated synonyms); it carries no cross-references of its own, so it contributes identifiers and labels/synonyms only — no concord — and every term is typedbiolink:Disease.Ingest
src/datahandlers/gard.pydownloads the Salesforce ContentVersion CSV (pull_gard) and parses it to per-prefixlabels/synonymsfiles (pull_gard_labels_and_synonyms). TheDisplayNameis emitted as a label and as an exact synonym; each pipe-separatedSynonymsvalue becomes its ownOIO:hasExactSynonymrow (matching the Orphanet/DOID convention). TheURLcolumn is not ingested: Babel handlers emit only labels/synonyms/taxa/descriptions and there is no per-identifier URL attribute file for it to go in.get_gard/get_gard_labels_and_synonymsrules insrc/snakefiles/datacollect.snakefile;disease_gard_ids(anawktransform of the labels file, mirroring DOID/Orphanet) insrc/snakefiles/diseasephenotype.snakefile. The download URL is configured asgard_download_urlinconfig.yaml(a query-string Salesforce link with no stable filename, fetched directly rather than viapull_via_urllib) and passed to the rule as aparamsvalue, so repointing it retriggers the download instead of reusing a stale CSV.GARDadded todisease_idsanddisease_labelsandsynonyms;GARDjoins the existingDisease.txt(no new compendium file), so theutil.pyaggregators andreport_tables.pyneed no change.Design (load-bearing)
GARD:0006038"Chikungunya fever"), but DOID — which does cross-reference GARD — emits the unpadded form for 2,164 of its 2,187 distinct GARD xrefs (GARD:6038, fromDOID:0050012"chikungunya"), the other 23 padded. Babel standardizes on the unpadded form:normalize_gard_curie()strips leading zeros when parsing the registry CSV and indoid.build_xrefs(), so DOID's padded outliers meet the rest. Without this, 1,886 rare diseases would carry two identifiers in two separate cliques (the registry singleton and DOID's) and no DOID GARD xref would ever pick up a registry label.extra_prefixes=[GARD]: GARD is registered neither in the Biolink Model'sdiseaseid_prefixesnor in its prefix map (verified against the pinnedbiolink_version), sowrite_compendiumwould silently drop every GARD CURIE — both the ~16k registry terms and the 2,186 that reach disease cliques through DOID's concord. The disease build passesextra_prefixes=[GARD]at thewrite_compendiumcall site inbuild_compendium(the documented escape hatch). Two network-marked tests intests/datahandlers/test_gard.pylock this in: one assertsGARDis absent fromdiseaseid_prefixes(and flips to prompt dropping the line once registered), the other assertsNodeFactory.create_node(..., extra_prefixes=[GARD])forbiolink:Diseasedoes not raise (mirroringtest_all_override_target_types_are_writable). The impact report's section-4 samples are flagged "NOT emitted — prefix not registered", which is exactly this precondition made visible.pull_gardrejects a response whoseContent-Typeis not a CSV (an expired ContentVersion link serves an HTML error page with HTTP 200, whichurllibdoes not raise on), and the parser raises if theID/DisplayNameheaders are missing or if no term parses at all. A scan of the published CSV found noDisplayName/Synonymsvalue with an embedded tab or newline and no row with an emptyDisplayName.Impact report
Regenerated and committed at
docs/sources/GARD/impact-report.md(+impact-report/detail CSVs), in synthetic mode against a complete localdiseaseintermediate set (all 10disease_idsfiles and all 8disease_concords), with GARD's own intermediates and the DOID concord rebuilt from the current DOID release so the report reflects the unpadded ids. Results:GARD:/biolink:Disease.Docs
docs/sources/GARD/README.md(new) + an entry in thedocs/sources/index + the committedimpact-report.md.[tool.ruff.format] exclude = ["*.md"]topyproject.toml:origin/mainlacks this CI fix and ruff 0.16.0 began formatting Python code fences inside Markdown (overlappingrumdl), which would fail this PR's "Check Python formatting with ruff" job. It is a prerequisite for this PR's CI and is documented inline.Testing
uv run pytest tests/datahandlers/test_gard.py tests/datahandlers/test_doid.py --network --no-cov -q→12 passeduv run pytest -m unit --no-cov -q→412 passed, 107 deselecteduv run ruff check/ruff format --check/snakefmt --check --compact-diff ./rumdl check .→ all cleanuv run snakemake -c 1 -n get_gard get_gard_labels_and_synonyms disease_gard_ids→ 3 jobs resolve in the correct chainuv run source-impact-report --source GARD→ report written (14,328 new cliques, 1,644 modified, 0 merges)TODO before / after merge
GARDin the Biolink Model. File an issue at https://github.com/biolink/biolink-model/issues and follow it with a PR addingGARDto the valid id prefixes forDisease(https://biolink.github.io/biolink-model/Disease/#valid-id-prefixes), and to the Biolink prefix map so GARD CURIEs resolve. Once it lands andbiolink_versionis bumped: dropextra_prefixes=[GARD]insrc/createcompendia/diseasephenotype.py, inverttest_gard_not_in_biolink_disease_id_prefixes, and updatedocs/sources/GARD/README.md. This is the same escape hatch GTDB is on (PR feat: add GTDB as a biolink:OrganismTaxon source in the taxon pipeline #978).Questions for the reviewer
extra_prefixesvs. Biolink-first. This PR ships theextra_prefixes=[GARD]escape hatch so the ~16k rare-disease terms actually surface inDisease.txtnow (rather than being silently dropped), with Biolink registration tracked as the TODO above — is that the preferred path, or should GARD wait on upstream Biolink registration before being kept?Disease.txtlabel-less alongside their DOID clique. Keeping them means data still citing a retired GARD id normalizes correctly; the alternative is filtering DOID's GARD xrefs against the ingested registry. This PR keeps them — say the word if you'd rather filter.