pytransdecoder 0.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.
Files changed (63) hide show
  1. pytransdecoder/__init__.py +7 -0
  2. pytransdecoder/__main__.py +6 -0
  3. pytransdecoder/cli.py +781 -0
  4. pytransdecoder/core/__init__.py +1 -0
  5. pytransdecoder/core/base_freqs.py +71 -0
  6. pytransdecoder/core/decoys.py +113 -0
  7. pytransdecoder/core/fasta.py +55 -0
  8. pytransdecoder/core/gene_model.py +143 -0
  9. pytransdecoder/core/gff3_parser.py +58 -0
  10. pytransdecoder/core/homology_starts.py +54 -0
  11. pytransdecoder/core/kozak.py +180 -0
  12. pytransdecoder/core/models.py +123 -0
  13. pytransdecoder/core/orf_finder.py +317 -0
  14. pytransdecoder/core/presets.py +152 -0
  15. pytransdecoder/core/prng.py +54 -0
  16. pytransdecoder/core/pwm.py +174 -0
  17. pytransdecoder/core/sequence.py +19 -0
  18. pytransdecoder/core/translator.py +143 -0
  19. pytransdecoder/core/writers.py +69 -0
  20. pytransdecoder/genome.py +338 -0
  21. pytransdecoder/longorfs.py +180 -0
  22. pytransdecoder/predict.py +1621 -0
  23. pytransdecoder/presets/apicomplexan/hexamer.scores.gz +0 -0
  24. pytransdecoder/presets/apicomplexan/kozak.tsv +9 -0
  25. pytransdecoder/presets/apicomplexan/preset.json +20 -0
  26. pytransdecoder/presets/apicomplexan/thresholds.tsv.gz +0 -0
  27. pytransdecoder/presets/apicomplexan/training_orfs.cds.gz +0 -0
  28. pytransdecoder/presets/ciliate/hexamer.scores.gz +0 -0
  29. pytransdecoder/presets/ciliate/kozak.tsv +9 -0
  30. pytransdecoder/presets/ciliate/preset.json +20 -0
  31. pytransdecoder/presets/ciliate/thresholds.tsv.gz +0 -0
  32. pytransdecoder/presets/ciliate/training_orfs.cds.gz +0 -0
  33. pytransdecoder/presets/fungus/hexamer.scores.gz +0 -0
  34. pytransdecoder/presets/fungus/kozak.tsv +9 -0
  35. pytransdecoder/presets/fungus/preset.json +20 -0
  36. pytransdecoder/presets/fungus/thresholds.tsv.gz +0 -0
  37. pytransdecoder/presets/fungus/training_orfs.cds.gz +0 -0
  38. pytransdecoder/presets/green_alga/hexamer.scores.gz +0 -0
  39. pytransdecoder/presets/green_alga/kozak.tsv +9 -0
  40. pytransdecoder/presets/green_alga/preset.json +20 -0
  41. pytransdecoder/presets/green_alga/thresholds.tsv.gz +0 -0
  42. pytransdecoder/presets/green_alga/training_orfs.cds.gz +0 -0
  43. pytransdecoder/presets/insect/hexamer.scores.gz +0 -0
  44. pytransdecoder/presets/insect/kozak.tsv +9 -0
  45. pytransdecoder/presets/insect/preset.json +20 -0
  46. pytransdecoder/presets/insect/thresholds.tsv.gz +0 -0
  47. pytransdecoder/presets/insect/training_orfs.cds.gz +0 -0
  48. pytransdecoder/presets/mammal/hexamer.scores.gz +0 -0
  49. pytransdecoder/presets/mammal/kozak.tsv +9 -0
  50. pytransdecoder/presets/mammal/preset.json +20 -0
  51. pytransdecoder/presets/mammal/thresholds.tsv.gz +0 -0
  52. pytransdecoder/presets/mammal/training_orfs.cds.gz +0 -0
  53. pytransdecoder/presets/plant/hexamer.scores.gz +0 -0
  54. pytransdecoder/presets/plant/kozak.tsv +9 -0
  55. pytransdecoder/presets/plant/preset.json +20 -0
  56. pytransdecoder/presets/plant/thresholds.tsv.gz +0 -0
  57. pytransdecoder/presets/plant/training_orfs.cds.gz +0 -0
  58. pytransdecoder-0.2.1.dist-info/METADATA +307 -0
  59. pytransdecoder-0.2.1.dist-info/RECORD +63 -0
  60. pytransdecoder-0.2.1.dist-info/WHEEL +5 -0
  61. pytransdecoder-0.2.1.dist-info/entry_points.txt +3 -0
  62. pytransdecoder-0.2.1.dist-info/licenses/LICENSE +28 -0
  63. pytransdecoder-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,174 @@
