allelign 0.1.0__py3-none-any.whl

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.
allelign/variants.py ADDED
@@ -0,0 +1,155 @@
1
+ """Reference variant tables: the variants (and counted alleles) to align to.
2
+
3
+ A :class:`Variants` table is what summary statistics or weights are aligned to:
4
+ an LD reference, a genotype file, or another GWAS. ``a1`` is the counted
5
+ allele -- the allele a dosage counts and an aligned effect refers to. The
6
+ attribute names match ldpred3's ``VariantTable``, so either table works with
7
+ either package's harmoniser.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ import os
14
+ import warnings
15
+
16
+ import numpy as np
17
+
18
+ from ._common import open_text
19
+
20
+ __all__ = ["Variants", "read_bim", "read_pvar", "read_variants"]
21
+
22
+
23
+ @dataclass
24
+ class Variants:
25
+ """Per-variant reference metadata as parallel arrays.
26
+
27
+ Mutable by default. :meth:`freeze` marks the matching columns read-only so
28
+ the harmoniser can cache its indices across calls (useful when many files
29
+ are aligned to one reference).
30
+ """
31
+
32
+ chrom: np.ndarray
33
+ id: np.ndarray
34
+ pos: np.ndarray
35
+ a1: np.ndarray
36
+ a2: np.ndarray
37
+ alternate_id: np.ndarray = None
38
+ cm: np.ndarray = None # genetic position, when the source has one
39
+
40
+ def __len__(self):
41
+ return len(self.id)
42
+
43
+ def freeze(self):
44
+ """Mark matching columns read-only, without copying; returns self."""
45
+ was_writable = False
46
+ for name in ("chrom", "id", "pos", "a1", "a2", "alternate_id"):
47
+ column = getattr(self, name)
48
+ if column is not None:
49
+ column = np.asarray(column)
50
+ was_writable |= column.flags.writeable
51
+ column.flags.writeable = False
52
+ setattr(self, name, column)
53
+ if was_writable:
54
+ from .align import _CACHE_ATTR, _INDEX_CACHE_LOCK
55
+ with _INDEX_CACHE_LOCK:
56
+ self.__dict__.pop(_CACHE_ATTR, None)
57
+ return self
58
+
59
+ def subset(self, idx):
60
+ """A new (mutable) table keeping the rows in ``idx``."""
61
+ idx = np.asarray(idx)
62
+ return Variants(
63
+ chrom=self.chrom[idx], id=self.id[idx], pos=self.pos[idx],
64
+ a1=self.a1[idx], a2=self.a2[idx],
65
+ alternate_id=(None if self.alternate_id is None
66
+ else self.alternate_id[idx]),
67
+ cm=None if self.cm is None else self.cm[idx])
68
+
69
+
70
+ def read_bim(path):
71
+ """Read a PLINK 1 ``.bim`` file; ``a1`` is column 5, PLINK's counted allele."""
72
+ chrom, ids, cm, pos, a1, a2 = [], [], [], [], [], []
73
+ pool = {}
74
+ with open_text(path) as fh:
75
+ for line_no, line in enumerate(fh, start=1):
76
+ f = line.split()
77
+ if not f:
78
+ continue
79
+ if len(f) < 6:
80
+ raise ValueError(f"{path}: line {line_no} has {len(f)} fields, "
81
+ "expected 6")
82
+ chrom.append(pool.setdefault(f[0], f[0]))
83
+ ids.append(f[1])
84
+ cm.append(float(f[2]))
85
+ pos.append(int(float(f[3])))
86
+ x, y = f[4].upper(), f[5].upper()
87
+ a1.append(pool.setdefault(x, x))
88
+ a2.append(pool.setdefault(y, y))
89
+ return Variants(chrom=np.array(chrom, dtype=object),
90
+ id=np.array(ids, dtype=object),
91
+ pos=np.array(pos, dtype=np.int64),
92
+ a1=np.array(a1, dtype=object),
93
+ a2=np.array(a2, dtype=object),
94
+ cm=np.array(cm, dtype=float))
95
+
96
+
97
+ def read_pvar(path):
98
+ """Read a PLINK 2 ``.pvar`` (or VCF-like) variant file.
99
+
100
+ ``a1`` is ``ALT``, the allele PLINK 2 dosages count by default, and ``a2``
101
+ is ``REF``. Multi-allelic records (comma in ``ALT``) are skipped with a
102
+ warning: a single dosage cannot represent them.
103
+ """
104
+ chrom, ids, pos, a1, a2 = [], [], [], [], []
105
+ pool = {}
106
+ n_multi = 0
107
+ col = None
108
+ with open_text(path) as fh:
109
+ for line in fh:
110
+ if line.startswith("##"):
111
+ continue
112
+ if line.startswith("#"):
113
+ header = [h.strip().upper() for h in line[1:].split()]
114
+ col = {name: i for i, name in enumerate(header)}
115
+ continue
116
+ f = line.split()
117
+ if not f:
118
+ continue
119
+ if col is None:
120
+ # A headerless .pvar follows the .bim column order.
121
+ col = {name: i for i, name in enumerate(
122
+ ["CHROM", "ID", "CM", "POS", "ALT", "REF"])}
123
+ try:
124
+ c, vid = f[col["CHROM"]], f[col["ID"]]
125
+ p, ref, alt = f[col["POS"]], f[col["REF"]], f[col["ALT"]]
126
+ except KeyError as exc:
127
+ raise ValueError(f"{path}: header lacks column {exc}") from None
128
+ if "," in alt:
129
+ n_multi += 1
130
+ continue
131
+ chrom.append(pool.setdefault(c, c))
132
+ ids.append(vid)
133
+ pos.append(int(float(p)))
134
+ alt, ref = alt.upper(), ref.upper()
135
+ a1.append(pool.setdefault(alt, alt))
136
+ a2.append(pool.setdefault(ref, ref))
137
+ if n_multi:
138
+ warnings.warn(f"{path}: skipped {n_multi:,} multi-allelic records",
139
+ RuntimeWarning, stacklevel=2)
140
+ return Variants(chrom=np.array(chrom, dtype=object),
141
+ id=np.array(ids, dtype=object),
142
+ pos=np.array(pos, dtype=np.int64),
143
+ a1=np.array(a1, dtype=object),
144
+ a2=np.array(a2, dtype=object))
145
+
146
+
147
+ def read_variants(path):
148
+ """Read a ``.bim`` or ``.pvar`` (optionally gzipped) by its extension."""
149
+ name = os.fspath(path)
150
+ stem = name[:-3] if name.endswith(".gz") else name
151
+ if stem.endswith(".bim"):
152
+ return read_bim(name)
153
+ if stem.endswith((".pvar", ".vcf")):
154
+ return read_pvar(name)
155
+ raise ValueError(f"{name}: expected a .bim or .pvar variant file")
@@ -0,0 +1,15 @@
1
+ """Stable fragments of the warnings and log reasons allelign emits.
2
+
3
+ Consumers that classify messages (SMARTpred's verdict, for one) import these
4
+ constants instead of copying the text, so a rewording cannot silently stop a
5
+ classifier from firing. The values match ldpred3's markers of the same name.
6
+ """
7
+
8
+ AF_ANTI_CORRELATED = "anti-correlated"
9
+ AF_SAFETY_LIMIT = "safety limit"
10
+ AF_CONCORDANCE_GUARD = "concordance guard"
11
+ LD_SCREEN_HIGH_DROP = "LD-consistency screen dropped"
12
+ LD_SCREEN_UNTESTED = "never entered a window"
13
+
14
+ ATTENTION_MARKERS = (AF_ANTI_CORRELATED, AF_SAFETY_LIMIT, AF_CONCORDANCE_GUARD,
15
+ LD_SCREEN_HIGH_DROP, LD_SCREEN_UNTESTED)