From 9a39ae62feadb1a57d154ba87f106806d66b7621 Mon Sep 17 00:00:00 2001 From: Kemal Yaylali Date: Tue, 29 Sep 2026 16:50:27 +0100 Subject: [PATCH] PGx: PharmVar genes (CYP1A2, CYP2A6, CYP2A13, CYP2C8, CYP3A4), CYP2A6 *4 from CN (PGX-001/006) fetch_sources.sh downloads PharmVar per-gene zips; pgx db build adds core alleles for genes CPIC does not define (normalised against the FASTA, *1 = reference). Masks re-derived with the new variants. GeT-RM: CYP3A4 15/15, CYP2A6 7/9; all comparable genes 154/168 (CYP1A2/CYP2C8 definitions differ from GeT-RM's). --- THIRD_PARTY_NOTICES.md | 2 + docs/validation/V2_pgx_star.md | 17 ++- docs/validation/V2_pgx_star_getrm16.json | 154 ++++++++++++++++++++++- scripts/pgx/fetch_sources.sh | 33 ++++- scripts/pgx/validate_star_getrm.py | 4 +- src/simurg/pgx/db.py | 21 ++++ src/simurg/pipeline/pgx_db.py | 50 +++++++- src/simurg/pipeline/pgx_star.py | 13 +- tests/unit/test_pgx_db.py | 9 ++ 9 files changed, 289 insertions(+), 14 deletions(-) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 4f6f11a..2410019 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -16,6 +16,8 @@ used only as external subprocess oracles and are not copied or linked. | [UCSC Genome Browser](https://genome.ucsc.edu/) `cytoBand` tables (hg38, hg19) and `ncbiRefSeq` (RefSeq transcripts, NCBI) | Free for all uses (UCSC data terms; RefSeq is public domain) | `simurg/cn/data/cytoBand_GRCh3{7,8}.txt.gz` and the derived centromere/acrocentric mask BEDs are shipped; `scripts/build_annotation_bundle.sh` downloads cytoBand + ncbiRefSeq for gene lists; `simurg/pgx/data/pgx_cn_regions.GRCh38.v1.bed` is derived from ncbiRefSeq exon coordinates (`scripts/pgx/build_cn_regions.py`) | | [illuminaio](https://github.com/HenrikBengtsson/illuminaio) | GPL-2 | Documentation of IDAT field codes only; oracle via subprocess | | [CPIC](https://cpicpgx.org) database via the CPIC API (allele definitions, allele function, diplotype→phenotype tables) | CC0 1.0 | downloaded by `scripts/pgx/fetch_sources.sh` into `data/` (never committed); `simurg pgx db build` derives the local PGx DB from it; source version and file hashes are recorded in the DB `meta` table | +| [PharmVar](https://www.pharmvar.org) haplotype definitions (Gaedigk et al. 2018 CPT 103:399) | PharmVar terms of use (attribution) | per-gene zips downloaded by `scripts/pgx/fetch_sources.sh` into `data/` (never committed); `simurg pgx db build` derives core-allele definitions for genes CPIC does not define | +| [GeT-RM](https://www.cdc.gov/lab-quality/php/get-rm/reference-materials.html) consolidated PGx/HLA table (CDC) | US government work | validation truth only (`scripts/pgx/getrm_truth.py`), not redistributed | ## BSD-2-Clause (BeadArrayFiles, verbatim from its README) diff --git a/docs/validation/V2_pgx_star.md b/docs/validation/V2_pgx_star.md index 6fef22a..2164381 100644 --- a/docs/validation/V2_pgx_star.md +++ b/docs/validation/V2_pgx_star.md @@ -41,7 +41,9 @@ Status: **implemented for CPIC star-allele genes, interim validation** (2026-09- | GSTM1 (CN: DEL/DEL vs present) | 9/9 | 9/9 | | GSTT1 (CN 0/1/2) | 7/9 | 7/9 | | UGT2B17 (CN) | 2/2 | 2/2 | -| **total** | **132/144 (91.7 %)** | **135/144 (93.8 %)** | +| CYP3A4 (PharmVar) | **15/15** | 15/15 | +| CYP2A6 (PharmVar; *4 deletion from CN) | 7/9 | 8/9 | +| **total** | **154/168 (91.7 %)** | **158/168 (94.0 %)** | PGX-032 target (≥ 90 % for CYP2D6/CYP2C19/CYP2C9 against truth): **42/45 = 93 %** (top solution), 45/45 within the array-equivalent feasible solutions. @@ -52,8 +54,10 @@ CYP2D6 structures (PGX-013) come from the three sub-region CNs (`simurg.pgx.star array cannot separate (both within the feasible set). Details and every discordant call: `V2_pgx_star_getrm16.json`. -Not in the total (GeT-RM uses pre-2023 names the CPIC definitions no longer carry): UGT1A1 (GeT-RM `*60`; -0/9 top) and SLCO1B1 (`*1B/*17/*21` vs CPIC `*37/*15/*20`; 9/15 top). +Not in the total (GeT-RM uses older names or definitions than CPIC/PharmVar today): UGT1A1 (GeT-RM `*60`; +0/9 top), SLCO1B1 (`*1B/*17/*21` vs CPIC `*37/*15/*20`; 9/15), CYP1A2 (lettered `*1F`; 0/9) and CYP2C8 (PharmVar now +defines rs2071426 alone as `*19`; the array calls it concordantly with 1000G, GeT-RM reports those carriers as +`*1`; 8/15). Remaining discordances: CYP2D6 *2 vs *1 where the array cannot separate them (prior picks *1), CYP2C19 *8, TPMT *21, CYP2B6 *27, CYP4F2 *2 — rare alleles whose defining variants are absent or masked on this array. @@ -84,5 +88,8 @@ so the per-call error is 1e-3 (GS ≥ 0.15) and errors are handled by the probe `Reference`, phenotype diplotype from the two lowest-activity carried variants), HLA-A/B tag variants (PGX-007, positions resolved from the manifest's probes, flag `tag_caveat`), and GSTM1/GSTT1/UGT2B17 `*0` genotypes from the targeted CN (PGX-006). -* Not yet: PharmVar-only - genes (CYP2A6, CYP3A4, CYP1A2, …); DPWG tables. +* PharmVar genes without CPIC definitions (CYP1A2, CYP2A6, CYP2A13, CYP2C8, CYP3A4; PharmVar 6.2.29): core alleles + from the GRCh38 haplotype VCFs, `*1` = reference, prior reference 0.8 (no population frequencies); genotype only + (no CPIC phenotype). The probe mask was re-derived with these variants (one more record: CYP2A6 19:40843845 C>A, + concordance 0.47, which had produced false *5 calls). +* Not yet: DPWG tables. diff --git a/docs/validation/V2_pgx_star_getrm16.json b/docs/validation/V2_pgx_star_getrm16.json index e77370d..01cc337 100644 --- a/docs/validation/V2_pgx_star_getrm16.json +++ b/docs/validation/V2_pgx_star_getrm16.json @@ -1,6 +1,16 @@ { "individuals": 30, "genes": { + "CYP1A2": { + "n": 9, + "top": 0, + "equivalent": 0 + }, + "CYP2A6": { + "n": 9, + "top": 7, + "equivalent": 8 + }, "CYP2B6": { "n": 14, "top": 12, @@ -12,6 +22,11 @@ "top": 14, "equivalent": 15 }, + "CYP2C8": { + "n": 15, + "top": 8, + "equivalent": 9 + }, "CYP2C9": { "n": 15, "top": 15, @@ -22,6 +37,11 @@ "top": 13, "equivalent": 15 }, + "CYP3A4": { + "n": 15, + "top": 15, + "equivalent": 15 + }, "CYP3A5": { "n": 15, "top": 15, @@ -79,15 +99,31 @@ } }, "legacy_nomenclature_genes": [ + "CYP1A2", + "CYP2C8", "SLCO1B1", "UGT1A1" ], "total": { - "n": 144, - "top": 132, - "equivalent": 135 + "n": 168, + "top": 154, + "equivalent": 158 }, "discordant": [ + { + "individual": "NA12156", + "gene": "CYP1A2", + "truth": "*1A/*1F", + "call": "*1/*15", + "within_equivalents": false + }, + { + "individual": "NA12156", + "gene": "CYP2C8", + "truth": "*1/*15", + "call": "*1/*1", + "within_equivalents": true + }, { "individual": "NA12156", "gene": "CYP4F2", @@ -116,6 +152,20 @@ "call": "*1/*1", "within_equivalents": false }, + { + "individual": "NA07019", + "gene": "CYP1A2", + "truth": "*1A/*1F", + "call": "*1/*15", + "within_equivalents": false + }, + { + "individual": "NA07019", + "gene": "CYP2C8", + "truth": "*1/*1", + "call": "*1/*19", + "within_equivalents": false + }, { "individual": "NA07019", "gene": "UGT1A1", @@ -123,6 +173,13 @@ "call": "*1/*1", "within_equivalents": false }, + { + "individual": "NA06991", + "gene": "CYP1A2", + "truth": "*1F/*1F", + "call": "*15/*24", + "within_equivalents": false + }, { "individual": "NA06991", "gene": "UGT1A1", @@ -137,6 +194,13 @@ "call": "rs9923231 T/T", "within_equivalents": false }, + { + "individual": "NA12878", + "gene": "CYP1A2", + "truth": "*1F/*1F", + "call": "*15/*24", + "within_equivalents": false + }, { "individual": "NA12878", "gene": "UGT1A1", @@ -144,6 +208,20 @@ "call": "*28/*80", "within_equivalents": false }, + { + "individual": "NA11993", + "gene": "CYP1A2", + "truth": "*1F/*1F", + "call": "*15/*24", + "within_equivalents": false + }, + { + "individual": "NA11993", + "gene": "CYP2A6", + "truth": "*9/*17", + "call": "*15/*17", + "within_equivalents": true + }, { "individual": "NA11993", "gene": "SLCO1B1", @@ -158,6 +236,20 @@ "call": "*28/*80", "within_equivalents": false }, + { + "individual": "NA12003", + "gene": "CYP1A2", + "truth": "*1A/*1F", + "call": "*15/*24", + "within_equivalents": false + }, + { + "individual": "NA12003", + "gene": "CYP2A6", + "truth": "*1/(*8)", + "call": "*1/*1", + "within_equivalents": false + }, { "individual": "NA12003", "gene": "UGT1A1", @@ -179,6 +271,13 @@ "call": "*1/*0", "within_equivalents": false }, + { + "individual": "NA07029", + "gene": "CYP1A2", + "truth": "*1A/*1F", + "call": "*1/*15", + "within_equivalents": false + }, { "individual": "NA07029", "gene": "CYP2B6", @@ -193,6 +292,13 @@ "call": "*1/*17", "within_equivalents": true }, + { + "individual": "NA07029", + "gene": "CYP2C8", + "truth": "*1/*3", + "call": "*19/*3", + "within_equivalents": false + }, { "individual": "NA07029", "gene": "UGT1A1", @@ -207,6 +313,27 @@ "call": "*1/*5", "within_equivalents": false }, + { + "individual": "NA10846", + "gene": "CYP2C8", + "truth": "*1/*1", + "call": "*1/*19", + "within_equivalents": false + }, + { + "individual": "NA07000", + "gene": "CYP2C8", + "truth": "*1/*1", + "call": "*1/*19", + "within_equivalents": false + }, + { + "individual": "NA11832", + "gene": "CYP1A2", + "truth": "*1F/*1F", + "call": "*15/*24", + "within_equivalents": false + }, { "individual": "NA11832", "gene": "NAT2", @@ -242,6 +369,20 @@ "call": "*1/*14", "within_equivalents": false }, + { + "individual": "NA12813", + "gene": "CYP1A2", + "truth": "*1F/*1L", + "call": "*15/*24", + "within_equivalents": false + }, + { + "individual": "NA12813", + "gene": "CYP2C8", + "truth": "*1/*1", + "call": "*1/*19", + "within_equivalents": false + }, { "individual": "NA12813", "gene": "CYP2D6", @@ -270,6 +411,13 @@ "call": "*1/*0", "within_equivalents": false }, + { + "individual": "NA11881", + "gene": "CYP2C8", + "truth": "*1/*1", + "call": "*1/*19", + "within_equivalents": false + }, { "individual": "NA11881", "gene": "SLCO1B1", diff --git a/scripts/pgx/fetch_sources.sh b/scripts/pgx/fetch_sources.sh index 4196c2b..be0f2a4 100644 --- a/scripts/pgx/fetch_sources.sh +++ b/scripts/pgx/fetch_sources.sh @@ -2,7 +2,9 @@ # Download the public PGx sources for `simurg pgx db build` (spec PGX-001) into a dated folder. # bash scripts/pgx/fetch_sources.sh [OUT_DIR] (default data/pgx/sources) # CPIC (CC0): tables from the CPIC API (https://api.cpicpgx.org, PostgREST) as JSON; the newest change-log date is -# recorded as the data version. Never committed (data/ is ignored). +# recorded as the data version. PharmVar (https://www.pharmvar.org; cite Gaedigk et al. 2018 CPT 103:399): per-gene +# zips (haplotype VCFs, GRCh37/GRCh38) for the star-allele genes CPIC does not define. Never committed (data/ is +# ignored); the DB records each source's version and file hashes. set -euo pipefail OUT=${1:-data/pgx/sources} DATE=$(date -u +%Y%m%d) @@ -25,3 +27,32 @@ files = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(d.gl "files": files}, indent=1)) print("CPIC data version", ver) PY + +# ---- PharmVar: star-allele genes without CPIC definitions +PV="$OUT/pharmvar-$DATE" +mkdir -p "$PV" +PV_GENES=${PV_GENES:-"CYP1A2 CYP2A6 CYP2A13 CYP2C8 CYP3A4"} # the PharmVar genes CPIC does not define (2026-09) +for g in $PV_GENES; do + if curl -sSf --retry 3 -o "$PV/$g.zip" \ + "https://www.pharmvar.org/get-download-file?name=$g&refSeq=ALL&fileType=zip&version=current"; then + echo "PharmVar $g $(unzip -l "$PV/$g.zip" | awk 'NR==4{print $4}' | cut -d/ -f1)" + else + echo "PharmVar $g: not available"; rm -f "$PV/$g.zip" + fi +done +python3 - "$PV" <<'PY' +import hashlib, json, pathlib, re, sys, zipfile +d = pathlib.Path(sys.argv[1]) +vers = set() +for z in sorted(d.glob("*.zip")): + top = zipfile.ZipFile(z).namelist()[0].split("/")[0] + m = re.search(r"-(\d+\.\d+\.\d+)$", top) + if m: + vers.add(m.group(1)) +files = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(d.glob("*.zip"))} +(d / "source.json").write_text(json.dumps({"name": "PharmVar", "version": ",".join(sorted(vers)), + "retrieved": d.name.split("-")[1], "url": "https://www.pharmvar.org", + "license": "PharmVar terms of use (cite Gaedigk et al. 2018)", + "files": files}, indent=1)) +print("PharmVar version", ",".join(sorted(vers))) +PY diff --git a/scripts/pgx/validate_star_getrm.py b/scripts/pgx/validate_star_getrm.py index 39bbeee..0854cc8 100644 --- a/scripts/pgx/validate_star_getrm.py +++ b/scripts/pgx/validate_star_getrm.py @@ -57,7 +57,9 @@ def _ugt2b17(t: str) -> int: # *2 = whole-gene deletion CN_TRUTH = {"GSTM1": _gstm1, "GSTT1": _gstt1, "UGT2B17": _ugt2b17} # capped: GSTM1 at 1, the others at 2 -LEGACY_NOMENCLATURE = {"UGT1A1", "SLCO1B1"} # GeT-RM uses pre-2023 names (*60, SLCO1B1 *1B/*17/*21) not in CPIC +# GeT-RM uses older names or definitions than the current CPIC/PharmVar ones: UGT1A1 *60 and SLCO1B1 *1B/*17/*21 +# (pre-2023), CYP1A2 lettered alleles (*1F …), CYP2C8 *19 (PharmVar now defines rs2071426 alone as *19; GeT-RM *1) +LEGACY_NOMENCLATURE = {"UGT1A1", "SLCO1B1", "CYP1A2", "CYP2C8"} def truth_alternatives(text: str) -> list[list[set[str]]]: diff --git a/src/simurg/pgx/db.py b/src/simurg/pgx/db.py index 5514a62..e32076e 100644 --- a/src/simurg/pgx/db.py +++ b/src/simurg/pgx/db.py @@ -237,3 +237,24 @@ def result_basis(gene: str, names: list[str]) -> str: if names and all(n.startswith("*") for n in names): return "star_allele" return "variant_based" + + +def pharmvar_core_alleles(gene: str, vcfs: dict[str, str]) -> dict[str, list[tuple[str, int, str, str, str | None]]]: + """PharmVar per-allele GRCh38 VCFs (file name → text) → core alleles (``*N``; sub-alleles ``*N.001`` skipped) and + their variants (chrom, pos, ref, alt, rsid) as written by PharmVar [DB: PharmVar haplotype VCFs].""" + out: dict[str, list[tuple[str, int, str, str, str | None]]] = {} + pat = re.compile(rf"^{re.escape(gene)}_(\d+)\.vcf$") + for name, text in vcfs.items(): + m = pat.match(name.rsplit("/", 1)[-1]) + if not m: + continue + recs = [] + for ln in text.splitlines(): + if not ln or ln.startswith("#"): + continue + f = ln.split("\t") + rsid = f[2] if f[2].startswith("rs") else None + for alt in f[4].split(","): + recs.append((f[0], int(f[1]), f[3].upper(), alt.upper(), rsid)) + out[f"*{m.group(1)}"] = recs + return out diff --git a/src/simurg/pipeline/pgx_db.py b/src/simurg/pipeline/pgx_db.py index acb3d72..644710d 100644 --- a/src/simurg/pipeline/pgx_db.py +++ b/src/simurg/pipeline/pgx_db.py @@ -168,6 +168,51 @@ def run_db_build(o: PgxDbOptions, rep: Reporter) -> int: rows["sites"].append((s.gene, lid, s.chrom, full_hgvs(s.chrom, s.hgvs), s.rsid, s.ref_value)) for val, (pos, ref, alt) in sorted(s.alleles.items()): rows["site_values"].append((s.gene, lid, val, pos, ref, alt)) + # PharmVar genes that CPIC does not define (PGX-001): core alleles from the GRCh38 haplotype VCFs + pv_src = None + pdir = _latest(o.sources, "pharmvar") + if pdir is not None: + import zipfile + + from simurg.pgx.db import normalize, pharmvar_core_alleles + + pv_src = json.loads((pdir / "source.json").read_text()) + lid = 900_000_000 + for zpath in sorted(pdir.glob("*.zip")): + gene = zpath.stem + if gene in defs: + continue + with zipfile.ZipFile(zpath) as z: + vcfs = {n: z.read(n).decode() for n in z.namelist() if "/GRCh38/" in n and n.endswith(".vcf")} + core = pharmvar_core_alleles(gene, vcfs) + if not core: + continue + core.setdefault("*1", []) # the reference haplotype has no core VCF of its own + chrom = None + n_var = 0 + for allele in sorted(core, key=lambda a: int(a[1:])): + bad = False + norm: list[tuple[str, int, str, str, str | None, str]] = [] + for c, vpos, r, alt_v, rsid in core[allele]: + contig = cmap.get(c) + if contig is None or genome.fetch(contig, vpos - 1, vpos - 1 + len(r)) != r: + bad = True + break + npos, nref, nalt = normalize(genome.fetch, contig, vpos, vpos + len(r) - 1, alt_v) + norm.append((contig, npos, nref, nalt, rsid, f"g.{vpos}{r}>{alt_v}")) + chrom = c + if bad: + rep.info(f"definition note: PharmVar {gene} {allele}: REF does not match the FASTA, skipped") + continue + kind = "reference" if allele == "*1" else "star_allele" + rows["alleles"].append((gene, allele, kind, None, None, 0, int(bool(norm) or allele == "*1"))) + for contig, pos, ref, alt, rsid, hg in norm: + lid += 1 + rows["allele_variants"].append((gene, allele, "GRCh38", contig, pos, ref, alt, + full_hgvs(contig, hg), rsid, lid, alt)) # fmt: skip + n_var += 1 + rows["genes"].append((gene, "star_allele", chrom)) + rep.info(f"PharmVar {gene}: {len(core)} core allele(s), {n_var} variant row(s)") results = {r["id"]: r for r in t["gene_result"]} for lk in t["gene_result_lookup"]: res = results.get(lk["phenotypeid"]) @@ -222,7 +267,10 @@ def run_db_build(o: PgxDbOptions, rep: Reporter) -> int: meta = {"schema": "simurg.pgx_db", "schema_version": str(DB_SCHEMA), "builder": f"SimurgArray {__version__}", "genome_build": "GRCh38", "fasta_sha256": genome.sha256, "sqlite_version": sqlite3.sqlite_version, "allele_sources": json.dumps([{"name": "CPIC", "version": csrc["version"], "retrieved": csrc["retrieved"], - "license": csrc.get("license")}]), + "license": csrc.get("license")}] + + ([{"name": "PharmVar", "version": pv_src["version"], + "retrieved": pv_src["retrieved"], "license": pv_src.get("license")}] + if pv_src else [])), "phenotype_sources": json.dumps([{"name": "CPIC", "version": csrc["version"]}]), "cpic_files_sha256": json.dumps(csrc.get("files", {}), sort_keys=True), "manifests": json.dumps(sorted(manifests)), "definition_notes": json.dumps(problems)} # fmt: skip diff --git a/src/simurg/pipeline/pgx_star.py b/src/simurg/pipeline/pgx_star.py index 96999a4..e2dc116 100644 --- a/src/simurg/pipeline/pgx_star.py +++ b/src/simurg/pipeline/pgx_star.py @@ -37,12 +37,15 @@ from simurg.pgx.star import ( MAX_EDIT_DISTANCE = 3 # PGX-019 -CN_REGION = {"CYP2D6": "CYP2D6.p5"} # PGX-013: total gene copies from the 5′ region -CN_SOLUTION_REGIONS = {"CYP2D6": ("CYP2D6.p5", "CYP2D6.intron.2", "CYP2D6.exon.9")} # PGX-027 copy_number_solution +CN_REGION = {"CYP2D6": "CYP2D6.p5", "CYP2A6": "CYP2A6.exon.1"} # PGX-013: gene copies from the 5′ region +CN_SOLUTION_REGIONS = { + "CYP2D6": ("CYP2D6.p5", "CYP2D6.intron.2", "CYP2D6.exon.9"), + "CYP2A6": ("CYP2A6.exon.1", "CYP2A6.intron.7"), +} # PGX-027 copy_number_solution CN_ONLY_GENES = {"GSTM1": "GSTM1", "GSTT1": "GSTT1", "UGT2B17": "UGT2B17"} # PGX-006: *0 from whole-gene CN ALL_BASES = ("star_allele", "haplotype_named", "variant_based", "tagging_variant") TAG_CAVEAT = "tag reliability varies by population" -DELETION_ALLELE = {"CYP2D6": "*5"} +DELETION_ALLELE = {"CYP2D6": "*5", "CYP2A6": "*4"} # named whole-gene deletions (PGX-006) @dataclass @@ -100,6 +103,10 @@ def load_models( if fr.get(n): freq[i] = float(np.mean(fr[n])) freq = np.nan_to_num(freq, nan=0.0) + if not freq.any() and names: # no population frequencies (PharmVar genes): reference 0.8, others share 0.2 + freq = np.full(len(names), 0.2 / max(1, len(names) - 1)) + if ref in names: + freq[names.index(ref)] = 0.8 fa = {a: (f, v) for a, f, v in con.execute("SELECT allele, function, activity FROM alleles WHERE gene = ?", (gene,))} # fmt: skip act = {} diff --git a/tests/unit/test_pgx_db.py b/tests/unit/test_pgx_db.py index 30f68c1..08b7598 100644 --- a/tests/unit/test_pgx_db.py +++ b/tests/unit/test_pgx_db.py @@ -147,3 +147,12 @@ def test_coverage_statuses(tmp_path: Path) -> None: # PGX-009 / PGX-035 st = {r["allele"]: r["status"] for r in coverage_rows(db, "M")} assert st["*1"] == "callable" and st["*2"] == "callable" assert st["*3"] == "masked" and st["*5"] == "not_covered" + + +def test_pharmvar_core_alleles() -> None: # PGX-001 PharmVar source + from simurg.pgx.db import pharmvar_core_alleles + + vcf = "##fileformat=VCFv4.1\n#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\nchr19\t100\trs1\tA\tC\t.\t.\t.\n" + core = pharmvar_core_alleles("CYP2A6", {"CYP2A6-6.2/GRCh38/CYP2A6_9.vcf": vcf, + "CYP2A6-6.2/GRCh38/CYP2A6_9.001.vcf": vcf}) # fmt: skip + assert core == {"*9": [("chr19", 100, "A", "C", "rs1")]} # sub-alleles (*9.001) are not core definitions