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,205 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ TOPSIS Configuration and Environment Logging Module
6
+ ====================================================
7
+ Provides TOPSIS evaluation setup, representative model saving, and environment logging.
8
+ """
9
+
10
+ import os
11
+ import traceback
12
+ import json
13
+ import joblib
14
+ import pandas as pd
15
+ from pathlib import Path
16
+ from sklearn.preprocessing import StandardScaler
17
+ from typing import Any, Tuple
18
+
19
+ from loguru import logger as main_logger
20
+
21
+ from gpse.config.constants import ModelConstants
22
+ from gpse.utils.configuration import load_topsis_config
23
+ from gpse.utils.genomic_utils import create_representative_model_directory
24
+ from threadpoolctl import threadpool_limits
25
+ from gpse.train._feature_selection import (
26
+ make_model_artifact,
27
+ )
28
+ from gpse.train._model_pipeline import build_training_pipeline
29
+
30
+
31
+ def _format_weight(weight: Any) -> str:
32
+ """Format a YAML weight value for TOPSISEvaluator's comma-string interface."""
33
+ if isinstance(weight, float):
34
+ return f"{weight:g}"
35
+ return str(weight)
36
+
37
+
38
+ def _parse_topsis_task_config(task_type: str) -> Tuple[list, list, str]:
39
+ """Parse and validate the TOPSIS config for a task type."""
40
+ config = load_topsis_config()
41
+ task_configs = config.get("tasks", {})
42
+ if not isinstance(task_configs, dict):
43
+ raise ValueError("TOPSIS config must define 'tasks' as a mapping")
44
+
45
+ task_key = "classification" if task_type == "classification" else "regression"
46
+ task_config = task_configs.get(task_key)
47
+
48
+ if not isinstance(task_config, dict):
49
+ raise ValueError(f"Missing TOPSIS configuration for task '{task_key}'")
50
+
51
+ criteria_config = task_config.get("criteria")
52
+ if not isinstance(criteria_config, list) or not criteria_config:
53
+ raise ValueError(f"TOPSIS task '{task_key}' must define a non-empty criteria list")
54
+
55
+ criteria = []
56
+ criteria_types = []
57
+ weights = []
58
+ for index, item in enumerate(criteria_config, start=1):
59
+ if not isinstance(item, dict):
60
+ raise ValueError(f"TOPSIS criterion #{index} for task '{task_key}' must be a mapping")
61
+
62
+ name = item.get("name")
63
+ criterion_type = item.get("type")
64
+ weight = item.get("weight")
65
+
66
+ if not name:
67
+ raise ValueError(f"TOPSIS criterion #{index} for task '{task_key}' is missing 'name'")
68
+ if criterion_type not in {"max", "min"}:
69
+ raise ValueError(
70
+ f"TOPSIS criterion '{name}' for task '{task_key}' must use type 'max' or 'min'"
71
+ )
72
+ if weight is None:
73
+ raise ValueError(f"TOPSIS criterion '{name}' for task '{task_key}' is missing 'weight'")
74
+ try:
75
+ float(weight)
76
+ except (TypeError, ValueError) as exc:
77
+ raise ValueError(
78
+ f"TOPSIS criterion '{name}' for task '{task_key}' has a non-numeric weight"
79
+ ) from exc
80
+
81
+ criteria.append(str(name))
82
+ criteria_types.append(str(criterion_type))
83
+ weights.append(weight)
84
+
85
+ manual_weights = ",".join(_format_weight(weight) for weight in weights)
86
+ return criteria, criteria_types, manual_weights
87
+
88
+
89
+ def get_topsis_configuration(self) -> Tuple[list, list, str]:
90
+ """
91
+ Get TOPSIS evaluation configuration based on task type.
92
+
93
+ Returns
94
+ -------
95
+ Tuple[list, list, str]
96
+ (criteria, criteria_types, manual_weights)
97
+ """
98
+ criteria, criteria_types, manual_weights = _parse_topsis_task_config(self.task_type)
99
+ task_name = "Classification" if self.task_type == "classification" else "Regression"
100
+ main_logger.info(
101
+ f"TOPSIS config: {task_name} task - "
102
+ f"{' : '.join(criteria)}; weights={manual_weights}"
103
+ )
104
+
105
+ return criteria, criteria_types, manual_weights
106
+
107
+
108
+ def _save_representative_model(
109
+ self,
110
+ model_name: str,
111
+ params: dict,
112
+ X: pd.DataFrame,
113
+ y: pd.Series,
114
+ model_dir: Path,
115
+ repeat_info: dict,
116
+ ) -> str:
117
+ """
118
+ Train and save a representative model.
119
+
120
+ Parameters
121
+ ----------
122
+ model_name : str
123
+ Model name.
124
+ params : dict
125
+ Model parameters.
126
+ X : pd.DataFrame
127
+ Feature matrix.
128
+ y : pd.Series
129
+ Target variable.
130
+ model_dir : Path
131
+ Model directory.
132
+ repeat_info : dict
133
+ Repeat information dictionary.
134
+
135
+ Returns
136
+ -------
137
+ str
138
+ Model save path, or None if saving failed.
139
+ """
140
+ representative_model_dir = create_representative_model_directory(model_dir)
141
+
142
+ try:
143
+ pipeline = build_training_pipeline(
144
+ self.create_model(model_name, params),
145
+ task_type=self.task_type,
146
+ model_name=model_name,
147
+ feature_selection_config=self.feature_selection_config,
148
+ genotype_imputation_config=self.genotype_imputation_config,
149
+ )
150
+
151
+ with threadpool_limits(limits=self.n_threads):
152
+ pipeline.fit(X, y)
153
+ preprocessor = pipeline.named_steps["preprocess"]
154
+
155
+ model_path = representative_model_dir / ModelConstants.model_pkl_file
156
+ joblib.dump(
157
+ make_model_artifact(
158
+ pipeline.named_steps["model"],
159
+ preprocessor.scaler_,
160
+ preprocessor.selector_,
161
+ self.feature_selection_config,
162
+ preprocessor.selected_features_,
163
+ imputer=preprocessor.imputer_,
164
+ imputation_config=self.genotype_imputation_config,
165
+ task_type=self.task_type,
166
+ ),
167
+ model_path,
168
+ )
169
+
170
+ info_path = representative_model_dir / ModelConstants.model_info_file
171
+ with open(info_path, "w") as f:
172
+ json.dump(
173
+ {
174
+ **repeat_info,
175
+ "feature_selection": self.feature_selection_config.as_dict(),
176
+ "genotype_imputation": self.genotype_imputation_config.as_dict(),
177
+ "selected_feature_count": len(preprocessor.selected_features_),
178
+ },
179
+ f,
180
+ indent=2,
181
+ )
182
+
183
+ return str(model_path)
184
+
185
+ except Exception as e:
186
+ main_logger.error(f"Saving representative model failed: {str(e)}")
187
+ main_logger.error(traceback.format_exc())
188
+ return None
189
+
190
+
191
+ def log_environment_settings(self) -> None:
192
+ """Log environment settings."""
193
+ main_logger.info("Initializing GenomicPredictorV2, base configuration:")
194
+ main_logger.info(f"- Results directory: {self.results_dir}")
195
+ main_logger.info(f"- Logs directory: {self.logs_dir}")
196
+ main_logger.info(f"- Random seed: {self.random_seed}")
197
+ main_logger.info(f"- Threads per model: {self.n_threads}")
198
+ main_logger.info(f"- Model workers: {self.max_parallel_jobs}")
199
+ main_logger.info(f"- Repeat workers: {self.repeat_workers}")
200
+ main_logger.info(f"- Train folds: {self.n_splits}")
201
+ main_logger.info(f"- Repeats: {self.n_repeats}")
202
+ main_logger.info(f"- Use default params: {self.use_default_params}")
203
+
204
+ for env_var in ModelConstants.thread_env_vars:
205
+ main_logger.info(f"{env_var}={os.environ.get(env_var, 'not set')}")
@@ -0,0 +1,127 @@
1
+ """Global constant configuration and log message templates."""
2
+
3
+ import argparse
4
+ import json
5
+ import numpy as np
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Callable
8
+
9
+ from gpse.utils.cli_display import show_gpse_logo
10
+
11
+
12
+ class _LogoHelpAction(argparse.Action):
13
+ """Custom help action that prints the logo before the help text."""
14
+
15
+ def __call__(self, parser, namespace, values, option_string=None):
16
+ show_gpse_logo()
17
+ parser.print_help()
18
+ parser.exit()
19
+
20
+
21
+ @dataclass
22
+ class ModelConfig:
23
+ """Configuration for model hyperparameter optimization"""
24
+ model_class: Any
25
+ param_func: Callable
26
+ is_regression: bool
27
+
28
+
29
+ @dataclass
30
+ class ClassificationModelConfig:
31
+ """Configuration for classification model hyperparameter optimization"""
32
+ model_class: Any
33
+ param_func: Callable
34
+
35
+
36
+ class NumpyEncoder(json.JSONEncoder):
37
+ """
38
+ JSON encoder that handles NumPy types
39
+ """
40
+ def default(self, obj):
41
+ if isinstance(obj, np.integer):
42
+ return int(obj)
43
+ if isinstance(obj, np.floating):
44
+ return float(obj)
45
+ if isinstance(obj, np.ndarray):
46
+ return obj.tolist()
47
+ return super(NumpyEncoder, self).default(obj)
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class _ModelConstants:
52
+ """
53
+ Constants configuration related to model training
54
+ """
55
+ # Optuna optimization related
56
+ optuna_n_startup_trials: int = 10
57
+ optuna_n_warmup_steps: int = 5
58
+ optuna_interval_steps: int = 1
59
+ optuna_n_jobs: int = 1 # Enforce Optuna serial execution
60
+
61
+ # Thread environment variables
62
+ thread_env_vars: tuple = field(default_factory=lambda: (
63
+ 'OMP_NUM_THREADS', 'MKL_NUM_THREADS', 'OPENBLAS_NUM_THREADS',
64
+ 'NUMEXPR_NUM_THREADS', 'VECLIB_MAXIMUM_THREADS', 'BLIS_NUM_THREADS'
65
+ ))
66
+
67
+ # Default directory names
68
+ default_results_dir: str = "optimization_results"
69
+ default_logs_dir: str = "logs"
70
+ default_cv_dir: str = "cv_folds"
71
+ representative_model_dir: str = "representative_model"
72
+
73
+ # Default file names
74
+ main_log_file: str = "main.log"
75
+ model_log_file: str = "model.log"
76
+ repeat_results_file: str = "repeat_results.json"
77
+ summary_results_file: str = "summary_results.json"
78
+ all_predictions_file: str = "all_predictions.json"
79
+ model_info_file: str = "info.json"
80
+ model_pkl_file: str = "model.pkl"
81
+ comparison_csv_file: str = "model_comparison.csv"
82
+
83
+ # Log format templates
84
+ log_separator_short: str = field(default_factory=lambda: "=" * 50)
85
+ log_separator_long: str = field(default_factory=lambda: "=" * 70)
86
+
87
+ # Numerical precision constants
88
+ metrics_precision: int = 6
89
+ time_precision: int = 2
90
+ float_precision: int = 4
91
+ correlation_min_threshold: float = 1e-10 # Threshold to avoid constant predictions
92
+
93
+
94
+ # NOTE: _LogMessages has been deprecated. All log messages are now inline strings.
95
+ # If you need to restore it, uncomment the block below.
96
+ #
97
+ # @dataclass(frozen=True)
98
+ # class _LogMessages:
99
+ # """Log message template constants"""
100
+ # logging_initialized: str = "Logging system initialization complete"
101
+ # predictor_init: str = "Initializing GenomicPredictorV2, basic configuration:"
102
+ # data_loading: str = "Loading data..."
103
+ # data_loaded: str = "Successfully loaded genotype data: {}, phenotype data: {}"
104
+ # common_samples: str = "Number of genotype samples: {}, number of phenotype samples: {}"
105
+ # final_data_info: str = "Final data dimensions - Number of features: {}, number of samples: {}"
106
+ # target_stats: str = "Target variable statistics - Mean: {:.4f}, Std: {:.4f}"
107
+ # model_training_start: str = "Starting training for model: {} repeat: {}"
108
+ # repeat_training_start: str = "Starting {} repetitions of training for model {}"
109
+ # fold_training_start: str = "Training fold {}/{}"
110
+ # parameter_optimization_start: str = "Starting parameter optimization for model {} (repeat {}/{})"
111
+ # optimization_start: str = "Starting parameter optimization, max trials: {}"
112
+ # fold_results: str = "Fold {} results:"
113
+ # repeat_average_performance: str = "Repeat {} average performance:"
114
+ # model_summary_performance: str = "Model {} - average performance across {} repeats:"
115
+ # ensemble_test_performance: str = "Fold ensemble test set performance:"
116
+ # data_file_not_found: str = "Failed to load data file: {}"
117
+ # data_file_read_error: str = "Error reading data file: {}"
118
+ # target_trait_not_found: str = "Target trait '{}' not found in phenotype data. Available columns: {}"
119
+ # id_column_missing_geno: str = "ID column '{}' missing in genotype data"
120
+ # id_column_missing_pheno: str = "ID column '{}' missing in phenotype data"
121
+ # no_common_samples: str = "No common sample IDs between genotype and phenotype data"
122
+ # dimension_mismatch: str = "Mismatch in number of samples between feature matrix and target variable: {} vs {}"
123
+
124
+
125
+ # Module-level singleton instance
126
+ ModelConstants = _ModelConstants()
127
+ # LogMessages = _LogMessages() # deprecated
@@ -0,0 +1,15 @@
1
+ software:
2
+ version: "0.0.2"
3
+ app_name: gpse
4
+ author: "XIAOLIU, JZHANG"
5
+ email: "1468835852@qq.com, zhangjian199567@outlook.com"
6
+ url: https://github.com/xsx123123/GPSE
7
+ description: "Genomic Prediction with Stacking Ensemble for horticultural crops"
8
+
9
+
10
+ logs:
11
+ log_level: INFO
12
+ more_info: false
13
+ Label: gpse
14
+
15
+
@@ -0,0 +1,20 @@
1
+ software:
2
+ version: "0.0.2"
3
+ app_name: gpse
4
+ author: "XIAOLIU, JZHANG"
5
+ email: "1468835852@qq.com, zhangjian199567@outlook.com"
6
+ url: https://github.com/xsx123123/GPSE
7
+ description: "Genomic Prediction with Stacking Ensemble for horticultural crops"
8
+
9
+ external_tools:
10
+ - name: plink
11
+ cmd: plink
12
+ version_flag: --version
13
+ min_version: "1.9"
14
+ required: true
15
+ description: "PLINK 1.9 for genomic data format conversion and SNP extraction"
16
+ - name: java
17
+ cmd: java
18
+ version_flag: -version
19
+ required: false
20
+ description: "Java runtime for optional Beagle imputation"
@@ -0,0 +1,18 @@
1
+ tasks:
2
+ classification:
3
+ criteria:
4
+ - name: "Test Accuracy"
5
+ type: max
6
+ weight: 0.8
7
+ - name: "Test Accuracy (std)"
8
+ type: min
9
+ weight: 0.2
10
+
11
+ regression:
12
+ criteria:
13
+ - name: "Test Pearson"
14
+ type: max
15
+ weight: 0.8
16
+ - name: "Test Pearson (std)"
17
+ type: min
18
+ weight: 0.2
@@ -0,0 +1,11 @@
1
+ """
2
+ GPSE convert package — data conversion and genotype QC utilities.
3
+
4
+ This package provides tools for converting between common genomic data
5
+ formats, running quality-control filters, and preparing genotype matrices
6
+ for downstream GPSE analyses.
7
+ """
8
+
9
+ from .processor import GenomicDataProcessor
10
+
11
+ __all__ = ["GenomicDataProcessor"]
gpse/convert/cli.py ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Command-line and workflow entry point for the GPSE convert module.
5
+
6
+ This module does two things:
7
+ 1. Defines the command-line arguments for ``gpse convert``.
8
+ 2. Starts the selected convert workflow: genotype conversion, QC/LD pruning,
9
+ numeric recoding, or external dependency validation.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import sys
15
+ import argparse
16
+ from datetime import datetime
17
+
18
+ from gpse.convert.workflow import run_convert_workflow, validate_convert_mode
19
+ from gpse.utils.cli_display import _build_convert_parser
20
+ from gpse.utils.log_utils import logger_init
21
+
22
+ # define convert main function
23
+ def main(argv: list[str] | None = None,*,formatter_class=None,prog: str | None = None,help_action=None,parents: list[argparse.ArgumentParser] | None = None) -> int:
24
+ """
25
+ Parse convert arguments, select a mode, and start the workflow.
26
+ """
27
+ parser = _build_convert_parser(
28
+ formatter_class = formatter_class or argparse.HelpFormatter,
29
+ prog = prog or "python -m gpse.convert.cli",
30
+ help_action = help_action,
31
+ parents = parents,
32
+ )
33
+ args = parser.parse_args(argv)
34
+
35
+ # Determine workflow mode first so that missing-argument errors and --help
36
+ # are handled before initialising the logger. This avoids spurious
37
+ # "GPSE config loaded" messages when the user only wants usage info.
38
+ mode = validate_convert_mode(parser, args)
39
+
40
+ # Initialise unified GPSE logging so that convert uses the same
41
+ # Rich-styled console format and file rotation as the train module.
42
+ log_level = getattr(args, "log_level", "INFO")
43
+ log_file = None
44
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
45
+ if getattr(args, "out_prefix", None):
46
+ log_dir = os.path.dirname(args.out_prefix) or "."
47
+ os.makedirs(log_dir, exist_ok=True)
48
+ log_file = os.path.join(log_dir, f"gpse_convert_{timestamp}.log")
49
+ elif getattr(args, "recode_prefix", None):
50
+ log_dir = os.path.dirname(args.recode_prefix) or "."
51
+ os.makedirs(log_dir, exist_ok=True)
52
+ log_file = os.path.join(log_dir, f"gpse_convert_{timestamp}.log")
53
+ else:
54
+ # No output prefix specified (e.g. --check-deps).
55
+ # Skip file logging — only use console output.
56
+ log_file = None
57
+
58
+ logger_init(logger_name=log_file, log_level=log_level)
59
+
60
+ return run_convert_workflow(args, mode)
61
+
62
+
63
+ if __name__ == "__main__":
64
+ # Allows direct execution: python -m gpse.convert.cli ...
65
+ sys.exit(main())