gpse 0.0.2__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.
Files changed (67) hide show
  1. gpse/__init__.py +11 -0
  2. gpse/batch/__init__.py +3 -0
  3. gpse/batch/cli.py +72 -0
  4. gpse/batch/runner.py +206 -0
  5. gpse/cli.py +215 -0
  6. gpse/config/__init__.py +15 -0
  7. gpse/config/_topsis_config.py +205 -0
  8. gpse/config/constants.py +127 -0
  9. gpse/config/default.yaml +15 -0
  10. gpse/config/software.yaml +20 -0
  11. gpse/config/topsis.yaml +18 -0
  12. gpse/convert/__init__.py +11 -0
  13. gpse/convert/cli.py +65 -0
  14. gpse/convert/external.py +400 -0
  15. gpse/convert/genotype_matrix.py +349 -0
  16. gpse/convert/phenotype.py +409 -0
  17. gpse/convert/processor.py +815 -0
  18. gpse/convert/qc.py +424 -0
  19. gpse/convert/validators.py +181 -0
  20. gpse/convert/workflow.py +214 -0
  21. gpse/models/__init__.py +30 -0
  22. gpse/models/classification_model_optimizer.py +393 -0
  23. gpse/models/classification_models.py +8 -0
  24. gpse/models/model_optimizers.py +14 -0
  25. gpse/models/regression_model_optimizer.py +679 -0
  26. gpse/predict/__init__.py +5 -0
  27. gpse/predict/__main__.py +3 -0
  28. gpse/predict/cli.py +95 -0
  29. gpse/predict/core.py +315 -0
  30. gpse/tasks/__init__.py +12 -0
  31. gpse/tasks/classification.py +565 -0
  32. gpse/tools/__init__.py +1 -0
  33. gpse/tools/analyze_phenotypes.py +140 -0
  34. gpse/train/__init__.py +26 -0
  35. gpse/train/_cv_manager.py +234 -0
  36. gpse/train/_data_io.py +261 -0
  37. gpse/train/_ensemble.py +235 -0
  38. gpse/train/_feature_selection.py +287 -0
  39. gpse/train/_fold_training.py +304 -0
  40. gpse/train/_model_pipeline.py +125 -0
  41. gpse/train/_model_tools.py +97 -0
  42. gpse/train/_optimization.py +244 -0
  43. gpse/train/_pipeline.py +490 -0
  44. gpse/train/_repeat_training.py +665 -0
  45. gpse/train/_results.py +262 -0
  46. gpse/train/cli.py +408 -0
  47. gpse/train/predictor.py +305 -0
  48. gpse/train/stacking.py +230 -0
  49. gpse/train/topsis.py +214 -0
  50. gpse/train/workflow.py +416 -0
  51. gpse/utils/__init__.py +54 -0
  52. gpse/utils/cli_display.py +529 -0
  53. gpse/utils/configuration.py +192 -0
  54. gpse/utils/dependency_checker.py +280 -0
  55. gpse/utils/feature_manifest.py +63 -0
  56. gpse/utils/genomic_utils.py +812 -0
  57. gpse/utils/log_utils.py +569 -0
  58. gpse/utils/logo.py +235 -0
  59. gpse/utils/paralle.py +266 -0
  60. gpse/utils/print_utils.py +75 -0
  61. gpse/utils/snp_ids.py +61 -0
  62. gpse/utils/version.py +159 -0
  63. gpse-0.0.2.dist-info/METADATA +1123 -0
  64. gpse-0.0.2.dist-info/RECORD +67 -0
  65. gpse-0.0.2.dist-info/WHEEL +4 -0
  66. gpse-0.0.2.dist-info/entry_points.txt +3 -0
  67. gpse-0.0.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Genotype format conversion and matrix construction utilities.
