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).
This commit is contained in:
1 parent
46422b8de4
commit
dfba22496a
9 files changed
+289
-14
No files matched your search
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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]]]:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = {}
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in new issue
Block a user