allelix 2.4.1__tar.gz → 2.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {allelix-2.4.1/allelix.egg-info → allelix-2.4.2}/PKG-INFO +1 -1
- {allelix-2.4.1 → allelix-2.4.2}/allelix/annotators/__init__.py +6 -1
- {allelix-2.4.1 → allelix-2.4.2}/allelix/annotators/_resolution.py +13 -6
- {allelix-2.4.1 → allelix-2.4.2}/allelix/annotators/base.py +27 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/annotators/clinvar.py +102 -20
- {allelix-2.4.1 → allelix-2.4.2}/allelix/annotators/pharmgkb.py +26 -4
- {allelix-2.4.1 → allelix-2.4.2}/allelix/annotators/rsid_identity.py +2 -1
- {allelix-2.4.1 → allelix-2.4.2}/allelix/annotators/snpedia.py +24 -12
- {allelix-2.4.1 → allelix-2.4.2}/allelix/databases/_versions.py +7 -1
- {allelix-2.4.1 → allelix-2.4.2}/allelix/databases/manager.py +54 -6
- {allelix-2.4.1 → allelix-2.4.2}/allelix/models.py +11 -2
- {allelix-2.4.1 → allelix-2.4.2}/allelix/reports/_pipeline.py +7 -3
- {allelix-2.4.1 → allelix-2.4.2}/allelix/utils/allele.py +3 -2
- {allelix-2.4.1 → allelix-2.4.2/allelix.egg-info}/PKG-INFO +1 -1
- {allelix-2.4.1 → allelix-2.4.2}/pyproject.toml +1 -1
- {allelix-2.4.1 → allelix-2.4.2}/LICENSE +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/MANIFEST.in +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/README.md +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/__init__.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/annotators/alphamissense.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/annotators/cadd.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/annotators/gnomad.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/annotators/gwas.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/cli/__init__.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/cli/_helpers.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/cli/_options.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/cli/analyze.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/cli/config.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/cli/db.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/cli/focused.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/cli/utility.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/compare.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/config.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/data/__init__.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/data/clinvar_clnsig_snapshot.yaml +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/data/high_value_snps.yaml +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/databases/__init__.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/databases/alphamissense_loader.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/databases/cadd_loader.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/databases/cpic_loader.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/databases/gnomad_loader.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/databases/gwas_loader.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/databases/loader_utils.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/databases/pharmgkb_loader.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/databases/rsid_identity_loader.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/databases/schema.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/databases/snpedia_loader.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/databases/snpedia_parser.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/exporters/__init__.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/exporters/plink.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/parsers/__init__.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/parsers/_helpers.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/parsers/ancestrydna.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/parsers/base.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/parsers/ftdna.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/parsers/ftdna_famfinder.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/parsers/ftdna_illumina.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/parsers/livingdna.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/parsers/myhappygenes.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/parsers/myheritage.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/parsers/twentythreeandme.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/parsers/vcf.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/py.typed +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/reports/__init__.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/reports/build_text.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/reports/diff.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/reports/high_value.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/reports/html.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/reports/json_report.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/reports/methylation.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/reports/terminal.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/reports/vcf.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/utils/__init__.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/utils/build_detect.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix/utils/plausibility.py +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix.egg-info/SOURCES.txt +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix.egg-info/dependency_links.txt +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix.egg-info/entry_points.txt +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix.egg-info/requires.txt +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/allelix.egg-info/top_level.txt +0 -0
- {allelix-2.4.1 → allelix-2.4.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: allelix
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.2
|
|
4
4
|
Summary: Open-source genotype analysis toolkit. Format-agnostic ingestion, database-agnostic annotation, offline-first.
|
|
5
5
|
Author: Allelix
|
|
6
6
|
Maintainer-email: dial481 <dial481@users.noreply.github.com>
|
|
@@ -54,9 +54,14 @@ def get_annotators(
|
|
|
54
54
|
data_dir,
|
|
55
55
|
clinvar_ref_provider=clinvar.reference_for,
|
|
56
56
|
palindromic_provider=rsid_identity.palindromic_rsids,
|
|
57
|
+
normal_allele_provider=clinvar.normal_allele_for,
|
|
57
58
|
)
|
|
58
59
|
gwas = GWASCatalogAnnotator(data_dir, filter_traits=gwas_filter_traits)
|
|
59
|
-
snpedia = SNPediaAnnotator(
|
|
60
|
+
snpedia = SNPediaAnnotator(
|
|
61
|
+
data_dir,
|
|
62
|
+
clinvar_ref_provider=clinvar.reference_for,
|
|
63
|
+
normal_allele_provider=clinvar.normal_allele_for,
|
|
64
|
+
)
|
|
60
65
|
gnomad = GnomadAnnotator(data_dir)
|
|
61
66
|
alphamissense = AlphaMissenseAnnotator(data_dir)
|
|
62
67
|
cadd = CaddAnnotator(data_dir, full_mode=cadd_full)
|
|
@@ -129,11 +129,18 @@ def resolve_rsidless(
|
|
|
129
129
|
|
|
130
130
|
|
|
131
131
|
def _populate_ref_from_identity(identity: RsidIdentityAnnotator, variants: list[Variant]) -> None:
|
|
132
|
-
"""Set ``Variant.ref`` from the identity table's REF
|
|
132
|
+
"""Set ``Variant.ref`` from the identity table's REF for the call's own rsID.
|
|
133
133
|
|
|
134
|
-
Only for calls that carry an rsID,
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
Only for calls that carry an rsID, only from the identity rows of that
|
|
135
|
+
rsID at the variant's locus, and only when those rows give a single REF
|
|
136
|
+
of the call's shape: single-base for single-base calls (an indel row's
|
|
137
|
+
anchor REF can share the position), any length otherwise.
|
|
138
|
+
|
|
139
|
+
A call whose rsID the identity table does not hold (a withdrawn or
|
|
140
|
+
merged ID) gets no REF, even when another rsID's rows sit at the same
|
|
141
|
+
position: nothing confirms that the other rsID is the same variant.
|
|
142
|
+
Such a call keeps ``ref`` None, and ClinVar matches it by locus only
|
|
143
|
+
when ClinVar holds its rsID.
|
|
137
144
|
|
|
138
145
|
A call without an rsID (array vendor probes such as 23andMe
|
|
139
146
|
``i5000123``) never gets a REF from position alone. The REF at a locus
|
|
@@ -154,9 +161,9 @@ def _populate_ref_from_identity(identity: RsidIdentityAnnotator, variants: list[
|
|
|
154
161
|
]
|
|
155
162
|
for build in {v.build for v in pending}:
|
|
156
163
|
group = [v for v in pending if v.build == build]
|
|
157
|
-
|
|
164
|
+
rows_at = identity.alleles_at_loci(build, {variant_locus(v) for v in group})
|
|
158
165
|
for v in group:
|
|
159
|
-
refs =
|
|
166
|
+
refs = {ref for ref, _alt, rsid in rows_at.get(variant_locus(v), []) if rsid == v.rsid}
|
|
160
167
|
if len(v.allele1) <= 1 and len(v.allele2) <= 1:
|
|
161
168
|
refs = {r for r in refs if len(r) == 1}
|
|
162
169
|
if len(refs) == 1:
|
|
@@ -10,6 +10,7 @@ from enum import Enum, auto
|
|
|
10
10
|
from typing import TYPE_CHECKING, ClassVar
|
|
11
11
|
|
|
12
12
|
from allelix.parsers._helpers import normalize_chromosome
|
|
13
|
+
from allelix.utils.allele import derive_alt_from_diploid
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
16
|
from collections.abc import Callable, Iterable, Iterator
|
|
@@ -95,6 +96,32 @@ def is_clinvar_homref(
|
|
|
95
96
|
return ref is not None and len(ref) == 1 and variant.allele1 == ref and variant.allele2 == ref
|
|
96
97
|
|
|
97
98
|
|
|
99
|
+
def carried_alt(
|
|
100
|
+
variant: Variant,
|
|
101
|
+
allele1: str,
|
|
102
|
+
allele2: str,
|
|
103
|
+
normal_allele_provider: Callable[[str, str], str | None] | None,
|
|
104
|
+
) -> tuple[str, bool]:
|
|
105
|
+
"""The non-normal allele of a matched genotype, for per-genotype sources.
|
|
106
|
+
|
|
107
|
+
Returns ``(alt, reference_carries_alt)``. The alt is derived against the
|
|
108
|
+
normal allele, which is ``variant.ref`` except where the build's
|
|
109
|
+
reference carries a ClinVar variant allele: there
|
|
110
|
+
``normal_allele_provider`` (``ClinVarAnnotator.normal_allele_for``) gives
|
|
111
|
+
the normal allele from the other build (C at F5 rs6025 on GRCh37, whose
|
|
112
|
+
reference is the Leiden T). The alt is then the same on both builds,
|
|
113
|
+
and it can equal ``variant.ref``, which the Annotation is told through
|
|
114
|
+
``reference_carries_alt``.
|
|
115
|
+
"""
|
|
116
|
+
normal = variant.ref
|
|
117
|
+
if normal is not None and normal_allele_provider is not None and variant.rsid.startswith("rs"):
|
|
118
|
+
other = normal_allele_provider(variant.rsid, variant.build)
|
|
119
|
+
if other is not None and len(other) == 1 and other != normal:
|
|
120
|
+
normal = other
|
|
121
|
+
alt = derive_alt_from_diploid(normal, allele1, allele2)
|
|
122
|
+
return alt, bool(alt) and alt == variant.ref
|
|
123
|
+
|
|
124
|
+
|
|
98
125
|
class Annotator(ABC):
|
|
99
126
|
"""Base class for all reference database annotators.
|
|
100
127
|
|
|
@@ -36,6 +36,7 @@ from allelix.utils.allele import (
|
|
|
36
36
|
resolve_rsid_at_locus,
|
|
37
37
|
strand_aware_carrier_match,
|
|
38
38
|
)
|
|
39
|
+
from allelix.utils.build_detect import BUILD_GRCH37, BUILD_GRCH38
|
|
39
40
|
|
|
40
41
|
if TYPE_CHECKING:
|
|
41
42
|
from collections.abc import Iterable, Iterator
|
|
@@ -248,6 +249,10 @@ class ClinVarAnnotator(Annotator):
|
|
|
248
249
|
# consults this as its primary non-finding filter. Built lazily
|
|
249
250
|
# on first lookup per build.
|
|
250
251
|
self._ref_lookups: dict[str, dict[str, str]] = {}
|
|
252
|
+
# Per build, the rsIDs where ``reference_for`` gives the normal
|
|
253
|
+
# allele from the other build (the build's reference carries a
|
|
254
|
+
# ClinVar variant allele), with that allele.
|
|
255
|
+
self._normal_alleles: dict[str, dict[str, str]] = {}
|
|
251
256
|
|
|
252
257
|
def _connection(self, build: str) -> sqlite3.Connection | None:
|
|
253
258
|
"""Return a lazy connection to the per-build cache, or None if missing.
|
|
@@ -383,6 +388,15 @@ class ClinVarAnnotator(Annotator):
|
|
|
383
388
|
return False
|
|
384
389
|
return True
|
|
385
390
|
|
|
391
|
+
def _cache_is_current(self, build: str) -> bool:
|
|
392
|
+
"""True when ``build``'s cache carries the current interpreter-version tag."""
|
|
393
|
+
if build not in self._db_paths:
|
|
394
|
+
return False
|
|
395
|
+
info = get_database_info(self._db_paths[build], clinvar_record_name(build))
|
|
396
|
+
return info is not None and info.get("local_version_tag") == (
|
|
397
|
+
f"iv:{CLINVAR_INTERPRETER_VERSION}"
|
|
398
|
+
)
|
|
399
|
+
|
|
386
400
|
def version(self) -> str | None:
|
|
387
401
|
"""Composite version string across managed builds.
|
|
388
402
|
|
|
@@ -413,6 +427,7 @@ class ClinVarAnnotator(Annotator):
|
|
|
413
427
|
conn.close()
|
|
414
428
|
self._conns.clear()
|
|
415
429
|
self._ref_lookups.clear()
|
|
430
|
+
self._normal_alleles.clear()
|
|
416
431
|
|
|
417
432
|
def reference_for(self, rsid: str, build: str) -> str | None:
|
|
418
433
|
"""Return ClinVar's single-base REF allele for `rsid` in `build`, or None.
|
|
@@ -426,6 +441,15 @@ class ClinVarAnnotator(Annotator):
|
|
|
426
441
|
skipped — array-based parsers can't call indels, so a multi-base
|
|
427
442
|
REF can't validly suppress a single-base genotype.
|
|
428
443
|
|
|
444
|
+
Where this build's reference carries a ClinVar variant allele (a
|
|
445
|
+
REF == ALT row, F5 c.1601G>A on GRCh37), a homozygous-reference
|
|
446
|
+
call is the variant genotype. There the normal allele is returned
|
|
447
|
+
instead: the REF of the same ClinVar variant on the other build,
|
|
448
|
+
when that row is a single-base REF != ALT (C for F5), so the same
|
|
449
|
+
call is a non-finding on both builds. Without such a row (only one
|
|
450
|
+
build's cache installed, or REF == ALT on both builds) it returns
|
|
451
|
+
None and nothing is suppressed.
|
|
452
|
+
|
|
429
453
|
Returns None when ClinVar has no data for the rsid in this build
|
|
430
454
|
(or has only indel REFs). Callers fall through to secondary tiers.
|
|
431
455
|
"""
|
|
@@ -449,17 +473,72 @@ class ClinVarAnnotator(Annotator):
|
|
|
449
473
|
# so peak memory is one row + the dict, not the entire result set
|
|
450
474
|
# twice.
|
|
451
475
|
cursor = conn.execute(
|
|
452
|
-
"SELECT DISTINCT rsid, ref FROM clinvar_variants WHERE length(ref) = 1"
|
|
476
|
+
"SELECT DISTINCT rsid, ref, alt, allele_id FROM clinvar_variants WHERE length(ref) = 1"
|
|
453
477
|
)
|
|
454
478
|
out: dict[str, str] = {}
|
|
455
|
-
|
|
479
|
+
# rsID -> VariationIDs whose row is REF == ALT: this build's
|
|
480
|
+
# reference carries their variant allele.
|
|
481
|
+
reference_carried: dict[str, set[int]] = {}
|
|
482
|
+
for rsid, ref, alt, variation_id in cursor:
|
|
483
|
+
if ref == alt:
|
|
484
|
+
reference_carried.setdefault(rsid, set()).add(variation_id)
|
|
456
485
|
# If a rsid has multiple single-base REFs (shouldn't happen at
|
|
457
486
|
# one position but defending against future data shapes), keep
|
|
458
487
|
# the first.
|
|
459
488
|
if rsid not in out:
|
|
460
489
|
out[rsid] = ref
|
|
490
|
+
# Only a current cache gives REF == ALT its one meaning; an older
|
|
491
|
+
# one still holds normal-allele records written REF == ALT, and
|
|
492
|
+
# keeps the locus REF (the 2.4.1 behaviour) until `db update`.
|
|
493
|
+
normal: dict[str, str] = {}
|
|
494
|
+
if reference_carried and self._cache_is_current(build):
|
|
495
|
+
normal = self._normal_alleles_on_other_build(build, reference_carried)
|
|
496
|
+
for rsid in reference_carried:
|
|
497
|
+
if rsid in normal:
|
|
498
|
+
out[rsid] = normal[rsid]
|
|
499
|
+
else:
|
|
500
|
+
out.pop(rsid, None)
|
|
501
|
+
self._normal_alleles[build] = normal
|
|
461
502
|
return out
|
|
462
503
|
|
|
504
|
+
def normal_allele_for(self, rsid: str, build: str) -> str | None:
|
|
505
|
+
"""The normal allele at `rsid` where `build`'s reference carries a variant, or None.
|
|
506
|
+
|
|
507
|
+
Set only where ``reference_for`` returns the other build's REF of
|
|
508
|
+
the same ClinVar variant (C at F5 rs6025 on GRCh37). ClinPGx and
|
|
509
|
+
SNPedia derive their ``alt`` against it there; everywhere else
|
|
510
|
+
they keep ``Variant.ref``, even where ClinVar's REF differs from it.
|
|
511
|
+
"""
|
|
512
|
+
self.reference_for(rsid, build)
|
|
513
|
+
return self._normal_alleles.get(build, {}).get(rsid)
|
|
514
|
+
|
|
515
|
+
def _normal_alleles_on_other_build(
|
|
516
|
+
self, build: str, reference_carried: dict[str, set[int]]
|
|
517
|
+
) -> dict[str, str]:
|
|
518
|
+
"""The normal allele at each reference-carried rsID, from the other build.
|
|
519
|
+
|
|
520
|
+
For each rsID, the single-base REF of the same VariationIDs on the
|
|
521
|
+
other build where that row's REF differs from its ALT. An rsID with
|
|
522
|
+
no such row, or more than one candidate REF, is left out.
|
|
523
|
+
"""
|
|
524
|
+
other = BUILD_GRCH38 if build == BUILD_GRCH37 else BUILD_GRCH37
|
|
525
|
+
conn = self._connection(other)
|
|
526
|
+
if conn is None or not self._cache_is_current(other):
|
|
527
|
+
return {}
|
|
528
|
+
rsids = sorted(reference_carried)
|
|
529
|
+
candidates: dict[str, set[str]] = {}
|
|
530
|
+
for start in range(0, len(rsids), _BATCH_CHUNK):
|
|
531
|
+
chunk = rsids[start : start + _BATCH_CHUNK]
|
|
532
|
+
placeholders = ",".join("?" * len(chunk))
|
|
533
|
+
for rsid, ref, alt, variation_id in conn.execute(
|
|
534
|
+
f"SELECT DISTINCT rsid, ref, alt, allele_id FROM clinvar_variants "
|
|
535
|
+
f"WHERE rsid IN ({placeholders})",
|
|
536
|
+
chunk,
|
|
537
|
+
):
|
|
538
|
+
if variation_id in reference_carried[rsid] and len(ref) == 1 and ref != alt:
|
|
539
|
+
candidates.setdefault(rsid, set()).add(ref)
|
|
540
|
+
return {rsid: refs.pop() for rsid, refs in candidates.items() if len(refs) == 1}
|
|
541
|
+
|
|
463
542
|
def fetch_remote_signal(self) -> str | None:
|
|
464
543
|
r"""Freshness signal derived from the per-SCV submission_summary md5.
|
|
465
544
|
|
|
@@ -691,9 +770,13 @@ class ClinVarAnnotator(Annotator):
|
|
|
691
770
|
|
|
692
771
|
A row annotates only when all of these hold:
|
|
693
772
|
|
|
694
|
-
- REF
|
|
695
|
-
|
|
696
|
-
allele
|
|
773
|
+
- REF == ALT means this build's reference carries the variant
|
|
774
|
+
allele (F5 c.1601G>A is T>T on GRCh37, whose reference base is
|
|
775
|
+
the Leiden allele). The loader keeps no other REF == ALT rows:
|
|
776
|
+
it drops normal-allele records (HGVS ``c.N=``) written REF ==
|
|
777
|
+
ALT and REF == ALT rows that are not SNVs (cache ``iv:5``). Such
|
|
778
|
+
a row annotates a user who carries its ALT, counting copies of
|
|
779
|
+
it, like any other row.
|
|
697
780
|
- The row's REF agrees with ``variant.ref`` when the input supplies
|
|
698
781
|
one (directly, or as the complement for a reverse-stranded
|
|
699
782
|
input). Together with the locus this identifies the variant by
|
|
@@ -705,26 +788,26 @@ class ClinVarAnnotator(Annotator):
|
|
|
705
788
|
|
|
706
789
|
Two guards decide whether the locus may be matched at all:
|
|
707
790
|
|
|
708
|
-
- A call with no reference allele
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
791
|
+
- A call with no reference allele is locus-matched only when
|
|
792
|
+
ClinVar holds its rsID on this build (``rsid_loci``, every
|
|
793
|
+
position ClinVar records for that rsID). That excludes array
|
|
794
|
+
vendor probes without an rsID (23andMe ``i5000123``) and array
|
|
795
|
+
calls whose rsID neither ClinVar nor the identity table holds (a
|
|
796
|
+
withdrawn or merged ID): nothing confirms that the row at the
|
|
797
|
+
position describes the same variant. The pipeline fills REF only
|
|
798
|
+
from the identity rows of the call's own rsID, so a call with a
|
|
799
|
+
REF is either one of those or a VCF record whose REF came from
|
|
800
|
+
the file, and the REF check applies to it.
|
|
715
801
|
- When the input carries an rsID that ClinVar places elsewhere
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
belong to a different variant and are not reported under the
|
|
719
|
-
input's rsID. An rsID ClinVar does not hold at all (a retired or
|
|
720
|
-
merged ID) still matches by locus.
|
|
802
|
+
and not at the input's locus, the rows at the locus belong to a
|
|
803
|
+
different variant and are not reported under the input's rsID.
|
|
721
804
|
|
|
722
805
|
The Annotation's rsID is the input's when it has one, otherwise
|
|
723
806
|
the matching row's. identical per-SCV rows collapse.
|
|
724
807
|
"""
|
|
725
808
|
if variant.is_no_call:
|
|
726
809
|
return []
|
|
727
|
-
if variant.ref is None and not
|
|
810
|
+
if variant.ref is None and not rsid_loci:
|
|
728
811
|
return []
|
|
729
812
|
if rsid_loci and variant_locus(variant) not in rsid_loci:
|
|
730
813
|
return []
|
|
@@ -746,8 +829,6 @@ class ClinVarAnnotator(Annotator):
|
|
|
746
829
|
allele_id,
|
|
747
830
|
row_rsid,
|
|
748
831
|
) = row
|
|
749
|
-
if ref == alt:
|
|
750
|
-
continue
|
|
751
832
|
if variant.ref is not None and variant.ref not in (ref, complement(ref)):
|
|
752
833
|
continue
|
|
753
834
|
clinvar_is_indel = len(ref) > 1 or len(alt) > 1
|
|
@@ -785,6 +866,7 @@ class ClinVarAnnotator(Annotator):
|
|
|
785
866
|
variant.allele1, variant.allele2, alt
|
|
786
867
|
),
|
|
787
868
|
locus=variant_locus(variant),
|
|
869
|
+
reference_carries_alt=ref == alt,
|
|
788
870
|
)
|
|
789
871
|
)
|
|
790
872
|
return _collapse_identical_scvs(annotations)
|
|
@@ -10,7 +10,13 @@ import sqlite3
|
|
|
10
10
|
import urllib.error
|
|
11
11
|
from typing import TYPE_CHECKING, ClassVar
|
|
12
12
|
|
|
13
|
-
from allelix.annotators.base import
|
|
13
|
+
from allelix.annotators.base import (
|
|
14
|
+
Annotator,
|
|
15
|
+
LicenseDescriptor,
|
|
16
|
+
carried_alt,
|
|
17
|
+
is_clinvar_homref,
|
|
18
|
+
variant_locus,
|
|
19
|
+
)
|
|
14
20
|
from allelix.databases._versions import PHARMGKB_INTERPRETER_VERSION
|
|
15
21
|
from allelix.databases.cpic_loader import (
|
|
16
22
|
fetch_cpic_allele_functions,
|
|
@@ -30,7 +36,7 @@ from allelix.databases.pharmgkb_loader import (
|
|
|
30
36
|
schema_is_current,
|
|
31
37
|
)
|
|
32
38
|
from allelix.models import Annotation
|
|
33
|
-
from allelix.utils.allele import
|
|
39
|
+
from allelix.utils.allele import strand_aware_genotype_match
|
|
34
40
|
|
|
35
41
|
if TYPE_CHECKING:
|
|
36
42
|
from collections.abc import Callable, Iterable, Iterator
|
|
@@ -86,6 +92,7 @@ class PharmGKBAnnotator(Annotator):
|
|
|
86
92
|
data_dir: Path,
|
|
87
93
|
clinvar_ref_provider: Callable[[str, str], str | None] | None = None,
|
|
88
94
|
palindromic_provider: Callable[[set[str]], set[str]] | None = None,
|
|
95
|
+
normal_allele_provider: Callable[[str, str], str | None] | None = None,
|
|
89
96
|
) -> None:
|
|
90
97
|
"""Resolve the ClinPGx SQLite cache path within `data_dir`.
|
|
91
98
|
|
|
@@ -100,12 +107,19 @@ class PharmGKBAnnotator(Annotator):
|
|
|
100
107
|
matching is skipped there. In production it's wired to
|
|
101
108
|
`RsidIdentityAnnotator.palindromic_rsids`. None keeps the per-genotype
|
|
102
109
|
palindrome guard only.
|
|
110
|
+
|
|
111
|
+
`normal_allele_provider` gives the normal allele where the build's
|
|
112
|
+
reference carries a ClinVar variant allele, for deriving `alt`
|
|
113
|
+
(`base.carried_alt`). In production it's wired to
|
|
114
|
+
`ClinVarAnnotator.normal_allele_for`. None derives `alt` against
|
|
115
|
+
`Variant.ref` everywhere.
|
|
103
116
|
"""
|
|
104
117
|
super().__init__(data_dir)
|
|
105
118
|
self._db_path = data_dir / PHARMGKB_DB_FILENAME
|
|
106
119
|
self._conn: sqlite3.Connection | None = None
|
|
107
120
|
self._clinvar_ref_provider = clinvar_ref_provider
|
|
108
121
|
self._palindromic_provider = palindromic_provider
|
|
122
|
+
self._normal_allele_provider = normal_allele_provider
|
|
109
123
|
|
|
110
124
|
def _connection(self) -> sqlite3.Connection:
|
|
111
125
|
if self._conn is None:
|
|
@@ -319,6 +333,9 @@ class PharmGKBAnnotator(Annotator):
|
|
|
319
333
|
references = (
|
|
320
334
|
[f"pharmgkb:annotation/{pgkb_annotation_id}"] if pgkb_annotation_id else []
|
|
321
335
|
)
|
|
336
|
+
alt, reference_carries_alt = carried_alt(
|
|
337
|
+
variant, variant.allele1, variant.allele2, self._normal_allele_provider
|
|
338
|
+
)
|
|
322
339
|
annotations.append(
|
|
323
340
|
Annotation(
|
|
324
341
|
source=self.name,
|
|
@@ -332,8 +349,9 @@ class PharmGKBAnnotator(Annotator):
|
|
|
332
349
|
references=references,
|
|
333
350
|
condition=phenotype or "",
|
|
334
351
|
gene=gene or "",
|
|
335
|
-
alt=
|
|
352
|
+
alt=alt,
|
|
336
353
|
locus_ref=variant.ref,
|
|
354
|
+
reference_carries_alt=reference_carries_alt,
|
|
337
355
|
carried_alleles=(variant.allele1, variant.allele2),
|
|
338
356
|
locus=variant_locus(variant),
|
|
339
357
|
)
|
|
@@ -422,6 +440,9 @@ class PharmGKBAnnotator(Annotator):
|
|
|
422
440
|
references = (
|
|
423
441
|
[f"pharmgkb:annotation/{pgkb_annotation_id}"] if pgkb_annotation_id else []
|
|
424
442
|
)
|
|
443
|
+
alt, reference_carries_alt = carried_alt(
|
|
444
|
+
variant, variant.allele1, variant.allele2, self._normal_allele_provider
|
|
445
|
+
)
|
|
425
446
|
yield Annotation(
|
|
426
447
|
source=self.name,
|
|
427
448
|
rsid=variant.rsid,
|
|
@@ -434,8 +455,9 @@ class PharmGKBAnnotator(Annotator):
|
|
|
434
455
|
references=references,
|
|
435
456
|
condition=phenotype or "",
|
|
436
457
|
gene=gene or "",
|
|
437
|
-
alt=
|
|
458
|
+
alt=alt,
|
|
438
459
|
locus_ref=variant.ref,
|
|
460
|
+
reference_carries_alt=reference_carries_alt,
|
|
439
461
|
carried_alleles=(variant.allele1, variant.allele2),
|
|
440
462
|
locus=variant_locus(variant),
|
|
441
463
|
)
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
Not a clinical source: it never produces Annotations. The pipeline uses it
|
|
6
6
|
for three things, all keyed by locus on the input's own build:
|
|
7
7
|
|
|
8
|
-
1. ``Variant.ref`` for array data on GRCh37 and GRCh38
|
|
8
|
+
1. ``Variant.ref`` for array data on GRCh37 and GRCh38, from the rows of
|
|
9
|
+
the call's own rsID at its locus (``alleles_at_loci``).
|
|
9
10
|
2. rsID recovery for rsID-less VCF records on both builds
|
|
10
11
|
(``alleles_at_loci`` feeding ``resolve_rsid_at_locus``).
|
|
11
12
|
3. Translating a GRCh37 ``(chrom, pos, ref, alt)`` to its GRCh38 key so the
|
|
@@ -18,7 +18,13 @@ import logging
|
|
|
18
18
|
import sqlite3
|
|
19
19
|
from typing import TYPE_CHECKING, ClassVar
|
|
20
20
|
|
|
21
|
-
from allelix.annotators.base import
|
|
21
|
+
from allelix.annotators.base import (
|
|
22
|
+
Annotator,
|
|
23
|
+
LicenseDescriptor,
|
|
24
|
+
carried_alt,
|
|
25
|
+
is_clinvar_homref,
|
|
26
|
+
variant_locus,
|
|
27
|
+
)
|
|
22
28
|
from allelix.databases.manager import (
|
|
23
29
|
download,
|
|
24
30
|
verify_file_hash,
|
|
@@ -29,7 +35,7 @@ from allelix.databases.snpedia_loader import (
|
|
|
29
35
|
install_prebuilt_cache,
|
|
30
36
|
)
|
|
31
37
|
from allelix.models import Annotation
|
|
32
|
-
from allelix.utils.allele import complement
|
|
38
|
+
from allelix.utils.allele import complement
|
|
33
39
|
|
|
34
40
|
if TYPE_CHECKING:
|
|
35
41
|
from collections.abc import Callable, Iterable, Iterator
|
|
@@ -125,6 +131,7 @@ class SNPediaAnnotator(Annotator):
|
|
|
125
131
|
self,
|
|
126
132
|
data_dir: Path,
|
|
127
133
|
clinvar_ref_provider: Callable[[str, str], str | None] | None = None,
|
|
134
|
+
normal_allele_provider: Callable[[str, str], str | None] | None = None,
|
|
128
135
|
) -> None:
|
|
129
136
|
"""Initialize with path to the data directory.
|
|
130
137
|
|
|
@@ -132,11 +139,16 @@ class SNPediaAnnotator(Annotator):
|
|
|
132
139
|
callable used by the ADR-0023 hom-ref check. In production it is
|
|
133
140
|
wired to ``ClinVarAnnotator.reference_for``. ``None`` disables the
|
|
134
141
|
check (tests, standalone use).
|
|
142
|
+
|
|
143
|
+
``normal_allele_provider`` gives the normal allele where the build's
|
|
144
|
+
reference carries a ClinVar variant allele, for deriving ``alt``
|
|
145
|
+
(``base.carried_alt``); wired to ``ClinVarAnnotator.normal_allele_for``.
|
|
135
146
|
"""
|
|
136
147
|
super().__init__(data_dir)
|
|
137
148
|
self._db_path = data_dir / SNPEDIA_DB_FILENAME
|
|
138
149
|
self._conn: sqlite3.Connection | None = None
|
|
139
150
|
self._clinvar_ref_provider = clinvar_ref_provider
|
|
151
|
+
self._normal_allele_provider = normal_allele_provider
|
|
140
152
|
|
|
141
153
|
def _connection(self) -> sqlite3.Connection:
|
|
142
154
|
if self._conn is None:
|
|
@@ -314,6 +326,9 @@ class SNPediaAnnotator(Annotator):
|
|
|
314
326
|
|
|
315
327
|
description = f"SNPedia: {summary}"
|
|
316
328
|
forward_allele1, forward_allele2 = forward_genotype
|
|
329
|
+
alt, reference_carries_alt = carried_alt(
|
|
330
|
+
variant, forward_allele1, forward_allele2, self._normal_allele_provider
|
|
331
|
+
)
|
|
317
332
|
genotype_match = f"{forward_allele1}{forward_allele2}"
|
|
318
333
|
|
|
319
334
|
annotations.append(
|
|
@@ -328,12 +343,9 @@ class SNPediaAnnotator(Annotator):
|
|
|
328
343
|
genotype_match=genotype_match,
|
|
329
344
|
references=[f"https://www.snpedia.com/index.php/{snp_url_path}"],
|
|
330
345
|
gene=gene or "",
|
|
331
|
-
alt=
|
|
332
|
-
variant.ref,
|
|
333
|
-
forward_allele1,
|
|
334
|
-
forward_allele2,
|
|
335
|
-
),
|
|
346
|
+
alt=alt,
|
|
336
347
|
locus_ref=variant.ref,
|
|
348
|
+
reference_carries_alt=reference_carries_alt,
|
|
337
349
|
carried_alleles=(forward_allele1, forward_allele2),
|
|
338
350
|
locus=variant_locus(variant),
|
|
339
351
|
)
|
|
@@ -418,6 +430,9 @@ class SNPediaAnnotator(Annotator):
|
|
|
418
430
|
category = _REPUTE_CATEGORY.get(repute_lower, "trait")
|
|
419
431
|
description = f"SNPedia: {summary}"
|
|
420
432
|
forward_allele1, forward_allele2 = forward_genotype
|
|
433
|
+
alt, reference_carries_alt = carried_alt(
|
|
434
|
+
variant, forward_allele1, forward_allele2, self._normal_allele_provider
|
|
435
|
+
)
|
|
421
436
|
genotype_match = f"{forward_allele1}{forward_allele2}"
|
|
422
437
|
yield Annotation(
|
|
423
438
|
source=self.name,
|
|
@@ -430,12 +445,9 @@ class SNPediaAnnotator(Annotator):
|
|
|
430
445
|
genotype_match=genotype_match,
|
|
431
446
|
references=[f"https://www.snpedia.com/index.php/{snp_url_path}"],
|
|
432
447
|
gene=gene or "",
|
|
433
|
-
alt=
|
|
434
|
-
variant.ref,
|
|
435
|
-
forward_allele1,
|
|
436
|
-
forward_allele2,
|
|
437
|
-
),
|
|
448
|
+
alt=alt,
|
|
438
449
|
locus_ref=variant.ref,
|
|
450
|
+
reference_carries_alt=reference_carries_alt,
|
|
439
451
|
carried_alleles=(forward_allele1, forward_allele2),
|
|
440
452
|
locus=variant_locus(variant),
|
|
441
453
|
)
|
|
@@ -15,7 +15,13 @@ reject stale caches without forcing a full re-download.
|
|
|
15
15
|
# ("-", "not specified", "no classification provided", etc.) in the cache.
|
|
16
16
|
# Without this bump, existing iv:3 caches stay poisoned — is_ready() would
|
|
17
17
|
# treat them as current and the fix would only reach fresh / --force builds.
|
|
18
|
-
|
|
18
|
+
# iv:4→iv:5: the loader classifies records by their HGVS name (2.4.2).
|
|
19
|
+
# Normal-allele records (c.N=) lose their REF == ALT rows and their
|
|
20
|
+
# Pathogenic / Likely pathogenic rows, and REF == ALT non-SNV rows are
|
|
21
|
+
# dropped, so a REF == ALT row means this build's reference carries the
|
|
22
|
+
# variant allele. An iv:4 cache still holds the old rows and must not be
|
|
23
|
+
# read with the new rule.
|
|
24
|
+
CLINVAR_INTERPRETER_VERSION = 5
|
|
19
25
|
# Also bump for a change to how CPIC allele-function data is loaded: a bump
|
|
20
26
|
# makes existing ClinPGx caches not ready, and `allelix db update` rebuilds
|
|
21
27
|
# them, fetching the CPIC table again, so the change reaches them.
|
|
@@ -9,6 +9,7 @@ import gzip
|
|
|
9
9
|
import hashlib
|
|
10
10
|
import logging
|
|
11
11
|
import os
|
|
12
|
+
import re
|
|
12
13
|
import sqlite3
|
|
13
14
|
import urllib.request
|
|
14
15
|
from pathlib import Path
|
|
@@ -167,6 +168,8 @@ def verify_file_hash(path: Path, algorithm: str, expected_hex: str) -> None:
|
|
|
167
168
|
# Both files use `\t` as field separator and `#` as comment-line prefix.
|
|
168
169
|
|
|
169
170
|
# variant_summary.txt.gz — 43 columns. We only need a handful.
|
|
171
|
+
_VS_TYPE = 1 # "Type": "single nucleotide variant", "Deletion", ...
|
|
172
|
+
_VS_NAME = 2 # "Name": HGVS, e.g. "NM_000130.5(F5):c.1601G>A (p.Arg534Gln)"
|
|
170
173
|
_VS_RS = 9 # "RS# (dbSNP)" — number only, no `rs` prefix
|
|
171
174
|
_VS_GENE_SYMBOL = 4 # "GeneSymbol"
|
|
172
175
|
_VS_REVIEW_STATUS = 24 # "ReviewStatus" — aggregate
|
|
@@ -227,6 +230,33 @@ _CLINVAR_PLACEHOLDER_CLNSIGS: frozenset[str] = frozenset(
|
|
|
227
230
|
)
|
|
228
231
|
|
|
229
232
|
|
|
233
|
+
# Per-SCV classifications dropped from normal-allele records whose VCF
|
|
234
|
+
# alleles differ on the build (see ``describes_reference_allele``).
|
|
235
|
+
_NORMAL_ALLELE_DROPPED_CLNSIGS: frozenset[str] = frozenset(
|
|
236
|
+
{
|
|
237
|
+
"pathogenic",
|
|
238
|
+
"likely pathogenic",
|
|
239
|
+
"pathogenic/likely pathogenic",
|
|
240
|
+
"pathogenic, low penetrance",
|
|
241
|
+
"likely pathogenic, low penetrance",
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
_HGVS_ALLELE = re.compile(r"\b[cgmn]\.([^\s(]+)")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def describes_reference_allele(name: str) -> bool:
|
|
249
|
+
"""Return True when a ClinVar record's HGVS name describes the reference sequence.
|
|
250
|
+
|
|
251
|
+
ClinVar writes the normal (reference-sequence) allele as ``c.N=``, for
|
|
252
|
+
example ``NM_000130.5(F5):c.1601=``. Only the nucleotide description
|
|
253
|
+
counts: a synonymous protein change such as ``c.123A>G (p.Leu41=)``
|
|
254
|
+
describes a real variant.
|
|
255
|
+
"""
|
|
256
|
+
match = _HGVS_ALLELE.search(name)
|
|
257
|
+
return match is not None and match.group(1).endswith("=")
|
|
258
|
+
|
|
259
|
+
|
|
230
260
|
def _open_tsv(path: Path) -> Iterator[list[str]]:
|
|
231
261
|
"""Stream a (possibly-gzipped) tab-delimited TSV, skipping comment lines."""
|
|
232
262
|
opener: object = gzip.open if path.suffix == ".gz" else open
|
|
@@ -345,7 +375,8 @@ def iter_clinvar_tsv_records(
|
|
|
345
375
|
" ref TEXT, "
|
|
346
376
|
" alt TEXT, "
|
|
347
377
|
" gene TEXT, "
|
|
348
|
-
" review_status TEXT"
|
|
378
|
+
" review_status TEXT, "
|
|
379
|
+
" normal_allele INTEGER"
|
|
349
380
|
");"
|
|
350
381
|
)
|
|
351
382
|
batch: list[tuple[object, ...]] = []
|
|
@@ -377,6 +408,16 @@ def iter_clinvar_tsv_records(
|
|
|
377
408
|
variation_id = int(cols[_VS_VARIATION_ID].strip())
|
|
378
409
|
except (ValueError, IndexError):
|
|
379
410
|
continue
|
|
411
|
+
normal_allele = describes_reference_allele(cols[_VS_NAME])
|
|
412
|
+
if ref == alt and (
|
|
413
|
+
normal_allele or cols[_VS_TYPE].strip() != "single nucleotide variant"
|
|
414
|
+
):
|
|
415
|
+
# REF == ALT has one meaning left in the cache: this
|
|
416
|
+
# build's reference carries the variant allele (F5
|
|
417
|
+
# c.1601G>A on GRCh37). A normal-allele record with
|
|
418
|
+
# REF == ALT describes the reference itself, and a
|
|
419
|
+
# non-SNV with REF == ALT has no alleles on this build.
|
|
420
|
+
continue
|
|
380
421
|
batch.append(
|
|
381
422
|
(
|
|
382
423
|
variation_id,
|
|
@@ -387,17 +428,18 @@ def iter_clinvar_tsv_records(
|
|
|
387
428
|
alt,
|
|
388
429
|
cols[_VS_GENE_SYMBOL].strip(),
|
|
389
430
|
cols[_VS_REVIEW_STATUS].strip(),
|
|
431
|
+
int(normal_allele),
|
|
390
432
|
)
|
|
391
433
|
)
|
|
392
434
|
if len(batch) >= INSERT_BATCH_SIZE:
|
|
393
435
|
tmp_conn.executemany(
|
|
394
|
-
"INSERT OR IGNORE INTO vs VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
436
|
+
"INSERT OR IGNORE INTO vs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
395
437
|
batch,
|
|
396
438
|
)
|
|
397
439
|
batch.clear()
|
|
398
440
|
if batch:
|
|
399
441
|
tmp_conn.executemany(
|
|
400
|
-
"INSERT OR IGNORE INTO vs VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
442
|
+
"INSERT OR IGNORE INTO vs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
401
443
|
batch,
|
|
402
444
|
)
|
|
403
445
|
tmp_conn.commit()
|
|
@@ -415,14 +457,20 @@ def iter_clinvar_tsv_records(
|
|
|
415
457
|
except (ValueError, IndexError):
|
|
416
458
|
continue
|
|
417
459
|
row = cursor.execute(
|
|
418
|
-
"SELECT rsid, chromosome, position, ref, alt, gene, review_status "
|
|
419
|
-
"FROM vs WHERE variation_id = ?",
|
|
460
|
+
"SELECT rsid, chromosome, position, ref, alt, gene, review_status, "
|
|
461
|
+
"normal_allele FROM vs WHERE variation_id = ?",
|
|
420
462
|
(variation_id,),
|
|
421
463
|
).fetchone()
|
|
422
464
|
if row is None:
|
|
423
465
|
continue
|
|
424
|
-
rsid, chromosome, position, ref, alt, gene, review_status_agg = row
|
|
466
|
+
rsid, chromosome, position, ref, alt, gene, review_status_agg, normal = row
|
|
425
467
|
clinical_significance = cols[_SS_CLIN_SIG].strip()
|
|
468
|
+
if normal and clinical_significance.lower() in _NORMAL_ALLELE_DROPPED_CLNSIGS:
|
|
469
|
+
# A normal-allele record names the reference sequence;
|
|
470
|
+
# carrying it is not a pathogenic finding (F5 c.1601=,
|
|
471
|
+
# written T>C on GRCh37). Its other classifications
|
|
472
|
+
# stay as they are.
|
|
473
|
+
continue
|
|
426
474
|
# ClinVar's submission_summary
|
|
427
475
|
# carries several placeholder values that mean "no clinical
|
|
428
476
|
# significance recorded." A placeholder that ISN'T in
|
|
@@ -211,6 +211,12 @@ class Annotation:
|
|
|
211
211
|
# analysis build, chromosome in Allelix's bare form. Lets enrichment
|
|
212
212
|
# key every annotation by position rather than by rsID (ADR-0040).
|
|
213
213
|
locus: tuple[str, int] | None = field(default=None, compare=False, repr=False)
|
|
214
|
+
# ``reference_carries_alt``: ``alt`` is the reference base because this
|
|
215
|
+
# build's reference sequence carries the variant allele (F5 c.1601G>A
|
|
216
|
+
# on GRCh37): a ClinVar row written REF == ALT, or a ClinPGx / SNPedia
|
|
217
|
+
# genotype whose alt is derived against the normal allele from the
|
|
218
|
+
# other build. Exempts the annotation from invariant 2 only.
|
|
219
|
+
reference_carries_alt: bool = field(default=False, compare=False, repr=False)
|
|
214
220
|
|
|
215
221
|
def __post_init__(self) -> None:
|
|
216
222
|
"""Enforce the allele-identity invariants.
|
|
@@ -222,7 +228,9 @@ class Annotation:
|
|
|
222
228
|
(``ALT_IS_NON_REFERENCE_SOURCES``), ``alt`` is never the
|
|
223
229
|
reference base. GWAS is exempt: its ``alt`` is the study's
|
|
224
230
|
effect (risk) allele, which is often the reference allele.
|
|
225
|
-
Checked when ``locus_ref`` is known.
|
|
231
|
+
Checked when ``locus_ref`` is known. An annotation whose alt is
|
|
232
|
+
the reference base because the build's reference carries the
|
|
233
|
+
variant allele is exempt (``reference_carries_alt``).
|
|
226
234
|
|
|
227
235
|
Annotators filter offending source rows before construction; a
|
|
228
236
|
violation here is a programming error, not a data condition.
|
|
@@ -237,6 +245,7 @@ class Annotation:
|
|
|
237
245
|
and self.alt
|
|
238
246
|
and self.locus_ref is not None
|
|
239
247
|
and self.alt == self.locus_ref
|
|
248
|
+
and not self.reference_carries_alt
|
|
240
249
|
):
|
|
241
250
|
raise AnnotationInvariantError(
|
|
242
251
|
f"{self.source} annotation for {self.rsid}: alt {self.alt!r} is the "
|
|
@@ -267,7 +276,7 @@ class Annotation:
|
|
|
267
276
|
# new public-output path, you're re-introducing the bug — add the field
|
|
268
277
|
# here and use ``annotation_to_public_dict`` instead.
|
|
269
278
|
_INTERNAL_ANNOTATION_FIELDS: frozenset[str] = frozenset(
|
|
270
|
-
{"is_must_include", "locus_ref", "carried_alleles", "locus"}
|
|
279
|
+
{"is_must_include", "locus_ref", "carried_alleles", "locus", "reference_carries_alt"}
|
|
271
280
|
)
|
|
272
281
|
|
|
273
282
|
|
|
@@ -498,7 +498,10 @@ def _enrichment_allele(a: Annotation) -> str | None:
|
|
|
498
498
|
The annotation's ``alt``; when it has none (a GWAS row with no reported
|
|
499
499
|
risk allele), the user's single carried non-reference allele. None when
|
|
500
500
|
the locus or its REF is unknown, when there is no such allele or more
|
|
501
|
-
than one, or when the allele is the reference
|
|
501
|
+
than one, or when the allele is the reference, unless the annotation
|
|
502
|
+
says the build's reference carries the variant allele
|
|
503
|
+
(``reference_carries_alt``, F5 c.1601G>A on GRCh37): that allele is
|
|
504
|
+
the variant, and its GRCh38 key is the real variant there.
|
|
502
505
|
"""
|
|
503
506
|
if a.locus is None or a.locus_ref is None:
|
|
504
507
|
return None
|
|
@@ -508,7 +511,7 @@ def _enrichment_allele(a: Annotation) -> str | None:
|
|
|
508
511
|
if len(carried) != 1:
|
|
509
512
|
return None
|
|
510
513
|
allele = carried.pop()
|
|
511
|
-
if allele == a.locus_ref:
|
|
514
|
+
if allele == a.locus_ref and not a.reference_carries_alt:
|
|
512
515
|
return None
|
|
513
516
|
return allele
|
|
514
517
|
|
|
@@ -544,7 +547,8 @@ def _enrichment_keys(
|
|
|
544
547
|
GWAS row with no reported risk allele) uses the user's single carried
|
|
545
548
|
non-reference allele, as the rsID-less position fallback always did,
|
|
546
549
|
and gets no key when there is none or more than one. An allele equal
|
|
547
|
-
to the reference has no variant to look up
|
|
550
|
+
to the reference has no variant to look up, except where the build's
|
|
551
|
+
reference carries the variant allele (``reference_carries_alt``). GRCh38 keys are used as
|
|
548
552
|
they are; GRCh37 keys go through the identity table; any other build
|
|
549
553
|
gets no keys.
|
|
550
554
|
"""
|
|
@@ -162,8 +162,9 @@ def resolve_rsid_at_locus(
|
|
|
162
162
|
"""Pick the rsID for an rsID-less call from the ``(ref, alt, rsid)`` rows at its locus.
|
|
163
163
|
|
|
164
164
|
Shared by every position resolver (ClinVar, gnomAD) so they apply one
|
|
165
|
-
rule. A row is consistent with the call when
|
|
166
|
-
|
|
165
|
+
rule. A row is consistent with the call when its REF differs from its
|
|
166
|
+
ALT (a REF == ALT row names the reference base itself, which no
|
|
167
|
+
non-reference call explains), its REF equals ``variant_ref``
|
|
167
168
|
when that is known, and the user carries its ALT. The call resolves
|
|
168
169
|
when those rows:
|
|
169
170
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: allelix
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.2
|
|
4
4
|
Summary: Open-source genotype analysis toolkit. Format-agnostic ingestion, database-agnostic annotation, offline-first.
|
|
5
5
|
Author: Allelix
|
|
6
6
|
Maintainer-email: dial481 <dial481@users.noreply.github.com>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "allelix"
|
|
7
|
-
version = "2.4.
|
|
7
|
+
version = "2.4.2"
|
|
8
8
|
description = "Open-source genotype analysis toolkit. Format-agnostic ingestion, database-agnostic annotation, offline-first."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|