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/__init__.py +69 -0
- allelign/__main__.py +5 -0
- allelign/_common.py +95 -0
- allelign/_numba.py +22 -0
- allelign/align.py +1252 -0
- allelign/alleles.py +90 -0
- allelign/build.py +57 -0
- allelign/catalog.py +88 -0
- allelign/cli.py +323 -0
- allelign/download.py +525 -0
- allelign/genotypes.py +241 -0
- allelign/ldcheck.py +771 -0
- allelign/qc.py +638 -0
- allelign/status.py +130 -0
- allelign/sumstats.py +1207 -0
- allelign/variants.py +155 -0
- allelign/warning_markers.py +15 -0
- allelign/weights.py +614 -0
- allelign-0.1.0.dist-info/METADATA +175 -0
- allelign-0.1.0.dist-info/RECORD +24 -0
- allelign-0.1.0.dist-info/WHEEL +5 -0
- allelign-0.1.0.dist-info/entry_points.txt +2 -0
- allelign-0.1.0.dist-info/licenses/LICENSE +21 -0
- allelign-0.1.0.dist-info/top_level.txt +1 -0
allelign/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""allelign: read, align and quality-control GWAS summary statistics and PGS weights.
|
|
2
|
+
|
|
3
|
+
One place for the steps every downstream method repeats -- parsing the many
|
|
4
|
+
summary-statistics dialects, getting effect alleles right against a reference,
|
|
5
|
+
and the checks that catch bad input before it becomes a confident wrong
|
|
6
|
+
number. NumPy-only; Numba and SciPy speed up the parser and the LD screen when
|
|
7
|
+
installed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
|
|
12
|
+
from .alleles import (allele_orientation, is_ambiguous, normalize_chrom,
|
|
13
|
+
reverse_complement)
|
|
14
|
+
from .build import BuildMismatchError, check_build, normalize_build
|
|
15
|
+
from .catalog import resolve_gwas_catalog_columns
|
|
16
|
+
from .download import download_ref, parse_ref, read_keep_ids, resolve_ref
|
|
17
|
+
from .genotypes import Bed, ld_consistency_screen_genotypes
|
|
18
|
+
from .align import (Harmonized, HarmonizedWeights,
|
|
19
|
+
count_positional_only_matches, coverage_message,
|
|
20
|
+
diagnose_match, harmonize, harmonize_weights,
|
|
21
|
+
reanchor_on_identifier, reference_coverage,
|
|
22
|
+
variant_indices)
|
|
23
|
+
from .ldcheck import (chromosome_blocks, dense_blocks, dentist_statistic,
|
|
24
|
+
ld_consistency_screen_selected,
|
|
25
|
+
ld_blocks_from_genotypes, ld_consistency_screen,
|
|
26
|
+
loo_statistic)
|
|
27
|
+
from .qc import (af_concordance_mask, genomic_control_factor,
|
|
28
|
+
genomic_inflation, gwas_af_on_counted_allele,
|
|
29
|
+
implied_sample_size, impute_n_eff, in_long_range_ld,
|
|
30
|
+
qc_sumstats, sd_consistency_mask, should_impute_n)
|
|
31
|
+
from .status import (QC_REASON_NAMES, new_status, status_counts,
|
|
32
|
+
write_status_table)
|
|
33
|
+
from .sumstats import (ColumnMappingError, Sumstats, anchor_n_eff,
|
|
34
|
+
detect_columns, inspect_columns, n_eff_case_control,
|
|
35
|
+
read_sumstats, rescale_n_eff, write_sumstats)
|
|
36
|
+
from .variants import Variants, read_bim, read_pvar, read_variants
|
|
37
|
+
from .weights import (SCORING_COLUMNS, ScoringFile, WeightsTable, read_scoring_file,
|
|
38
|
+
read_weights, scoring_file_id, write_pgs_scoring_file,
|
|
39
|
+
write_weights)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"__version__", "SCORING_COLUMNS", "resolve_gwas_catalog_columns",
|
|
43
|
+
# reading and writing
|
|
44
|
+
"Sumstats", "read_sumstats", "write_sumstats", "detect_columns",
|
|
45
|
+
"inspect_columns", "ColumnMappingError", "n_eff_case_control",
|
|
46
|
+
"rescale_n_eff", "anchor_n_eff",
|
|
47
|
+
"Variants", "read_bim", "read_pvar", "read_variants", "Bed",
|
|
48
|
+
"WeightsTable", "read_weights", "write_weights", "ScoringFile",
|
|
49
|
+
"read_scoring_file", "scoring_file_id", "write_pgs_scoring_file",
|
|
50
|
+
# alignment
|
|
51
|
+
"Harmonized", "harmonize", "HarmonizedWeights", "harmonize_weights",
|
|
52
|
+
"reanchor_on_identifier", "reference_coverage", "coverage_message",
|
|
53
|
+
"diagnose_match", "count_positional_only_matches", "variant_indices",
|
|
54
|
+
"normalize_chrom", "reverse_complement", "is_ambiguous",
|
|
55
|
+
"allele_orientation", "normalize_build", "check_build",
|
|
56
|
+
"BuildMismatchError",
|
|
57
|
+
# quality control
|
|
58
|
+
"qc_sumstats", "sd_consistency_mask", "af_concordance_mask",
|
|
59
|
+
"gwas_af_on_counted_allele", "should_impute_n", "impute_n_eff",
|
|
60
|
+
"genomic_control_factor", "implied_sample_size", "genomic_inflation",
|
|
61
|
+
"in_long_range_ld", "ld_consistency_screen",
|
|
62
|
+
"ld_consistency_screen_selected", "dentist_statistic",
|
|
63
|
+
"loo_statistic", "ld_blocks_from_genotypes", "dense_blocks",
|
|
64
|
+
"chromosome_blocks", "ld_consistency_screen_genotypes",
|
|
65
|
+
# per-variant status
|
|
66
|
+
"QC_REASON_NAMES", "new_status", "status_counts", "write_status_table",
|
|
67
|
+
# downloading
|
|
68
|
+
"parse_ref", "resolve_ref", "download_ref", "read_keep_ids",
|
|
69
|
+
]
|
allelign/__main__.py
ADDED
allelign/_common.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Small shared helpers: control validation, text input and atomic output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import gzip
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def finite_control(name, value, *, lower=None, upper=None):
|
|
15
|
+
"""Coerce a finite scalar control, rejecting booleans and vectors."""
|
|
16
|
+
if isinstance(value, (bool, np.bool_)) or np.ndim(value) != 0:
|
|
17
|
+
raise ValueError(f"{name} must be a finite number")
|
|
18
|
+
try:
|
|
19
|
+
value = float(value)
|
|
20
|
+
except (TypeError, ValueError, OverflowError):
|
|
21
|
+
raise ValueError(f"{name} must be a finite number") from None
|
|
22
|
+
if not np.isfinite(value):
|
|
23
|
+
raise ValueError(f"{name} must be a finite number")
|
|
24
|
+
if lower is not None and value < lower:
|
|
25
|
+
raise ValueError(f"{name} must be >= {lower}")
|
|
26
|
+
if upper is not None and value > upper:
|
|
27
|
+
raise ValueError(f"{name} must be <= {upper}")
|
|
28
|
+
return value
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def integer_at_least(name, value, minimum):
|
|
32
|
+
"""Return an integer control, rejecting bools and silent coercion."""
|
|
33
|
+
if (isinstance(value, (bool, np.bool_))
|
|
34
|
+
or not isinstance(value, (int, np.integer))
|
|
35
|
+
or int(value) < minimum):
|
|
36
|
+
raise ValueError(f"{name} must be an integer >= {minimum}")
|
|
37
|
+
return int(value)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def validate_boolean(**controls):
|
|
41
|
+
"""Reject truthy/falsy surrogates for controls that are genuinely boolean."""
|
|
42
|
+
for name, value in controls.items():
|
|
43
|
+
if not isinstance(value, (bool, np.bool_)):
|
|
44
|
+
raise ValueError(f"{name} must be a boolean")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def validate_seed(seed):
|
|
48
|
+
"""Return a seed usable by ``numpy.random.SeedSequence``, or ``None``."""
|
|
49
|
+
if seed is None:
|
|
50
|
+
return None
|
|
51
|
+
if (isinstance(seed, (bool, np.bool_))
|
|
52
|
+
or not isinstance(seed, (int, np.integer))
|
|
53
|
+
or not 0 <= int(seed) <= np.iinfo(np.uint32).max):
|
|
54
|
+
raise ValueError("seed must be an integer in [0, 2**32 - 1] or None")
|
|
55
|
+
return int(seed)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def open_text(path):
|
|
59
|
+
"""Open a plain or gzip/bgzip text file for reading.
|
|
60
|
+
|
|
61
|
+
``utf-8-sig`` strips a UTF-8 byte-order mark, which Excel and several GWAS
|
|
62
|
+
portals emit; left in place it glues itself to the first header token,
|
|
63
|
+
which then matches no alias, so the first column (usually the variant ID)
|
|
64
|
+
is silently never mapped.
|
|
65
|
+
"""
|
|
66
|
+
name = os.fspath(path)
|
|
67
|
+
if name.endswith((".gz", ".bgz")):
|
|
68
|
+
return gzip.open(name, "rt", encoding="utf-8-sig")
|
|
69
|
+
return open(name, "r", encoding="utf-8-sig")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@contextmanager
|
|
73
|
+
def atomic_text(path):
|
|
74
|
+
"""Write text so that only a complete file is ever published at ``path``.
|
|
75
|
+
|
|
76
|
+
A ``.gz`` destination is gzip-compressed. The temporary file lives on the
|
|
77
|
+
destination filesystem, so the final ``os.replace`` is atomic.
|
|
78
|
+
"""
|
|
79
|
+
path = Path(path)
|
|
80
|
+
fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.",
|
|
81
|
+
dir=path.parent or ".")
|
|
82
|
+
try:
|
|
83
|
+
if path.name.endswith(".gz"):
|
|
84
|
+
os.close(fd)
|
|
85
|
+
with gzip.open(temporary, "wt", encoding="utf-8", compresslevel=1) as fh:
|
|
86
|
+
yield fh
|
|
87
|
+
else:
|
|
88
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
89
|
+
yield fh
|
|
90
|
+
fh.flush()
|
|
91
|
+
os.fsync(fh.fileno())
|
|
92
|
+
os.replace(temporary, path)
|
|
93
|
+
finally:
|
|
94
|
+
if os.path.exists(temporary):
|
|
95
|
+
os.unlink(temporary)
|
allelign/_numba.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Optional Numba support. Everything has a pure-NumPy/Python fallback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
try: # pragma: no cover - depends on the environment
|
|
6
|
+
import numba as _numba
|
|
7
|
+
from numba import config as numba_config
|
|
8
|
+
HAVE_NUMBA = True
|
|
9
|
+
except Exception: # pragma: no cover - a missing or broken install
|
|
10
|
+
_numba = None
|
|
11
|
+
numba_config = None
|
|
12
|
+
HAVE_NUMBA = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def jit_available():
|
|
16
|
+
"""True when compiled kernels can actually run (not disabled by config)."""
|
|
17
|
+
return HAVE_NUMBA and not getattr(numba_config, "DISABLE_JIT", False)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def jit_nogil(fn):
|
|
21
|
+
"""Compile ``fn`` in nopython, GIL-releasing mode with an on-disk cache."""
|
|
22
|
+
return _numba.njit(cache=True, nogil=True)(fn)
|