Amalgam is a biomedical metaontology: a cross-resource integration layer of entity identifiers, names, and typed cross-references organized by Biolink class. It integrates source ontologies rather than replacing their semantics; typed associations are not equivalence assertions.
The Spark/Scala pipeline turns BABEL outputs (plus your own ontologies
and hand-curated manual entries) into a Hugging Face dataset with cliques and crossrefs subsets, with one
hard invariant:
Within a given class x taxon scope, a synonym belongs to exactly one CURIE.
The dataset ships original strings with junk filtered out. Normalization (stemming, level keys) is deliberately NOT done here — downstream consumers (e.g. Tablassert’s fullmap DB) own the real NLP. This codebase contains no stemmer.
Source code: SkyeAv/Amalgam (public GitHub repository).
The Scala package and Mill module remain biolinkamalgam; existing build commands are unchanged.
Latest published build: SkyeAv/amalgam (public HF
dataset; snapshot 2026jul22, 138,885,412 cliques). Publish a fresh run with make publish.
cliques/<class>.parquet, one row per surviving clique leader, one file/split per Biolink class.
Split names use snake_case without biolink: (Gene becomes gene, SmallMolecule becomes small_molecule).
All public column headers are uppercase:
| Column | Type | Meaning |
|---|---|---|
|
string |
clique leader identifier |
|
string |
the clique’s preferred name, as written |
|
string |
Biolink category, e.g. |
|
array<string> |
synonyms as originally written, sorted |
|
array<string> |
the clique’s NCBITaxon CURIEs, sorted; empty when taxonless |
Taxa and the top-taxa cutoff: config/taxa.yaml taxon_allowlist holds the same top-100 experimental taxa as
Tablassert’s fullmap. A non-biolink:OrganismTaxon clique that carries taxa ships only if at least one is in the
list. Taxonless cliques ship (fullmap parity), but first config/curie_taxa.yaml gives taxonless cliques the
taxon of single-species identifier namespaces (MGI, RGD, ZFIN, FB, WormBase, SGD, HGNC and the Ensembl stable-id
species prefixes in config/curie_taxa.yaml), so e.g. pig ENSSSC genes no longer bypass the cutoff. Inferred
taxa also move such cliques out of the taxonless merge/arbitration scope into their species scope, so e.g.
human Ensembl-only genes arbitrate against BABEL-tagged human cliques (the namespace veto still guards merges).
validate checks the cutoff on the shipped parquet.
Beside the data: run_manifest.json (local stage metrics), LICENSE.md (upstream data terms), and README.md — a generated Hugging Face dataset card with
frontmatter, schema docs, filter summary, and run stats. Pass --audit to also emit conflicts.tsv
(arbitration audit) and merges.tsv (clique merge audit); they are impractical at BABEL scale, so they
default off.
CURIE-like or missing preferred names use the first sorted, gate-admitted non-CURIE synonym when available. Existing human-readable preferred names are preserved. This runs before merging and arbitration, after the ingest cache.
make setup # resolve deps, compile
make test # offline test suite (fixtures only, no network)
make lint # scalafix --check + scalafmt --check
make fix # apply scalafix rewrites + reformat
make check # lint + test (the quality gate)Heavy runs (full BABEL snapshot) belong on a big box; the wenceslaus profile in config/profiles.yaml is sized
for 80 cores / 187GB.
# Download a whole BABEL snapshot at once (bulk aria2c, resumable), then build:
./mill biolinkamalgam.run run --download 2026jul22 --profile wenceslaus \
--out out/run1 --snapshot-dir /data/snapshots/2026jul22
# Against an already-downloaded snapshot:
./mill biolinkamalgam.run run --babel /path/to/snapshot --out out/run1
# Fast iteration: cap raw rows read per source (smoke-test config changes in seconds):
./mill biolinkamalgam.run run --babel /path/to/snapshot --sample 200000 --out out/sample1
# Benchmark the download engines against one URL (evidence for download.yaml):
./mill biolinkamalgam.run benchDownload --url <big-file-url> --out /tmp/dlbenchThe downloader is pluggable in config/download.yaml (engine: aria2 | jdk-chunked | jdk), one bulk invocation for
the whole snapshot, resume/skip via HEAD size checks. Split compendium parts (Gene.txt.00…) are preferred over
their whole-file twins automatically (never both — no double ingest).
| File | Decides |
|---|---|
|
major-taxon bucketing + the top-100 |
|
category drops ( |
|
identifier promotion: quoted |
|
download engine + tuning, snapshot base URL, file exclusions (Publication parts skipped) |
|
named Spark profiles (cores/memory/master/shuffle), cluster targeting as pure YAML |
Drop files into a directory and pass --custom <dir>:
-
.tsv—curie\tlabel\tsynonyms\tcategory\ttaxa(pipe-separated lists) -
.jsonl— rows matching the custom schema (curie,label,synonyms,category,taxa) -
.ndjson— manual entries (BABEL PR #887 shape):curie,type,preferred,alternatives, optionalequivalents(multi-CURIE cliques;curiestays the leader), optionaltaxa. Priority 0: manual entries override — they win preferred names and arbitration tiebreaks against every other source.
{"curie": "EUPATH:0009259", "type": "biolink:ClinicalFinding", "preferred": "Shannon-indexed alpha diversity data", "alternatives": ["Shannon alpha diversity", "Alpha diversity"]}For anything more complex, subclass SynonymSource and add one line to Registry.forConfig.
--clinvar <variant_summary.txt.gz> adds NCBI ClinVar variations and the rsIDs
referenced by those variations. It does not claim exhaustive dbSNP coverage.
Each CLINVAR: variation and each DBSNP:rs… locus stays a separate
biolink:SequenceVariant clique: one rsID can reference several alleles. Gene
and disease annotations never become variant synonyms.
scripts/fetch-obo.sh <dir> downloads the reviewed config/obo_sources.yaml
list. Pass --obo <dir> [--obo-config <yaml>] to ingest it: MP, ZP, FYPO,
WBPhenotype, APO, TO, MAXO, HsapDv, MmusDv, ZFS, FBdv, WBls, MOD, TAXRANK,
and FAO. The list records the Biolink class, taxa, PURL and OBO Foundry license.
Only active own-prefix terms, names, EXACT synonyms and alt_ids enter cliques;
BROAD/NARROW/RELATED synonyms and imported foreign-prefix terms are excluded.
Malformed upstream alt_ids are logged and skipped (for example APO’s alt_id: curators),
not inserted as names or identifiers. Valid terms remain available.
The config comments document gaps, including mixed-category ontologies and
resources without a current OBO export. This is not complete Biolink coverage.
When either source is enabled, typed edges are written beside the main parquet,
one file per source: crossrefs/<source>.parquet, columns
SUBJECT, PREDICATE, OBJECT. Source is encoded in the split name, not a column:
clinvar, obo_mp, etc. Predicates remain typed associations, not equivalence:
-
dbsnp_reference: ClinVar variation to dbSNP locus (many-to-many). -
gene_association: ClinVar variation to NCBIGene. -
xref: OBO cross-reference (relevance only, not equivalence). -
replaced_by: obsolete OBO term to its successor.
Edges retain source CURIEs, not post-merge leaders; an obsolete term may appear
only in crossrefs/. No crossref is used as a synonym or merge key in the main
parquet. There are exactly two HF subsets: cliques (default) and crossrefs.
If no source contributes edges, crossrefs has a schema-bearing zero-row empty split.
Use streaming=True for that split; datasets rejects zero-row splits in materialized mode:
load_dataset("SkyeAv/amalgam", "cliques", split="gene")
load_dataset("SkyeAv/amalgam", "crossrefs", split="clinvar")
publish-hf.sh validates the card and uploads only its exact declared files, README.md and LICENSE.md.
The same commit removes obsolete clique/crossref parquet files and the old single-file layout.
Audit files, local metrics and interrupted-write debris stay local. Existing runs must be regenerated.
Run upload-contract tests offline with uv run --with pyyaml python -m unittest discover -s scripts -p 'test_publish_hf.py'.
Amalgam code and original compilation rights (selection and arrangement, to the extent protectable)
are licensed under PolyForm Noncommercial 1.0.0.
See LICENSE for the unmodified license and LICENSING.md for scope.
Upstream data is not relicensed; independently available upstream rights remain unchanged.
The generated LICENSE.md preserves upstream terms, including attribution/share-alike and restricted-use resources.
Previously distributed Apache-2.0 versions retain their existing grants.
A license notice grants no redistribution rights; confirm authorization before public publication.
Per-synonym provenance is not exported, so dropping restricted-prefix leaders alone cannot establish compliance.
make run ARGS="crossrefs --out <run> --top 200 --profile wenceslaus" writes
crossref-report/summary.tsv (edge and distinct-node counts by predicate, source
and namespace pair) and degree.tsv (bounded inbound/outbound high-degree nodes
per predicate). Degree counts distinct neighbors, not independent evidence or
error probabilities. Predicates never mix; no components are merged. This first
report does not compute statistical confidence, source independence or precision.
The existing BABEL/custom/GTDB ingest cache stays reusable. Additional sources are loaded as a small fresh layer each run. A prefix scan rejects overlapping core leaders rather than silently creating duplicate CURIEs. Use a new output directory for each run; sidecars from old runs are not a source of truth.
Merge computation materializes narrow contested rows, mergeable rows and the final
loser-to-winner mapping as parquet before reusing them. Wide name payloads are
fetched only after the mapping is settled. The merged table is also materialized
before arbitration. Temporary stages live beside the output and are removed on
normal completion or a caught failure; a killed JVM can leave .amalgam-stages-*
directories that require manual cleanup after confirming the run has stopped.
This bounds recomputation, not total input size. Provision scratch for shuffles
and materialized data and measure representative samples before full builds.
make run ARGS="benchMerge --input <canonical-clique.parquet> --out <new-dir> --master local[8]"
exercises the same merge boundaries. It prints input/output/absorbed counts and
elapsed milliseconds; the output retains stages for inspection. Sample whole
preferred-name groups, not isolated rows, to preserve candidate density.
Equal labels do not prove identity. biolink:SequenceVariant cliques never merge
by preferred name. Other preferred-name groups stay separate when members use
the same identifier prefix but share no identifier under it (for example, two
PubChem CIDs with the label "Cobalt;erbium"). An ambiguous group does not choose
an arbitrary target for a third clique. Distinct CURIEs still arbitrate shared
names within class and taxon scope, and rows with empty synonyms survive.
make run ARGS="scrub --out <finished-run> --profile wenceslaus" writes
<finished-run>/scrub-report/ counts and evidence samples. It never modifies or
writes a replacement dataset. A diagnostic row is a signal, not a proven error;
valid abbreviations, translations, chemical names and cross-taxon names can look
like outliers.
config/synonym_scrub.yaml controls the bounds. Cohesion scores only cliques
between min_clique_members and max_clique_members names. Larger cliques are
skipped, not certified clean. Preferred names and CURIE-shaped aliases (including
INCHIKEY: and GTDB:) are protected from all signals. The cross signal checks
only cohesion outliers against exact stripped preferred-name keys, skipping keys
with more than cross_key_max_targets targets. It does not search fuzzy near
matches. This intentionally trades recall for bounded work; the previous global
MinHash join exhausted scratch space on the full snapshot.
Hub counts are same-class recurrence among non-preferred, non-identifier aliases, not taxon-scoped evidence of an error. Signal counts overlap and are not unique synonym counts. No flags are applied automatically.
-
No conflation, ever:
synonyms-conflated/and*Conflated*files are never read. -
Taxon bucketing: only
major_taxaget their own scope; the long tail sharesOTHER_TAXA, taxonless cliques shareNO_TAXON. The same name MAY recur across categories or taxa. -
Winner order (merge and arbitration): preferred-name match, most synonyms, identifier count, source priority (lower wins; manual/custom 0, BABEL 10), smallest CURIE. Total order — reruns reproduce.
-
Tests are offline by construction:
FixtureSnapshotProviderthrows on non-local paths.
The Ralph bulk-source implementation is in progress. The current published/run coverage remains unchanged until full-source ingestion and validation finish.
config/dbsnp_bulk.yaml pins all 30 RefSNP partitions in NCBI human dbSNP b157:
25 chromosome partitions plus other, nosnppos, unsupported, merged and
withdrawn. The sample rs268 file is excluded because it duplicates real data.
The inventory totals 485,701,336,617 compressed bytes. A GRCh38 VCF alone is not
this scope. Historical nonhuman dbSNP/EVA releases are not part of the human pin.
config/wikidata_bulk.yaml pins the official 2026-10-19 complete entity JSON
bzip2 dump (103,488,697,311 bytes). Biomedical inclusion rules will be explicit
and versioned; this pin is the input scan, not an assertion that every item is
medical. Wikidata structured data are CC0; Wikipedia article text is not imported.
The inventory implementation validates HTTPS release pins, exact sizes and MD5 checksums. Projection receipts require committed parquet parts and match the entire manifest fingerprint and a required projection fingerprint (code version and scope config). A receipt permits releasing a processed source shard while retaining resumability. Receipts track projected file names/sizes, not cryptographic hashes of projected data; they do not replace final dataset validation. No large bulk download is started by tests.
src/biolinkamalgam/source/DbSnpSource.scala projects every lifecycle partition
into one compact identity row per rsID plus typed association edges:
-
One
statusper record from exactly one snapshot block: active (primary), merged, withdrawn, unsupported, locationless (nosnppos). Contradictory or missing lifecycle fails validation. -
Output keeps only identity names: CURIE
DBSNP:rs<id>, preferredrs<id>. No allele, gene symbol or placement string ever becomes a locus alias. Live partitions (active/unsupported/locationless) emit rows; merged and withdrawn remain typed history, not synonym rows. -
Typed crossref edges:
gene_associationto NCBIGene ids,citesto PMIDs,merged_intoto DBSNP:rs ids. Duplicated ids collapse per record. -
Partial Spark schema reads only identifier/lifecycle fields; the huge placements/frequencies payloads are never materialized. Splittable bzip2 keeps shard reads parallel without staging decompressed JSON.
-
Fixtures in
test/resources/dbsnp/are reduced-column records from the official b157 sample and first records of each exceptional partition; identifiers and lifecycle fields are verbatim, other payloads dropped.