5
+
6
+ Pure functions for converting between VCF, PLINK binary, PED/MAP, and
7
+ numeric CSV matrix formats. No class state required — the caller
8
+ (typically ``GenomicDataProcessor``) passes in the logger and tool paths.
9
+ """
10
+
11
+ import os
12
+ import glob
13
+ from datetime import datetime
14
+
15
+ from gpse.convert.external import resolve_configured_tool, run_command, ensure_log_dir
16
+ from gpse.utils.feature_manifest import write_feature_manifest
17
+ from gpse.utils.snp_ids import canonical_ids_from_map_file
18
+
19
+ try:
20
+ from gpse.utils.log_utils import logger as _default_logger
21
+ except Exception: # pragma: no cover
22
+ import logging
23
+ _default_logger = logging.getLogger(__name__)
24
+
25
+ # Additive genotype encoding map
26
+ GENO_DICT = {
27
+ '00': '0', # Homozygous reference
28
+ '01': '1', # Heterozygous
29
+ '10': '1', # Heterozygous
30
+ '11': '2', # Homozygous alternate
31
+ }
32
+
33
+ # Centered additive encoding, Azodi et al. 2019 style: [-1, 0, 1] = [aa, Aa, AA]
34
+ GENO_DICT_CENTERED = {
35
+ '00': '-1', # Homozygous reference (aa)
36
+ '01': '0', # Heterozygous (Aa)
37
+ '10': '0', # Heterozygous (Aa)
38
+ '11': '1', # Homozygous alternate (AA)
39
+ }
40
+
41
+ # Supported genotype encodings for convert_to_matrix().
42
+ GENO_ENCODINGS = {
43
+ '012': GENO_DICT,
44
+ '-101': GENO_DICT_CENTERED,
45
+ }
46
+
47
+
48
+ def _resolve_plink(plink_path, config_path=None, auto_project_config=False):
49
+ """Resolve the PLINK executable from config or command override."""
50
+ return resolve_configured_tool(
51
+ "plink",
52
+ command_override=plink_path,
53
+ config_path=config_path,
54
+ auto_project_config=auto_project_config,
55
+ )
56
+
57
+
58
+ def _timestamp_plink_log(out_prefix, logger=None):
59
+ """Move PLINK's auto-generated ``<prefix>.log`` into the ``log/`` sub-directory.
60
+
61
+ If a log with the same destination name already exists, a timestamp is
62
+ appended to avoid collisions.
63
+ """
64
+ log_path = f"{out_prefix}.log"
65
+ if not os.path.exists(log_path):
66
+ return
67
+ log_dir = ensure_log_dir(out_prefix)
68
+ base_name = os.path.basename(out_prefix)
69
+ dest = os.path.join(log_dir, f"{base_name}.log")
70
+ if os.path.exists(dest):
71
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
72
+ dest = os.path.join(log_dir, f"{base_name}_{timestamp}.log")
73
+ os.rename(log_path, dest)
74
+ if logger is not None:
75
+ logger.info(f"PLINK log moved to: {dest}")
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # VCF → PLINK BED
80
+ # ---------------------------------------------------------------------------
81
+
82
+ def vcf_to_plink(
83
+ vcf_file,
84
+ out_prefix,
85
+ *,
86
+ plink_path="plink",
87
+ config_path=None,
88
+ auto_project_config=False,
89
+ allow_extra_chr=False,
90
+ logger=None,
91
+ ):
92
+ """Convert a VCF file to PLINK binary format (BED/BIM/FAM)."""
93
+ log = logger or _default_logger
94
+ log.info(f"Converting VCF file {vcf_file} to PLINK binary format...")
95
+
96
+ # Skip when output already exists.
97
+ if all(os.path.exists(f"{out_prefix}{ext}") for ext in (".bed", ".bim", ".fam")):
98
+ log.info(f"PLINK binary files already exist: {out_prefix}")
99
+ log.info("Skipping conversion step...")
100
+ return out_prefix
101
+
102
+ plink = _resolve_plink(plink_path, config_path, auto_project_config)
103
+ cmd = [plink, "--vcf", vcf_file, "--make-bed", "--out", out_prefix, "--double-id"]
104
+ if allow_extra_chr:
105
+ cmd.extend(["--allow-extra-chr"])
106
+ run_command(cmd, logger=log)
107
+ _timestamp_plink_log(out_prefix, logger=log)
108
+
109
+ log.info(f"VCF conversion completed: {out_prefix}.bed, {out_prefix}.bim, {out_prefix}.fam")
110
+ return out_prefix
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # BED → PED/MAP (with SNP extraction)
115
+ # ---------------------------------------------------------------------------
116
+
117
+ def extract_snps(
118
+ bfile,
119
+ extract_file,
120
+ out_prefix,
121
+ *,
122
+ plink_path="plink",
123
+ config_path=None,
124
+ auto_project_config=False,
125
+ allow_extra_chr=False,
126
+ logger=None,
127
+ ):
128
+ """Extract selected SNPs from a PLINK binary dataset to PED/MAP."""
129
+ log = logger or _default_logger
130
+ log.info(f"Extracting SNPs from {bfile}...")
131
+
132
+ if os.path.exists(f"{out_prefix}.ped") and os.path.exists(f"{out_prefix}.map"):
133
+ log.info(f"PLINK PED/MAP files already exist: {out_prefix}")
134
+ log.info("Skipping extraction step...")
135
+ return out_prefix
136
+
137
+ plink = _resolve_plink(plink_path, config_path, auto_project_config)
138
+ cmd = [
139
+ plink,
140
+ "--bfile", bfile,
141
+ "--out", out_prefix,
142
+ "--extract", extract_file,
143
+ "--recode", "compound-genotypes", "01",
144
+ "--output-missing-genotype", "3",
145
+ ]
146
+ if allow_extra_chr:
147
+ cmd.extend(["--allow-extra-chr"])
148
+ run_command(cmd, logger=log)
149
+ _timestamp_plink_log(out_prefix, logger=log)
150
+
151
+ log.info(f"SNP extraction completed: {out_prefix}.ped, {out_prefix}.map")
152
+ return out_prefix
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # BED → PED/MAP (full, no filtering)
157
+ # ---------------------------------------------------------------------------
158
+
159
+ def convert_bfile_to_ped(
160
+ bfile,
161
+ out_prefix,
162
+ *,
163
+ plink_path="plink",
164
+ config_path=None,
165
+ auto_project_config=False,
166
+ allow_extra_chr=False,
167
+ logger=None,
168
+ ):
169
+ """Convert PLINK binary files directly to PED/MAP without SNP filtering."""
170
+ log = logger or _default_logger
171
+ log.info(f"Converting PLINK binary dataset {bfile} to PED/MAP format...")
172
+
173
+ if os.path.exists(f"{out_prefix}.ped") and os.path.exists(f"{out_prefix}.map"):
174
+ log.info(f"PLINK PED/MAP files already exist: {out_prefix}")
175
+ log.info("Skipping conversion step...")
176
+ return out_prefix
177
+
178
+ plink = _resolve_plink(plink_path, config_path, auto_project_config)
179
+ cmd = [
180
+ plink,
181
+ "--bfile", bfile,
182
+ "--out", out_prefix,
183
+ "--recode", "compound-genotypes", "01",
184
+ "--output-missing-genotype", "3",
185
+ ]
186
+ if allow_extra_chr:
187
+ cmd.extend(["--allow-extra-chr"])
188
+ run_command(cmd, logger=log)
189
+ _timestamp_plink_log(out_prefix, logger=log)
190
+
191
+ log.info(f"Conversion completed: {out_prefix}.ped, {out_prefix}.map")
192
+ return out_prefix
193
+
194
+
195
+ # ---------------------------------------------------------------------------
196
+ # PED/MAP → numeric CSV matrix
197
+ # ---------------------------------------------------------------------------
198
+
199
+ def convert_to_matrix(fileprefix, out_file=None, *, out_format="parquet", geno_encoding="012", logger=None):
200
+ """Convert PLINK PED/MAP genotype data to a numeric CSV or binary matrix.
201
+
202
+ Encoding (``geno_encoding="012"``): ``00→0, 01→1, 10→1, 11→2``,
203
+ missing → ``3``. With ``geno_encoding="-101"`` (Azodi et al. 2019
204
+ style): ``00→-1, 01→0, 10→0, 11→1``, missing → ``3``.
205
+ """
206
+ log = logger or _default_logger
207
+ if geno_encoding not in GENO_ENCODINGS:
208
+ raise ValueError(
209
+ f"Unknown geno_encoding '{geno_encoding}'. "
210
+ f"Supported: {sorted(GENO_ENCODINGS)}"
211
+ )
212
+ geno_dict = GENO_ENCODINGS[geno_encoding]
213
+ log.info(f"Converting {fileprefix}.ped and {fileprefix}.map to matrix format...")
214
+ log.info(f"Genotype encoding: {geno_encoding}")
215
+
216
+ # Detect pyarrow
217
+ out_format = out_format.lower()
218
+ if out_format in ('parquet', 'feather'):
219
+ try:
220
+ import pyarrow
221
+ except ImportError:
222
+ log.warning(
223
+ f"Output format '{out_format}' requires the 'pyarrow' package, which is not installed. "
224
+ "Falling back to 'csv'. Please run 'pip install pyarrow' to enable highly optimized binary formats."
225
+ )
226
+ out_format = 'csv'
227
+
228
+ ped_path = fileprefix + '.ped'
229
+ map_path = fileprefix + '.map'
230
+
231
+ # Try alternate paths when the prefix contains a directory component.
232
+ if not os.path.exists(ped_path) and '/' in fileprefix:
233
+ base_name = os.path.basename(fileprefix)
234
+ dir_name = os.path.dirname(fileprefix)
235
+ alt_ped = os.path.join(dir_name, base_name + '.ped')
236
+ alt_map = os.path.join(dir_name, base_name + '.map')
237
+ if os.path.exists(alt_ped) and os.path.exists(alt_map):
238
+ ped_path = alt_ped
239
+ map_path = alt_map
240
+
241
+ if out_file is None:
242
+ ext = '.parquet' if out_format == 'parquet' else '.feather' if out_format == 'feather' else '.csv'
243
+ out_file = fileprefix + ext
244
+
245
+ manifest_file = os.path.splitext(out_file)[0] + ".features.json"
246
+ if os.path.exists(out_file) and os.path.exists(manifest_file):
247
+ log.info(f"Matrix file already exists: {out_file}")
248
+ log.info("Skipping conversion step...")
249
+ return out_file
250
+ if os.path.exists(out_file):
251
+ log.warning("Existing matrix has no feature manifest; regenerating it with canonical SNP IDs.")
252
+
253
+ if not os.path.exists(ped_path) or not os.path.exists(map_path):
254
+ raise FileNotFoundError(f"Input file not found: {ped_path} or {map_path}")
255
+
256
+ # Use stable UCSC-style IDs from chromosome and base-pair coordinates.
257
+ snpid_list = canonical_ids_from_map_file(map_path)
258
+
259
+ # Read sample IDs and genotypes from .ped.
260
+ sample_genotypes = []
261
+ with open(ped_path) as ped_file:
262
+ for row in ped_file:
263
+ row = row.strip().split()
264
+ sample_id = row[1]
265
+
266
+ # Normalize sample IDs produced from VCF conversion.
267
+ if sample_id.endswith('_'):
268
+ sample_id = sample_id.rstrip('_')
269
+
270
+ geno = row[6:] # Genotypes start at column 7.
271
+ encoded_geno = [geno_dict.get(g, '3') for g in geno]
272
+ sample_genotypes.append((sample_id, encoded_geno))
273
+
274
+ # Write matrix based on format.
275
+ if out_format in ('parquet', 'feather'):
276
+ import pandas as pd
277
+ data = {sample_id: genotypes for sample_id, genotypes in sample_genotypes}
278
+ df = pd.DataFrame.from_dict(data, orient='index', columns=snpid_list)
279
+ df.index.name = 'ID'
280
+ df_reset = df.reset_index()
281
+ if out_format == 'parquet':
282
+ df_reset.to_parquet(out_file, index=False)
283
+ else:
284
+ df_reset.to_feather(out_file)
285
+ else:
286
+ # Write CSV matrix.
287
+ with open(out_file, 'w') as csv_file:
288
+ csv_file.write("ID," + ",".join(snpid_list) + '\n')
289
+ for sample_id, genotypes in sample_genotypes:
290
+ csv_file.write(sample_id + "," + ",".join(genotypes) + '\n')
291
+
292
+ manifest_path = write_feature_manifest(
293
+ os.path.dirname(out_file) or ".",
294
+ snpid_list,
295
+ source_file=out_file,
296
+ filename=os.path.basename(manifest_file),
297
+ )
298
+ log.info(f"Feature manifest written: {manifest_path}")
299
+ log.info(f"Matrix conversion completed: {out_file}")
300
+ return out_file
301
+
302
+
303
+
304
+ # ---------------------------------------------------------------------------
305
+ # Batch processing: SNP directory
306
+ # ---------------------------------------------------------------------------
307
+
308
+ def process_snp_dir(
309
+ bfile,
310
+ snp_dir,
311
+ out_dir,
312
+ *,
313
+ plink_path="plink",
314
+ config_path=None,
315
+ auto_project_config=False,
316
+ allow_extra_chr=False,
317
+ geno_encoding="012",
318
+ logger=None,
319
+ ):
320
+ """Process all SNP list files (*.txt) in a directory."""
321
+ log = logger or _default_logger
322
+ os.makedirs(out_dir, exist_ok=True)
323
+
324
+ snp_files = glob.glob(os.path.join(snp_dir, "*.txt"))
325
+ if not snp_files:
326
+ log.warning(f"No .txt files found in {snp_dir}")
327
+ return
328
+
329
+ log.info(f"Found {len(snp_files)} SNP list file(s). Starting batch processing...")
330
+
331
+ for snp_file in snp_files:
332
+ phenotype = os.path.basename(snp_file).replace('.txt', '')
333
+ log.info("")
334
+ log.info(f"Processing phenotype: {phenotype}")
335
+
336
+ out_prefix = os.path.join(out_dir, phenotype)
337
+ try:
338
+ extract_snps(
339
+ bfile, snp_file, out_prefix,
340
+ plink_path=plink_path,
341
+ config_path=config_path,
342
+ auto_project_config=auto_project_config,
343
+ allow_extra_chr=allow_extra_chr,
344
+ logger=log,
345
+ )
346
+ matrix_file = convert_to_matrix(out_prefix, geno_encoding=geno_encoding, logger=log)
347
+ log.info(f"Phenotype {phenotype} completed. CSV matrix: {matrix_file}")
348
+ except Exception as e:
349
+ log.error(f"Error while processing phenotype {phenotype}: {str(e)}")