pytransdecoder 0.3.0__tar.gz → 0.4.0__tar.gz

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 (82) hide show
  1. {pytransdecoder-0.3.0/pytransdecoder.egg-info → pytransdecoder-0.4.0}/PKG-INFO +5 -3
  2. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/README.md +4 -2
  3. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pyproject.toml +1 -1
  4. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/__init__.py +1 -1
  5. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/cli.py +10 -4
  6. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/writers.py +11 -11
  7. pytransdecoder-0.4.0/pytransdecoder/longorfs.py +247 -0
  8. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0/pytransdecoder.egg-info}/PKG-INFO +5 -3
  9. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder.egg-info/SOURCES.txt +1 -0
  10. pytransdecoder-0.4.0/tests/test_longorfs_parallel.py +85 -0
  11. pytransdecoder-0.3.0/pytransdecoder/longorfs.py +0 -180
  12. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/LICENSE +0 -0
  13. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/__main__.py +0 -0
  14. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/__init__.py +0 -0
  15. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/base_freqs.py +0 -0
  16. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/decoys.py +0 -0
  17. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/fasta.py +0 -0
  18. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/gene_model.py +0 -0
  19. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/gff3_parser.py +0 -0
  20. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/homology_starts.py +0 -0
  21. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/kozak.py +0 -0
  22. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/markov_fast.py +0 -0
  23. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/models.py +0 -0
  24. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/orf_finder.py +0 -0
  25. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/parallel.py +0 -0
  26. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/presets.py +0 -0
  27. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/prng.py +0 -0
  28. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/pwm.py +0 -0
  29. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/sequence.py +0 -0
  30. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/core/translator.py +0 -0
  31. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/genome.py +0 -0
  32. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/predict.py +0 -0
  33. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/apicomplexan/hexamer.scores.gz +0 -0
  34. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/apicomplexan/kozak.tsv +0 -0
  35. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/apicomplexan/preset.json +0 -0
  36. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/apicomplexan/thresholds.tsv.gz +0 -0
  37. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/apicomplexan/training_orfs.cds.gz +0 -0
  38. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/ciliate/hexamer.scores.gz +0 -0
  39. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/ciliate/kozak.tsv +0 -0
  40. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/ciliate/preset.json +0 -0
  41. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/ciliate/thresholds.tsv.gz +0 -0
  42. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/ciliate/training_orfs.cds.gz +0 -0
  43. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/fungus/hexamer.scores.gz +0 -0
  44. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/fungus/kozak.tsv +0 -0
  45. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/fungus/preset.json +0 -0
  46. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/fungus/thresholds.tsv.gz +0 -0
  47. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/fungus/training_orfs.cds.gz +0 -0
  48. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/green_alga/hexamer.scores.gz +0 -0
  49. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/green_alga/kozak.tsv +0 -0
  50. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/green_alga/preset.json +0 -0
  51. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/green_alga/thresholds.tsv.gz +0 -0
  52. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/green_alga/training_orfs.cds.gz +0 -0
  53. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/insect/hexamer.scores.gz +0 -0
  54. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/insect/kozak.tsv +0 -0
  55. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/insect/preset.json +0 -0
  56. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/insect/thresholds.tsv.gz +0 -0
  57. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/insect/training_orfs.cds.gz +0 -0
  58. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/mammal/hexamer.scores.gz +0 -0
  59. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/mammal/kozak.tsv +0 -0
  60. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/mammal/preset.json +0 -0
  61. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/mammal/thresholds.tsv.gz +0 -0
  62. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/mammal/training_orfs.cds.gz +0 -0
  63. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/plant/hexamer.scores.gz +0 -0
  64. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/plant/kozak.tsv +0 -0
  65. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/plant/preset.json +0 -0
  66. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/plant/thresholds.tsv.gz +0 -0
  67. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder/presets/plant/training_orfs.cds.gz +0 -0
  68. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder.egg-info/dependency_links.txt +0 -0
  69. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder.egg-info/entry_points.txt +0 -0
  70. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder.egg-info/requires.txt +0 -0
  71. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/pytransdecoder.egg-info/top_level.txt +0 -0
  72. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/setup.cfg +0 -0
  73. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/setup.py +0 -0
  74. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/tests/test_basic.py +0 -0
  75. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/tests/test_decoys.py +0 -0
  76. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/tests/test_fasta.py +0 -0
  77. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/tests/test_genome_bed.py +0 -0
  78. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/tests/test_homology_starts.py +0 -0
  79. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/tests/test_markov_fast.py +0 -0
  80. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/tests/test_parallel.py +0 -0
  81. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/tests/test_qvalue_modes.py +0 -0
  82. {pytransdecoder-0.3.0 → pytransdecoder-0.4.0}/tests/test_translator_memo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytransdecoder
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: TransDecoder, the original Perl version ported to Python by AI/LLMs with new features integrated
5
5
  Author: Brian Haas, ported to python by AI/LLMs
