BioMapper

io.github.trentleslie由 GitHub 帳號 trentleslie 發布v1.6.3更新於 Nov 7, 2026

Resolve metabolite, lipid, protein, gene and lab names to knowledge-graph IDs.

已驗證STDIO僅桌面知識與記憶資料與分析

概覽

AI 產生的概覽

把代謝物、脂質、蛋白質、基因與檢驗項目名稱對應到 CHEBI、HMDB、PubChem、RefMet 等知識圖譜標準識別碼。

可以做什麼
  • 查詢支援的實體類型、詞彙表與標註器
  • 將單一名稱(可附帶識別碼提示)對應到知識圖譜 ID 與信心等級
  • 每次請求批次對應最多五個實體
  • 把任意長度的 TSV 或 CSV 當成可續跑的背景工作處理,並查看、續跑或取消工作
  • 查看伺服器版本、知識圖譜來源、限制以及允許存取的資料夾
適用情境
適合需要把試算表、世代面板或實驗室匯出檔中的生物名稱正規化為標準識別碼,或在後續分析前確認某個名稱能否以高信心解析的情況。也適用於需要識別碼層級連結的跨世代協調工作。
執行需求
以本機 Python 套件形式執行,用 uvx 啟動(此指令需 1.6.2 或更新版本)。需要連線到託管的 BioMapper API,預設不需金鑰。選用環境變數:BIOMAPPER_API_KEY 用於自架或需驗證的部署,BIOMAPPER_BASE_URL 指向其他 API 根位址,BIOMAPPER_MCP_HOME 指定工作狀態目錄,BIOMAPPER_MCP_ROOTS 指定 map_file 可讀寫的資料夾。
安裝前請注意
實體名稱與檔案內容會傳送到託管的 BioMapper API,知識圖譜版本資訊也來自外部服務。BIOMAPPER_API_KEY 是密鑰,設定後會以請求標頭送出。map_file 會讀寫檔案,應只透過 BIOMAPPER_MCP_ROOTS 開放需要的目錄。原始透傳列屬於不可信的外部資料;憑證狀態為 contradicted 或 unavailable 時,代表需要人工複核對應結果。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 BioMapper,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

biomapper

Python client for the BioMapper2 API — map biological entity names to standardized knowledge-graph identifiers (CHEBI, HMDB, PubChem, RefMet, and more).

python
from biomapper import map_entity
result = map_entity("L-Histidine")print(result.primary_curie)     # RM:0129894print(result.confidence_tier)   # highprint(result.ids_for("CHEBI"))  # ['15971']print(result.equivalent_ids_for("HMDB"))  # ['HMDB0000177']

Installation

bash
# Core (async HTTP client + Pydantic models)pip install biomapper
# With the external benchmark suitepip install 'biomapper[benchmarks]'

The core install includes the MCP server for agents (Claude Code, Claude Desktop, other MCP clients); launch it with uvx biomapper (see Use from an agent (MCP)).

Use 1.6.2 or later for uvx biomapper. 1.6.0 and 1.6.1 shipped the server behind an mcp extra (uvx --from 'biomapper[mcp]' biomapper-mcp); that line still works on 1.6.2.

Use 1.5.6 or later for harmonize_cohorts() and ambiguous-link flags (see Harmonization).

Use 1.5.4 or later to call the hosted deployment without a key (see API keys).

Use 1.5.3 or later for the benchmark suite. Earlier published releases, up to and including 1.4.0, ship neither biomapper.benchmarks nor the benchmarks extra, so pip install 'biomapper[benchmarks]' against them warns that the extra is unknown and installs only the core client. 1.5.2 contains the suite but its extras were published empty, so the same command installs the core client without pandas, requests, rdkit, openpyxl or defusedxml and the suite fails on first import; install those by hand if you are pinned to 1.5.2. 1.5.3 is the first release whose benchmarks extra actually carries its dependencies. The changelog records which versions reached the index; 1.5.0 and 1.5.1 are tagged (not published).


Notebooks

  • notebooks/biomapper_tutorial.ipynb: the harmonization protocol end to end, keyless and build-pinned: resolve two cohort panels, link them by identifier-only CURIE sets, and read a certified and a refused structural certificate.
  • notebooks/monti_overlap_benchmark.ipynb: BioMapper vs Monti et al. 2026 on the NECS cross-cohort overlap, as linked-count coverage for four cohorts plus a structural certification of the NECS to Arivale links, replayed from a pinned run with a small live check.
  • notebooks/ukbb_arivale_harmonization_sop.ipynb: a standard operating procedure for harmonizing two cohorts with this package, worked on UK Biobank × Arivale (metabolites, proteins, clinical labs): inclusion rules, map, match, verify, and results asserted against the Activity 2 milestone, replayed offline from shipped panel metadata.

API keys (optional)

