PGx: PharmVar genes (CYP1A2, CYP2A6, CYP2A13, CYP2C8, CYP3A4), CYP2A6 *4 from CN (PGX-001/006)
ci / test (3.12) (push) Failing after 8s
ci / test (3.13) (push) Failing after 10s

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).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01814GKVzZVL3FuF3DeiZXhv
This commit is contained in:
kemalandClaude Opus 5.5 committed 2026-09-29 16:50:27 +01:00
1 parent 1428535e91
commit 4176ef33de
9 files changed
+289 -14

No files matched your search

+2
View File
@@ -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`) | | [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 | | [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 | | [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) ## BSD-2-Clause (BeadArrayFiles, verbatim from its README)
+12 -5
View File
@@ -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 | | GSTM1 (CN: DEL/DEL vs present) | 9/9 | 9/9 |
| GSTT1 (CN 0/1/2) | 7/9 | 7/9 | | GSTT1 (CN 0/1/2) | 7/9 | 7/9 |
| UGT2B17 (CN) | 2/2 | 2/2 | | 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 PGX-032 target (≥ 90 % for CYP2D6/CYP2C19/CYP2C9 against truth): **42/45 = 93 %** (top solution), 45/45 within
the array-equivalent feasible solutions. 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). array cannot separate (both within the feasible set).
Details and every discordant call: `V2_pgx_star_getrm16.json`. 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`; Not in the total (GeT-RM uses older names or definitions than CPIC/PharmVar today): UGT1A1 (GeT-RM `*60`;
0/9 top) and SLCO1B1 (`*1B/*17/*21` vs CPIC `*37/*15/*20`; 9/15 top). 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 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. 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, `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 positions resolved from the manifest's probes, flag `tag_caveat`), and GSTM1/GSTT1/UGT2B17 `*0` genotypes from
the targeted CN (PGX-006). the targeted CN (PGX-006).
* Not yet: PharmVar-only * PharmVar genes without CPIC definitions (CYP1A2, CYP2A6, CYP2A13, CYP2C8, CYP3A4; PharmVar 6.2.29): core alleles
genes (CYP2A6, CYP3A4, CYP1A2, …); DPWG tables. 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.
+151 -3
View File
@@ -1,6 +1,16 @@
{ {
"individuals": 30, "individuals": 30,
"genes": { "genes": {
"CYP1A2": {
"n": 9,
"top": 0,
"equivalent": 0
},
"CYP2A6": {
"n": 9,
"top": 7,
"equivalent": 8
},
"CYP2B6": { "CYP2B6": {
"n": 14, "n": 14,
"top": 12, "top": 12,
@@ -12,6 +22,11 @@
"top": 14, "top": 14,
"equivalent": 15 "equivalent": 15
}, },
"CYP2C8": {
"n": 15,
"top": 8,
"equivalent": 9
},
"CYP2C9": { "CYP2C9": {
"n": 15, "n": 15,
"top": 15, "top": 15,
@@ -22,6 +37,11 @@
"top": 13, "top": 13,
"equivalent": 15 "equivalent": 15
}, },
"CYP3A4": {
"n": 15,
"top": 15,
"equivalent": 15
},
"CYP3A5": { "CYP3A5": {
"n": 15, "n": 15,
"top": 15, "top": 15,
@@ -79,15 +99,31 @@
} }
}, },
"legacy_nomenclature_genes": [ "legacy_nomenclature_genes": [
"CYP1A2",
"CYP2C8",
"SLCO1B1", "SLCO1B1",
"UGT1A1" "UGT1A1"
], ],
"total": { "total": {
"n": 144, "n": 168,
"top": 132, "top": 154,
"equivalent": 135 "equivalent": 158
}, },
"discordant": [ "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", "individual": "NA12156",
"gene": "CYP4F2", "gene": "CYP4F2",
@@ -116,6 +152,20 @@
"call": "*1/*1", "call": "*1/*1",
"within_equivalents": false "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", "individual": "NA07019",
"gene": "UGT1A1", "gene": "UGT1A1",
@@ -123,6 +173,13 @@
"call": "*1/*1", "call": "*1/*1",
"within_equivalents": false "within_equivalents": false
}, },
{
"individual": "NA06991",
"gene": "CYP1A2",
"truth": "*1F/*1F",
"call": "*15/*24",
"within_equivalents": false
},
{ {
"individual": "NA06991", "individual": "NA06991",
"gene": "UGT1A1", "gene": "UGT1A1",
@@ -137,6 +194,13 @@
"call": "rs9923231 T/T", "call": "rs9923231 T/T",
"within_equivalents": false "within_equivalents": false
}, },
{
"individual": "NA12878",
"gene": "CYP1A2",
"truth": "*1F/*1F",
"call": "*15/*24",
"within_equivalents": false
},
{ {
"individual": "NA12878", "individual": "NA12878",
"gene": "UGT1A1", "gene": "UGT1A1",
@@ -144,6 +208,20 @@
"call": "*28/*80", "call": "*28/*80",
"within_equivalents": false "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", "individual": "NA11993",
"gene": "SLCO1B1", "gene": "SLCO1B1",
@@ -158,6 +236,20 @@
"call": "*28/*80", "call": "*28/*80",
"within_equivalents": false "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", "individual": "NA12003",
"gene": "UGT1A1", "gene": "UGT1A1",
@@ -179,6 +271,13 @@
"call": "*1/*0", "call": "*1/*0",
"within_equivalents": false "within_equivalents": false
}, },
{
"individual": "NA07029",
"gene": "CYP1A2",
"truth": "*1A/*1F",
"call": "*1/*15",
"within_equivalents": false
},
{ {
"individual": "NA07029", "individual": "NA07029",
"gene": "CYP2B6", "gene": "CYP2B6",
@@ -193,6 +292,13 @@
"call": "*1/*17", "call": "*1/*17",
"within_equivalents": true "within_equivalents": true
}, },
{
"individual": "NA07029",
"gene": "CYP2C8",
"truth": "*1/*3",
"call": "*19/*3",
"within_equivalents": false
},
{ {
"individual": "NA07029", "individual": "NA07029",
"gene": "UGT1A1", "gene": "UGT1A1",
@@ -207,6 +313,27 @@
"call": "*1/*5", "call": "*1/*5",
"within_equivalents": false "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", "individual": "NA11832",
"gene": "NAT2", "gene": "NAT2",
@@ -242,6 +369,20 @@
"call": "*1/*14", "call": "*1/*14",
"within_equivalents": false "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", "individual": "NA12813",
"gene": "CYP2D6", "gene": "CYP2D6",
@@ -270,6 +411,13 @@
"call": "*1/*0", "call": "*1/*0",
"within_equivalents": false "within_equivalents": false
}, },
{
"individual": "NA11881",
"gene": "CYP2C8",
"truth": "*1/*1",
"call": "*1/*19",
"within_equivalents": false
},
{ {
"individual": "NA11881", "individual": "NA11881",
"gene": "SLCO1B1", "gene": "SLCO1B1",
+32 -1
View File
@@ -2,7 +2,9 @@
# Download the public PGx sources for `simurg pgx db build` (spec PGX-001) into a dated folder. # 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) # 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 # 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 set -euo pipefail
OUT=${1:-data/pgx/sources} OUT=${1:-data/pgx/sources}
DATE=$(date -u +%Y%m%d) 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)) "files": files}, indent=1))
print("CPIC data version", ver) print("CPIC data version", ver)
PY 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
+3 -1
View File
@@ -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 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]]]: def truth_alternatives(text: str) -> list[list[set[str]]]:
+21
View File
@@ -237,3 +237,24 @@ def result_basis(gene: str, names: list[str]) -> str:
if names and all(n.startswith("*") for n in names): if names and all(n.startswith("*") for n in names):
return "star_allele" return "star_allele"
return "variant_based" 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
+49 -1
View File
@@ -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)) 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()): for val, (pos, ref, alt) in sorted(s.alleles.items()):
rows["site_values"].append((s.gene, lid, val, pos, ref, alt)) 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"]} results = {r["id"]: r for r in t["gene_result"]}
for lk in t["gene_result_lookup"]: for lk in t["gene_result_lookup"]:
res = results.get(lk["phenotypeid"]) 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__}", 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, "genome_build": "GRCh38", "fasta_sha256": genome.sha256, "sqlite_version": sqlite3.sqlite_version,
"allele_sources": json.dumps([{"name": "CPIC", "version": csrc["version"], "retrieved": csrc["retrieved"], "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"]}]), "phenotype_sources": json.dumps([{"name": "CPIC", "version": csrc["version"]}]),
"cpic_files_sha256": json.dumps(csrc.get("files", {}), sort_keys=True), "cpic_files_sha256": json.dumps(csrc.get("files", {}), sort_keys=True),
"manifests": json.dumps(sorted(manifests)), "definition_notes": json.dumps(problems)} # fmt: skip "manifests": json.dumps(sorted(manifests)), "definition_notes": json.dumps(problems)} # fmt: skip
+10 -3
View File
@@ -37,12 +37,15 @@ from simurg.pgx.star import (
MAX_EDIT_DISTANCE = 3 # PGX-019 MAX_EDIT_DISTANCE = 3 # PGX-019
CN_REGION = {"CYP2D6": "CYP2D6.p5"} # PGX-013: total gene copies from the 5′ region 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")} # PGX-027 copy_number_solution 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 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") ALL_BASES = ("star_allele", "haplotype_named", "variant_based", "tagging_variant")
TAG_CAVEAT = "tag reliability varies by population" TAG_CAVEAT = "tag reliability varies by population"
DELETION_ALLELE = {"CYP2D6": "*5"} DELETION_ALLELE = {"CYP2D6": "*5", "CYP2A6": "*4"} # named whole-gene deletions (PGX-006)
@dataclass @dataclass
@@ -100,6 +103,10 @@ def load_models(
if fr.get(n): if fr.get(n):
freq[i] = float(np.mean(fr[n])) freq[i] = float(np.mean(fr[n]))
freq = np.nan_to_num(freq, nan=0.0) 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 = ?", fa = {a: (f, v) for a, f, v in con.execute("SELECT allele, function, activity FROM alleles WHERE gene = ?",
(gene,))} # fmt: skip (gene,))} # fmt: skip
act = {} act = {}
+9
View File
@@ -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")} st = {r["allele"]: r["status"] for r in coverage_rows(db, "M")}
assert st["*1"] == "callable" and st["*2"] == "callable" assert st["*1"] == "callable" and st["*2"] == "callable"
assert st["*3"] == "masked" and st["*5"] == "not_covered" 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