6
6
  License: BSD-3-Clause
@@ -81,6 +81,7 @@ This creates a directory `transcripts.fasta.transdecoder_dir/` with:
81
81
  -O, --output-dir PATH Output directory
82
82
  --gene-trans-map PATH Gene-to-transcript mapping file
83
83
  --complete-orfs-only Only output complete ORFs
84
+ --cpu INT Worker processes (default: 1); the output does not depend on it
84
85
  -v, --verbose Verbose output
85
86
  ```
86
87
 
@@ -179,8 +180,9 @@ pyTransdecoder -t transcripts.fasta --pfam-search-db Pfam-A.hmm
179
180
  --decoy-seed INT Seed of the decoy shuffles (default: 42)
180
181
  --preset NAME|DIR Models and thresholds from a preset instead of the input
181
182
  (recommended below ~1,000 transcripts or for mostly partial sets)
182
- --cpu INT Worker processes for scoring candidate and decoy ORFs
183
- (default: 1); the output does not depend on it
183
+ --cpu INT Worker processes for scoring candidate and decoy ORFs, and in
184
+ pyTransdecoder also for LongOrfs (default: 1); the output does
185
+ not depend on it
184
186
  -G, --genetic-code TEXT Genetic code (default: Standard)
185
187
  -v, --verbose Verbose output
186
188
  ```
@@ -60,6 +60,7 @@ This creates a directory `transcripts.fasta.transdecoder_dir/` with:
60
60
  -O, --output-dir PATH Output directory
61
61
  --gene-trans-map PATH Gene-to-transcript mapping file
62
62
  --complete-orfs-only Only output complete ORFs
63
+ --cpu INT Worker processes (default: 1); the output does not depend on it
63
64
  -v, --verbose Verbose output