No API key is needed for the hosted deployment (https://biomapper.expertintheloop.io). From 1.5.4 the client is keyless by default: with no key configured it sends no X-API-Key header, and every entry point, including the synchronous wrappers, works as shown below.

A key is only for a self-hosted or authenticated deployment. Pass it as api_key=, or set it in your environment:

bash
export BIOMAPPER_API_KEY=your-key-here

or in a .env file in your project root:

BIOMAPPER_API_KEY=your-key-here

A configured key is sent unless anonymous=True is passed. To force a keyless call while BIOMAPPER_API_KEY is set, use BioMapperClient(anonymous=True). If a deployment does require a key and none was sent, the first request raises BioMapperAuthError saying so.

Releases before 1.5.4 raise BioMapperConfigError when no key is configured, and their synchronous wrappers cannot reach a keyless deployment; upgrade, or use BioMapperClient(anonymous=True) directly.


Quick start

Single lookup (synchronous)

python
from biomapper import map_entity
result = map_entity("L-Histidine")
print(result.resolved)          # Trueprint(result.primary_curie)     # RM:0129894print(result.chosen_kg_id)      # RM:0129894print(result.chosen_kg_id_review)  # None  (or 'divergent_refmet' when a ChEBI conflict is flagged for review)print(result.confidence_score)  # 2.489print(result.confidence_tier)   # high  (≥2.0)print(result.ids_for("CHEBI"))  # ['15971']print(result.ids_for("refmet_id"))  # ['RM0129894']
# KG equivalent IDs — all identifiers from the resolved knowledge graph nodeprint(result.kg_equivalent_ids)            # {'CHEBI': ['15971', '44637'], 'HMDB': ['HMDB0000177'], ...}print(result.equivalent_ids_for("HMDB"))   # ['HMDB0000177']

Batch mapping (synchronous)

python
from biomapper import map_entities, summarize
records = [    {"name": "L-Histidine"},    {"name": "Glucose", "identifiers": {"HMDB": "HMDB00122"}},    {"name": "Sphinganine"},]
results = map_entities(records, progress=True)  # tqdm bar with [notebook]summary = summarize(results)
print(f"{summary.resolved}/{summary.total_queried} resolved")print(f"Resolution rate: {summary.resolution_rate:.1%}")print(summary.vocabulary_coverage)

Inputs are auto-chunked at 1000 entities per request against the native POST /map/batch endpoint, so 10,000 records cost 10 round-trips.

Dataset upload (synchronous)

For larger inputs, hand the server a TSV/CSV file directly and stream results back. The server processes the file row-by-row over the POST /map/dataset/stream endpoint:

python
from pathlib import Pathfrom biomapper import map_dataset_file_sync
result = map_dataset_file_sync(    Path("compounds.tsv"),    name_column="name",    provided_id_columns=["hmdb_id"],    progress=True,         # tqdm bar    total_hint=1000,       # optional; enables % progress)result.raise_for_error()   # opt-in: raise BioMapperError if the stream truncatedprint(f"resolved {sum(1 for r in result.results if r.resolved)} of {len(result.results)}")

name_column and provided_id_columns are required — the server uses them to map your file's columns to entity names and identifier hints. For per-result streaming into a UI or custom processing, use the async BioMapperClient.map_dataset_file_iter method (see the tutorial notebook in notebooks/).

Discovering what the API supports

python
from biomapper import list_annotators, list_vocabularies, list_entity_types
for a in list_annotators():    print(f"{a.slug:30s} {a.name}")
# 300+ supported vocabularies (CHEBI, HMDB, PubChem, …)vocabs = list_vocabularies()print(f"{len(vocabs)} vocabularies supported")
# Biolink entity types with aliases and default vocabulary prefixesfor et in list_entity_types():    print(f"{et.type}: {', '.join(et.aliases)}")    if et.default_prefixes:        print(f"  prefixes: {', '.join(et.default_prefixes)}")

Tuning resolution

The mapping calls accept the API's resolution options as keyword arguments. An option you do not pass is omitted from the request, so the server's own default applies and the payload is unchanged for callers who ignore them.

Coverage is uneven, so check this before reaching for one. Passing an option to a call that does not accept it raises TypeError locally, before any request:

vocabprefer_humanprefer_canonicalcandidate_limitkestrel_top_narray_delimiters
map_entity, map_entities (sync and async)yesyesyesyesyesyes
BioMapperClient.map_dataset_file_iter (async)yesyesyesyesyesno
map_dataset_file_syncyesnonononono

array_delimiters is absent from the dataset routes because the API does not accept it there. The four missing from map_dataset_file_sync are a gap in that wrapper rather than an API limitation: the async map_dataset_file_iter underneath it does accept them, so use that directly if you need them on a file-based run.

python
from biomapper import map_entity  # map_entity / map_entities accept all six
result = map_entity(    "PC 34:1",    vocab="refmet",             # restrict to one vocabulary (or a list)    prefer_human=False,         # gene/protein: allow a non-human ortholog to win    prefer_canonical=False,     # non-gene: allow a non-canonical-namespace node to win    candidate_limit=20,         # candidates each Kestrel search annotator retrieves (1..100)    array_delimiters=["|"],     # how delimited ID strings are split    kestrel_top_n=5,            # opt in to raw Kestrel passthrough rows (1..100))
OptionApplies toDefaultNotes
vocaballserverstr or list[str], e.g. "refmet"
prefer_humangene/proteinTrue (server)Prefer the human, HGNC-bearing candidate over a wrong-species ortholog
prefer_canonicalnon-geneTrue (server)Prefer the canonical-namespace node (CHEBI/HMDB/RefMet) over a same-text node from UMLS/ICD/KEGG
candidate_limitallserver1..100. Validated client-side, so an out-of-range value raises ValueError before the request
kestrel_top_nalloff1..100. Passthrough only: it never changes chosen_kg_id, assigned_ids or the certificate
array_delimitersmap_entity, map_entities[",", ";"]Not accepted by the dataset routes

kestrel_top_n populates result.kestrel_results with the raw rows Kestrel returned, exactly as returned, for each search endpoint the pipeline used. Those rows are untrusted external data: treat every field as unescaped and unverified.

Resolution certificates and refusal

A mapping result carries the structural certificate the API computed for chosen_kg_id, which is how you tell "we could not check this" apart from "we checked and disagree".

python
result = map_entity("cortisone")
cert = result.certificateif cert is not None:    print(cert.state)                    # corroborated | uncorroborated | contradicted |                                         # unavailable | not_applicable    print(cert.independent_source)       # registry consulted, e.g. "pubchem"    print(cert.independent_of_selection)  # False => corroboration would be circular    print(cert.refmet_availability)      # voted | no_match | unavailable | not_queried
# Why the resolver declined, when it did. Distinct from `error`:# an error means the call failed; a refusal means the pipeline ran and declined to assert.print(result.refusal_reason)

state="contradicted" means a human should look, not that the resolver is wrong: a name lookup at an external registry can itself return a related-but-different compound. state="unavailable" means there was nothing to check against, which is unverifiable rather than wrong.

Async usage

python
import asynciofrom biomapper import BioMapperClient
async def main() -> None:    async with BioMapperClient() as client:        # Verify connectivity        health = await client.health_check()        print(health)  # {'status': 'healthy', ...}
        # Single        result = await client.map_entity(            "L-Histidine",            identifiers={"HMDB": "HMDB00177"},        )
        # Batch — auto-chunked at 1000 entities per request        results = await client.map_entities(            [{"name": "L-Histidine"}, {"name": "Glucose"}],            progress=True,        )
        # Stream from a file — per-result as they arrive        from pathlib import Path        async for r in client.map_dataset_file_iter(            Path("compounds.tsv"),            name_column="name",            provided_id_columns=["hmdb_id"],        ):            print(r.query_name, r.primary_curie)
asyncio.run(main())

map_dataset_file_iter is the primitive for UIs and custom processing that want per-result reactivity. Callers needing a blocking, fully-collected result should use map_dataset_file_sync instead (see above).

Jupyter notebooks

Apply nest_asyncio before using sync helpers inside a running event loop:

python
import nest_asyncionest_asyncio.apply()  # required in Jupyter
from biomapper import map_entitiesresults = map_entities([{"name": "L-Histidine"}], progress=True)

The worked notebooks are listed under Notebooks. Both run against the keyless hosted deployment, so no API key is needed, and both print the exact KRAKEN build that answered.

Preprocessing functions

python
from biomapper.extras.metabolon import clean_compound_name, extract_hmdb_id
# Strip quotes and collision-energy suffixesclean_compound_name('"1,3-Diphenylguanidine_CE45"')  # '1,3-Diphenylguanidine'clean_compound_name('"4,6-DIOXOHEPTANOIC ACID"')     # '4,6-DIOXOHEPTANOIC ACID'clean_compound_name('L-Histidine')                   # 'L-Histidine'  (unchanged)
# Extract HMDB accessions from ms1_compound_name formatextract_hmdb_id('HMDB:HMDB03349-2257 L-Dihydroorotic acid')  # 'HMDB03349'extract_hmdb_id('HMDB00177')                                  # 'HMDB00177'extract_hmdb_id(None)                                         # None

Harmonization (cross-dataset equivalence)

biomapper.harmonize links two already-resolved datasets locally. It runs entirely on the client: no extra requests, no knowledge-graph access, so it works offline and is fully testable without a network. Two entities, one per cohort, link when any of these holds, and each link records which one as its basis (strongest first):

basisrule
nodetheir identifier-only CURIE sets intersect and both resolved to the same chosen node
identifiertheir identifier-only CURIE sets intersect, on different nodes
name_exacttheir names are equal after whitespace normalization
name_casefoldtheir names are equal only after casefold (see the short-name rule below)

Name links (from 1.5.5, opt-in with link_by_name=True) catch entities that resolution put on different nodes, such as acetylcarnitine (c2) and acetylcarnitine (C2). They are currently intended for small-molecule / metabolite panels; other entity types are pending review of case-sensitive naming conventions (gene and protein symbols, for example). Rules:

  • Only entities that resolved on both sides link by name; an unresolved one stays a refusal candidate. Names are compared on query_name, even when you pass a custom key=; links are reported under the keys.
  • No fuzzy matching and no punctuation stripping, only whitespace normalization.
  • A casefold-only match where either name is NAME_CASEFOLD_MIN_LENGTH_EXCLUSIVE (4) characters or fewer is withheld, not linked, and listed in report.name_match_withheld for review: Co (cobalt) and CO (carbon monoxide) are different things.
  • Pass results of one entity type per harmonize() call. The results carry no type to check, so this is the caller's responsibility; a cross-type collision such as cAMP (metabolite) vs CAMP (gene) is out of scope.
  • Off by default: harmonize(...) links by node and identifier only, exactly as before 1.5.5.

report.summary() counts links by basis (links_by_basis) and reports n_name_only_links (links with no identifier evidence) and n_name_match_withheld, so a report can say "X linked by identifier, Y by name only". Every link also carries bases, the full set of bases that apply.

Ambiguous links (from 1.5.6). Every link records a_partners and b_partners, how many entities each side links to. link.ambiguous is true when either is above one: several distinct entities landed on one entry, so the pair is a review candidate rather than a confident match. report.one_to_one_links and report.ambiguous_links split the links, and summary() adds n_one_to_one_links and n_ambiguous_links. All links are still returned; nothing is dropped. On NECS x Xu et al. 2022 scored against Metabolon compound IDs, one-to-one links were 397 of 398 correct (99.7%) versus 439 of 575 (76.3%) for all links.

Identification-confidence markers (unreleased). Metabolon marks how a compound was identified with asterisks on its name: no asterisk means confirmed with an authentic chemical standard, * means identified with confidence but not confirmed with a standard, and ** means reasonably identified without an available standard. Every link records each side's marker as a_id_confidence / b_id_confidence (0, 1 or 2, read from each result's query_name; see biomapper.harmonize.id_confidence_marker), and link.id_confidence_differs is true when they differ, as in glyco-beta-muricholate** vs glyco-beta-muricholate. A marker is a run of one or two asterisks that directly follows a non-space character and either ends the name or comes right before ( (1-palmitoyl-gpi* (16:0)); any other asterisk is ignored, and when a name carries two markers the longer one counts (1-arachidonoyl-GPC* (20:4)* is *). Names without markers are always 0, so panels that do not use Metabolon names are unaffected; a marked name linked to an unmarked one is flagged. link.needs_review is ambiguous or id_confidence_differs; ambiguous and one_to_one_links keep their meaning. report.review_links lists the links that need review, every other link is a confident match, and summary() adds n_id_confidence_differs and n_needs_review. Name linking compares names with their asterisks intact, so a pair that differs only in its marker links by identifier, never by name. On NECS x Xu et al. 2022 (engine v1.2.1, 2026-11-05), 390 links need no review and 389 of them share a Metabolon compound ID; before this flag the confident set was 399 links, 9 of them with differing markers.

Mixture names (unreleased). A name that lists alternative compounds, such as butyrate/isobutyrate (4:0), describes a measurement that may be either compound. Linking it to a name that does not list the same alternatives is a review candidate even when the link is one-to-one. Every link records a_is_mixture / b_is_mixture. biomapper.harmonize.is_mixture_name applies this rule to the whitespace-normalized, casefolded name; any one of these makes it a mixture:

  1. or as a word between spaces: linolenate [alpha or gamma; (18:3n3 or 6)], dihomo-linolenate (20:3n3 or n6).
  2. + with a space on each side: dimethylarginine (sdma + adma), (n(1) + n(8))-acetylspermidine (but not NAD+).
  3. / with at least three letters right before it and a letter or a locant (2-, 5alpha-) right after it: butyrate/isobutyrate (4:0), 2-hydroxybutyrate/2-hydroxyisobutyrate. Lipid chain notation (PC 16:0/18:1, d18:1/17:0) has a digit or ) before the slash and never counts; neither does a slash before a unit word (mmol/mol) or any slash in a name containing the word ratio.
  4. A bracketed group that is a comma-separated list of two or more lipid chains, Metabolon's multi-isomer names: sphingomyelin (d18:1/17:0, d17:1/18:0, d19:1/16:0). Other comma lists (androstenediol (3alpha, 17alpha) monosulfate, bilirubin (z,z)) do not count.

link.mixture (the mixture review reason) is true when the two sides differ as mixtures:

  • exactly one side is a mixture (butyrate/isobutyrate (4:0) vs butyrate (4:0)), or
  • both are, and they list different alternatives (dihomo-linolenate (20:3n3 or n6) vs dihomolinolenate (20:3n3 or 3n6)).

Two mixture names listing the same alternatives in any order are not flagged: dimethylarginine (sdma + adma) vs dimethylarginine (adma + sdma), or the same Metabolon multi-isomer string on both sides. The comparison uses biomapper.harmonize.mixture_alternatives_key (stored on the link as a_mixture_key / b_mixture_key): the casefolded name without its identification-confidence markers, with the alternatives inside each bracket level ((...) and [...] alike) sorted. Alternatives are only reordered within their own bracket level, so moving a shared qualifier between levels (butyrate/isobutyrate (4:0) vs isobutyrate (4:0)/butyrate) still counts as different, which errs toward review.

The engine's own mixture flag is honoured too: engine v1.3.1 reports lipid_resolution.name_is_mixture on lipid names (a top-level name_is_mixture is also read if a server sends one). A True marks the name a mixture; a False does not override the name rule, and a server without the field changes nothing. link.needs_review is now ambiguous, id_confidence_differs or mixture; link.review_reasons names which, report.mixture_links lists the mixture links, and summary() adds n_mixture_links. The rule is written against Metabolon-style metabolite names.

Possible missed matches (unreleased). report.possible_missed lists every entity that ended up in no link although its mapping already carries a review signal, so a reviewer sees it next to the review links instead of it disappearing. Each entry (PossibleMiss) has the cohort label, key, name, chosen_kg_id, whether it resolved, and its reasons: the mapping's chosen_kg_id_review value (divergent_refmet, conflict_no_structure), certificate_contradicted, and/or mixture (per entity: the entity's own name is a mixture). Errored entities are never listed. report.a_possible_missed / b_possible_missed split it by side, and summary() adds n_possible_missed plus a possible_missed count in each cohort block. Entries are listed per cohort; nothing pairs them across cohorts.

On NECS x Xu et al. 2022 (engine v1.3.1, offline re-score 2026-11-06), 6 links are mixture reviews and 2 of them are flagged for that reason alone (butyrate/isobutyrate (4:0) with butyrate (4:0), and the two linolenate spellings; both share a Metabolon compound ID). 401 links need no review and 400 of them share a compound ID; 99 links need review. 77 entities are listed as possible misses (68 NECS, 9 Xu), including both sides of the one Monti et al. pair BioMapper does not link (dihomo-linolenate (20:3n3 or n6) with dihomolinolenate (20:3n3 or 3n6)).

python
from biomapper import map_entitiesfrom biomapper.harmonize import harmonize
ukbb    = map_entities([{"name": "Glucose"},   {"name": "Urea"}])arivale = map_entities([{"name": "D-glucose"}, {"name": "X-12345"}])
report = harmonize(ukbb, arivale, a_label="ukbb", b_label="arivale")
report.n_links          # 1report.links[0].basis   # 'node'report.links[0].shared  # frozenset({'CHEBI:17234', 'KEGG:C00031'}): the identifiers sharedreport.b_unresolved     # ('X-12345',) — a refusal candidate, never silently droppedreport.summary()

Two rules the linker is built around:

  1. No structure identifiers. INCHIKEY, INCHI and SMILES are excluded. Linking on a structure hash would make any downstream structural certificate circular and would make precision 100% by construction.
  2. Prefix synonyms normalize. KEGG.COMPOUND:C00031 equals KEGG:C00031; genuinely different identifier spaces such as KEGG.GLYCAN stay distinct.

An entity that resolved to nothing is a refusal candidate, not a link. It is named in a_unresolved / b_unresolved, counted in summary(), and left out of the link-rate denominator so non-resolution is never scored as non-equivalence. An entity whose mapping call errored is tracked separately again in a_errors / b_errors.

Import it as from biomapper.harmonize import harmonize. The name is deliberately not bound on the package root, where it would shadow the submodule.

Harmonizing two cohorts in one call: harmonize_cohorts()

harmonize_cohorts() runs the whole protocol: it reads both cohort tables, maps them, links them with harmonize(), and writes a pinned report. It exists because the obvious manual path can quietly make results worse. When only one cohort publishes an identifier type, supplying it moves that cohort onto code-specific entries the other cohort never reaches by name: on the UK Biobank x Arivale clinical labs, supplying Arivale's LOINC codes cut matched pairs from 31 to 2.

python
from biomapper import harmonize_cohorts
report = harmonize_cohorts(    "ukbb_labs.tsv",                  # a DataFrame, a list of dicts, or a TSV/CSV path    "arivale_labs.tsv",               # ('#' comment lines are skipped)    entity="labs",                    # -> biolink:ClinicalMeasurement    a_name_column="field_name",    b_name_column="Display Name",    b_vocabularies={"LOINC": ["Labcorp LOINC ID", "Quest LOINC ID"]},    a_label="ukbb",    b_label="arivale",)# UserWarning: LOINC is declared only for 'arivale' ... not supplied as mapping inputreport.summary()["arms"]["names_only"]   # pairs, one-to-one/ambiguous, link bases, ...report.review_queue                      # codes whose entry disagrees with the name's entry# harmonize_cohorts: wrote results to .../biomapper_runs/harmonize_cohorts_20261008T190000Z

What it does:

  1. Names-only arm. Both cohorts are mapped by name with the resolved category and no identifiers. entity takes metabolites (SmallMolecule), proteins (Protein), genes (Gene), labs (ClinicalMeasurement, where lab tests live in Biolink) or any raw biolink: category; category= overrides an alias. Name linking is on for SmallMolecule only, as in harmonize().
  2. Identifier arm, shared vocabularies only. You declare identifier columns per vocabulary and cohort ({"LOINC": [...]}); names are normalized, so KEGG.COMPOUND equals KEGG. Only vocabularies both cohorts declare are supplied as mapping input, and only rows carrying one are re-mapped; the rest reuse their names-only result. With nothing shared the arm is skipped and the report says why; with no identifier columns only the names-only arm runs.
  3. Review queue for one-sided codes. Each code of a vocabulary only one cohort declares is resolved on its own (annotation_mode="none", one request per row and code) and compared with the row's names-only entry by identifier-set intersection. Every code gets a status (agree, disagree, code_unresolved, name_unresolved_code_resolved, errored); report.review_queue lists everything but agree, with both entries. On the labs this lists Arivale "Glucose", whose name lands on one entry while its LOINC code 2345-7 says another.
  4. Report. Both HarmonizationResults (report.names_only, report.identifier), their diff (pairs found only by names, only with identifiers, by both), one-to-one and ambiguous link counts per arm (n_one_to_one_links / n_ambiguous_links, as in harmonize()), link bases, warnings, excluded rows (blank names), and pins. Each arm's link table carries a_partners, b_partners and a one_to_one flag, so ambiguous links can be reviewed, plus a_id_confidence, b_id_confidence, id_confidence_differs, needs_review, a_is_mixture, b_is_mixture, mixture and review_reasons; each arm's summary adds n_id_confidence_differs, n_mixture_links, n_needs_review and n_possible_missed. Each arm also writes possible_missed_<arm>.tsv (cohort, key, name, chosen_kg_id, resolved, reasons).

Output by default. Results are written to biomapper_runs/harmonize_cohorts_<UTC stamp>/ under the current directory (override with output_dir=; save=False to skip; report.write(path) any time): mapping results per arm and cohort, written as each arm finishes so a failure keeps finished work, link and possible-missed tables per arm, diff.tsv, review_queue.tsv, settings.json and summary.json. Nothing beyond what you passed in is written, and input files are recorded by name and SHA-256, never by absolute path.

Pins. The package version; the API's /health status and self-reported version (labelled "self-reported, known stale": the API does not expose its engine release, which is recorded as unavailable); Kestrel's /health kestrel_version and kg_build together with the Kestrel URL used (kestrel_url=); and a UTC timestamp. probe_pins=False skips the two reads.

Requests. The default mapper sends batches of 10 with a 300 s timeout (batch_size=, timeout=, progress=True to see them); a failed batch becomes errored rows that are counted, not an exception. Pass mapper= (any (records, *, entity_type, annotation_mode) -> list[MappingResult] callable) to replay recorded results offline; the tests replay the UK Biobank x Arivale labs this way (tests/fixtures/cohorts_labs_cm/, KRAKEN kg 2.3.0 / 3dd08a5b: 37 pairs, 21 one-to-one by name).


Use from an agent (MCP)

biomapper ships a local stdio MCP server (the biomapper command, alias biomapper-mcp), so an agent can resolve entities through the same public, keyless API as the Python client. It runs on your machine; nothing is hosted. Launch it with one command (needs uv):

bash
uvx biomapper

Claude Code (the server sees your project folder through MCP roots):

bash
claude mcp add --scope user biomapper -- uvx biomapper

Claude Desktop (claude_desktop_config.json). Desktop does not inherit your shell PATH, so use the absolute path that which uvx prints, and list the folders map_file may use, because Desktop shares no project roots:

json
{  "mcpServers": {    "biomapper": {      "command": "/absolute/path/to/uvx",      "args": ["biomapper"],      "env": { "BIOMAPPER_MCP_ROOTS": "/Users/you/data" }    }  }}

Tools

toolwhat it does
list_entity_types, list_vocabularies, list_annotatorsdiscovery; call list_entity_types first to pick entity_type
map_entityone name (plus optional identifier hints) to KG identifiers
map_batchup to 5 entities in one request; more is refused and pointed to map_file
map_filea TSV/CSV of any length as a resumable background job; returns a job_id and an ETA at once
job_status, resume_job, cancel_jobfollow, continue, or stop a file job
server_infoversion, KG provenance, limits, and the folders map_file may use

Every result says resolved, unresolved, refused, or error. An error is a failed call, never a confirmed no-match. Results carry a compact provenance block (client version, API URL, KG build per Kestrel /health, labeled assumed, not verified, and kg_verified).

Throughput, and why the limits exist

The hosted deployment is one shared instance that answers about 4-5 s per entity and slows sharply under concurrent load, so the server is deliberately polite:

  • One request in flight per server process. Interactive calls and file-job chunks share one gate; a call that cannot get it within ~10 s returns "busy, retry shortly".
  • Every tool call returns in under 60 s (Claude Desktop cancels at ~60 s), which is why map_batch stops at 5 and long work runs as a job.
  • Loop guard: more than 20 map_entity/map_batch calls in 5 minutes are refused with a pointer to map_file. Discovery and status tools do not count.

File jobs

map_file(path, name_column, entity_type, identifier_columns?, output_path?) validates the file (readable, columns present, rows counted, output not already there, /health answering) and queues it. identifier_columns maps a column to its vocabulary, e.g. {"hmdb_id": "HMDB"}.

  • Output: <input>.biomapper.tsv next to the input plus <input>.biomapper.provenance.json (full pins: API, Kestrel, KG build). It never overwrites a file. Progress is written to <output>.partial until the job completes.
  • Resumable: rows are mapped in chunks of 5 and cached as they land. If the agent session ends, the job is paused; the next session in the same folder resumes it automatically, without re-mapping cached rows. Jobs idle for more than 7 days become expired and resume only through resume_job.
  • Honest failure: three whole-chunk failures in a row, or one chunk slower than 240 s, move the job to stalled and it stops sending requests; resume_job retries after a health check. A row that errors three times is written as error. A changed input file, or a KG build that differs from the cached rows, fails the job; rerun=true starts over.
  • Folders: map_file only reads and writes inside the client's MCP roots plus BIOMAPPER_MCP_ROOTS, with symlinks resolved. With neither, it refuses and says how to set them; the interactive tools still work.

Environment

variabledefaultpurpose
BIOMAPPER_BASE_URLthe hosted APIAPI root, e.g. a staging or local engine
BIOMAPPER_KESTREL_URLhttps://kestrel.krakenkg.com/apiwhere the KG build is read for provenance
BIOMAPPER_API_KEYunset (keyless)sent as X-API-Key when set; never written to job files, results, or logs
BIOMAPPER_MCP_ROOTSunsetextra folders map_file may use (:-separated; ; on Windows)
BIOMAPPER_MCP_HOME~/.cache/biomapper/mcpjob state (owner-only permissions)

On HPC or shared hosts whose home directory is on NFS/SMB, set BIOMAPPER_MCP_HOME to local disk: job ownership uses OS file locks, which network filesystems may not honor across machines.


API reference

MappingResult

AttributeTypeDescription
query_namestrName submitted to the API
resolvedboolWhether any identifier was returned
primary_curiestr | NoneThe CURIE to report: the server's primary_curie (engine v1.3.0+), which identifies chosen_kg_id's node. Falls back to the first of the response's curies (alphabetical, so possibly a losing annotator hit) only on an older server that does not send it
curies_by_annotatordict[str, list[str]]Which annotator produced each assigned CURIE, {annotator: [CURIEs]} (server curies_assigned, engine v1.3.0+; empty from older servers)
curies_providedlist[str]CURIEs normalized from the identifiers you supplied (engine v1.3.0+; empty from older servers)
chosen_kg_idstr | NoneResolver-selected knowledge graph ID
chosen_kg_id_reviewstr | NoneReview flag for source-weighted small-molecule ChEBI conflicts: "divergent_refmet", "conflict_no_structure", or None
confidence_scorefloat | NoneHighest score across annotators
confidence_tierstr"high" (≥2.0) / "medium" (1–2) / "low" (<1) / "unknown"
identifiersdict[str, list[str]]Vocabulary → IDs, e.g. {"CHEBI": ["15971"]}
kg_equivalent_idsdict[str, list[str]]All equivalent IDs from the resolved KG node, by CURIE prefix
certificateResolutionCertificate | NoneStructural certificate for chosen_kg_id (state, independent source, RefMet provenance)
refusal_reasonstr | NoneWhy the resolver declined, read from the certificate. None when there is no certificate
lipid_resolutionLipidResolution | NoneLipid hierarchy detail (goslin parse, mapping_relation); None for non-lipid rows
refmet_availabilitystr"voted" / "no_match" / "unavailable" / "not_queried"
refmet_sourcestr"local_snapshot" / "not_in_snapshot" / "live_api" / "unavailable" / "not_queried"
refmet_snapshot_versionstr | NonePinned RefMet freeze that served the row
tier_b_snapshot_versionstr | NoneTier B freeze that produced a frozen independent result
kestrel_resultslist[KestrelSearchResult] | NoneRaw passthrough rows; only when kestrel_top_n was set
hmdb_hintstr | NoneHMDB hint passed in the request
errorstr | NoneError message if mapping failed
python
result.ids_for("CHEBI")        # ['15971']result.ids_for("refmet_id")    # ['RM0129894']result.ids_for("PUBCHEM.COMPOUND")  # []
# KG equivalent IDs — all identifiers from the resolved knowledge graph noderesult.equivalent_ids_for("HMDB")  # ['HMDB0000177']result.equivalent_ids_for("LM")   # ['ST01010001', 'ST01010093']

DatasetMappingResult

Return type of map_dataset_file_sync. Captures per-row results plus an opt-in error signal for partial runs.

AttributeTypeDescription
resultslist[MappingResult]Per-row mapping outcomes in server-emitted order
statsdict[str, Any]Server-provided summary. Empty unless the stream emits a terminal summary line
metadataApiMetadataRequest metadata; stays at defaults when the stream truncates before completion
errorstr | NoneMid-stream transport failure text. None on clean runs
python
result.raise_for_error()   # raises BioMapperError if .error is set; else no-op

raise_for_error mirrors httpx.Response.raise_for_status and turns the partial-result contract into an explicit caller opt-in — silent consumption of a truncated run (using .results without checking .error) is the footgun this model is designed to prevent.

Note: confidence_score on dataset-stream results is always None — the /map/dataset/stream endpoint emits a slimmer per-row payload than /map/batch and does not include the annotator assigned_ids block. Use map_entity / map_entities if you need confidence tiers.

Confidence tiers

ScoreTierRecommended action
≥ 2.0highAccept without review
1.0–2.0mediumQuick sanity check
< 1.0lowManual review recommended
NoneunknownNo score returned (e.g. HMDB-hint resolved)

Error handling

python
from biomapper import (    BioMapperError,       # base class    BioMapperAuthError,   # 401/403: a key is required, or the key was rejected    BioMapperRateLimitError,  # 429 — throttled    BioMapperServerError,     # 5xx    BioMapperTimeoutError,    # request timeout    BioMapperConfigError,     # invalid config, e.g. api_key= together with anonymous=True)
try:    result = map_entity("Glucose")except BioMapperAuthError as e:    # Only for a deployment that requires a key; the message says whether one was    # missing or rejected.    print(e)else:    if result.error:        print(f"mapping failed: {result.error}")

An authentication failure (401/403) raises BioMapperAuthError from every entry point, sync or async, because it is systemic: every later request would fail the same way. The synchronous map_entity and map_entities go through the batch endpoint, where transient failures (429, 5xx, timeouts, a malformed response) and per-entity problems do not raise: each affected record comes back as MappingResult(error=...) rather than aborting the batch. Check .error on every result. The single-entity call on the async client, await client.map_entity(...), raises the typed exceptions (BioMapperRateLimitError, BioMapperServerError, BioMapperTimeoutError) directly.

Dataset streaming (map_dataset_file_sync) uses a two-tier contract:

  • Initial-request errors (401, 422, 500, connect timeout) raise as typed exceptions — these happen before any row is processed, so partial results don't exist to preserve.
  • Mid-stream transport failures are captured into DatasetMappingResult.error with the partial results preserved in .results. Call .raise_for_error() to get exception semantics, or inspect .error directly for "accept partial, log the rest" workflows.

Callback exceptions raised from on_result propagate unwrapped and replace the return value — partial results collected up to that point are lost. For UI consumers with failure-prone callbacks, wrap the callback body in your own try/except if you want partial data to survive.


External benchmark suite

An API-only reproduction harness for the benchmarks the BioMapper preprint reports. It runs against a deployment, not a local engine checkout, which matters for two reasons: running in-process against a checkout measures a library rather than the deployed service, and pointing at a deployment pins provenance to the backend that actually served the answers.

bash
pip install 'biomapper[benchmarks]'
python -m biomapper.benchmarks list                 # the 11 arms and the 2 deliberate skipspython -m biomapper.benchmarks all                  # all arms, production endpointpython -m biomapper.benchmarks arm hajjar           # a single armpython -m biomapper.benchmarks --endpoint dev all   # dev, for testing

The public KRAKEN endpoint is keyless, and a BioMapper2 deployment with no keys configured is open, so the suite runs unauthenticated by default. Set BIOMAPPER_API_KEY if your deployment requires one; the key is never accepted on the command line, because argv is visible to other processes and lands in shell history.

The arms

ArmInputMetricReportable as
hajjarmetabolite nameInChIKey structure oracle, strict + KG-equivalence-setaccuracy candidate
necsmetabolite namestructure oracle, strict + charge-normalizedaccuracy, with the gold caveat
srm1950metabolite namestructure oracle (gold derived from certified SMILES)accuracy candidate
metlinkrmetabolite namecurator cross-link agreement + structural concordanceaccuracy candidate
metabenchmixed ID→ID and name→IDCURIE equality over 1,000 grounding pairspartly circular
metaboliteannotatormetabolite namename-hit rate, per ion modecoverage by construction
refmetmetabolite namestructure oraclecoverage
lmsdlipid nameshorthand resolvability, floor-gatedcoverage (capability regression)
hgncgene symbolCURIE equality per target namespacecoverage
nlmgenegene mentionaccuracy (unambiguous) + flag rate (ambiguous)accuracy candidate
swisslipidslipid name—currently unsourceable, reports as skipped

Read the Reportable as column before quoting anything. Four of the graph's ingested sources (lipidmaps, refmet, loinc, ncbigene) are gold sources for arms above, so those arms measure coverage, not independent accuracy: the gold identifier and BioMapper's answer come from the same place. Each run labels every arm from the build's own source list rather than from a static note, so the label tracks the graph.

Two further reporting rules the suite enforces rather than documents:

  • Gene arms report accuracy per target namespace. The any-namespace roll-up is emitted flagged quotable: false, because the namespaces perform very differently and the roll-up has been quoted as though it described all of them.
  • A skipped arm is not a zero and not a pass. It carries a reason into the manifest.

Provenance

Every run records what actually served it, read from Kestrel /health (keyless) rather than hardcoded: kestrel_version, kg_version, biolink_version, build_timestamp, git_commit, the full source_versions map, the package version, each dataset's source SHA, and a run id. The build is read once before the arms start and re-read afterwards; if it moved mid-suite, the manifest says so, because the pins would no longer describe every number.

Results are saved by default to a timestamped directory (override with --out). There is no flag that discards them: the expensive part of a run is live API traffic.

What stayed in the engine repo

The CI regression gates (gate.py, conflation_gate.py, test_regression_gate.py, kg-regression.yml) guard merges and need engine internals. The --resolver-mode {weighted,vote} A/B toggles a resolver constructor argument that is deliberately not on the API surface.


Development

bash
git clone https://github.com/Phenome-Health/biomappercd biomapperpoetry install --with dev --extras all
make check          # format → lint → type-check → testmake test           # tests onlymake coverage       # HTML coverage report

docs/solutions/ holds documented solutions to past problems — bugs, best practices, and workflow patterns — organized by category with YAML frontmatter (module, tags, problem_type). Relevant when implementing or debugging in an area something has already been written about.


License

MIT — see LICENSE.


Related

  • BioMapper2 API: https://biomapper.expertintheloop.io

來源:README.md,提交 603b86b

工具

0
SourceWeft 尚未在沙箱中啟動過該軟體包,因此其工具未知。

版本歷史

2
  1. v1.6.3最新Nov 7, 2026
  2. v1.6.2Nov 5, 2026

評分

已滿足適用檢查項的 50%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分

    請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。

  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具15 分

    請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。

  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分