ldclust 0.2.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.
- ldclust/__init__.py +20 -0
- ldclust/cli.py +431 -0
- ldclust/library.py +1285 -0
- ldclust-0.2.0.dist-info/METADATA +112 -0
- ldclust-0.2.0.dist-info/RECORD +8 -0
- ldclust-0.2.0.dist-info/WHEEL +5 -0
- ldclust-0.2.0.dist-info/entry_points.txt +2 -0
- ldclust-0.2.0.dist-info/top_level.txt +1 -0
ldclust/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""ldclust: LD-based SNP clustering + haplotype-testing protocol."""
|
|
2
|
+
|
|
3
|
+
from .library import (PROTOCOL_P, DISTANCES, load_ld, load_ld_h5, load_ids,
|
|
4
|
+
drop_nan_snps, distances, spectral_embedding,
|
|
5
|
+
cluster_hdbscan, cluster_optics, cluster_soptics,
|
|
6
|
+
cluster_dbscan, cluster_fof, cluster_louvain,
|
|
7
|
+
spectral_eig, cluster_spectral, cluster_ap,
|
|
8
|
+
cluster_mcl, cluster_lpa, cluster_leiden,
|
|
9
|
+
cluster_walktrap, cluster_infomap, cluster_pam,
|
|
10
|
+
cluster_cnm, cluster_cw, cluster_kmeans,
|
|
11
|
+
cluster_minibatch_kmeans, load_dosage,
|
|
12
|
+
cross_product_matrix, cluster_gmm, cluster_hc,
|
|
13
|
+
dynamic_tree_cut, cluster_dpblocks, cluster_sbm,
|
|
14
|
+
cluster_cdhit, coassociation, cluster_consensus,
|
|
15
|
+
cluster_dpgmm, write_blocks,
|
|
16
|
+
write_hdbscan_file, write_hlist, run_hap_assoc,
|
|
17
|
+
parse_assoc_hap, select_blocks, causal_snps,
|
|
18
|
+
snp_positions, clustering_stats, pairwise_ari)
|
|
19
|
+
|
|
20
|
+
__version__ = "0.2.0"
|
ldclust/cli.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ldclust command line: run one clustering method on an LD matrix and
|
|
3
|
+
write the benchmark artifacts (blocks csv, labels npy, hlist for PLINK
|
|
4
|
+
--hap-assoc, and a row in <out-dir>/summary.tsv).
|
|
5
|
+
|
|
6
|
+
Methods:
|
|
7
|
+
hdbscan HDBSCAN on precomputed distances (--dist d1|d2, default d2)
|
|
8
|
+
optics OPTICS on precomputed distances d2 (sklearn)
|
|
9
|
+
soptics OPTICS on the spectral embedding of the r2 matrix
|
|
10
|
+
fof friends-of-friends percolation on r2 >= --eps (no noise label)
|
|
11
|
+
dbscan DBSCAN on precomputed distances d2 with --eps
|
|
12
|
+
louvain modularity communities on the weighted r2 graph
|
|
13
|
+
spectral normalized-affinity spectral clustering (Ng et al. 2002)
|
|
14
|
+
ap affinity propagation on the raw r2 similarity
|
|
15
|
+
mcl Markov clustering on the weighted r2 graph
|
|
16
|
+
lpa asynchronous label propagation on the r2 graph
|
|
17
|
+
leiden Leiden RBConfiguration partition on the r2 graph
|
|
18
|
+
walktrap Walktrap communities on the r2 graph
|
|
19
|
+
infomap Infomap two-level partition on the r2 graph
|
|
20
|
+
pam similarity k-medoids on the raw r2
|
|
21
|
+
cnm greedy modularity (CNM) on the r2 graph
|
|
22
|
+
cw Chinese Whispers on the r2 graph
|
|
23
|
+
gmm Gaussian mixture on the cross-product-matrix embedding
|
|
24
|
+
kmeans K-Means on the Euclidean embedding of 1 - r2
|
|
25
|
+
minibatch Mini-Batch K-Means on the same embedding
|
|
26
|
+
hc agglomerative linkage on 1-r2 + dynamic tree cut (noise = -1)
|
|
27
|
+
dpblocks dynamic-programming contiguous blocks minimizing tag SNPs
|
|
28
|
+
sbm degree-corrected stochastic block model (needs graph-tool;
|
|
29
|
+
run inside its conda environment - see README)
|
|
30
|
+
cdhit CD-HIT-style greedy seed-and-recruit on r2 >= --eps
|
|
31
|
+
consensus evidence-accumulation consensus of existing runs' labels
|
|
32
|
+
(--bases tag1,tag2,... --consensus dtc|fof)
|
|
33
|
+
dpgmm Dirichlet-process GMM on the gmm embedding (--k = cap only)
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
ldclust -p <prefix> --method <name> [--eps 0.5] [--dist d2]
|
|
37
|
+
[--out-dir data/bench] [--check <labels.npy>]
|
|
38
|
+
(prefix names prefix.ld.h5 + prefix.snplist; --method hdbscan|gmm
|
|
39
|
+
also read prefix.gmm.raw via PLINK --recodeA output)
|
|
40
|
+
|
|
41
|
+
Author: Gennady Khvorykh, info@inzilico.com
|
|
42
|
+
Started: 2026-09-21
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
import argparse
|
|
46
|
+
import os
|
|
47
|
+
import time
|
|
48
|
+
|
|
49
|
+
import numpy as np
|
|
50
|
+
|
|
51
|
+
from . import __version__
|
|
52
|
+
from . import library as ld
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_parser():
|
|
56
|
+
parser = argparse.ArgumentParser(
|
|
57
|
+
prog="ldclust",
|
|
58
|
+
description="LD-based clustering runner (ldclust package)")
|
|
59
|
+
parser.add_argument("-p", "--prefix", required=True,
|
|
60
|
+
help="/path/to/prefix of prefix.ld.h5 file")
|
|
61
|
+
parser.add_argument("--method", required=True,
|
|
62
|
+
choices=["hdbscan", "optics", "soptics", "fof",
|
|
63
|
+
"dbscan", "louvain", "spectral", "ap",
|
|
64
|
+
"mcl", "lpa", "leiden", "walktrap",
|
|
65
|
+
"infomap", "pam", "cnm", "cw", "gmm",
|
|
66
|
+
"kmeans", "minibatch", "hc", "dpblocks",
|
|
67
|
+
"sbm", "cdhit", "consensus", "dpgmm"])
|
|
68
|
+
parser.add_argument("--dist", choices=["d1", "d2"], default="d2")
|
|
69
|
+
parser.add_argument("--eps", type=float, default=None,
|
|
70
|
+
help="threshold for fof and cdhit (r2), dbscan "
|
|
71
|
+
"(distance), consensus-fof (co-association)")
|
|
72
|
+
parser.add_argument("--min-samples", type=int, default=5)
|
|
73
|
+
parser.add_argument("--xi", type=float, default=0.05)
|
|
74
|
+
parser.add_argument("--variance", type=float, default=0.95)
|
|
75
|
+
parser.add_argument("--k", type=int, default=None,
|
|
76
|
+
help="components: soptics embedding dim / pam "
|
|
77
|
+
"blocks / gmm components / dpgmm cap")
|
|
78
|
+
parser.add_argument("--dim", type=int, default=50,
|
|
79
|
+
help="embedding dimension for gmm/dpgmm")
|
|
80
|
+
parser.add_argument("--gamma", type=float, default=None,
|
|
81
|
+
help="resolution for louvain")
|
|
82
|
+
parser.add_argument("--theta", type=float, default=None,
|
|
83
|
+
help="eigenvalue cutoff for spectral "
|
|
84
|
+
"(k = #(w > theta))")
|
|
85
|
+
parser.add_argument("--preference", type=float, default=None,
|
|
86
|
+
help="self-similarity (diagonal) for ap")
|
|
87
|
+
parser.add_argument("--inflation", type=float, default=None,
|
|
88
|
+
help="inflation for mcl")
|
|
89
|
+
parser.add_argument("--cutoff", type=float, default=0.05,
|
|
90
|
+
help="edge cutoff on r2 for graph methods; "
|
|
91
|
+
"consensus bases count; sbm")
|
|
92
|
+
parser.add_argument("--steps", type=int, default=4,
|
|
93
|
+
help="random-walk length for walktrap")
|
|
94
|
+
parser.add_argument("--linkage", choices=["average", "complete"],
|
|
95
|
+
default="average", help="agglomeration for hc")
|
|
96
|
+
parser.add_argument("--min-cluster-size", type=int, default=3,
|
|
97
|
+
help="dynamic tree cut minimum block size for hc")
|
|
98
|
+
parser.add_argument("--deep-split", type=int, default=None,
|
|
99
|
+
choices=[0, 1, 2, 3, 4],
|
|
100
|
+
help="dynamic tree cut deepSplit preset for hc")
|
|
101
|
+
parser.add_argument("--tag-thr", type=float, default=None,
|
|
102
|
+
help="r2 tag-coverage threshold for dpblocks")
|
|
103
|
+
parser.add_argument("--max-len", type=int, default=None,
|
|
104
|
+
help="maximum block length in SNPs for dpblocks")
|
|
105
|
+
parser.add_argument("--no-deg-corr", action="store_true",
|
|
106
|
+
help="plain SBM (no degree correction) for sbm")
|
|
107
|
+
parser.add_argument("--binary", action="store_true",
|
|
108
|
+
help="ignore r2 weights (binary graph) for sbm")
|
|
109
|
+
parser.add_argument("--nested", action="store_true",
|
|
110
|
+
help="nested SBM (level-0 partition) for sbm")
|
|
111
|
+
parser.add_argument("--priority", choices=["degree", "order"],
|
|
112
|
+
default="degree", help="seed priority for cdhit")
|
|
113
|
+
parser.add_argument("--recruit", choices=["seed", "member"],
|
|
114
|
+
default="seed", help="recruit semantics for cdhit")
|
|
115
|
+
parser.add_argument("--bases", default=None,
|
|
116
|
+
help="comma-separated base run tags for consensus "
|
|
117
|
+
"(labels.npy in --out-dir)")
|
|
118
|
+
parser.add_argument("--consensus", choices=["dtc", "fof"], default="dtc",
|
|
119
|
+
help="consensus function on the co-association "
|
|
120
|
+
"matrix")
|
|
121
|
+
parser.add_argument("--conc", type=float, default=None,
|
|
122
|
+
help="DP weight concentration prior for dpgmm "
|
|
123
|
+
"(default: sklearn 1/cap)")
|
|
124
|
+
parser.add_argument("--out-dir", default="data/bench")
|
|
125
|
+
parser.add_argument("--check", default=None,
|
|
126
|
+
help="optional labels.npy to verify determinism")
|
|
127
|
+
parser.add_argument("--version", action="version",
|
|
128
|
+
version="%(prog)s " + __version__)
|
|
129
|
+
return parser
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main(argv=None):
|
|
133
|
+
t1 = time.time()
|
|
134
|
+
parser = build_parser()
|
|
135
|
+
args = parser.parse_args(argv)
|
|
136
|
+
|
|
137
|
+
if args.method == "fof" and args.eps is None:
|
|
138
|
+
parser.error("--eps is required for fof")
|
|
139
|
+
if args.method == "dbscan" and args.eps is None:
|
|
140
|
+
parser.error("--eps is required for dbscan")
|
|
141
|
+
if args.method == "louvain" and args.gamma is None:
|
|
142
|
+
parser.error("--gamma is required for louvain")
|
|
143
|
+
if args.method == "spectral" and args.theta is None:
|
|
144
|
+
parser.error("--theta is required for spectral")
|
|
145
|
+
if args.method == "ap" and args.preference is None:
|
|
146
|
+
parser.error("--preference is required for ap")
|
|
147
|
+
if args.method == "mcl" and args.inflation is None:
|
|
148
|
+
parser.error("--inflation is required for mcl")
|
|
149
|
+
if args.method == "leiden" and args.gamma is None:
|
|
150
|
+
parser.error("--gamma is required for leiden")
|
|
151
|
+
if args.method == "pam" and args.k is None:
|
|
152
|
+
parser.error("--k is required for pam")
|
|
153
|
+
if args.method == "gmm" and args.k is None:
|
|
154
|
+
parser.error("--k is required for gmm")
|
|
155
|
+
if args.method == "kmeans" and args.k is None:
|
|
156
|
+
parser.error("--k is required for kmeans")
|
|
157
|
+
if args.method == "minibatch" and args.k is None:
|
|
158
|
+
parser.error("--k is required for minibatch")
|
|
159
|
+
if args.method == "dpgmm" and args.k is None:
|
|
160
|
+
parser.error("--k (component cap) is required for dpgmm")
|
|
161
|
+
if args.method == "hc" and args.deep_split is None:
|
|
162
|
+
parser.error("--deep-split is required for hc")
|
|
163
|
+
if args.method == "cdhit" and args.eps is None:
|
|
164
|
+
parser.error("--eps is required for cdhit")
|
|
165
|
+
if args.method == "consensus" and not args.bases:
|
|
166
|
+
parser.error("--bases is required for consensus")
|
|
167
|
+
if (args.method == "consensus" and args.consensus == "fof"
|
|
168
|
+
and args.eps is None):
|
|
169
|
+
parser.error("--eps (co-association threshold) is required for "
|
|
170
|
+
"fof consensus")
|
|
171
|
+
if args.method == "dpblocks" and (args.tag_thr is None
|
|
172
|
+
or args.max_len is None):
|
|
173
|
+
parser.error("--tag-thr and --max-len are required for dpblocks")
|
|
174
|
+
|
|
175
|
+
os.makedirs(args.out_dir, exist_ok=True)
|
|
176
|
+
name = os.path.basename(args.prefix)
|
|
177
|
+
|
|
178
|
+
ids, r2, layout = ld.load_ld(args.prefix)
|
|
179
|
+
r2, ids, n_dropped = ld.drop_nan_snps(r2, ids)
|
|
180
|
+
if n_dropped:
|
|
181
|
+
print(f"Dropped {n_dropped} SNPs with NaN")
|
|
182
|
+
print(f"Loaded {ids.size} SNPs ({layout})")
|
|
183
|
+
|
|
184
|
+
substrate = "precomputed-" + args.dist
|
|
185
|
+
if args.method == "hdbscan":
|
|
186
|
+
labels, probs, sec = ld.cluster_hdbscan(ld.distances(r2, args.dist))
|
|
187
|
+
tag = f"hdbscan.{args.dist}"
|
|
188
|
+
elif args.method == "optics":
|
|
189
|
+
labels, probs, sec = ld.cluster_optics(ld.distances(r2, "d2"),
|
|
190
|
+
args.min_samples, args.xi)
|
|
191
|
+
tag = "optics"
|
|
192
|
+
elif args.method == "soptics":
|
|
193
|
+
ksfx = f".emb{args.k}" if args.k else ""
|
|
194
|
+
emb_file = os.path.join(args.out_dir, f"{name}{ksfx}.embedding.npy")
|
|
195
|
+
if os.path.isfile(emb_file):
|
|
196
|
+
coords = np.load(emb_file)
|
|
197
|
+
print(f"Loaded embedding from {emb_file} (k = {coords.shape[1]})")
|
|
198
|
+
else:
|
|
199
|
+
coords, k, _ = ld.spectral_embedding(r2, args.variance, args.k)
|
|
200
|
+
np.save(emb_file, coords)
|
|
201
|
+
print(f"Built spectral embedding (k = {k}), saved to {emb_file}")
|
|
202
|
+
labels, probs, sec = ld.cluster_soptics(coords, args.min_samples,
|
|
203
|
+
args.xi)
|
|
204
|
+
tag = "soptics" + (f"k{args.k}" if args.k else "")
|
|
205
|
+
substrate = "spectral-embedding"
|
|
206
|
+
elif args.method == "fof":
|
|
207
|
+
labels, probs, sec = ld.cluster_fof(r2, args.eps)
|
|
208
|
+
tag = f"fof{args.eps}"
|
|
209
|
+
substrate = "r2-graph"
|
|
210
|
+
elif args.method == "louvain":
|
|
211
|
+
labels, probs, sec = ld.cluster_louvain(r2, args.gamma)
|
|
212
|
+
tag = f"louvain{args.gamma}"
|
|
213
|
+
substrate = "r2-graph"
|
|
214
|
+
elif args.method == "spectral":
|
|
215
|
+
labels, probs, sec = ld.cluster_spectral(r2, args.theta)
|
|
216
|
+
tag = f"spectral{args.theta}"
|
|
217
|
+
substrate = "normalized-affinity"
|
|
218
|
+
elif args.method == "ap":
|
|
219
|
+
labels, probs, sec = ld.cluster_ap(r2, args.preference)
|
|
220
|
+
tag = f"ap{args.preference}"
|
|
221
|
+
substrate = "r2-similarity"
|
|
222
|
+
elif args.method == "mcl":
|
|
223
|
+
labels, probs, sec = ld.cluster_mcl(r2, args.inflation, args.cutoff)
|
|
224
|
+
tag = f"mcl{args.inflation}"
|
|
225
|
+
substrate = "r2-graph"
|
|
226
|
+
elif args.method == "lpa":
|
|
227
|
+
labels, probs, sec = ld.cluster_lpa(r2, args.cutoff)
|
|
228
|
+
tag = "lpa"
|
|
229
|
+
substrate = "r2-graph"
|
|
230
|
+
elif args.method == "leiden":
|
|
231
|
+
labels, probs, sec = ld.cluster_leiden(r2, args.gamma, args.cutoff)
|
|
232
|
+
tag = f"leiden{args.gamma}"
|
|
233
|
+
substrate = "r2-graph"
|
|
234
|
+
elif args.method == "walktrap":
|
|
235
|
+
labels, probs, sec = ld.cluster_walktrap(r2, args.steps, args.cutoff)
|
|
236
|
+
tag = f"walktrap{args.steps}"
|
|
237
|
+
substrate = "r2-graph"
|
|
238
|
+
elif args.method == "infomap":
|
|
239
|
+
labels, probs, sec = ld.cluster_infomap(r2, args.cutoff)
|
|
240
|
+
tag = "infomap"
|
|
241
|
+
substrate = "r2-graph"
|
|
242
|
+
elif args.method == "pam":
|
|
243
|
+
labels, probs, sec = ld.cluster_pam(r2, args.k)
|
|
244
|
+
tag = f"pam{args.k}"
|
|
245
|
+
substrate = "r2-similarity"
|
|
246
|
+
elif args.method == "cnm":
|
|
247
|
+
labels, probs, sec = ld.cluster_cnm(r2, args.cutoff)
|
|
248
|
+
tag = "cnm"
|
|
249
|
+
substrate = "r2-graph"
|
|
250
|
+
elif args.method == "cw":
|
|
251
|
+
labels, probs, sec = ld.cluster_cw(r2, args.cutoff)
|
|
252
|
+
tag = "cw"
|
|
253
|
+
substrate = "r2-graph"
|
|
254
|
+
elif args.method == "gmm":
|
|
255
|
+
# relationship matrix from the genotype dosages, then GMM on
|
|
256
|
+
# its spectral embedding with a full per-component covariance
|
|
257
|
+
gmm_prefix = args.prefix + ".gmm" # genotype prefix must expose .raw
|
|
258
|
+
emb_file = os.path.join(args.out_dir,
|
|
259
|
+
f"{name}.gmm{args.dim}.embedding.npy")
|
|
260
|
+
if os.path.isfile(emb_file):
|
|
261
|
+
coords = np.load(emb_file)
|
|
262
|
+
print(f"Loaded embedding from {emb_file} (d = {coords.shape[1]})")
|
|
263
|
+
else:
|
|
264
|
+
k_grm, raw_snps = ld.cross_product_matrix(gmm_prefix)
|
|
265
|
+
if not np.array_equal(raw_snps, ids):
|
|
266
|
+
print("Cross-product SNP order differs from snplist - "
|
|
267
|
+
"reordering")
|
|
268
|
+
pos_of = {s: i for i, s in enumerate(raw_snps)}
|
|
269
|
+
order = np.array([pos_of[s] for s in ids])
|
|
270
|
+
k_grm = k_grm[np.ix_(order, order)]
|
|
271
|
+
coords, dim_used, _ = ld.spectral_embedding(k_grm, k=args.dim)
|
|
272
|
+
del k_grm
|
|
273
|
+
np.save(emb_file, coords)
|
|
274
|
+
print(f"Built cross-product embedding (d = {dim_used}), "
|
|
275
|
+
f"saved to {emb_file}")
|
|
276
|
+
labels, probs, sec = ld.cluster_gmm(k=args.k, dim=args.dim,
|
|
277
|
+
coords=coords)
|
|
278
|
+
tag = f"gmm{args.k}d{args.dim}"
|
|
279
|
+
substrate = "cross-product-embedding"
|
|
280
|
+
elif args.method == "kmeans":
|
|
281
|
+
# K-Means on the Euclidean embedding of the distance-converted LD
|
|
282
|
+
# matrix (d = 1 - r2 is squared Euclidean; classical MDS coords)
|
|
283
|
+
emb_file = os.path.join(
|
|
284
|
+
args.out_dir, f"{name}.kmeans{args.dim}.embedding.npy")
|
|
285
|
+
if os.path.isfile(emb_file):
|
|
286
|
+
coords = np.load(emb_file)
|
|
287
|
+
print(f"Loaded embedding from {emb_file} (d = {coords.shape[1]})")
|
|
288
|
+
else:
|
|
289
|
+
coords, dim_used, _ = ld.spectral_embedding(r2, k=args.dim)
|
|
290
|
+
np.save(emb_file, coords)
|
|
291
|
+
print(f"Built r2 embedding (d = {dim_used}), saved to {emb_file}")
|
|
292
|
+
labels, probs, sec = ld.cluster_kmeans(r2, args.k, args.dim)
|
|
293
|
+
tag = f"kmeans{args.k}d{args.dim}"
|
|
294
|
+
substrate = "r2-embedding"
|
|
295
|
+
elif args.method == "minibatch":
|
|
296
|
+
# Mini-Batch K-Means on the same embedding as kmeans (shared cache)
|
|
297
|
+
emb_file = os.path.join(
|
|
298
|
+
args.out_dir, f"{name}.kmeans{args.dim}.embedding.npy")
|
|
299
|
+
if os.path.isfile(emb_file):
|
|
300
|
+
coords = np.load(emb_file)
|
|
301
|
+
print(f"Loaded embedding from {emb_file} (d = {coords.shape[1]})")
|
|
302
|
+
else:
|
|
303
|
+
coords, dim_used, _ = ld.spectral_embedding(r2, k=args.dim)
|
|
304
|
+
np.save(emb_file, coords)
|
|
305
|
+
print(f"Built r2 embedding (d = {dim_used}), saved to {emb_file}")
|
|
306
|
+
labels, probs, sec = ld.cluster_minibatch_kmeans(r2, args.k, args.dim)
|
|
307
|
+
tag = f"mbk{args.k}d{args.dim}"
|
|
308
|
+
substrate = "r2-embedding"
|
|
309
|
+
elif args.method == "hc":
|
|
310
|
+
# WGCNA-style: scipy linkage on the condensed d1 = 1 - r2 (the
|
|
311
|
+
# analogue of WGCNA's 1 - |cor|), then the dynamic tree cut
|
|
312
|
+
# (Langfelder et al. 2008, tree stage) on the dendrogram
|
|
313
|
+
labels, probs, sec = ld.cluster_hc(ld.distances(r2, "d1"),
|
|
314
|
+
args.linkage,
|
|
315
|
+
args.min_cluster_size,
|
|
316
|
+
args.deep_split)
|
|
317
|
+
tag = f"dtc{args.deep_split}" + ("" if args.linkage == "average"
|
|
318
|
+
else args.linkage[0])
|
|
319
|
+
substrate = "precomputed-d1"
|
|
320
|
+
elif args.method == "dpblocks":
|
|
321
|
+
# Zhang et al. 2002 DP framework, r2-tagging cost: optimal
|
|
322
|
+
# contiguous partition of the ordered SNPs minimizing total tag
|
|
323
|
+
# SNPs (every SNP covered at r2 >= tag_thr; greedy set cover per
|
|
324
|
+
# candidate block)
|
|
325
|
+
labels, probs, sec = ld.cluster_dpblocks(r2, args.tag_thr,
|
|
326
|
+
args.max_len)
|
|
327
|
+
tag = f"dp{args.tag_thr}L{args.max_len}"
|
|
328
|
+
substrate = "r2-ordered"
|
|
329
|
+
elif args.method == "sbm":
|
|
330
|
+
# degree-corrected SBM on the weighted r2 graph (Peixoto 2017 MDL,
|
|
331
|
+
# graph-tool); run this CLI inside the graph-tool environment
|
|
332
|
+
labels, probs, sec = ld.cluster_sbm(r2, args.cutoff,
|
|
333
|
+
deg_corr=not args.no_deg_corr,
|
|
334
|
+
weighted=not args.binary,
|
|
335
|
+
nested=args.nested)
|
|
336
|
+
tag = ("sbm" + ("nd" if args.no_deg_corr else "")
|
|
337
|
+
+ ("b" if args.binary else "w") + str(args.cutoff)
|
|
338
|
+
+ ("n" if args.nested else ""))
|
|
339
|
+
substrate = "r2-graph-mdl"
|
|
340
|
+
elif args.method == "cdhit":
|
|
341
|
+
# CD-HIT-style greedy seed-and-recruit: hub SNPs (weighted degree)
|
|
342
|
+
# seed blocks first, recruits need r2 >= eps to the seed (no
|
|
343
|
+
# transitive chaining - the anti-FoF)
|
|
344
|
+
labels, probs, sec = ld.cluster_cdhit(r2, args.eps, args.priority,
|
|
345
|
+
args.recruit)
|
|
346
|
+
tag = (f"cd{args.eps}" + ("o" if args.priority == "order" else "")
|
|
347
|
+
+ ("m" if args.recruit == "member" else ""))
|
|
348
|
+
substrate = "r2-similarity"
|
|
349
|
+
elif args.method == "consensus":
|
|
350
|
+
# evidence-accumulation consensus: co-association matrix of the
|
|
351
|
+
# base partitions, then DTC on 1 - C (C is PSD like r2) or
|
|
352
|
+
# percolation
|
|
353
|
+
bases = args.bases.split(",")
|
|
354
|
+
base_labels = [np.load(os.path.join(
|
|
355
|
+
args.out_dir, f"{name}.{b}.labels.npy")) for b in bases]
|
|
356
|
+
ds = args.deep_split if args.deep_split is not None else 2
|
|
357
|
+
labels, probs, sec = ld.cluster_consensus(
|
|
358
|
+
base_labels, method=args.consensus, thr=args.eps, deep_split=ds,
|
|
359
|
+
min_cluster_size=args.min_cluster_size)
|
|
360
|
+
if args.consensus == "dtc":
|
|
361
|
+
tag = f"consdtc{ds}"
|
|
362
|
+
else:
|
|
363
|
+
tag = f"consfof{args.eps:g}"
|
|
364
|
+
substrate = "coassociation-of:" + "+".join(bases)
|
|
365
|
+
elif args.method == "dpgmm":
|
|
366
|
+
# Dirichlet-process GMM on the same cached embedding as gmm; --k is
|
|
367
|
+
# only an upper bound - the DP empties unsupported components
|
|
368
|
+
emb_file = os.path.join(
|
|
369
|
+
args.out_dir, f"{name}.gmm{args.dim}.embedding.npy")
|
|
370
|
+
if os.path.isfile(emb_file):
|
|
371
|
+
coords = np.load(emb_file)
|
|
372
|
+
print(f"Loaded embedding from {emb_file} (d = {coords.shape[1]})")
|
|
373
|
+
else:
|
|
374
|
+
gmm_prefix = args.prefix + ".gmm"
|
|
375
|
+
k_grm, raw_snps = ld.cross_product_matrix(gmm_prefix)
|
|
376
|
+
if not np.array_equal(raw_snps, ids):
|
|
377
|
+
print("Cross-product SNP order differs from snplist - "
|
|
378
|
+
"reordering")
|
|
379
|
+
pos_of = {s: i for i, s in enumerate(raw_snps)}
|
|
380
|
+
order = np.array([pos_of[s] for s in ids])
|
|
381
|
+
k_grm = k_grm[np.ix_(order, order)]
|
|
382
|
+
coords, dim_used, _ = ld.spectral_embedding(k_grm, k=args.dim)
|
|
383
|
+
del k_grm
|
|
384
|
+
np.save(emb_file, coords)
|
|
385
|
+
print(f"Built cross-product embedding (d = {dim_used}), "
|
|
386
|
+
f"saved to {emb_file}")
|
|
387
|
+
labels, probs, sec = ld.cluster_dpgmm(
|
|
388
|
+
coords=coords, n_components=args.k, dim=args.dim,
|
|
389
|
+
weight_concentration_prior=args.conc)
|
|
390
|
+
tag = f"dpgmm{args.k}" + (f"a{args.conc:g}" if args.conc else "adef")
|
|
391
|
+
substrate = "cross-product-embedding"
|
|
392
|
+
elif args.method == "dbscan":
|
|
393
|
+
labels, probs, sec = ld.cluster_dbscan(ld.distances(r2, "d2"),
|
|
394
|
+
args.eps, args.min_samples)
|
|
395
|
+
tag = f"dbscan{args.eps}"
|
|
396
|
+
|
|
397
|
+
stats = ld.clustering_stats(labels)
|
|
398
|
+
print(f"{tag}: {stats['n_blocks']} blocks, noise {stats['noise']:.2%}, "
|
|
399
|
+
f"median size {stats['size_median']}, max {stats['size_max']}, "
|
|
400
|
+
f"{sec:.0f} s")
|
|
401
|
+
|
|
402
|
+
if args.check:
|
|
403
|
+
ref = np.load(args.check)
|
|
404
|
+
if ref.size == labels.size:
|
|
405
|
+
print(f"Determinism check vs {args.check}: "
|
|
406
|
+
f"{int((ref != labels).sum())} label differences")
|
|
407
|
+
|
|
408
|
+
ld.write_blocks(os.path.join(args.out_dir, f"{name}.{tag}.blocks.csv"),
|
|
409
|
+
ids, labels)
|
|
410
|
+
np.save(os.path.join(args.out_dir, f"{name}.{tag}.labels.npy"), labels)
|
|
411
|
+
ld.write_hlist(os.path.join(args.out_dir, f"{name}.{tag}.hlist"),
|
|
412
|
+
ids, labels)
|
|
413
|
+
|
|
414
|
+
row = {"method": args.method, "tag": tag, "substrate": substrate,
|
|
415
|
+
"params": f"eps={args.eps};min_samples={args.min_samples};"
|
|
416
|
+
f"xi={args.xi};dist={args.dist}", **stats,
|
|
417
|
+
"seconds": sec}
|
|
418
|
+
summary = os.path.join(args.out_dir, "summary.tsv")
|
|
419
|
+
header = not os.path.isfile(summary)
|
|
420
|
+
with open(summary, "a") as f:
|
|
421
|
+
if header:
|
|
422
|
+
f.write("\t".join(row.keys()) + "\n")
|
|
423
|
+
f.write("\t".join(str(v) for v in row.values()) + "\n")
|
|
424
|
+
print("Appended summary row to", summary)
|
|
425
|
+
dur = time.strftime("%H:%M:%S", time.gmtime(time.time() - t1))
|
|
426
|
+
print(f"Done {tag}, time elapsed: {dur}")
|
|
427
|
+
return 0
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
if __name__ == "__main__":
|
|
431
|
+
raise SystemExit(main())
|