statgen 0.2.0__tar.gz → 0.2.1__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.
- {statgen-0.2.0 → statgen-0.2.1}/PKG-INFO +1 -1
- {statgen-0.2.0 → statgen-0.2.1}/pyproject.toml +1 -1
- statgen-0.2.1/statgen/__init__.py +1 -0
- {statgen-0.2.0 → statgen-0.2.1}/statgen/_ld_npz.py +40 -19
- statgen-0.2.1/statgen/_ld_reference.py +57 -0
- {statgen-0.2.0 → statgen-0.2.1}/statgen/_ld_schema.py +38 -2
- {statgen-0.2.0 → statgen-0.2.1}/statgen/_ld_writer.py +76 -1
- {statgen-0.2.0 → statgen-0.2.1}/statgen/annotations.py +7 -27
- {statgen-0.2.0 → statgen-0.2.1}/statgen/ld.py +65 -27
- {statgen-0.2.0 → statgen-0.2.1}/statgen.egg-info/PKG-INFO +1 -1
- {statgen-0.2.0 → statgen-0.2.1}/statgen.egg-info/SOURCES.txt +1 -0
- statgen-0.2.0/statgen/__init__.py +0 -1
- {statgen-0.2.0 → statgen-0.2.1}/README.md +0 -0
- {statgen-0.2.0 → statgen-0.2.1}/setup.cfg +0 -0
- {statgen-0.2.0 → statgen-0.2.1}/statgen/_utils.py +0 -0
- {statgen-0.2.0 → statgen-0.2.1}/statgen/genotype.py +0 -0
- {statgen-0.2.0 → statgen-0.2.1}/statgen/reference.py +0 -0
- {statgen-0.2.0 → statgen-0.2.1}/statgen/sumstats.py +0 -0
- {statgen-0.2.0 → statgen-0.2.1}/statgen.egg-info/dependency_links.txt +0 -0
- {statgen-0.2.0 → statgen-0.2.1}/statgen.egg-info/requires.txt +0 -0
- {statgen-0.2.0 → statgen-0.2.1}/statgen.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.1"
|
|
@@ -19,30 +19,51 @@ from ._ld_schema import (
|
|
|
19
19
|
|
|
20
20
|
def validate_npz_distribution(path, check_payload_structure=False) -> dict:
|
|
21
21
|
path = Path(path)
|
|
22
|
+
if not path.is_dir():
|
|
23
|
+
raise ValueError("validate_npz_distribution: path must identify a panel root directory")
|
|
24
|
+
|
|
25
|
+
manifest = read_manifest(path / "ld_manifest.json", expected_runtime=PY_RUNTIME_FORMAT)
|
|
26
|
+
runtime = manifest["runtime_format"]
|
|
27
|
+
seen_reference_bim = set()
|
|
28
|
+
for entry in manifest["shards"]:
|
|
29
|
+
shard_path = path / entry["file"]
|
|
30
|
+
require_file(shard_path)
|
|
31
|
+
file_md5 = md5_file(shard_path)
|
|
32
|
+
if file_md5 != entry.get("file_md5"):
|
|
33
|
+
raise ValueError(f"{shard_path}: file_md5 does not match manifest")
|
|
34
|
+
_ld_r, _a1freq, meta = read_npz_shard(
|
|
35
|
+
shard_path,
|
|
36
|
+
check_payload_structure=check_payload_structure,
|
|
37
|
+
)
|
|
38
|
+
validate_manifest_entry_agreement(entry, meta, shard_path)
|
|
39
|
+
reference_key = (
|
|
40
|
+
entry["reference_bim"],
|
|
41
|
+
entry["chr"],
|
|
42
|
+
int(entry["num_snp"]),
|
|
43
|
+
entry["reference_checksum"],
|
|
44
|
+
)
|
|
45
|
+
if reference_key not in seen_reference_bim:
|
|
46
|
+
_validate_bundled_reference(root=path, entry=entry)
|
|
47
|
+
seen_reference_bim.add(reference_key)
|
|
48
|
+
|
|
49
|
+
return {"ok": True}
|
|
22
50
|
|
|
23
|
-
if path.is_dir():
|
|
24
|
-
manifest = read_manifest(path / "ld_manifest.json", expected_runtime=PY_RUNTIME_FORMAT)
|
|
25
|
-
runtime = manifest["runtime_format"]
|
|
26
|
-
for entry in manifest["shards"]:
|
|
27
|
-
shard_path = path / entry["file"]
|
|
28
|
-
require_file(shard_path)
|
|
29
|
-
file_md5 = md5_file(shard_path)
|
|
30
|
-
if file_md5 != entry.get("file_md5"):
|
|
31
|
-
raise ValueError(f"{shard_path}: file_md5 does not match manifest")
|
|
32
|
-
_ld_r, _a1freq, meta = read_npz_shard(
|
|
33
|
-
shard_path,
|
|
34
|
-
check_payload_structure=check_payload_structure,
|
|
35
|
-
)
|
|
36
|
-
validate_manifest_entry_agreement(entry, meta, shard_path)
|
|
37
|
-
else:
|
|
38
|
-
runtime = runtime_from_suffix(path)
|
|
39
|
-
if runtime != PY_RUNTIME_FORMAT:
|
|
40
|
-
raise ValueError(f"{path}: unsupported LD runtime {runtime!r}")
|
|
41
|
-
read_npz_shard(path, check_payload_structure=check_payload_structure)
|
|
42
51
|
|
|
52
|
+
def validate_npz_shard_file(path, check_payload_structure=False) -> dict:
|
|
53
|
+
path = Path(path)
|
|
54
|
+
runtime = runtime_from_suffix(path)
|
|
55
|
+
if runtime != PY_RUNTIME_FORMAT:
|
|
56
|
+
raise ValueError(f"{path}: unsupported LD runtime {runtime!r}")
|
|
57
|
+
read_npz_shard(path, check_payload_structure=check_payload_structure)
|
|
43
58
|
return {"ok": True}
|
|
44
59
|
|
|
45
60
|
|
|
61
|
+
def _validate_bundled_reference(*, root: Path, entry: dict) -> None:
|
|
62
|
+
from ._ld_reference import validate_bundled_reference_bim
|
|
63
|
+
|
|
64
|
+
validate_bundled_reference_bim(root / entry["reference_bim"], entry, target="manifest")
|
|
65
|
+
|
|
66
|
+
|
|
46
67
|
def read_npz_shard(path: Path, check_payload_structure: bool):
|
|
47
68
|
require_file(path)
|
|
48
69
|
with np.load(path, allow_pickle=False) as data:
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from ._ld_schema import require_file
|
|
4
|
+
from ._utils import validate_requested_shards
|
|
5
|
+
from .reference import ReferencePanel, load_reference
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def validate_bundled_reference_bim(path, entry: dict, *, target: str):
|
|
9
|
+
path = Path(path)
|
|
10
|
+
require_file(path)
|
|
11
|
+
panel = load_reference(path)
|
|
12
|
+
ref_shards = panel.shards
|
|
13
|
+
if len(ref_shards) != 1:
|
|
14
|
+
raise ValueError(f"{path}: bundled reference_bim must contain exactly one shard")
|
|
15
|
+
ref = ref_shards[0]
|
|
16
|
+
validate_reference_shard_for_ld_entry(ref, entry, path, target=target)
|
|
17
|
+
return ref
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_reference_shard_for_ld_entry(ref_shard, entry: dict, path, *, target: str) -> None:
|
|
21
|
+
path = Path(path)
|
|
22
|
+
if ref_shard.label != entry["chr"]:
|
|
23
|
+
raise ValueError(f"{path}: bundled reference_bim chr does not match {target}")
|
|
24
|
+
if ref_shard.num_snp != int(entry["num_snp"]):
|
|
25
|
+
raise ValueError(f"{path}: bundled reference_bim num_snp does not match {target}")
|
|
26
|
+
if ref_shard.checksum != entry["reference_checksum"]:
|
|
27
|
+
raise ValueError(f"{path}: bundled reference_bim reference_checksum does not match {target}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_bundled_reference_panel(root, manifest: dict, shards, *, where: str) -> ReferencePanel:
|
|
31
|
+
root = Path(root)
|
|
32
|
+
available = _manifest_chr_labels(manifest)
|
|
33
|
+
selected = validate_requested_shards(shards, available, where)
|
|
34
|
+
entries_by_chr = {}
|
|
35
|
+
for entry in manifest["shards"]:
|
|
36
|
+
entries_by_chr.setdefault(entry["chr"], []).append(entry)
|
|
37
|
+
|
|
38
|
+
ref_shards = []
|
|
39
|
+
for label in selected:
|
|
40
|
+
entries = entries_by_chr[label]
|
|
41
|
+
reference_path = root / entries[0]["reference_bim"]
|
|
42
|
+
ref_shard = validate_bundled_reference_bim(reference_path, entries[0], target="manifest")
|
|
43
|
+
for entry in entries[1:]:
|
|
44
|
+
validate_reference_shard_for_ld_entry(ref_shard, entry, reference_path, target="manifest")
|
|
45
|
+
ref_shards.append(ref_shard)
|
|
46
|
+
return ReferencePanel(ref_shards)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _manifest_chr_labels(manifest: dict) -> list[str]:
|
|
50
|
+
labels = []
|
|
51
|
+
seen = set()
|
|
52
|
+
for entry in manifest["shards"]:
|
|
53
|
+
label = entry["chr"]
|
|
54
|
+
if label not in seen:
|
|
55
|
+
labels.append(label)
|
|
56
|
+
seen.add(label)
|
|
57
|
+
return labels
|
|
@@ -33,17 +33,30 @@ def read_manifest(path: Path, expected_runtime: str | None) -> dict:
|
|
|
33
33
|
raise ValueError(f"{path}: shards must be a non-empty list")
|
|
34
34
|
|
|
35
35
|
seen = set()
|
|
36
|
+
reference_bim_by_chr = {}
|
|
36
37
|
for i, entry in enumerate(shards):
|
|
37
38
|
validate_manifest_entry(entry, f"{path}:shards[{i}]")
|
|
38
39
|
key = (entry["chr"], entry["sex"])
|
|
39
40
|
if key in seen:
|
|
40
41
|
raise ValueError(f"{path}: duplicate manifest entry for chr {key[0]} sex {key[1]!r}")
|
|
41
42
|
seen.add(key)
|
|
43
|
+
prior_reference_bim = reference_bim_by_chr.setdefault(entry["chr"], entry["reference_bim"])
|
|
44
|
+
if entry["reference_bim"] != prior_reference_bim:
|
|
45
|
+
raise ValueError(f"{path}: manifest entries for chr {entry['chr']} must share reference_bim")
|
|
42
46
|
return manifest
|
|
43
47
|
|
|
44
48
|
|
|
45
49
|
def validate_manifest_entry(entry: dict, where: str) -> None:
|
|
46
|
-
required = {
|
|
50
|
+
required = {
|
|
51
|
+
"chr",
|
|
52
|
+
"sex",
|
|
53
|
+
"file",
|
|
54
|
+
"file_md5",
|
|
55
|
+
"num_snp",
|
|
56
|
+
"nnz",
|
|
57
|
+
"reference_checksum",
|
|
58
|
+
"reference_bim",
|
|
59
|
+
}
|
|
47
60
|
missing = sorted(required.difference(entry))
|
|
48
61
|
if missing:
|
|
49
62
|
raise ValueError(f"{where}: missing required fields: {', '.join(missing)}")
|
|
@@ -58,6 +71,7 @@ def validate_manifest_entry(entry: dict, where: str) -> None:
|
|
|
58
71
|
raise ValueError(f"{where}: reference_checksum must be a non-empty string")
|
|
59
72
|
if not isinstance(entry["file_md5"], str) or len(entry["file_md5"]) != 32:
|
|
60
73
|
raise ValueError(f"{where}: file_md5 must be a lowercase MD5 hex string")
|
|
74
|
+
validate_reference_bim_filename(entry["reference_bim"], f"{where}: reference_bim")
|
|
61
75
|
|
|
62
76
|
|
|
63
77
|
def validate_shard_metadata(meta: dict, path: Path, expected_format: str) -> None:
|
|
@@ -73,6 +87,8 @@ def validate_shard_metadata(meta: dict, path: Path, expected_format: str) -> Non
|
|
|
73
87
|
"diagonal",
|
|
74
88
|
"value",
|
|
75
89
|
"reference_checksum",
|
|
90
|
+
"reference_bim",
|
|
91
|
+
"num_monomorphic_snps",
|
|
76
92
|
}
|
|
77
93
|
missing = sorted(required.difference(meta))
|
|
78
94
|
if missing:
|
|
@@ -86,6 +102,12 @@ def validate_shard_metadata(meta: dict, path: Path, expected_format: str) -> Non
|
|
|
86
102
|
validate_chr_sex(meta["chr"], meta["sex"], f"{path}: metadata")
|
|
87
103
|
validate_positive_int(meta["num_snp"], f"{path}: metadata num_snp")
|
|
88
104
|
validate_nonnegative_int(meta["nnz"], f"{path}: metadata nnz")
|
|
105
|
+
validate_nonnegative_int(
|
|
106
|
+
meta["num_monomorphic_snps"],
|
|
107
|
+
f"{path}: metadata num_monomorphic_snps",
|
|
108
|
+
)
|
|
109
|
+
if int(meta["num_monomorphic_snps"]) > int(meta["num_snp"]):
|
|
110
|
+
raise ValueError(f"{path}: metadata num_monomorphic_snps must not exceed num_snp")
|
|
89
111
|
if int(meta["nnz"]) >= 2**31:
|
|
90
112
|
raise ValueError(f"{path}: CSC32 metadata nnz must be < 2^31")
|
|
91
113
|
if meta["matrix"] != "symmetric":
|
|
@@ -96,6 +118,7 @@ def validate_shard_metadata(meta: dict, path: Path, expected_format: str) -> Non
|
|
|
96
118
|
raise ValueError(f"{path}: metadata value must be 'r'")
|
|
97
119
|
if not isinstance(meta["reference_checksum"], str) or meta["reference_checksum"] == "":
|
|
98
120
|
raise ValueError(f"{path}: metadata reference_checksum must be a non-empty string")
|
|
121
|
+
validate_reference_bim_filename(meta["reference_bim"], f"{path}: metadata reference_bim")
|
|
99
122
|
if expected_format == NPZ_FORMAT:
|
|
100
123
|
if meta.get("sparse_layout") != "csc":
|
|
101
124
|
raise ValueError(f"{path}: metadata sparse_layout must be 'csc'")
|
|
@@ -119,7 +142,7 @@ def validate_chrx_sex(sex, where: str) -> str:
|
|
|
119
142
|
|
|
120
143
|
|
|
121
144
|
def validate_manifest_entry_agreement(entry: dict, meta: dict, path: Path) -> None:
|
|
122
|
-
for key in ("chr", "sex", "num_snp", "nnz", "reference_checksum"):
|
|
145
|
+
for key in ("chr", "sex", "num_snp", "nnz", "reference_checksum", "reference_bim"):
|
|
123
146
|
if entry.get(key) != meta.get(key):
|
|
124
147
|
raise ValueError(f"{path}: manifest/per-file metadata mismatch for {key}")
|
|
125
148
|
suffix_runtime = runtime_from_suffix(path)
|
|
@@ -127,6 +150,19 @@ def validate_manifest_entry_agreement(entry: dict, meta: dict, path: Path) -> No
|
|
|
127
150
|
raise ValueError(f"{path}: manifest file extension and metadata format disagree")
|
|
128
151
|
|
|
129
152
|
|
|
153
|
+
def default_reference_bim_filename(chr_label: str) -> str:
|
|
154
|
+
return f"reference_chr{chr_label}.bim"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def validate_reference_bim_filename(value, where: str) -> str:
|
|
158
|
+
if not isinstance(value, str) or value == "":
|
|
159
|
+
raise ValueError(f"{where} must be a non-empty filename")
|
|
160
|
+
path = Path(value)
|
|
161
|
+
if path.is_absolute() or len(path.parts) != 1 or value in {".", ".."} or ".." in value:
|
|
162
|
+
raise ValueError(f"{where} must be a plain relative filename")
|
|
163
|
+
return value
|
|
164
|
+
|
|
165
|
+
|
|
130
166
|
def runtime_from_suffix(path: Path) -> str:
|
|
131
167
|
if path.suffix == ".npz":
|
|
132
168
|
return PY_RUNTIME_FORMAT
|
|
@@ -14,11 +14,14 @@ from ._ld_schema import (
|
|
|
14
14
|
NPZ_REQUIRED,
|
|
15
15
|
PY_RUNTIME_FORMAT,
|
|
16
16
|
SHARD_SCHEMA,
|
|
17
|
+
default_reference_bim_filename,
|
|
17
18
|
md5_file,
|
|
18
19
|
validate_chr_sex,
|
|
19
20
|
validate_positive_int,
|
|
21
|
+
validate_reference_bim_filename,
|
|
20
22
|
validate_shard_metadata,
|
|
21
23
|
)
|
|
24
|
+
from ._ld_reference import validate_bundled_reference_bim
|
|
22
25
|
from ._utils import CHR_RANK
|
|
23
26
|
|
|
24
27
|
|
|
@@ -70,8 +73,14 @@ def write_ld_npz_shards(root, shard_specs) -> list[dict]:
|
|
|
70
73
|
raise ValueError(f"duplicate LD shard output file: {file_key}")
|
|
71
74
|
seen_files.add(file_key)
|
|
72
75
|
|
|
73
|
-
shard_payload = {
|
|
76
|
+
shard_payload = {
|
|
77
|
+
k: v
|
|
78
|
+
for k, v in normalized.items()
|
|
79
|
+
if k not in {"file", "reference_shard"}
|
|
80
|
+
}
|
|
74
81
|
meta = _write_ld_npz_shard(path, **shard_payload)
|
|
82
|
+
reference_bim = meta["reference_bim"]
|
|
83
|
+
_ensure_reference_bim(root / reference_bim, normalized["reference_shard"], meta)
|
|
75
84
|
records.append(
|
|
76
85
|
{
|
|
77
86
|
"chr": meta["chr"],
|
|
@@ -81,6 +90,7 @@ def write_ld_npz_shards(root, shard_specs) -> list[dict]:
|
|
|
81
90
|
"num_snp": meta["num_snp"],
|
|
82
91
|
"nnz": meta["nnz"],
|
|
83
92
|
"reference_checksum": meta["reference_checksum"],
|
|
93
|
+
"reference_bim": reference_bim,
|
|
84
94
|
}
|
|
85
95
|
)
|
|
86
96
|
return records
|
|
@@ -131,6 +141,7 @@ def _ld_npz_manifest_record(path: Path, root: Path) -> dict:
|
|
|
131
141
|
"num_snp": meta["num_snp"],
|
|
132
142
|
"nnz": meta["nnz"],
|
|
133
143
|
"reference_checksum": meta["reference_checksum"],
|
|
144
|
+
"reference_bim": _require_reference_bim(root, meta),
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
|
|
@@ -155,6 +166,7 @@ def _normalize_ld_shard_write_spec(spec: dict) -> dict:
|
|
|
155
166
|
"build_metadata",
|
|
156
167
|
"extra_metadata",
|
|
157
168
|
"file",
|
|
169
|
+
"reference_bim",
|
|
158
170
|
}
|
|
159
171
|
unknown = sorted(set(spec).difference(allowed))
|
|
160
172
|
if unknown:
|
|
@@ -178,12 +190,26 @@ def _normalize_ld_shard_write_spec(spec: dict) -> dict:
|
|
|
178
190
|
reference_checksum = spec.get("reference_checksum", getattr(ref, "checksum", None))
|
|
179
191
|
if not isinstance(reference_checksum, str) or reference_checksum == "":
|
|
180
192
|
raise ValueError("LD shard write spec reference_checksum must be a non-empty string")
|
|
193
|
+
reference_bim = validate_reference_bim_filename(
|
|
194
|
+
spec.get("reference_bim", default_reference_bim_filename(chr_label)),
|
|
195
|
+
"LD shard write spec reference_bim",
|
|
196
|
+
)
|
|
197
|
+
if ref is None:
|
|
198
|
+
raise ValueError("LD shard write spec must provide reference_shard so reference_bim can be bundled")
|
|
199
|
+
if getattr(ref, "label", None) != chr_label:
|
|
200
|
+
raise ValueError("LD shard write spec reference_shard label must match chr")
|
|
201
|
+
if getattr(ref, "num_snp", None) != num_snp:
|
|
202
|
+
raise ValueError("LD shard write spec reference_shard num_snp must match num_snp")
|
|
203
|
+
if getattr(ref, "checksum", None) != reference_checksum:
|
|
204
|
+
raise ValueError("LD shard write spec reference_shard checksum must match reference_checksum")
|
|
181
205
|
|
|
182
206
|
return {
|
|
183
207
|
"chr_label": chr_label,
|
|
184
208
|
"sex": sex,
|
|
185
209
|
"num_snp": num_snp,
|
|
186
210
|
"reference_checksum": reference_checksum,
|
|
211
|
+
"reference_shard": ref,
|
|
212
|
+
"reference_bim": reference_bim,
|
|
187
213
|
"a1freq": _coerce_a1freq(spec.get("a1freq"), num_snp),
|
|
188
214
|
"ld_pairs": spec.get("ld_pairs"),
|
|
189
215
|
"build_metadata": dict(spec.get("build_metadata", {})),
|
|
@@ -199,6 +225,7 @@ def _write_ld_npz_shard(
|
|
|
199
225
|
sex,
|
|
200
226
|
num_snp,
|
|
201
227
|
reference_checksum,
|
|
228
|
+
reference_bim,
|
|
202
229
|
a1freq,
|
|
203
230
|
ld_pairs,
|
|
204
231
|
build_metadata=None,
|
|
@@ -217,7 +244,9 @@ def _write_ld_npz_shard(
|
|
|
217
244
|
sex=sex,
|
|
218
245
|
num_snp=num_snp,
|
|
219
246
|
nnz=nnz,
|
|
247
|
+
num_monomorphic_snps=int(np.count_nonzero((a1freq == 0.0) | (a1freq == 1.0))),
|
|
220
248
|
reference_checksum=reference_checksum,
|
|
249
|
+
reference_bim=reference_bim,
|
|
221
250
|
build_metadata=build_metadata,
|
|
222
251
|
extra_metadata=extra_metadata,
|
|
223
252
|
)
|
|
@@ -337,7 +366,9 @@ def _ld_npz_metadata(
|
|
|
337
366
|
sex,
|
|
338
367
|
num_snp,
|
|
339
368
|
nnz,
|
|
369
|
+
num_monomorphic_snps,
|
|
340
370
|
reference_checksum,
|
|
371
|
+
reference_bim,
|
|
341
372
|
build_metadata=None,
|
|
342
373
|
extra_metadata=None,
|
|
343
374
|
) -> dict:
|
|
@@ -354,7 +385,9 @@ def _ld_npz_metadata(
|
|
|
354
385
|
"matrix": "symmetric",
|
|
355
386
|
"diagonal": "explicit_unit",
|
|
356
387
|
"value": "r",
|
|
388
|
+
"num_monomorphic_snps": int(num_monomorphic_snps),
|
|
357
389
|
"reference_checksum": reference_checksum,
|
|
390
|
+
"reference_bim": reference_bim,
|
|
358
391
|
}
|
|
359
392
|
build = dict(LD_BUILD_METADATA_DEFAULTS)
|
|
360
393
|
if build_metadata:
|
|
@@ -362,6 +395,17 @@ def _ld_npz_metadata(
|
|
|
362
395
|
meta.update(build)
|
|
363
396
|
if extra_metadata:
|
|
364
397
|
meta.update(extra_metadata)
|
|
398
|
+
meta.update(
|
|
399
|
+
{
|
|
400
|
+
"chr": chr_label,
|
|
401
|
+
"sex": sex,
|
|
402
|
+
"num_snp": int(num_snp),
|
|
403
|
+
"nnz": int(nnz),
|
|
404
|
+
"num_monomorphic_snps": int(num_monomorphic_snps),
|
|
405
|
+
"reference_checksum": reference_checksum,
|
|
406
|
+
"reference_bim": reference_bim,
|
|
407
|
+
}
|
|
408
|
+
)
|
|
365
409
|
validate_shard_metadata(meta, Path("<generated>"), expected_format=NPZ_FORMAT)
|
|
366
410
|
return meta
|
|
367
411
|
|
|
@@ -372,6 +416,37 @@ def _default_ld_shard_filename(chr_label: str, sex, suffix: str) -> str:
|
|
|
372
416
|
return f"ld_chr{chr_label}{suffix}"
|
|
373
417
|
|
|
374
418
|
|
|
419
|
+
def _ensure_reference_bim(path: Path, reference_shard, meta: dict) -> None:
|
|
420
|
+
if path.exists():
|
|
421
|
+
_validate_existing_reference_bim(path, meta)
|
|
422
|
+
return
|
|
423
|
+
_write_reference_bim_from_shard(path, reference_shard)
|
|
424
|
+
_validate_existing_reference_bim(path, meta)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _write_reference_bim_from_shard(path: Path, reference_shard) -> None:
|
|
428
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
429
|
+
with open(path, "w", encoding="utf-8", newline="") as f:
|
|
430
|
+
for chr_label, snp, bp, a1, a2 in zip(
|
|
431
|
+
reference_shard.chr,
|
|
432
|
+
reference_shard.snp,
|
|
433
|
+
reference_shard.bp,
|
|
434
|
+
reference_shard.a1,
|
|
435
|
+
reference_shard.a2,
|
|
436
|
+
):
|
|
437
|
+
f.write(f"{chr_label}\t{snp}\t0\t{int(bp)}\t{a1}\t{a2}\n")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _validate_existing_reference_bim(path: Path, meta: dict) -> None:
|
|
441
|
+
validate_bundled_reference_bim(path, meta, target="LD shard")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _require_reference_bim(root: Path, meta: dict) -> str:
|
|
445
|
+
reference_bim = meta["reference_bim"]
|
|
446
|
+
_validate_existing_reference_bim(root / reference_bim, meta)
|
|
447
|
+
return reference_bim
|
|
448
|
+
|
|
449
|
+
|
|
375
450
|
def _write_deterministic_npz(path: Path, arrays: dict[str, np.ndarray]) -> None:
|
|
376
451
|
with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
377
452
|
for name, array in arrays.items():
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import io
|
|
3
2
|
from pathlib import Path
|
|
4
3
|
|
|
5
4
|
import numpy as np
|
|
@@ -12,27 +11,6 @@ from ._utils import validate_requested_shards
|
|
|
12
11
|
_CACHE_SCHEMA = "annotations_cache/0.1"
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
def _is_ignored_bed_line(line: str) -> bool:
|
|
16
|
-
return (
|
|
17
|
-
line == ""
|
|
18
|
-
or line.startswith("#")
|
|
19
|
-
or line.startswith("track ")
|
|
20
|
-
or line.startswith("browser ")
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _read_bed_payload(path: Path) -> str:
|
|
25
|
-
kept = []
|
|
26
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
27
|
-
for raw in f:
|
|
28
|
-
line = raw.rstrip("\r\n")
|
|
29
|
-
if _is_ignored_bed_line(line):
|
|
30
|
-
continue
|
|
31
|
-
kept.append(line)
|
|
32
|
-
if not kept:
|
|
33
|
-
raise ValueError(f"{path}: BED file is empty")
|
|
34
|
-
return "\n".join(kept) + "\n"
|
|
35
|
-
|
|
36
14
|
|
|
37
15
|
def _as_sparse_binary(mat) -> sparse.csr_matrix:
|
|
38
16
|
if sparse.issparse(mat):
|
|
@@ -94,14 +72,16 @@ def _annotation_name_from_path(path: Path) -> str:
|
|
|
94
72
|
|
|
95
73
|
|
|
96
74
|
def _parse_bed(path: Path) -> dict[str, np.ndarray]:
|
|
97
|
-
|
|
75
|
+
# Lines beginning with '#' and blank lines are skipped natively.
|
|
76
|
+
# 'track'/'browser' metadata lines are not supported; prefix with '#' if present.
|
|
98
77
|
try:
|
|
99
78
|
df = pd.read_csv(
|
|
100
|
-
|
|
101
|
-
sep=
|
|
79
|
+
path,
|
|
80
|
+
sep="\t",
|
|
102
81
|
header=None,
|
|
103
82
|
dtype=str,
|
|
104
|
-
|
|
83
|
+
comment="#",
|
|
84
|
+
skip_blank_lines=True,
|
|
105
85
|
keep_default_na=False,
|
|
106
86
|
na_filter=False,
|
|
107
87
|
)
|
|
@@ -113,7 +93,7 @@ def _parse_bed(path: Path) -> dict[str, np.ndarray]:
|
|
|
113
93
|
if df.shape[0] == 0:
|
|
114
94
|
raise ValueError(f"{path}: BED file is empty")
|
|
115
95
|
if df.shape[1] < 3:
|
|
116
|
-
raise ValueError(f"{path}: BED must have at least 3
|
|
96
|
+
raise ValueError(f"{path}: BED must have at least 3 tab-separated columns")
|
|
117
97
|
|
|
118
98
|
chr_col = df.iloc[:, 0]
|
|
119
99
|
start_raw = df.iloc[:, 1]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
+
import warnings
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
4
5
|
from scipy import sparse
|
|
@@ -15,7 +16,16 @@ from ._utils import validate_requested_shards
|
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class LDShard:
|
|
18
|
-
def __init__(
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
chr_label,
|
|
22
|
+
sex,
|
|
23
|
+
num_snp,
|
|
24
|
+
ld_r,
|
|
25
|
+
a1freq,
|
|
26
|
+
reference_checksum,
|
|
27
|
+
num_monomorphic_snps=0,
|
|
28
|
+
):
|
|
19
29
|
self._chr = str(chr_label)
|
|
20
30
|
self._sex = sex
|
|
21
31
|
self._num_snp = int(num_snp)
|
|
@@ -23,6 +33,7 @@ class LDShard:
|
|
|
23
33
|
self._ld_r2 = _build_ld_r2(self._ld_r)
|
|
24
34
|
self._a1freq = np.asarray(a1freq, dtype=np.float32).reshape(-1)
|
|
25
35
|
self._reference_checksum = str(reference_checksum)
|
|
36
|
+
self._num_monomorphic_snps = int(num_monomorphic_snps)
|
|
26
37
|
|
|
27
38
|
@property
|
|
28
39
|
def chr(self) -> str:
|
|
@@ -56,11 +67,16 @@ class LDShard:
|
|
|
56
67
|
def checksum(self) -> str:
|
|
57
68
|
return self._reference_checksum
|
|
58
69
|
|
|
70
|
+
@property
|
|
71
|
+
def num_monomorphic_snps(self) -> int:
|
|
72
|
+
return self._num_monomorphic_snps
|
|
73
|
+
|
|
59
74
|
|
|
60
75
|
class LDPanel:
|
|
61
|
-
def __init__(self, shard_groups, default_chrX_sex="female"):
|
|
76
|
+
def __init__(self, shard_groups, default_chrX_sex="female", reference=None):
|
|
62
77
|
self._shard_groups = [list(g) for g in shard_groups]
|
|
63
78
|
self._default_chrX_sex = validate_chrx_sex(default_chrX_sex, "default_chrX_sex")
|
|
79
|
+
self._reference = reference
|
|
64
80
|
self._shard_offsets = []
|
|
65
81
|
pos = 0
|
|
66
82
|
for group in self._shard_groups:
|
|
@@ -77,6 +93,8 @@ class LDPanel:
|
|
|
77
93
|
)
|
|
78
94
|
pos += first.num_snp
|
|
79
95
|
self._num_snp = pos
|
|
96
|
+
if self._reference is not None:
|
|
97
|
+
self._validate_reference_shape()
|
|
80
98
|
self._validate_default_chrX_sex()
|
|
81
99
|
|
|
82
100
|
@property
|
|
@@ -108,6 +126,10 @@ class LDPanel:
|
|
|
108
126
|
def default_chrX_sex(self) -> str:
|
|
109
127
|
return self._default_chrX_sex
|
|
110
128
|
|
|
129
|
+
@property
|
|
130
|
+
def reference(self):
|
|
131
|
+
return self._reference
|
|
132
|
+
|
|
111
133
|
def a1freq(self, chrX_sex=None) -> np.ndarray:
|
|
112
134
|
shards = [shard for shard, _start, _stop in self.iter_shards_with_offsets(chrX_sex)]
|
|
113
135
|
if not shards:
|
|
@@ -118,7 +140,8 @@ class LDPanel:
|
|
|
118
140
|
available = [group[0].label for group in self._shard_groups]
|
|
119
141
|
selected = validate_requested_shards(shards, available, "LDPanel.select_shards")
|
|
120
142
|
by_label = {group[0].label: group for group in self._shard_groups}
|
|
121
|
-
|
|
143
|
+
reference = None if self._reference is None else self._reference.select_shards(selected)
|
|
144
|
+
return LDPanel([by_label[label] for label in selected], self._default_chrX_sex, reference)
|
|
122
145
|
|
|
123
146
|
def multiply_r2(self, M, chrX_sex=None):
|
|
124
147
|
if sparse.issparse(M):
|
|
@@ -156,6 +179,17 @@ class LDPanel:
|
|
|
156
179
|
f"got {self._default_chrX_sex!r}, present {sorted(present)!r}"
|
|
157
180
|
)
|
|
158
181
|
|
|
182
|
+
def _validate_reference_shape(self) -> None:
|
|
183
|
+
ref_shards = list(self._reference.shards)
|
|
184
|
+
if len(ref_shards) != len(self._shard_groups):
|
|
185
|
+
raise ValueError("LDPanel reference shard count must match LD shard groups")
|
|
186
|
+
for ref_shard, group in zip(ref_shards, self._shard_groups):
|
|
187
|
+
first = group[0]
|
|
188
|
+
if ref_shard.label != first.label:
|
|
189
|
+
raise ValueError("LDPanel reference shard labels must match LD shard groups")
|
|
190
|
+
if ref_shard.num_snp != first.num_snp:
|
|
191
|
+
raise ValueError("LDPanel reference shard sizes must match LD shard groups")
|
|
192
|
+
|
|
159
193
|
def _multiply_r2_dense(self, M, chrX_sex=None):
|
|
160
194
|
arr = np.asarray(M)
|
|
161
195
|
if arr.ndim == 1:
|
|
@@ -213,26 +247,32 @@ class LDPanel:
|
|
|
213
247
|
return sparse.vstack(parts, format=M.getformat()).astype(out_dtype)
|
|
214
248
|
|
|
215
249
|
|
|
216
|
-
def load_ld(path, reference, default_chrX_sex=None) -> LDPanel:
|
|
250
|
+
def load_ld(path, reference=None, shards=None, default_chrX_sex=None) -> LDPanel:
|
|
217
251
|
default_chrX_sex = validate_chrx_sex(
|
|
218
252
|
"female" if default_chrX_sex is None else default_chrX_sex,
|
|
219
253
|
"default_chrX_sex",
|
|
220
254
|
)
|
|
221
255
|
path = Path(path)
|
|
256
|
+
if not path.is_dir():
|
|
257
|
+
raise ValueError("load_ld: path must identify a panel root directory")
|
|
258
|
+
|
|
259
|
+
manifest = read_manifest(path / "ld_manifest.json", expected_runtime=PY_RUNTIME_FORMAT)
|
|
260
|
+
if reference is None:
|
|
261
|
+
reference = _load_bundled_reference(path, manifest, shards)
|
|
262
|
+
elif shards is not None:
|
|
263
|
+
reference = reference.select_shards(shards)
|
|
264
|
+
|
|
222
265
|
ref_shards = list(reference.shards)
|
|
223
266
|
if not ref_shards:
|
|
224
267
|
raise ValueError("load_ld: reference must contain at least one shard")
|
|
225
268
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
groups = _load_panel_root(path, manifest, ref_shards)
|
|
229
|
-
else:
|
|
230
|
-
groups = _load_single_shard(path, ref_shards)
|
|
231
|
-
|
|
232
|
-
return LDPanel(groups, default_chrX_sex=default_chrX_sex)
|
|
269
|
+
groups = _load_panel_root(path, manifest, ref_shards)
|
|
270
|
+
return LDPanel(groups, default_chrX_sex=default_chrX_sex, reference=reference)
|
|
233
271
|
|
|
234
272
|
|
|
235
273
|
def validate_ld_distribution(path, check_payload_structure=False) -> dict:
|
|
274
|
+
if not Path(path).is_dir():
|
|
275
|
+
raise ValueError("validate_ld_distribution: path must identify a panel root directory")
|
|
236
276
|
return validate_npz_distribution(path, check_payload_structure=check_payload_structure)
|
|
237
277
|
|
|
238
278
|
|
|
@@ -311,6 +351,12 @@ def _write_ld_npz_distribution(*args, **kwargs):
|
|
|
311
351
|
return write_ld_npz_distribution(*args, **kwargs)
|
|
312
352
|
|
|
313
353
|
|
|
354
|
+
def _load_bundled_reference(root: Path, manifest: dict, shards):
|
|
355
|
+
from ._ld_reference import load_bundled_reference_panel
|
|
356
|
+
|
|
357
|
+
return load_bundled_reference_panel(root, manifest, shards, where="load_ld")
|
|
358
|
+
|
|
359
|
+
|
|
314
360
|
def _load_panel_root(root: Path, manifest: dict, ref_shards: list) -> list[list[LDShard]]:
|
|
315
361
|
entries = list(manifest["shards"])
|
|
316
362
|
groups = []
|
|
@@ -346,22 +392,6 @@ def _load_panel_root(root: Path, manifest: dict, ref_shards: list) -> list[list[
|
|
|
346
392
|
return groups
|
|
347
393
|
|
|
348
394
|
|
|
349
|
-
def _load_single_shard(path: Path, ref_shards: list) -> list[list[LDShard]]:
|
|
350
|
-
shard, _meta = _load_npz_shard(path)
|
|
351
|
-
if len(ref_shards) != 1:
|
|
352
|
-
raise ValueError(
|
|
353
|
-
"single-shard LD loads require a single-shard reference; "
|
|
354
|
-
f"reference has {len(ref_shards)} shards"
|
|
355
|
-
)
|
|
356
|
-
if ref_shards[0].label != shard.chr:
|
|
357
|
-
raise ValueError(
|
|
358
|
-
"single-shard LD chromosome does not match single-shard reference: "
|
|
359
|
-
f"LD chr {shard.chr!r}, reference chr {ref_shards[0].label!r}"
|
|
360
|
-
)
|
|
361
|
-
_validate_reference_compatibility(shard, ref_shards[0], path)
|
|
362
|
-
return [[shard]]
|
|
363
|
-
|
|
364
|
-
|
|
365
395
|
def _load_npz_shard(path: Path) -> tuple[LDShard, dict]:
|
|
366
396
|
require_file(path)
|
|
367
397
|
ld_r, a1freq, meta = read_npz_shard(path, check_payload_structure=False)
|
|
@@ -372,7 +402,15 @@ def _load_npz_shard(path: Path) -> tuple[LDShard, dict]:
|
|
|
372
402
|
ld_r,
|
|
373
403
|
a1freq,
|
|
374
404
|
meta["reference_checksum"],
|
|
405
|
+
int(meta["num_monomorphic_snps"]),
|
|
375
406
|
)
|
|
407
|
+
if shard.num_monomorphic_snps:
|
|
408
|
+
warnings.warn(
|
|
409
|
+
f"{path}: LD shard contains {shard.num_monomorphic_snps} monomorphic SNPs; "
|
|
410
|
+
"LD involving those SNPs is undefined and represented by omitted off-diagonal entries",
|
|
411
|
+
RuntimeWarning,
|
|
412
|
+
stacklevel=2,
|
|
413
|
+
)
|
|
376
414
|
return shard, meta
|
|
377
415
|
|
|
378
416
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.0"
|
|
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
|