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.
- pytransdecoder/__init__.py +7 -0
- pytransdecoder/__main__.py +6 -0
- pytransdecoder/cli.py +781 -0
- pytransdecoder/core/__init__.py +1 -0
- pytransdecoder/core/base_freqs.py +71 -0
- pytransdecoder/core/decoys.py +113 -0
- pytransdecoder/core/fasta.py +55 -0
- pytransdecoder/core/gene_model.py +143 -0
- pytransdecoder/core/gff3_parser.py +58 -0
- pytransdecoder/core/homology_starts.py +54 -0
- pytransdecoder/core/kozak.py +180 -0
- pytransdecoder/core/models.py +123 -0
- pytransdecoder/core/orf_finder.py +317 -0
- pytransdecoder/core/presets.py +152 -0
- pytransdecoder/core/prng.py +54 -0
- pytransdecoder/core/pwm.py +174 -0
- pytransdecoder/core/sequence.py +19 -0
- pytransdecoder/core/translator.py +143 -0
- pytransdecoder/core/writers.py +69 -0
- pytransdecoder/genome.py +338 -0
- pytransdecoder/longorfs.py +180 -0
- pytransdecoder/predict.py +1621 -0
- pytransdecoder/presets/apicomplexan/hexamer.scores.gz +0 -0
- pytransdecoder/presets/apicomplexan/kozak.tsv +9 -0
- pytransdecoder/presets/apicomplexan/preset.json +20 -0
- pytransdecoder/presets/apicomplexan/thresholds.tsv.gz +0 -0
- pytransdecoder/presets/apicomplexan/training_orfs.cds.gz +0 -0
- pytransdecoder/presets/ciliate/hexamer.scores.gz +0 -0
- pytransdecoder/presets/ciliate/kozak.tsv +9 -0
- pytransdecoder/presets/ciliate/preset.json +20 -0
- pytransdecoder/presets/ciliate/thresholds.tsv.gz +0 -0
- pytransdecoder/presets/ciliate/training_orfs.cds.gz +0 -0
- pytransdecoder/presets/fungus/hexamer.scores.gz +0 -0
- pytransdecoder/presets/fungus/kozak.tsv +9 -0
- pytransdecoder/presets/fungus/preset.json +20 -0
- pytransdecoder/presets/fungus/thresholds.tsv.gz +0 -0
- pytransdecoder/presets/fungus/training_orfs.cds.gz +0 -0
- pytransdecoder/presets/green_alga/hexamer.scores.gz +0 -0
- pytransdecoder/presets/green_alga/kozak.tsv +9 -0
- pytransdecoder/presets/green_alga/preset.json +20 -0
- pytransdecoder/presets/green_alga/thresholds.tsv.gz +0 -0
- pytransdecoder/presets/green_alga/training_orfs.cds.gz +0 -0
- pytransdecoder/presets/insect/hexamer.scores.gz +0 -0
- pytransdecoder/presets/insect/kozak.tsv +9 -0
- pytransdecoder/presets/insect/preset.json +20 -0
- pytransdecoder/presets/insect/thresholds.tsv.gz +0 -0
- pytransdecoder/presets/insect/training_orfs.cds.gz +0 -0
- pytransdecoder/presets/mammal/hexamer.scores.gz +0 -0
- pytransdecoder/presets/mammal/kozak.tsv +9 -0
- pytransdecoder/presets/mammal/preset.json +20 -0
- pytransdecoder/presets/mammal/thresholds.tsv.gz +0 -0
- pytransdecoder/presets/mammal/training_orfs.cds.gz +0 -0
- pytransdecoder/presets/plant/hexamer.scores.gz +0 -0
- pytransdecoder/presets/plant/kozak.tsv +9 -0
- pytransdecoder/presets/plant/preset.json +20 -0
- pytransdecoder/presets/plant/thresholds.tsv.gz +0 -0
- pytransdecoder/presets/plant/training_orfs.cds.gz +0 -0
- pytransdecoder-0.2.1.dist-info/METADATA +307 -0
- pytransdecoder-0.2.1.dist-info/RECORD +63 -0
- pytransdecoder-0.2.1.dist-info/WHEEL +5 -0
- pytransdecoder-0.2.1.dist-info/entry_points.txt +3 -0
- pytransdecoder-0.2.1.dist-info/licenses/LICENSE +28 -0
- 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')
|