1
+ """
2
+ Position weight matrix utilities for start codon refinement.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import math
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Dict, Iterable, List, Optional, Sequence, Tuple
11
+
12
+
13
+ BASE_ORDER = ("G", "A", "T", "C")
14
+ PSEUDOCOUNT = 0.1
15
+
16
+
17
+ @dataclass
18
+ class PWM:
19
+ """Minimal PWM implementation matching the Perl PWM.pm behavior."""
20
+
21
+ pos_freqs: List[Dict[str, int]] = field(default_factory=list)
22
+ pos_probs: List[Dict[str, float]] = field(default_factory=list)
23
+ built: bool = False
24
+
25
+ def add_feature(self, feature_seq: str) -> None:
26
+ feature_seq = feature_seq.upper()
27
+ if any(base not in BASE_ORDER for base in feature_seq):
28
+ return
29
+
30
+ if self.pos_freqs and len(feature_seq) != self.length:
31
+ raise ValueError(
32
+ f"PWM length {self.length} does not match feature length {len(feature_seq)}"
33
+ )
34
+
35
+ while len(self.pos_freqs) < len(feature_seq):
36
+ self.pos_freqs.append({})
37
+
38
+ for index, base in enumerate(feature_seq):
39
+ self.pos_freqs[index][base] = self.pos_freqs[index].get(base, 0) + 1
40
+
41
+ self.built = False
42
+
43
+ def remove_feature(self, feature_seq: str) -> None:
44
+ feature_seq = feature_seq.upper()
45
+ if len(feature_seq) != self.length:
46
+ raise ValueError(
47
+ f"PWM length {self.length} does not match feature length {len(feature_seq)}"
48
+ )
49
+
50
+ for index, base in enumerate(feature_seq):
51
+ current = self.pos_freqs[index].get(base, 0)
52
+ self.pos_freqs[index][base] = current - 1
53
+
54
+ self.built = False
55
+
56
+ @property
57
+ def length(self) -> int:
58
+ if self.pos_probs:
59
+ return len(self.pos_probs)
60
+ return len(self.pos_freqs)
61
+
62
+ def build(self) -> None:
63
+ self.pos_probs = []
64
+ for pos_freq in self.pos_freqs:
65
+ total = sum(pos_freq.values())
66
+ pos_probs = {}
67
+ for base in BASE_ORDER:
68
+ count = pos_freq.get(base, 0)
69
+ pos_probs[base] = (count + PSEUDOCOUNT) / (total + 4 * PSEUDOCOUNT)
70
+ self.pos_probs.append(pos_probs)
71
+
72
+ self.built = True
73
+
74
+ def write(self, filename: Path) -> None:
75
+ if not self.built:
76
+ self.build()
77
+
78
+ with open(filename, "w") as handle:
79
+ handle.write("pos\tG\tA\tT\tC\n")
80
+ for index, pos_probs in enumerate(self.pos_probs):
81
+ handle.write(
82
+ f"{index}\t{pos_probs['G']:.6f}\t{pos_probs['A']:.6f}\t"
83
+ f"{pos_probs['T']:.6f}\t{pos_probs['C']:.6f}\n"
84
+ )
85
+
86
+ @classmethod
87
+ def load(cls, filename: Path) -> "PWM":
88
+ pwm = cls()
89
+ with open(filename) as handle:
90
+ header = next(handle, None)
91
+ if header is None:
92
+ raise ValueError(f"PWM file is empty: {filename}")
93
+ for line in handle:
94
+ line = line.strip()
95
+ if not line:
96
+ continue
97
+ parts = line.split("\t")
98
+ if len(parts) != 5:
99
+ continue
100
+ pwm.pos_probs.append(
101
+ {
102
+ "G": float(parts[1]),
103
+ "A": float(parts[2]),
104
+ "T": float(parts[3]),
105
+ "C": float(parts[4]),
106
+ }
107
+ )
108
+ pwm.built = True
109
+ return pwm
110
+
111
+ def score_plus_minus(
112
+ self,
113
+ target_sequence: str,
114
+ pwm_minus: "PWM",
115
+ pwm_range: Optional[Tuple[int, int]] = None,
116
+ ) -> Optional[float]:
117
+ target_sequence = target_sequence.upper()
118
+ if any(base not in BASE_ORDER for base in target_sequence):
119
+ return None
120
+ if not self.built or not pwm_minus.built:
121
+ raise ValueError("Both PWMs must be built before scoring")
122
+ if len(target_sequence) != self.length or self.length != pwm_minus.length:
123
+ raise ValueError("Target sequence and PWMs must have matching lengths")
124
+
125
+ start, end = (0, self.length - 1) if pwm_range is None else pwm_range
126
+ motif_score = 0.0
127
+ for index in range(start, end + 1):
128
+ base = target_sequence[index]
129
+ pos_prob = self.pos_probs[index].get(base)
130
+ neg_prob = pwm_minus.pos_probs[index].get(base)
131
+ if not pos_prob or not neg_prob:
132
+ return None
133
+ motif_score += math.log(pos_prob / neg_prob)
134
+
135
+ return motif_score
136
+
137
+
138
+ def score_start_window(
139
+ pwm_plus: PWM, pwm_minus: PWM, feature: str, atg_position: int, up: int, down: int
140
+ ) -> Optional[float]:
141
+ """Length-normalized +/- log-odds score of a start-codon feature.
142
+
143
+ Scores `up` bases upstream of the ATG, the ATG itself, and `down` bases downstream,
144
+ then divides by (up + down). Both PWM training (ROC/threshold selection) and start
145
+ refinement must score candidates through this function so that the selected
146
+ threshold is applied on the same scale and window it was learned on.
147
+ """
148
+ score = pwm_plus.score_plus_minus(
149
+ feature, pwm_minus, pwm_range=(atg_position - up, atg_position + 2 + down)
150
+ )
151
+ if score is None:
152
+ return None
153
+ return score / (up + down)
154
+
155
+
156
+ def build_pwm(features: Iterable[str]) -> PWM:
157
+ pwm = PWM()
158
+ for feature in features:
159
+ pwm.add_feature(feature)
160
+ pwm.build()
161
+ return pwm
162
+
163
+
164
+ def trapezoid_auc(points: Sequence[Tuple[float, float]]) -> float:
165
+ ordered = sorted(points, key=lambda item: (item[0], item[1]))
166
+ ordered = [(0.0, 0.0)] + ordered + [(1.0, 1.0)]
167
+ total_auc = 0.0
168
+ prev_x, prev_y = ordered[0]
169
+ for curr_x, curr_y in ordered[1:]:
170
+ total_auc += ((curr_x - prev_x) * min(prev_y, curr_y)) + (
171
+ 0.5 * (curr_x - prev_x) * abs(curr_y - prev_y)
172
+ )
173
+ prev_x, prev_y = curr_x, curr_y
174
+ return total_auc
@@ -0,0 +1,19 @@
1
+ """
2
+ Sequence utilities
3
+ """
4
+
5
+ from Bio.Seq import Seq
6
+
7
+
8
+ def reverse_complement(sequence: str) -> str:
9
+ """
10
+ Return reverse complement of a DNA/RNA sequence.
11
+
12
+ Args:
13
+ sequence: DNA or RNA sequence
14
+
15
+ Returns:
16
+ Reverse complement sequence
17
+ """
18
+ seq = Seq(sequence)
19
+ return str(seq.reverse_complement())
@@ -0,0 +1,143 @@
1
+ """
2
+ Nucleotide to protein translation using BioPython genetic codes.
3
+
4
+ Replaces Nuc_translator.pm functionality.
5
+ """
6
+
7
+ from typing import List, Set
8
+ from Bio.Data import CodonTable
9
+ from Bio.Seq import Seq
10
+
11
+
12
+ class Translator:
13
+ """
14
+ Handles translation of nucleotide sequences to protein.
15
+
16
+ Supports multiple genetic codes via BioPython's CodonTable.
17
+ """
18
+
19
+ # Mapping from TransDecoder genetic code names to BioPython table IDs
20
+ GENETIC_CODE_MAP = {
21
+ 'universal': 1,
22
+ 'standard': 1,
23
+ 'vertebrate_mitochondrial': 2,
24
+ 'yeast_mitochondrial': 3,
25
+ 'mold_mitochondrial': 4,
26
+ 'invertebrate_mitochondrial': 5,
27
+ 'ciliate': 6,
28
+ 'dasycladacean': 6,
29
+ 'hexamita': 6,
30
+ 'echinoderm_mitochondrial': 9,
31
+ 'euplotid': 10,
32
+ 'bacterial': 11,
33
+ 'alternative_yeast': 12,
34
+ 'ascidian_mitochondrial': 13,
35
+ 'flatworm_mitochondrial': 14,
36
+ 'chlorophycean_mitochondrial': 16,
37
+ 'trematode_mitochondrial': 21,
38
+ 'scenedesmus_obliquus_mitochondrial': 22,
39
+ 'thraustochytrium_mitochondrial': 23,
40
+ 'pterobranchia_mitochondrial': 24,
41
+ 'sr1_gracilibacteria': 25,
42
+ 'pachysolen_tannophilus': 26,
43
+ 'candida': 12, # CUG encodes Ser instead of Leu
44
+ 'acetabularia': 6,
45
+ 'tetrahymena': 6,
46
+ 'mesodinium': 29, # UAA/UAG encode Tyr
47
+ 'peritrich': 30, # UAA/UAG encode Glu
48
+ # names used by the Perl TransDecoder (Nuc_translator.pm)
49
+ 'mitochondrial_vertebrates': 2,
50
+ 'mitochondrial_yeast': 3,
51
+ 'mitochondrial_protozoan': 4,
52
+ 'mitochondrial_invertebrates': 5,
53
+ 'mitochondrial_echinoderm': 9,
54
+ 'mitochondrial_ascidian': 13,
55
+ 'mitochondrial_chlorophycean': 16,
56
+ 'mitochondrial_trematode': 21,
57
+ 'mitochondrial_scenedesmus_obliquus': 22,
58
+ 'mitochondrial_thraustochytrium': 23,
59
+ 'mitochondrial_pterobranchia': 24,
60
+ }
61
+
62
+ def __init__(self, genetic_code: str = "universal"):
63
+ """
64
+ Initialize translator with specified genetic code.
65
+
66
+ Args:
67
+ genetic_code: Name of genetic code (case-insensitive)
68
+ """
69
+ code_name = genetic_code.lower().replace(' ', '_').replace('-', '_')
70
+
71
+ if code_name not in self.GENETIC_CODE_MAP:
72
+ available = ', '.join(sorted(self.GENETIC_CODE_MAP.keys()))
73
+ raise ValueError(
74
+ f"Unknown genetic code: {genetic_code}. "
75
+ f"Available codes: {available}"
76
+ )
77
+
78
+ self.genetic_code_name = genetic_code
79
+ self.table_id = self.GENETIC_CODE_MAP[code_name]
80
+ self.codon_table = CodonTable.unambiguous_dna_by_id[self.table_id]
81
+
82
+ def get_stop_codons(self) -> List[str]:
83
+ """
84
+ Get stop codons for current genetic code.
85
+
86
+ Returns:
87
+ List of stop codon sequences (uppercase)
88
+ """
89
+ return sorted(self.codon_table.stop_codons)
90
+
91
+ def get_start_codons(self, allow_non_met: bool = False) -> List[str]:
92
+ """
93
+ Get start codons for current genetic code.
94
+
95
+ Args:
96
+ allow_non_met: If True, return all possible start codons.
97
+ If False, return only ATG (standard start).
98
+
99
+ Returns:
100
+ List of start codon sequences (uppercase)
101
+ """
102
+ if allow_non_met:
103
+ # Use all start codons from the table
104
+ return sorted(self.codon_table.start_codons)
105
+ else:
106
+ # Standard start codon only
107
+ return ['ATG']
108
+
109
+ def translate(self, sequence: str, frame: int = 0) -> str:
110
+ """
111
+ Translate nucleotide sequence to protein.
112
+
113
+ Args:
114
+ sequence: DNA/RNA sequence
115
+ frame: Reading frame offset (0, 1, or 2)
116
+
117
+ Returns:
118
+ Protein sequence (stops are represented as *)
119
+ """
120
+ # Adjust for frame
121
+ seq = sequence[frame:].upper()
122
+
123
+ # Convert to Bio.Seq and translate
124
+ bio_seq = Seq(seq)
125
+
126
+ try:
127
+ protein = str(bio_seq.translate(table=self.table_id, to_stop=False))
128
+ except Exception as e:
129
+ # Handle translation errors (e.g., incomplete codons)
130
+ # Translate as much as possible
131
+ complete_codons = len(seq) // 3
132
+ truncated = seq[:complete_codons * 3]
133
+ if truncated:
134
+ protein = str(Seq(truncated).translate(table=self.table_id, to_stop=False))
135
+ else:
136
+ protein = ""
137
+
138
+ return protein
139
+
140
+ @classmethod
141
+ def get_available_genetic_codes(cls) -> List[str]:
142
+ """Get list of supported genetic code names"""
143
+ return sorted(cls.GENETIC_CODE_MAP.keys())
@@ -0,0 +1,69 @@
1
+ """
2
+ Output file writers for TransDecoder
3
+ """
4
+
5
+ from typing import List, TextIO
6
+ from pathlib import Path
7
+ from .models import ORF
8
+
9
+
10
+ class GFF3Writer:
11
+ """Write ORFs to GFF3 format"""
12
+
13
+ def __init__(self, filepath: Path):
14
+ self.filepath = filepath
15
+ self.file: TextIO = None
16
+
17
+ def __enter__(self):
18
+ self.file = open(self.filepath, 'w')
19
+ self._write_header()
20
+ return self
21
+
22
+ def __exit__(self, exc_type, exc_val, exc_tb):
23
+ if self.file:
24
+ self.file.close()
25
+
26
+ def _write_header(self):
27
+ """Write GFF3 header"""
28
+ self.file.write("##gff-version 3\n")
29
+
30
+ def write_orf(self, orf: ORF):
31
+ """Write a single ORF"""
32
+ self.file.write(orf.to_gff3() + "\n")
33
+
34
+
35
+ class FastaWriter:
36
+ """Write sequences to FASTA format"""
37
+
38
+ def __init__(self, filepath: Path):
39
+ self.filepath = filepath
40
+ self.file: TextIO = None
41
+
42
+ def __enter__(self):
43
+ self.file = open(self.filepath, 'w')
44
+ return self
45
+
46
+ def __exit__(self, exc_type, exc_val, exc_tb):
47
+ if self.file:
48
+ self.file.close()
49
+
50
+ def write_sequence(self, header: str, sequence: str, wrap: int = 60):
51
+ """
52
+ Write a sequence in FASTA format.
53
+
54
+ Args:
55
+ header: FASTA header (should start with >)
56
+ sequence: Sequence string
57
+ wrap: Line width for wrapping (0 = no wrap)
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')