64
65
  ```
65
66
 
@@ -158,8 +159,9 @@ pyTransdecoder -t transcripts.fasta --pfam-search-db Pfam-A.hmm
158
159
  --decoy-seed INT Seed of the decoy shuffles (default: 42)
159
160
  --preset NAME|DIR Models and thresholds from a preset instead of the input
160
161
  (recommended below ~1,000 transcripts or for mostly partial sets)
161
- --cpu INT Worker processes for scoring candidate and decoy ORFs
162
- (default: 1); the output does not depend on it
162
+ --cpu INT Worker processes for scoring candidate and decoy ORFs, and in
163
+ pyTransdecoder also for LongOrfs (default: 1); the output does
164
+ not depend on it
163
165
  -G, --genetic-code TEXT Genetic code (default: Standard)
164
166
  -v, --verbose Verbose output
165
167
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pytransdecoder"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "TransDecoder, the original Perl version ported to Python by AI/LLMs with new features integrated"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -4,4 +4,4 @@ PyTransDecoder - Python port of TransDecoder
4
4
  Identifies candidate protein-coding regions within transcript sequences.
5
5
  """
6
6
 
7
- __version__ = "0.3.0"
7
+ __version__ = "0.4.0"
@@ -80,6 +80,10 @@ Example:
80
80
  help='Show version and exit'
81
81
  )
82
82
 
83
+ parser.add_argument('--cpu', type=int, default=1, dest='cpu',
84
+ help="Worker processes for ORF extraction; the output does not depend on it "
85
+ "(default: %(default)s)")
86
+
83
87
  parser.set_defaults(func=longorfs_cmd)
84
88
  return parser
85
89
 
@@ -107,8 +111,8 @@ def _add_selection_args(parser):
107
111
  parser.add_argument('--decoy-seed', type=int, default=DEFAULT_DECOY_SEED, dest='decoy_seed',
108
112
  help="Seed of the decoy shuffles (default: %(default)s)")
109
113
  parser.add_argument('--cpu', type=int, default=1, dest='cpu',
110
- help="Worker processes for scoring candidate and decoy ORFs; results do not depend "
111
- "on it (default: %(default)s)")
114
+ help="Worker processes for scoring candidate and decoy ORFs (pyTransdecoder: also for "
115
+ "extracting ORFs); results do not depend on it (default: %(default)s)")
112
116
  from .core.presets import bundled_presets
113
117
  parser.add_argument('--preset', default=None,
114
118
  help="Use a preset's coding model, start model and q-value thresholds instead of "
@@ -279,7 +283,8 @@ def longorfs_cmd(args):
279
283
  output_dir=args.output_dir,
280
284
  gene_trans_map_file=args.gene_trans_map,
281
285
  complete_orfs_only=args.complete_orfs_only,
282
- verbose=args.verbose
286
+ verbose=args.verbose,
287
+ cpu=args.cpu
283
288
  )
284
289
  except Exception as e:
285
290
  print(f"Error: {e}", file=sys.stderr)
@@ -496,7 +501,8 @@ def pipeline_cmd(args):
496
501
  output_dir=args.output_dir,
497
502
  gene_trans_map_file=args.gene_trans_map,
498
503
  complete_orfs_only=args.complete_orfs_only,
499
- verbose=args.verbose
504
+ verbose=args.verbose,
505
+ cpu=args.cpu
500
506
  )
501
507
  except Exception as e:
502
508
  print(f"Error during LongOrfs phase: {e}", file=sys.stderr)
@@ -56,14 +56,14 @@ class FastaWriter:
56
56
  sequence: Sequence string
57
57
  wrap: Line width for wrapping (0 = no wrap)
58
58
  """
59
- if not header.startswith('>'):
60
- header = '>' + header
61
-
62
- self.file.write(header + '\n')
63
-
64
- if wrap > 0:
65
- # Wrap sequence
66
- for i in range(0, len(sequence), wrap):
67
- self.file.write(sequence[i:i+wrap] + '\n')
68
- else:
69
- self.file.write(sequence + '\n')
59
+ self.file.write(fasta_entry(header, sequence, wrap))
60
+
61
+
62
+ def fasta_entry(header: str, sequence: str, wrap: int = 60) -> str:
63
+ """One FASTA record as text: header line (a '>' is added if missing), then the sequence
64
+ in lines of `wrap` characters (0 = one line)."""
65
+ if not header.startswith('>'):
66
+ header = '>' + header
67
+ if wrap > 0:
68
+ return header + '\n' + ''.join(sequence[i:i + wrap] + '\n' for i in range(0, len(sequence), wrap))
69
+ return header + '\n' + sequence + '\n'
@@ -0,0 +1,247 @@
1
+ """
2
+ TransDecoder.LongOrfs - Extract long ORFs from transcript sequences
3
+
4
+ Phase 1 of TransDecoder pipeline.
5
+ """
6
+
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Optional, Dict
10
+ from .core.fasta import read_fasta
11
+ from tqdm import tqdm
12
+
13
+ from .core.orf_finder import ORFFinder
14
+ from .core.writers import fasta_entry
15
+ from .core.base_freqs import write_base_frequencies
16
+
17
+ # Minimum ORF length (aa). Predict's decoy q-values (stratified by ORF length) decide how much coding
18
+ # evidence short ORFs need.
19
+ DEFAULT_MIN_PROTEIN_LENGTH = 50
20
+
21
+
22
+ def run_longorfs(
23
+ transcripts_file: Path,
24
+ min_protein_length: int = DEFAULT_MIN_PROTEIN_LENGTH,
25
+ genetic_code: str = "universal",
26
+ strand_specific: bool = False,
27
+ output_dir: Optional[Path] = None,
28
+ gene_trans_map_file: Optional[Path] = None,
29
+ complete_orfs_only: bool = False,
30
+ verbose: bool = False,
31
+ cpu: int = 1
32
+ ):
33
+ """
34
+ Extract long ORFs from transcripts (Phase 1).
35
+
36
+ Args:
37
+ transcripts_file: Input FASTA file
38
+ min_protein_length: Minimum protein length in amino acids
39
+ genetic_code: Genetic code name
40
+ strand_specific: Only analyze top strand
41
+ output_dir: Output directory
42
+ gene_trans_map_file: Gene to transcript mapping file
43
+ complete_orfs_only: Only output complete ORFs
44
+ verbose: Verbose output
45
+ cpu: Worker processes for ORF extraction (output does not depend on it)
46
+ """
47
+
48
+ # Set up output directory
49
+ if output_dir is None:
50
+ output_dir = Path.cwd()
51
+ output_dir = Path(output_dir)
52
+
53
+ # Create transdecoder_dir
54
+ workdir = output_dir / f"{transcripts_file.name}.transdecoder_dir"
55
+ workdir.mkdir(parents=True, exist_ok=True)
56
+
57
+ if verbose:
58
+ print(f"Output directory: {workdir}", file=sys.stderr)
59
+
60
+ # Load gene-transcript mapping if provided
61
+ gene_trans_map = {}
62
+ if gene_trans_map_file:
63
+ with open(gene_trans_map_file) as f:
64
+ for line in f:
65
+ parts = line.strip().split('\t')
66
+ if len(parts) >= 2:
67
+ gene_id, trans_id = parts[0], parts[1]
68
+ gene_trans_map[trans_id] = gene_id
69
+
70
+ # LongOrfs settings, for Predict's decoy ORFs (same finder settings on shuffled transcripts)
71
+ import json
72
+ with open(workdir / "longorfs.params.json", "w") as fh:
73
+ json.dump({"min_protein_length": min_protein_length, "genetic_code": genetic_code,
74
+ "strand_specific": bool(strand_specific),
75
+ "complete_orfs_only": bool(complete_orfs_only)}, fh, indent=1)
76
+
77
+ # Output files
78
+ prefix = workdir / "longest_orfs"
79
+ cds_file = prefix.with_suffix('.cds')
80
+ gff3_file = prefix.with_suffix('.gff3')
81
+ pep_file = prefix.with_suffix('.pep')
82
+
83
+ finder_args = dict(
84
+ min_protein_length=min_protein_length,
85
+ allow_5prime_partial=not complete_orfs_only,
86
+ allow_3prime_partial=not complete_orfs_only,
87
+ forward_strand=True,
88
+ reverse_strand=not strand_specific,
89
+ genetic_code=genetic_code,
90
+ complete_orfs_only=complete_orfs_only
91
+ )
92
+ settings = (finder_args, gene_trans_map, genetic_code, strand_specific)
93
+ cpu = max(1, int(cpu))
94
+
95
+ if verbose:
96
+ print("\n\n- extracting ORFs from transcripts.\n", file=sys.stderr)
97
+
98
+ if cpu == 1:
99
+ # one pass over the input, streamed (a counting pass for the progress bar only when verbose)
100
+ total_transcripts = sum(1 for _ in read_fasta(transcripts_file)) if verbose else None
101
+ chunks = _batches(((r.id, str(r.seq)) for r in read_fasta(transcripts_file)), SERIAL_BATCH)
102
+ else:
103
+ records = [(r.id, str(r.seq)) for r in read_fasta(transcripts_file)]
104
+ total_transcripts = len(records)
105
+ # Model IDs are <acc>.p1, .p2, ... numbered within a transcript; IDs of different accessions
106
+ # never coincide (the last '.p' of an ID starts its numeric suffix), so chunks can be numbered
107
+ # independently. A repeated accession continues the numbering of its earlier copy, so inputs
108
+ # with repeated accessions are processed in one chunk.
109
+ n_repeated = total_transcripts - len({acc for acc, _ in records})
110
+ if n_repeated:
111
+ print(f"Warning: {n_repeated} repeated transcript accessions; extracting ORFs in a single "
112
+ f"process", file=sys.stderr)
113
+ chunks, cpu = [records], 1
114
+ else:
115
+ chunks = _chunks_by_size(records, cpu * 16)
116
+ del records
117
+
118
+ if verbose:
119
+ print(f"-total transcripts to examine: {total_transcripts}\n", file=sys.stderr)
120
+
121
+ base_counts = {base: 0 for base in 'ACGT'}
122
+ progress = tqdm(total=total_transcripts, desc="Processing transcripts", disable=not verbose,
123
+ file=sys.stderr)
124
+ with open(gff3_file, 'w') as gff_fh, open(cds_file, 'w') as cds_fh, open(pep_file, 'w') as pep_fh:
125
+ gff_fh.write("##gff-version 3\n")
126
+ for n, counts, gff, cds, pep in _map_chunks(chunks, settings, cpu):
127
+ for base in 'ACGT':
128
+ base_counts[base] += counts[base]
129
+ gff_fh.write(gff)
130
+ cds_fh.write(cds)
131
+ pep_fh.write(pep)
132
+ progress.update(n)
133
+ progress.close()
134
+
135
+ # Base frequencies (the per-chunk counts are integers, so their sum is exact)
136
+ write_base_frequencies(base_counts, str(workdir / "base_freqs.dat"))
137
+
138
+ # Print summary
139
+ if verbose:
140
+ print(f"\n\n#################################", file=sys.stderr)
141
+ print(f"### Done preparing long ORFs ###", file=sys.stderr)
142
+ print(f"##################################", file=sys.stderr)
143
+ print(f"\n\tUse file: {pep_file} for Pfam and/or BlastP searches to enable homology-based coding region identification.\n\n", file=sys.stderr)
144
+ print(f"\tThen, run TransDecoder.Predict\n\n", file=sys.stderr)
145
+
146
+
147
+ # -- ORF extraction of chunks of transcripts (in this process or in worker processes) -------------
148
+
149
+ _SETTINGS = None
150
+ _SEEN_PROT_IDS: set = set()
151
+ SERIAL_BATCH = 2000 # transcripts per batch when streaming with --cpu 1
152
+
153
+
154
+ def _init_worker(settings):
155
+ """Settings, and one set of issued model IDs per process for the whole run."""
156
+ global _SETTINGS, _SEEN_PROT_IDS
157
+ _SETTINGS = settings
158
+ _SEEN_PROT_IDS = set()
159
+
160
+
161
+ def _extract_chunk(records):
162
+ """(n transcripts, base counts, gff3 text, cds text, pep text) of an iterable of (acc, sequence),
163
+ numbering model IDs as the original single loop does."""
164
+ finder_args, gene_trans_map, genetic_code, strand_specific = _SETTINGS
165
+ orf_finder = ORFFinder(**finder_args)
166
+ complement = {'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G'}
167
+ counts = {base: 0 for base in 'ACGT'}
168
+ gff, cds, pep = [], [], []
169
+ seen_prot_ids = _SEEN_PROT_IDS
170
+ n = 0
171
+ for acc, sequence in records:
172
+ n += 1
173
+ # base counts, as core.base_freqs.compute_base_counts
174
+ useq = sequence.upper()
175
+ for base in 'ACGT':
176
+ k = useq.count(base)
177
+ counts[base] += k
178
+ if not strand_specific:
179
+ counts[complement[base]] += k
180
+
181
+ for orf in orf_finder.find_all_orfs(sequence, acc):
182
+ # Generate unique model ID
183
+ pcounter = 1
184
+ model_id = f"{acc}.p{pcounter}"
185
+ while model_id in seen_prot_ids:
186
+ pcounter += 1
187
+ model_id = f"{acc}.p{pcounter}"
188
+ seen_prot_ids.add(model_id)
189
+
190
+ gene_id = gene_trans_map.get(acc, f"GENE.{acc}")
191
+ gene_id = f"{gene_id}~~{model_id}"
192
+
193
+ orf.model_id = model_id
194
+ orf.gene_id = gene_id
195
+ orf.description = f"ORF type:{orf.orf_type} ({orf.strand})"
196
+
197
+ gff.append(orf.to_gff3() + "\n")
198
+ cds.append(fasta_entry(
199
+ f">{model_id} type:{orf.orf_type} {acc}:{orf.start}-{orf.end}({orf.strand})", orf.sequence))
200
+ pep.append(fasta_entry(
201
+ f">{model_id} type:{orf.orf_type} gc:{genetic_code} {acc}:{orf.start}-{orf.end}({orf.strand})",
202
+ orf.protein))
203
+ return n, counts, "".join(gff), "".join(cds), "".join(pep)
204
+
205
+
206
+ def _batches(it, size):
207
+ """Consecutive lists of up to `size` items of an iterator."""
208
+ from itertools import islice
209
+ while True:
210
+ b = list(islice(it, size))
211
+ if not b:
212
+ return
213
+ yield b
214
+
215
+
216
+ def _chunks_by_size(records, n_chunks):
217
+ """Consecutive chunks of roughly equal total sequence length (order preserved)."""
218
+ total = sum(len(s) for _, s in records) or 1
219
+ target = total / n_chunks
220
+ chunks, cur, acc = [], [], 0
221
+ for rec in records:
222
+ cur.append(rec)
223
+ acc += len(rec[1])
224
+ if acc >= target:
225
+ chunks.append(cur)
226
+ cur, acc = [], 0
227
+ if cur:
228
+ chunks.append(cur)
229
+ return chunks
230
+
231
+
232
+ def _map_chunks(chunks, settings, cpu):
233
+ """_extract_chunk results in chunk order."""
234
+ if cpu <= 1:
235
+ _init_worker(settings)
236
+ try:
237
+ for c in chunks:
238
+ yield _extract_chunk(c)
239
+ finally:
240
+ _init_worker(None) # release the ID set
241
+ return
242
+ from concurrent.futures import ProcessPoolExecutor
243
+ import multiprocessing as mp
244
+ ctx = mp.get_context("fork") if "fork" in mp.get_all_start_methods() else None
245
+ with ProcessPoolExecutor(max_workers=cpu, mp_context=ctx, initializer=_init_worker,
246
+ initargs=(settings,)) as ex:
247
+ yield from ex.map(_extract_chunk, chunks)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytransdecoder
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: TransDecoder, the original Perl version ported to Python by AI/LLMs with new features integrated
5
5
  Author: Brian Haas, ported to python by AI/LLMs
6
6
  License: BSD-3-Clause
@@ -81,6 +81,7 @@ This creates a directory `transcripts.fasta.transdecoder_dir/` with:
81
81
  -O, --output-dir PATH Output directory
82
82
  --gene-trans-map PATH Gene-to-transcript mapping file
83
83
  --complete-orfs-only Only output complete ORFs
84
+ --cpu INT Worker processes (default: 1); the output does not depend on it
84
85
  -v, --verbose Verbose output
85
86
  ```
