geneml 1.0.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.
geneml/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from ._version import __version__
2
+
3
+ __all__ = ["__version__"]
geneml/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '1.0.0'
22
+ __version_tuple__ = version_tuple = (1, 0, 0)
23
+
24
+ __commit_id__ = commit_id = None
geneml/args.py ADDED
@@ -0,0 +1,316 @@
1
+ import argparse
2
+ import os
3
+ import re
4
+ from argparse import Namespace
5
+
6
+ from geneml import __version__
7
+ from geneml.params import Strand
8
+
9
+
10
+ def positive_int(value) -> int:
11
+ """Validate that input is a positive integer.
12
+
13
+ Args:
14
+ value: Input value to validate.
15
+
16
+ Returns:
17
+ The validated integer value.
18
+ """
19
+ ivalue = int(value)
20
+ if ivalue < 1:
21
+ raise argparse.ArgumentTypeError(f"Must be a positive integer, got {value}.")
22
+ return ivalue
23
+
24
+
25
+ def unit_float(value) -> float:
26
+ """Validate that input is a float in the range [0, 1].
27
+
28
+ Args:
29
+ value: Input value to validate.
30
+
31
+ Returns:
32
+ The validated float value.
33
+ """
34
+ fvalue = float(value)
35
+ if not (0.0 <= fvalue <= 1.0):
36
+ raise argparse.ArgumentTypeError(f"Must be between 0 and 1, got {value}.")
37
+ return fvalue
38
+
39
+
40
+ def gene_score(value) -> str | float:
41
+ """Validate gene score as 'dynamic' or a float in the range [0, 1].
42
+
43
+ Args:
44
+ value: Input value to validate.
45
+
46
+ Returns:
47
+ The string 'dynamic' or a validated float value.
48
+ """
49
+ if value == 'dynamic':
50
+ return value
51
+ try:
52
+ return unit_float(value)
53
+ except argparse.ArgumentTypeError as e:
54
+ raise argparse.ArgumentTypeError(
55
+ f"Must be a float between 0 and 1, or 'dynamic', got {value}."
56
+ ) from e
57
+
58
+
59
+ def gene_id_prefix(value: str) -> str:
60
+ """Validate a gene ID prefix for output identifiers.
61
+
62
+ Args:
63
+ value: Prefix value to validate.
64
+
65
+ Returns:
66
+ The validated prefix.
67
+ """
68
+ if not value:
69
+ raise argparse.ArgumentTypeError("Gene ID prefix must not be empty.")
70
+ if re.fullmatch(r'[A-Za-z0-9_.-]+', value) is None:
71
+ raise argparse.ArgumentTypeError(
72
+ "Gene ID prefix may contain only letters, digits, '.', '_', and '-'."
73
+ )
74
+ return value
75
+
76
+
77
+ def get_basepath(inpath: str, outpath: str | None) -> str:
78
+ """Resolve the base output path without extension.
79
+
80
+ Args:
81
+ inpath: Input sequence path.
82
+ outpath: Optional explicit output path.
83
+
84
+ Returns:
85
+ Output base path without file extension.
86
+ """
87
+ if outpath:
88
+ return os.path.splitext(outpath)[0]
89
+ return os.path.splitext(inpath)[0]
90
+
91
+
92
+ def check_input_path(parser, path: str, label: str) -> None:
93
+ """Validate that an input path exists and is readable.
94
+
95
+ Args:
96
+ parser: Argument parser used to report validation errors.
97
+ path: Path to validate.
98
+ label: Human-readable argument label for error messages.
99
+
100
+ Returns:
101
+ None.
102
+ """
103
+ if not os.path.exists(path):
104
+ parser.error(f"{label} does not exist: {path}")
105
+ if not os.path.isfile(path):
106
+ parser.error(f"{label} is not a file: {path}")
107
+ if not os.access(path, os.R_OK):
108
+ parser.error(f"{label} is not readable: {path}")
109
+
110
+
111
+ def check_output_path(parser, path: str, label: str) -> None:
112
+ """Validate that an output path can be written.
113
+
114
+ Args:
115
+ parser: Argument parser used to report validation errors.
116
+ path: Path to validate.
117
+ label: Human-readable argument label for error messages.
118
+
119
+ Returns:
120
+ None.
121
+ """
122
+ if os.path.isdir(path):
123
+ parser.error(f"{label} points to a directory, not a file: {path}")
124
+
125
+ parent_dir = os.path.dirname(os.path.abspath(path)) or os.curdir
126
+ if not os.path.exists(parent_dir):
127
+ parser.error(f"Parent directory for {label} does not exist: {parent_dir}")
128
+ if not os.path.isdir(parent_dir):
129
+ parser.error(f"Parent path for {label} is not a directory: {parent_dir}")
130
+
131
+ if os.path.exists(path):
132
+ if not os.path.isfile(path):
133
+ parser.error(f"{label} is not a regular file: {path}")
134
+ if not os.access(path, os.W_OK):
135
+ parser.error(f"{label} is not writable: {path}")
136
+ elif not os.access(parent_dir, os.W_OK):
137
+ parser.error(f"Parent directory for {label} is not writable: {parent_dir}")
138
+
139
+
140
+ def check_paths(parser, args) -> None:
141
+ """Validate input, output, and log file paths.
142
+
143
+ Args:
144
+ parser: Argument parser used to report validation errors.
145
+ args: Parsed CLI arguments namespace.
146
+
147
+ Returns:
148
+ None.
149
+ """
150
+ basepath = get_basepath(args.sequence, args.output)
151
+
152
+ check_input_path(parser, args.sequence, "sequence path")
153
+ if args.model:
154
+ check_input_path(parser, args.model, "--model")
155
+
156
+ check_output_path(
157
+ parser,
158
+ args.output if args.output else ''.join([basepath, '.gff3']),
159
+ "annotation output path",
160
+ )
161
+ check_output_path(parser, ''.join([basepath, '.log']), "log output path")
162
+
163
+ if args.genes:
164
+ check_output_path(parser, args.genes, "--genes")
165
+ if args.proteins:
166
+ check_output_path(parser, args.proteins, "--proteins")
167
+
168
+
169
+ def check_args(parser, args) -> None:
170
+ """Validate combinations of parsed CLI arguments and check file paths.
171
+
172
+ Args:
173
+ parser: Argument parser used to report validation errors.
174
+ args: Parsed CLI arguments namespace.
175
+
176
+ Returns:
177
+ None.
178
+ """
179
+ if args.model and not args.context_length:
180
+ parser.error("--context-length is required when using a custom model.")
181
+ if args.min_exon_size > args.max_exon_size:
182
+ parser.error(f"--min-exon-size ({args.min_exon_size}) cannot exceed "
183
+ f"--max-exon-size ({args.max_exon_size}).")
184
+ if args.min_intron_size > args.max_intron_size:
185
+ parser.error(f"--min-intron-size ({args.min_intron_size}) cannot exceed "
186
+ f"--max-intron-size ({args.max_intron_size}).")
187
+ check_paths(parser, args)
188
+
189
+
190
+ def parse_args(argv=None) -> Namespace:
191
+ """Parse and validate command-line arguments for geneML.
192
+
193
+ Args:
194
+ argv: Optional argument list. If None, arguments are read from sys.argv.
195
+
196
+ Returns:
197
+ Parsed and validated CLI arguments.
198
+ """
199
+ version_string = f"geneML {__version__}"
200
+ parser = argparse.ArgumentParser(description=version_string, add_help=False)
201
+
202
+ parser.add_argument('-h', '--help',
203
+ action='help',
204
+ help="Show this help message and exit.")
205
+ parser.add_argument('--version',
206
+ action='version',
207
+ version=version_string,
208
+ help="Show version number and exit.")
209
+ parser.add_argument('sequence',
210
+ type=str,
211
+ help="Sequence file in FASTA/GenBank/EMBL format.")
212
+ parser.add_argument('-o', '--output',
213
+ type=str,
214
+ help="Gene annotations output path (default: based on input filename).")
215
+ parser.add_argument('-g', '--genes',
216
+ type=str,
217
+ help="Gene sequences output path (default: None).")
218
+ parser.add_argument('-p', '--proteins',
219
+ type=str,
220
+ help="Protein sequences output path (default: None).")
221
+ parser.add_argument('--gene-id-prefix',
222
+ type=gene_id_prefix,
223
+ help=("Prefix for gene IDs in output (default: None)."))
224
+ parser.add_argument('-m', '--model',
225
+ type=str,
226
+ help="Path to model file (default: models/geneML_default.keras).")
227
+ parser.add_argument('-cl', '--context-length',
228
+ type=positive_int,
229
+ help="Context length of the model.")
230
+ parser.add_argument('-c', '--cores',
231
+ type=positive_int,
232
+ help="Number of cores to use for processing (default: all available).")
233
+
234
+ advanced = parser.add_argument_group("advanced options")
235
+ advanced.add_argument('-v', '--verbose',
236
+ action='store_true',
237
+ help="Enable verbose mode.")
238
+ advanced.add_argument('-d', '--debug',
239
+ action='store_true',
240
+ help="Enable debug mode.")
241
+ advanced.add_argument('--cpu-only',
242
+ action='store_true',
243
+ help="Use CPU only for inference, disable GPU usage.")
244
+ advanced.add_argument('--strand',
245
+ type=str,
246
+ choices=[x.value for x in Strand],
247
+ default='both',
248
+ help="On which strand to predict genes (default: %(default)s).")
249
+ advanced.add_argument('--contigs-filter',
250
+ type=str,
251
+ help="Run only on selected contigs (comma separated string).")
252
+ advanced.add_argument('--write-raw-scores',
253
+ action='store_true',
254
+ help=("Instead of running gene calling, "
255
+ "output the raw model scores as a .seg file."))
256
+ advanced.add_argument('--max-transcripts',
257
+ type=positive_int,
258
+ default=5,
259
+ help="Maximum number of transcripts per gene (default: %(default)s).")
260
+ advanced.add_argument('--allow-opposite-strand-overlaps',
261
+ choices=['true', 'false'],
262
+ default='true',
263
+ help=("Predict overlapping genes on opposite strands "
264
+ "(default: %(default)s)."))
265
+ advanced.add_argument('--min-gene-score',
266
+ type=gene_score,
267
+ default='dynamic',
268
+ help=("Minimum gene score for gene reporting. "
269
+ "Can be a float value or 'dynamic' (default: %(default)s). "
270
+ "Dynamic mode requires >=100,000 bp total input."))
271
+ advanced.add_argument('--min-exon-size',
272
+ type=positive_int,
273
+ default=1,
274
+ help="Minimum exon size (default: %(default)s).")
275
+ advanced.add_argument('--max-exon-size',
276
+ type=positive_int,
277
+ default=30000,
278
+ help="Maximum exon size (default: %(default)s).")
279
+ advanced.add_argument('--min-intron-size',
280
+ type=positive_int,
281
+ default=10,
282
+ help="Minimum intron size (default: %(default)s).")
283
+ advanced.add_argument('--max-intron-size',
284
+ type=positive_int,
285
+ default=400,
286
+ help="Maximum intron size (default: %(default)s).")
287
+ advanced.add_argument('--cds-start-min-score',
288
+ type=unit_float,
289
+ default=0.01,
290
+ help=("Minimum model score for considering a CDS start "
291
+ "(default: %(default)s)."))
292
+ advanced.add_argument('--cds-end-min-score',
293
+ type=unit_float,
294
+ default=0.01,
295
+ help=("Minimum model score for considering a CDS end "
296
+ "(default: %(default)s)."))
297
+ advanced.add_argument('--exon-start-min-score',
298
+ type=unit_float,
299
+ default=0.01,
300
+ help=("Minimum model score for considering an exon start "
301
+ "(default: %(default)s)."))
302
+ advanced.add_argument('--exon-end-min-score',
303
+ type=unit_float,
304
+ default=0.01,
305
+ help=("Minimum model score for considering an exon end "
306
+ "(default: %(default)s)."))
307
+ advanced.add_argument('--gene-candidates',
308
+ type=positive_int,
309
+ default=5000,
310
+ help=("Maximum number of gene candidates to consider per locus "
311
+ "(default: %(default)s)."))
312
+
313
+ args = parser.parse_args(argv)
314
+ check_args(parser, args)
315
+
316
+ return args