pygapit-ng 1.2.1__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.
pygapit/__init__.py ADDED
@@ -0,0 +1,108 @@
1
+ """GAPIT-style genomic association and prediction tools for Python."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("pygapit-ng")
7
+ except PackageNotFoundError: # source tree imported without installation
8
+ __version__ = "0+unknown"
9
+ __author__ = "pyGAPIT contributors (based on GAPIT by Jiabo Wang & Zhiwu Zhang)"
10
+ __license__ = "GPL-3.0"
11
+
12
+ from .gapit import GAPIT, GAPITOutputFiles, GAPITResult, ModelRunResult
13
+ from .gs.blup import (
14
+ GBLUPResult,
15
+ SUPERSelectionResult,
16
+ cblup,
17
+ gblup,
18
+ predict_new,
19
+ sblup,
20
+ select_super_qtns,
21
+ )
22
+ from .gwas.blink import BLINKResult, blink_gwas
23
+ from .gwas.farmcpu import FarmCPUResult, farmcpu_gwas
24
+ from .gwas.glm import GLMResult, glm_gwas
25
+ from .gwas.mlm import MLMResult, cmlm_gwas, mlm_gwas
26
+ from .gwas.mlmm import MLMMResult, mlmm_gwas
27
+ from .io.formats import (
28
+ AlignedData,
29
+ GenotypeData,
30
+ PhenotypeData,
31
+ align_inputs,
32
+ align_taxa,
33
+ maf_filter,
34
+ read_hapmap,
35
+ read_numeric,
36
+ read_phenotype,
37
+ )
38
+ from .stats.emma import emma_remle, emmax_p3d
39
+ from .stats.kinship import vanraden_kinship, zhang_kinship
40
+ from .stats.pca import build_covariate_matrix, compute_pca
41
+ from .stats.testing import (
42
+ benjamini_hochberg,
43
+ bonferroni_threshold,
44
+ genomic_inflation_factor,
45
+ get_significant_snps,
46
+ )
47
+ from .visualization.plots import (
48
+ gs_scatter,
49
+ kinship_heatmap,
50
+ manhattan_interactive,
51
+ manhattan_plot,
52
+ pca_plot_2d,
53
+ pca_plot_3d_interactive,
54
+ phenotype_distribution,
55
+ qq_plot,
56
+ )
57
+
58
+ __all__ = [
59
+ "GAPIT",
60
+ "AlignedData",
61
+ "BLINKResult",
62
+ "FarmCPUResult",
63
+ "GAPITOutputFiles",
64
+ "GAPITResult",
65
+ "GBLUPResult",
66
+ "GLMResult",
67
+ "GenotypeData",
68
+ "MLMMResult",
69
+ "MLMResult",
70
+ "ModelRunResult",
71
+ "PhenotypeData",
72
+ "SUPERSelectionResult",
73
+ "align_inputs",
74
+ "align_taxa",
75
+ "benjamini_hochberg",
76
+ "blink_gwas",
77
+ "bonferroni_threshold",
78
+ "build_covariate_matrix",
79
+ "cblup",
80
+ "cmlm_gwas",
81
+ "compute_pca",
82
+ "emma_remle",
83
+ "emmax_p3d",
84
+ "farmcpu_gwas",
85
+ "gblup",
86
+ "genomic_inflation_factor",
87
+ "get_significant_snps",
88
+ "glm_gwas",
89
+ "gs_scatter",
90
+ "kinship_heatmap",
91
+ "maf_filter",
92
+ "manhattan_interactive",
93
+ "manhattan_plot",
94
+ "mlm_gwas",
95
+ "mlmm_gwas",
96
+ "pca_plot_2d",
97
+ "pca_plot_3d_interactive",
98
+ "phenotype_distribution",
99
+ "predict_new",
100
+ "qq_plot",
101
+ "read_hapmap",
102
+ "read_numeric",
103
+ "read_phenotype",
104
+ "sblup",
105
+ "select_super_qtns",
106
+ "vanraden_kinship",
107
+ "zhang_kinship",
108
+ ]
pygapit/_typing.py ADDED
@@ -0,0 +1,81 @@
1
+ """Shared NumPy array types used across PyGAPIT."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typing as t
6
+
7
+ import numpy as np
8
+
9
+ Array: t.TypeAlias = np.ndarray[tuple[int, ...], np.dtype[np.generic]]
10
+ Vector: t.TypeAlias = np.ndarray[tuple[int], np.dtype[np.generic]]
11
+ Matrix: t.TypeAlias = np.ndarray[tuple[int, int], np.dtype[np.generic]]
12
+
13
+ NumericVector: t.TypeAlias = np.ndarray[tuple[int], np.dtype[np.number]]
14
+
15
+ FloatVector: t.TypeAlias = np.ndarray[tuple[int], np.dtype[np.float64]]
16
+ FloatMatrix: t.TypeAlias = np.ndarray[tuple[int, int], np.dtype[np.float64]]
17
+
18
+ IntVector: t.TypeAlias = np.ndarray[tuple[int], np.dtype[np.int_]]
19
+ BoolVector: t.TypeAlias = np.ndarray[tuple[int], np.dtype[np.bool_]]
20
+ StrVector: t.TypeAlias = np.ndarray[tuple[int], np.dtype[np.str_]]
21
+
22
+ LabelVector: t.TypeAlias = StrVector | NumericVector
23
+ ArrayT = t.TypeVar("ArrayT", bound=Array)
24
+
25
+
26
+ def readonly_copy(values: ArrayT) -> ArrayT:
27
+ """Return an independent NumPy array whose contents cannot be mutated."""
28
+ result = values.copy()
29
+ result.setflags(write=False)
30
+ return result
31
+
32
+
33
+ def as_float_vector(values: object, *, name: str = "array") -> FloatVector:
34
+ """Convert values to a one-dimensional float64 array."""
35
+ try:
36
+ result = np.asarray(values, dtype=np.float64)
37
+ except (TypeError, ValueError) as exc:
38
+ raise ValueError(f"{name} must contain numeric values") from exc
39
+ if result.ndim != 1:
40
+ raise ValueError(f"{name} must be one-dimensional; got {result.ndim}D")
41
+ return result
42
+
43
+
44
+ def as_float_matrix(values: object, *, name: str = "array") -> FloatMatrix:
45
+ """Convert values to a two-dimensional float64 array."""
46
+ try:
47
+ result = np.asarray(values, dtype=np.float64)
48
+ except (TypeError, ValueError) as exc:
49
+ raise ValueError(f"{name} must contain numeric values") from exc
50
+ if result.ndim != 2:
51
+ raise ValueError(f"{name} must be two-dimensional; got {result.ndim}D")
52
+ return result
53
+
54
+
55
+ def as_str_vector(values: object, *, name: str = "array") -> StrVector:
56
+ """Convert values to a one-dimensional Unicode array."""
57
+ result = np.asarray(values, dtype=str)
58
+ if result.ndim != 1:
59
+ raise ValueError(f"{name} must be one-dimensional; got {result.ndim}D")
60
+ return result
61
+
62
+
63
+ def require_length(values: Vector, expected: int, *, name: str) -> None:
64
+ """Require a vector to have the expected number of elements."""
65
+ if len(values) != expected:
66
+ raise ValueError(f"{name} must have length {expected}; got {len(values)}")
67
+
68
+
69
+ def require_row_count(values: Matrix, expected: int, *, name: str) -> None:
70
+ """Require a matrix to have the expected number of rows."""
71
+ if values.shape[0] != expected:
72
+ raise ValueError(f"{name} must have {expected} rows; got {values.shape[0]}")
73
+
74
+
75
+ def require_square(values: Matrix, *, name: str, size: int | None = None) -> None:
76
+ """Require a square matrix, optionally with an exact side length."""
77
+ rows, columns = values.shape
78
+ if rows != columns:
79
+ raise ValueError(f"{name} must be square; got shape {values.shape}")
80
+ if size is not None and rows != size:
81
+ raise ValueError(f"{name} must have shape ({size}, {size}); got {values.shape}")
pygapit/cli.py ADDED
@@ -0,0 +1,299 @@
1
+ """
2
+ Command-line interface for pyGAPIT.
3
+
4
+ Usage
5
+ -----
6
+ pygapit --Y pheno.txt --GD geno.txt --GM map.txt --model BLINK
7
+ pygapit --Y pheno.txt --GD geno.txt --GM map.txt --model MLM FarmCPU BLINK
8
+ pygapit --Y pheno.txt --G hapmap.hmp.txt --model BLINK --PCA_total 5
9
+ pygapit --Y pheno.txt --GD geno.txt --GM map.txt --model gBLUP
10
+ """
11
+
12
+ import argparse
13
+ from pathlib import Path
14
+
15
+ from pygapit.gapit import GAPITResult
16
+
17
+
18
+ def main() -> None:
19
+ parser = argparse.ArgumentParser(
20
+ prog="pygapit",
21
+ description="pyGAPIT: Genome Association and Prediction Integrated Tool (Python)",
22
+ formatter_class=argparse.RawDescriptionHelpFormatter,
23
+ epilog="""
24
+ Examples:
25
+ # BLINK GWAS (default, highest power):
26
+ pygapit --Y traits.txt --GD geno.txt --GM map.txt --model BLINK
27
+
28
+ # Multiple models:
29
+ pygapit --Y traits.txt --GD geno.txt --GM map.txt --model GLM MLM BLINK FarmCPU
30
+
31
+ # Genomic prediction:
32
+ pygapit --Y traits.txt --GD geno.txt --GM map.txt --model gBLUP
33
+
34
+ # HapMap format:
35
+ pygapit --Y traits.txt --G genotype.hmp.txt --model BLINK
36
+
37
+ # Custom settings:
38
+ pygapit --Y traits.txt --GD geno.txt --GM map.txt \\
39
+ --model BLINK --PCA_total 5 --maf_threshold 0.01 --output_dir results/
40
+ """,
41
+ )
42
+
43
+ # Input files
44
+ io_group = parser.add_argument_group("Input data")
45
+ io_group.add_argument(
46
+ "--Y", required=True, help="Phenotype file (tab-delimited, col1=Taxa)"
47
+ )
48
+ io_group.add_argument(
49
+ "--GD", help="Numeric genotype file (col1=taxa, col2+=SNPs 0/1/2)"
50
+ )
51
+ io_group.add_argument(
52
+ "--GM", help="SNP map file (3 cols: SNP, Chromosome, Position)"
53
+ )
54
+ io_group.add_argument(
55
+ "--G", help="HapMap genotype file (alternative to --GD + --GM)"
56
+ )
57
+ io_group.add_argument(
58
+ "--KI", help="Kinship matrix file (optional; computed from GD if absent)"
59
+ )
60
+ io_group.add_argument("--CV", help="Covariate file (optional)")
61
+
62
+ # Model selection
63
+ model_group = parser.add_argument_group("Model")
64
+ model_group.add_argument(
65
+ "--model",
66
+ nargs="+",
67
+ default=["BLINK"],
68
+ choices=[
69
+ "GLM",
70
+ "MLM",
71
+ "CMLM",
72
+ "MLMM",
73
+ "FarmCPU",
74
+ "BLINK",
75
+ "gBLUP",
76
+ "cBLUP",
77
+ "sBLUP",
78
+ ],
79
+ help="GWAS/GS model(s) to run (default: BLINK)",
80
+ )
81
+ model_group.add_argument(
82
+ "--trait", help="Trait name or column index to analyze (default: all traits)"
83
+ )
84
+
85
+ # PCA / QC
86
+ qc_group = parser.add_argument_group("Quality control & PCA")
87
+ qc_group.add_argument(
88
+ "--PCA_total",
89
+ type=int,
90
+ default=3,
91
+ help="Number of PCs for population structure control (default: 3)",
92
+ )
93
+ qc_group.add_argument(
94
+ "--maf_threshold",
95
+ type=float,
96
+ default=0.05,
97
+ help="Minimum minor allele frequency (default: 0.05)",
98
+ )
99
+ qc_group.add_argument(
100
+ "--SNP_impute",
101
+ default="middle",
102
+ choices=["middle", "major", "minor", "mean", "none"],
103
+ help="Missing genotype imputation method (default: middle)",
104
+ )
105
+
106
+ # GWAS thresholds
107
+ thresh_group = parser.add_argument_group("Significance thresholds")
108
+ thresh_group.add_argument(
109
+ "--cutOff",
110
+ type=float,
111
+ default=None,
112
+ help="P-value threshold (default: Bonferroni 0.05/m)",
113
+ )
114
+ thresh_group.add_argument(
115
+ "--LD",
116
+ type=float,
117
+ default=0.7,
118
+ help="LD threshold for BLINK pruning (default: 0.7)",
119
+ )
120
+ thresh_group.add_argument(
121
+ "--maxLoop",
122
+ type=int,
123
+ default=10,
124
+ help="Max iterations for BLINK/FarmCPU (default: 10)",
125
+ )
126
+
127
+ # CMLM parameters
128
+ cmlm_group = parser.add_argument_group("CMLM parameters")
129
+ cmlm_group.add_argument(
130
+ "--group_from", type=int, default=1, help="Min groups for CMLM (default: 1)"
131
+ )
132
+ cmlm_group.add_argument(
133
+ "--group_to",
134
+ type=int,
135
+ default=None,
136
+ help="Max groups for CMLM (default: n individuals)",
137
+ )
138
+
139
+ # FarmCPU parameters
140
+ farm_group = parser.add_argument_group("FarmCPU parameters")
141
+ farm_group.add_argument(
142
+ "--bin_size",
143
+ type=int,
144
+ default=5_000_000,
145
+ help="Bin size in bp for FarmCPU (default: 5000000)",
146
+ )
147
+
148
+ super_group = parser.add_argument_group("SUPER/sBLUP parameters")
149
+ super_group.add_argument(
150
+ "--super_bin_size",
151
+ type=int,
152
+ default=10_000,
153
+ help="Genomic bin size in bp for SUPER selection (default: 10000)",
154
+ )
155
+ super_group.add_argument(
156
+ "--super_qtn_counts",
157
+ type=int,
158
+ nargs="+",
159
+ default=None,
160
+ help="Pseudo-QTN counts evaluated by SUPER (default: 10,20,...,100)",
161
+ )
162
+
163
+ # Simulation
164
+ sim_group = parser.add_argument_group("Phenotype simulation")
165
+ sim_group.add_argument(
166
+ "--h2",
167
+ type=float,
168
+ default=None,
169
+ help="Heritability for phenotype simulation (e.g. 0.7)",
170
+ )
171
+ sim_group.add_argument(
172
+ "--NQTN", type=int, default=None, help="Number of QTNs for simulation (e.g. 20)"
173
+ )
174
+
175
+ # Output
176
+ out_group = parser.add_argument_group("Output")
177
+ out_group.add_argument(
178
+ "--output_dir",
179
+ default=".",
180
+ help="Output directory for results (default: current dir)",
181
+ )
182
+ out_group.add_argument(
183
+ "--no_file_output",
184
+ action="store_true",
185
+ help="Suppress file output (only return object)",
186
+ )
187
+ out_group.add_argument(
188
+ "--buspred", action="store_true", help="Run genomic prediction after GWAS"
189
+ )
190
+
191
+ args = parser.parse_args()
192
+
193
+ # Validate inputs
194
+ if args.G is None and (args.GD is None or args.GM is None):
195
+ parser.error(
196
+ "Provide either --G (HapMap) or both --GD and --GM (numeric format)."
197
+ )
198
+
199
+ if args.h2 is not None and args.NQTN is None:
200
+ parser.error("--NQTN is required when --h2 is provided for simulation.")
201
+
202
+ print("=" * 60)
203
+ print(" pyGAPIT — Genome Association & Prediction Tool (Python)")
204
+ print("=" * 60)
205
+
206
+ import warnings
207
+
208
+ warnings.filterwarnings("ignore")
209
+
210
+ import pandas as pd
211
+
212
+ from pygapit import GAPIT
213
+
214
+ # Load data
215
+ print(f"\n[CLI] Loading phenotype: {args.Y}")
216
+ Y = pd.read_csv(args.Y, sep="\t")
217
+
218
+ GD = GM = G = None
219
+ if args.G:
220
+ print(f"[CLI] Loading HapMap: {args.G}")
221
+ G = pd.read_csv(args.G, sep="\t", header=None)
222
+ else:
223
+ print(f"[CLI] Loading genotype: {args.GD}")
224
+ print(f"[CLI] Loading map: {args.GM}")
225
+ GD = pd.read_csv(args.GD, sep="\t")
226
+ GM = pd.read_csv(args.GM, sep="\t")
227
+
228
+ KI = pd.read_csv(args.KI, sep="\t", header=None) if args.KI else None
229
+ CV = pd.read_csv(args.CV, sep="\t") if args.CV else None
230
+
231
+ print(f"[CLI] Model(s): {args.model}")
232
+ print(f"[CLI] Output directory: {Path(args.output_dir).resolve()}")
233
+
234
+ # Run GAPIT
235
+ result = GAPIT(
236
+ Y=Y,
237
+ G=G,
238
+ GD=GD,
239
+ GM=GM,
240
+ KI=KI,
241
+ CV=CV,
242
+ model=args.model,
243
+ trait=args.trait,
244
+ PCA_total=args.PCA_total,
245
+ maf_threshold=args.maf_threshold,
246
+ SNP_impute=args.SNP_impute,
247
+ cutOff=args.cutOff,
248
+ LD=args.LD,
249
+ maxLoop=args.maxLoop,
250
+ group_from=args.group_from,
251
+ group_to=args.group_to,
252
+ bin_size=args.bin_size,
253
+ super_bin_size=args.super_bin_size,
254
+ super_qtn_counts=args.super_qtn_counts,
255
+ h2=args.h2,
256
+ NQTN=args.NQTN,
257
+ file_output=not args.no_file_output,
258
+ output_dir=args.output_dir,
259
+ buspred=args.buspred,
260
+ )
261
+
262
+ # Summary
263
+ print("\n" + "=" * 60)
264
+ print(" Results Summary")
265
+ print("=" * 60)
266
+
267
+ if isinstance(result, dict):
268
+ for key, r in result.items():
269
+ _print_summary(key, r)
270
+ else:
271
+ _print_summary(f"{result.trait} / {result.model}", result)
272
+
273
+ print("\n[CLI] Done.")
274
+
275
+
276
+ def _print_summary(label: str, result: GAPITResult) -> None:
277
+ print(f"\n {label}")
278
+ print(f" h^2 = {result.h2:.4f}")
279
+ print(f" lambda (GC) = {result.lambda_gc:.4f}")
280
+ if result.GWAS is not None:
281
+ print(f" SNPs = {len(result.GWAS):,}")
282
+ if result.significant is not None and len(result.significant) > 0:
283
+ print(f" Sig SNPs = {len(result.significant)} (Bonferroni)")
284
+ top = result.significant.nsmallest(3, "P.value")
285
+ for _, row in top.iterrows():
286
+ print(
287
+ f" {row['SNP']} chr{row['Chr']}:{int(float(str(row['Pos']))):,} "
288
+ f"p={row['P.value']:.2e} effect={row['effect']:.4f}"
289
+ )
290
+ else:
291
+ print(" Sig SNPs = 0 (no Bonferroni-significant hits)")
292
+ if result.QTNs is not None and len(result.QTNs) > 0:
293
+ print(f" QTNs selected = {len(result.QTNs)}")
294
+ if result.runtime_seconds > 0:
295
+ print(f" Runtime = {result.runtime_seconds:.1f}s")
296
+
297
+
298
+ if __name__ == "__main__":
299
+ main()