86
87
 
@@ -179,8 +180,9 @@ pyTransdecoder -t transcripts.fasta --pfam-search-db Pfam-A.hmm
179
180
  --decoy-seed INT Seed of the decoy shuffles (default: 42)
180
181
  --preset NAME|DIR Models and thresholds from a preset instead of the input
181
182
  (recommended below ~1,000 transcripts or for mostly partial sets)
182
- --cpu INT Worker processes for scoring candidate and decoy ORFs
183
- (default: 1); the output does not depend on it
183
+ --cpu INT Worker processes for scoring candidate and decoy ORFs, and in
184
+ pyTransdecoder also for LongOrfs (default: 1); the output does
185
+ not depend on it
184
186
  -G, --genetic-code TEXT Genetic code (default: Standard)
185
187
  -v, --verbose Verbose output
186
188
  ```
@@ -72,6 +72,7 @@ tests/test_decoys.py
72
72
  tests/test_fasta.py
73
73
  tests/test_genome_bed.py
74
74
  tests/test_homology_starts.py
75
+ tests/test_longorfs_parallel.py
75
76
  tests/test_markov_fast.py
76
77
  tests/test_parallel.py
77
78
  tests/test_qvalue_modes.py
@@ -0,0 +1,85 @@
1
+ """LongOrfs --cpu: output files identical to the single-process run."""
2
+
3
+ import random
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from pytransdecoder.longorfs import run_longorfs
9
+
10
+ FILES = ["longest_orfs.gff3", "longest_orfs.cds", "longest_orfs.pep", "base_freqs.dat",
11
+ "longorfs.params.json"]
12
+
13
+
14
+ def _random_fasta(path: Path, ids, seed=7):
15
+ rng = random.Random(seed)
16
+ with open(path, "w") as fh:
17
+ for i, acc in enumerate(ids):
18
+ n = rng.randint(100, 3000)
19
+ # coding-like stretches (no in-frame stop) mixed with random sequence, some IUPAC / lowercase
20
+ seq = "".join(rng.choice("ACGT") for _ in range(n))
21
+ if i % 3 == 0:
22
+ body = "".join(rng.choice(["GCT", "AAA", "CTG", "GAG", "TCC", "ATG"]) for _ in range(150))
23
+ seq = seq[:50] + "ATG" + body + "TAA" + seq[50:]
24
+ if i % 5 == 0:
25
+ seq = seq[:30] + "N" + seq[31:]
26
+ if i % 7 == 0:
27
+ seq = seq.lower()
28
+ fh.write(f">{acc} desc {i}\n")
29
+ for k in range(0, len(seq), 70):
30
+ fh.write(seq[k:k + 70] + "\n")
31
+
32
+
33
+ def _run(tmp_path, name, fasta, cpu, **kw):
34
+ out = tmp_path / name
35
+ out.mkdir()
36
+ run_longorfs(fasta, output_dir=out, cpu=cpu, **kw)
37
+ d = out / f"{fasta.name}.transdecoder_dir"
38
+ return {f: (d / f).read_bytes() for f in FILES}
39
+
40
+
41
+ @pytest.mark.parametrize("strand_specific", [False, True])
42
+ @pytest.mark.parametrize("genetic_code", ["universal", "Tetrahymena"])
43
+ def test_cpu_parity(tmp_path, strand_specific, genetic_code):
44
+ ids = [f"tx{i}" for i in range(120)] + ["A", "A.p1", "A.p1.p", "B.p2"] # IDs that look like model IDs
45
+ fasta = tmp_path / "t.fa"
46
+ _random_fasta(fasta, ids)
47
+ kw = dict(strand_specific=strand_specific, genetic_code=genetic_code, min_protein_length=30)
48
+ one = _run(tmp_path, "c1", fasta, 1, **kw)
49
+ assert one["longest_orfs.pep"].count(b">") > 50
50
+ import pytransdecoder.longorfs as lo
51
+ lo.SERIAL_BATCH, keep = 7, lo.SERIAL_BATCH
52
+ try:
53
+ assert _run(tmp_path, "c1b", fasta, 1, **kw) == one
54
+ finally:
55
+ lo.SERIAL_BATCH = keep
56
+ for cpu in (2, 3, 8):
57
+ assert _run(tmp_path, f"c{cpu}", fasta, cpu, **kw) == one
58
+
59
+
60
+ @pytest.mark.parametrize("batch", [2000, 3])
61
+ def test_repeated_accessions(tmp_path, monkeypatch, batch):
62
+ """A repeated accession continues the .pN numbering of its earlier copy, at any --cpu and
63
+ across the batches of the streamed single-process run."""
64
+ import pytransdecoder.longorfs as lo
65
+ monkeypatch.setattr(lo, "SERIAL_BATCH", batch)
66
+ ids = [f"tx{i}" for i in range(40)] + ["tx3", "tx0", "tx3"]
67
+ fasta = tmp_path / "t.fa"
68
+ _random_fasta(fasta, ids)
69
+ one = _run(tmp_path, "c1", fasta, 1, min_protein_length=30)
70
+ assert _run(tmp_path, "c4", fasta, 4, min_protein_length=30) == one
71
+ pep = one["longest_orfs.pep"].decode()
72
+ tx3 = [l.split()[0] for l in pep.splitlines() if l.startswith(">tx3.p")]
73
+ assert len(tx3) == len(set(tx3)) > 1
74
+
75
+
76
+ def test_gene_trans_map_and_complete_only(tmp_path):
77
+ ids = [f"tx{i}" for i in range(60)]
78
+ fasta = tmp_path / "t.fa"
79
+ _random_fasta(fasta, ids)
80
+ gtm = tmp_path / "gtm.tsv"
81
+ gtm.write_text("".join(f"gene{i // 2}\ttx{i}\n" for i in range(0, 60, 3)))
82
+ for kw in (dict(gene_trans_map_file=gtm), dict(complete_orfs_only=True)):
83
+ kw["min_protein_length"] = 30
84
+ one = _run(tmp_path, f"c1_{len(kw)}_{'g' if 'gene_trans_map_file' in kw else 'c'}", fasta, 1, **kw)
85
+ assert _run(tmp_path, f"c3_{'g' if 'gene_trans_map_file' in kw else 'c'}", fasta, 3, **kw) == one
@@ -1,180 +0,0 @@
1
- """
2
- TransDecoder.LongOrfs - Extract long ORFs from transcript sequences
3
-
4
- Phase 1 of TransDecoder pipeline.
5
- """
6
-
7
- import sys
8
- from pathlib import Path
9
- from typing import Optional, Dict
10
- from collections import defaultdict
11
- from .core.fasta import read_fasta
12
- from tqdm import tqdm
13
-
14
- from .core.orf_finder import ORFFinder
15
- from .core.models import ORF
16
- from .core.writers import GFF3Writer, FastaWriter
17
- from .core.base_freqs import compute_base_counts, write_base_frequencies
18
-
19
- # Minimum ORF length (aa). Predict's decoy q-values (stratified by ORF length) decide how much coding
20
- # evidence short ORFs need.
21
- DEFAULT_MIN_PROTEIN_LENGTH = 50
22
-
23
-
24
- def run_longorfs(
25
- transcripts_file: Path,
26
- min_protein_length: int = DEFAULT_MIN_PROTEIN_LENGTH,
27
- genetic_code: str = "universal",
28
- strand_specific: bool = False,
29
- output_dir: Optional[Path] = None,
30
- gene_trans_map_file: Optional[Path] = None,
31
- complete_orfs_only: bool = False,
32
- verbose: bool = False
33
- ):
34
- """
35
- Extract long ORFs from transcripts (Phase 1).
36
-
37
- Args:
38
- transcripts_file: Input FASTA file
39
- min_protein_length: Minimum protein length in amino acids
40
- genetic_code: Genetic code name
41
- strand_specific: Only analyze top strand
42
- output_dir: Output directory
43
- gene_trans_map_file: Gene to transcript mapping file
44
- complete_orfs_only: Only output complete ORFs
45
- verbose: Verbose output
46
- """
47
-
48
- # Set up output directory
49
- if output_dir is None:
50
- output_dir = Path.cwd()
51
- output_dir = Path(output_dir)
52
-
53
- # Create transdecoder_dir
54
- workdir = output_dir / f"{transcripts_file.name}.transdecoder_dir"
55
- workdir.mkdir(parents=True, exist_ok=True)
56
-
57
- if verbose:
58
- print(f"Output directory: {workdir}", file=sys.stderr)
59
-
60
- # Load gene-transcript mapping if provided
61
- gene_trans_map = {}
62
- if gene_trans_map_file:
63
- with open(gene_trans_map_file) as f:
64
- for line in f:
65
- parts = line.strip().split('\t')
66
- if len(parts) >= 2:
67
- gene_id, trans_id = parts[0], parts[1]
68
- gene_trans_map[trans_id] = gene_id
69
-
70
- # LongOrfs settings, for Predict's decoy ORFs (same finder settings on shuffled transcripts)
71
- import json
72
- with open(workdir / "longorfs.params.json", "w") as fh:
73
- json.dump({"min_protein_length": min_protein_length, "genetic_code": genetic_code,
74
- "strand_specific": bool(strand_specific),
75
- "complete_orfs_only": bool(complete_orfs_only)}, fh, indent=1)
76
-
77
- # Step 1: Compute base frequencies
78
- if verbose:
79
- print("\n\n-first extracting base frequencies, we'll need them later.\n", file=sys.stderr)
80
-
81
- base_freqs_file = workdir / "base_freqs.dat"
82
- base_counts = compute_base_counts(str(transcripts_file), strand_specific)
83
- write_base_frequencies(base_counts, str(base_freqs_file))
84
-
85
- # Step 2: Extract ORFs
86
- if verbose:
87
- print("\n\n- extracting ORFs from transcripts.\n", file=sys.stderr)
88
-
89
- # Initialize ORF finder
90
- orf_finder = ORFFinder(
91
- min_protein_length=min_protein_length,
92
- allow_5prime_partial=not complete_orfs_only,
93
- allow_3prime_partial=not complete_orfs_only,
94
- forward_strand=True,
95
- reverse_strand=not strand_specific,
96
- genetic_code=genetic_code,
97
- complete_orfs_only=complete_orfs_only
98
- )
99
-
100
- # Output files
101
- prefix = workdir / "longest_orfs"
102
- cds_file = prefix.with_suffix('.cds')
103
- gff3_file = prefix.with_suffix('.gff3')
104
- pep_file = prefix.with_suffix('.pep')
105
-
106
- # Count total transcripts for progress bar
107
- total_transcripts = sum(1 for _ in read_fasta(transcripts_file))
108
-
109
- if verbose:
110
- print(f"-total transcripts to examine: {total_transcripts}\n", file=sys.stderr)
111
-
112
- # Process transcripts
113
- model_counter = 0
114
- trans_counter = 0
115
- seen_prot_ids = set()
116
-
117
- with GFF3Writer(gff3_file) as gff_writer, \
118
- FastaWriter(cds_file) as cds_writer, \
119
- FastaWriter(pep_file) as pep_writer:
120
-
121
- progress = tqdm(
122
- read_fasta(transcripts_file),
123
- total=total_transcripts,
124
- desc="Processing transcripts",
125
- disable=not verbose,
126
- file=sys.stderr
127
- )
128
-
129
- for record in progress:
130
- trans_counter += 1
131
-
132
- acc = record.id
133
- sequence = str(record.seq)
134
-
135
- # Find all ORFs
136
- orfs = orf_finder.find_all_orfs(sequence, acc)
137
-
138
- # Process each ORF
139
- for orf in orfs:
140
- model_counter += 1
141
-
142
- # Generate unique model ID
143
- pcounter = 1
144
- model_id = f"{acc}.p{pcounter}"
145
- while model_id in seen_prot_ids:
146
- pcounter += 1
147
- model_id = f"{acc}.p{pcounter}"
148
- seen_prot_ids.add(model_id)
149
-
150
- # Generate gene ID
151
- if acc in gene_trans_map:
152
- gene_id = gene_trans_map[acc]
153
- else:
154
- gene_id = f"GENE.{acc}"
155
-
156
- gene_id = f"{gene_id}~~{model_id}"
157
-
158
- # Update ORF with IDs
159
- orf.model_id = model_id
160
- orf.gene_id = gene_id
161
- orf.description = f"ORF type:{orf.orf_type} ({orf.strand})"
162
-
163
- # Write outputs
164
- gff_writer.write_orf(orf)
165
-
166
- # Write CDS
167
- cds_header = f">{model_id} type:{orf.orf_type} {acc}:{orf.start}-{orf.end}({orf.strand})"
168
- cds_writer.write_sequence(cds_header, orf.sequence)
169
-
170
- # Write protein
171
- pep_header = f">{model_id} type:{orf.orf_type} gc:{genetic_code} {acc}:{orf.start}-{orf.end}({orf.strand})"
172
- pep_writer.write_sequence(pep_header, orf.protein)
173
-
174
- # Print summary
175
- if verbose:
176
- print(f"\n\n#################################", file=sys.stderr)
177
- print(f"### Done preparing long ORFs ###", file=sys.stderr)
178
- print(f"##################################", file=sys.stderr)
179
- print(f"\n\tUse file: {pep_file} for Pfam and/or BlastP searches to enable homology-based coding region identification.\n\n", file=sys.stderr)
180
- print(f"\tThen, run TransDecoder.Predict\n\n", file=sys.stderr)
File without changes
File without changes
File without changes