buttle 0.3.0__cp315-cp315-win_amd64.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.
- buttle/__init__.py +0 -0
- buttle/__main__.py +3 -0
- buttle/buttle_rust.cp315-win_amd64.pyd +0 -0
- buttle/buttle_rust.pyi +38 -0
- buttle/cli.py +565 -0
- buttle/comparisons.py +79 -0
- buttle/constants.py +273 -0
- buttle/decorators.py +30 -0
- buttle/dialogs.py +14 -0
- buttle/fonts/JetBrainsMono-Regular.ttf +0 -0
- buttle/fonts/OFL.txt +93 -0
- buttle/models.py +266 -0
- buttle/msa.py +455 -0
- buttle/pairwise_align.py +219 -0
- buttle/parsers.py +86 -0
- buttle/renderers.py +365 -0
- buttle/sequences.py +128 -0
- buttle-0.3.0.dist-info/METADATA +268 -0
- buttle-0.3.0.dist-info/RECORD +23 -0
- buttle-0.3.0.dist-info/WHEEL +4 -0
- buttle-0.3.0.dist-info/entry_points.txt +2 -0
- buttle-0.3.0.dist-info/licenses/LICENSE +11 -0
- buttle-0.3.0.dist-info/sboms/buttle_rust.cyclonedx.json +537 -0
buttle/__init__.py
ADDED
|
File without changes
|
buttle/__main__.py
ADDED
|
Binary file
|
buttle/buttle_rust.pyi
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
def _align_protein(
|
|
2
|
+
seq1: str,
|
|
3
|
+
seq2: str
|
|
4
|
+
) -> tuple[str, str]:
|
|
5
|
+
"""
|
|
6
|
+
Perform pairwise protein sequence alignment using the Gotoh algorithm.
|
|
7
|
+
|
|
8
|
+
Uses BLOSUM62 substitution scores and affine gap penalties.
|
|
9
|
+
|
|
10
|
+
Implemented in Rust. For use only with validated Protein objects.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
seq1: First protein sequence.
|
|
14
|
+
seq2: Second protein sequence.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
A tuple containing the two aligned protein sequences,
|
|
18
|
+
including gap characters.
|
|
19
|
+
"""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
def _merge(
|
|
23
|
+
left_names:list[str],
|
|
24
|
+
left_sequences: list[str],
|
|
25
|
+
right_names:list[str],
|
|
26
|
+
right_sequences: list[str],
|
|
27
|
+
gap_open:float,
|
|
28
|
+
gap_extend:float,
|
|
29
|
+
) -> tuple[list[str], list[str]]:
|
|
30
|
+
'''
|
|
31
|
+
Globally align two profiles, made up of already aligned sequences, using affine gap penalties.
|
|
32
|
+
|
|
33
|
+
Inputs are two Profiles to merge (separated out into their constituent lists of names and sequences to pass to rust),
|
|
34
|
+
as well as the gap open and gap extend penalties to use. This internal function does not perform input validation.
|
|
35
|
+
|
|
36
|
+
Returns a tuple of a list of sequence names, and a list of sequence alignments.
|
|
37
|
+
'''
|
|
38
|
+
...
|
buttle/cli.py
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import argparse, sys
|
|
2
|
+
lazy from pathlib import Path
|
|
3
|
+
lazy from . import msa as btmsa
|
|
4
|
+
lazy from . import constants as btc
|
|
5
|
+
lazy from . import renderers as btr
|
|
6
|
+
lazy from . import sequences as bts
|
|
7
|
+
lazy from . import comparisons as btcomp
|
|
8
|
+
lazy from . import pairwise_align as bta
|
|
9
|
+
lazy from . import models as btm
|
|
10
|
+
lazy from . import pairwise_align as btpa
|
|
11
|
+
lazy from . import buttle_rust as btrust
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
|
|
16
|
+
# Argument Parser Construction
|
|
17
|
+
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
prog="buttle",
|
|
20
|
+
usage="buttle <command> [options]",
|
|
21
|
+
add_help=False,
|
|
22
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"-h","--help",
|
|
27
|
+
action="store_true"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
subparsers = parser.add_subparsers(
|
|
31
|
+
title="programs",
|
|
32
|
+
metavar=""
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
parser_transcribe = subparsers.add_parser(
|
|
36
|
+
"transcribe",
|
|
37
|
+
help="Transcribe a DNA sequence into RNA."
|
|
38
|
+
)
|
|
39
|
+
parser_transcribe.add_argument(
|
|
40
|
+
"dna",
|
|
41
|
+
help="Provide a DNA sequence to transcribe into RNA."
|
|
42
|
+
)
|
|
43
|
+
parser_transcribe.set_defaults(func=cli_transcribe)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
parser_translate = subparsers.add_parser(
|
|
47
|
+
"translate",
|
|
48
|
+
help="Translate a DNA sequence into protein."
|
|
49
|
+
)
|
|
50
|
+
parser_translate.add_argument(
|
|
51
|
+
"dna",
|
|
52
|
+
help="Provide a DNA sequence to translate into protein."
|
|
53
|
+
)
|
|
54
|
+
parser_translate.set_defaults(func=cli_translate)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
parser_rcomp = subparsers.add_parser(
|
|
58
|
+
"rcomp",
|
|
59
|
+
help="Return the reverse complement of a DNA sequence."
|
|
60
|
+
)
|
|
61
|
+
parser_rcomp.add_argument(
|
|
62
|
+
"dna",
|
|
63
|
+
help="Provide a DNA sequence to receive back the reverse complement sequence."
|
|
64
|
+
)
|
|
65
|
+
parser_rcomp.set_defaults(func=cli_reverse_comp)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
parser_gc = subparsers.add_parser(
|
|
69
|
+
"gc",
|
|
70
|
+
help = "Return the GC content of a DNA sequence."
|
|
71
|
+
)
|
|
72
|
+
parser_gc.add_argument(
|
|
73
|
+
"dna",
|
|
74
|
+
help="Provide a DNA sequence to return the GC content."
|
|
75
|
+
)
|
|
76
|
+
parser_gc.set_defaults(func=cli_gc)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
parser_charge = subparsers.add_parser(
|
|
80
|
+
"charge",
|
|
81
|
+
help="Return the net charge of a protein sequence."
|
|
82
|
+
)
|
|
83
|
+
parser_charge.add_argument(
|
|
84
|
+
"protein",
|
|
85
|
+
help="Provide a protein sequence and receive back the net charge of that protein."
|
|
86
|
+
)
|
|
87
|
+
parser_charge.set_defaults(func=cli_charge)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
parser_mw = subparsers.add_parser(
|
|
91
|
+
"mw",
|
|
92
|
+
help="Return the molecular weight of a protein sequence."
|
|
93
|
+
)
|
|
94
|
+
parser_mw.add_argument(
|
|
95
|
+
"protein",
|
|
96
|
+
help="Provide a protein sequence and receive back the molecular weight of that protein."
|
|
97
|
+
)
|
|
98
|
+
parser_mw.set_defaults(func=cli_mw)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
parser_ec = subparsers.add_parser(
|
|
102
|
+
"ec",
|
|
103
|
+
help="Return the extinction coefficient of a protein sequence."
|
|
104
|
+
)
|
|
105
|
+
parser_ec.add_argument(
|
|
106
|
+
"protein",
|
|
107
|
+
help="Provide a protein sequence and receive back the extinction coefficient of that protein."
|
|
108
|
+
)
|
|
109
|
+
parser_ec.set_defaults(func=cli_ec)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
parser_props = subparsers.add_parser(
|
|
113
|
+
"props",
|
|
114
|
+
help="Return the molecular weight, extinction coefficient, and charge of a protein sequence."
|
|
115
|
+
)
|
|
116
|
+
parser_props.add_argument(
|
|
117
|
+
"protein",
|
|
118
|
+
help="Provide a protein sequence and receive back the extinction coefficient, molecular weight, and charge of that protein."
|
|
119
|
+
)
|
|
120
|
+
parser_props.set_defaults(func=cli_props)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
parser_glycosylation = subparsers.add_parser(
|
|
124
|
+
"glycosylation",
|
|
125
|
+
help = "Returns the protein sequence with N-linked glycosylation sites highlighted, if they are present."
|
|
126
|
+
)
|
|
127
|
+
parser_glycosylation.add_argument(
|
|
128
|
+
"protein",
|
|
129
|
+
help="Provide a protein sequence to locate potential N-linked glycosylation sites."
|
|
130
|
+
)
|
|
131
|
+
parser_glycosylation.set_defaults(func=cli_glycosylation)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
parser_motifs = subparsers.add_parser(
|
|
135
|
+
"motifs",
|
|
136
|
+
help = "Given 2 protein sequences, looks for regions of amino acid similarity."
|
|
137
|
+
)
|
|
138
|
+
parser_motifs.add_argument(
|
|
139
|
+
"s1",
|
|
140
|
+
help="First sequence for comparison."
|
|
141
|
+
)
|
|
142
|
+
parser_motifs.add_argument(
|
|
143
|
+
"s2",
|
|
144
|
+
help="Second sequence for comparison."
|
|
145
|
+
)
|
|
146
|
+
parser_motifs.set_defaults(func=cli_motifs)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
parser_length = subparsers.add_parser(
|
|
150
|
+
"length",
|
|
151
|
+
help = "Given a protein or DNA sequence, returns the length of the sequence."
|
|
152
|
+
)
|
|
153
|
+
parser_length.add_argument(
|
|
154
|
+
"sequence",
|
|
155
|
+
help = "Sequence to determine the length of."
|
|
156
|
+
)
|
|
157
|
+
parser_length.set_defaults(func=cli_length)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
parser_find_pos = subparsers.add_parser(
|
|
161
|
+
"find_pos",
|
|
162
|
+
help = "Given a protein or DNA sequence, and a search string, returns the position of the search string in the sequence, if it exists."
|
|
163
|
+
)
|
|
164
|
+
parser_find_pos.add_argument(
|
|
165
|
+
"sequence",
|
|
166
|
+
help = "Sequence to search in."
|
|
167
|
+
)
|
|
168
|
+
parser_find_pos.add_argument(
|
|
169
|
+
"search_string",
|
|
170
|
+
help = "Sequence to search for."
|
|
171
|
+
)
|
|
172
|
+
parser_find_pos.set_defaults(func=cli_find_pos)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
parser_pairwise_dna = subparsers.add_parser(
|
|
176
|
+
"dna_align",
|
|
177
|
+
help = "Align two given DNA sequences."
|
|
178
|
+
)
|
|
179
|
+
parser_pairwise_dna.add_argument(
|
|
180
|
+
"dna1",
|
|
181
|
+
help = "Sequence 1 for alignment."
|
|
182
|
+
)
|
|
183
|
+
parser_pairwise_dna.add_argument(
|
|
184
|
+
"dna2",
|
|
185
|
+
help = "Sequence 2 for alignment."
|
|
186
|
+
)
|
|
187
|
+
parser_pairwise_dna.add_argument(
|
|
188
|
+
"--save",
|
|
189
|
+
metavar = "FILE",
|
|
190
|
+
help = "Save a PNG image of the alignment with the provided filename."
|
|
191
|
+
)
|
|
192
|
+
parser_pairwise_dna.add_argument(
|
|
193
|
+
"--diff",
|
|
194
|
+
action="store_true",
|
|
195
|
+
help = "This option will output dots for bases that are identical to the first sequence."
|
|
196
|
+
)
|
|
197
|
+
parser_pairwise_dna.add_argument(
|
|
198
|
+
"--s1",
|
|
199
|
+
metavar = "Seq 1 Start",
|
|
200
|
+
type=int,
|
|
201
|
+
default=1,
|
|
202
|
+
help = "The base number that sequence 1 starts at. Defaults to 1."
|
|
203
|
+
)
|
|
204
|
+
parser_pairwise_dna.add_argument(
|
|
205
|
+
"--s2",
|
|
206
|
+
metavar = "Seq 2 Start",
|
|
207
|
+
type=int,
|
|
208
|
+
default=1,
|
|
209
|
+
help = "The base number that sequence 2 starts at. Defaults to 1."
|
|
210
|
+
)
|
|
211
|
+
parser_pairwise_dna.set_defaults(func=cli_dna_align)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
parser_pairwise_protein = subparsers.add_parser(
|
|
215
|
+
"protein_align",
|
|
216
|
+
help = "Align two given protein sequences."
|
|
217
|
+
)
|
|
218
|
+
parser_pairwise_protein.add_argument(
|
|
219
|
+
"seq1",
|
|
220
|
+
help = "Sequence 1 for alignment."
|
|
221
|
+
)
|
|
222
|
+
parser_pairwise_protein.add_argument(
|
|
223
|
+
"seq2",
|
|
224
|
+
help = "Sequence 2 for alignment."
|
|
225
|
+
)
|
|
226
|
+
parser_pairwise_protein.add_argument(
|
|
227
|
+
"--diff",
|
|
228
|
+
action="store_true",
|
|
229
|
+
help = "This option will output dots for bases that are identical to the first sequence."
|
|
230
|
+
)
|
|
231
|
+
parser_pairwise_protein.add_argument(
|
|
232
|
+
"--name1",
|
|
233
|
+
metavar = "NAME",
|
|
234
|
+
help = "The name of sequence 1. Defaults to '1'."
|
|
235
|
+
)
|
|
236
|
+
parser_pairwise_protein.add_argument(
|
|
237
|
+
"--name2",
|
|
238
|
+
metavar = "NAME",
|
|
239
|
+
help = "The name of sequence 2. Defaults to '2'."
|
|
240
|
+
)
|
|
241
|
+
parser_pairwise_protein.add_argument(
|
|
242
|
+
"--wrap",
|
|
243
|
+
metavar = "N",
|
|
244
|
+
type=_positive_int,
|
|
245
|
+
default=300,
|
|
246
|
+
help = "Specfiy how many residues of the alignment are rendered per line (image only). Defaults to 300."
|
|
247
|
+
)
|
|
248
|
+
parser_pairwise_protein.add_argument(
|
|
249
|
+
"--real-ruler",
|
|
250
|
+
nargs="?",
|
|
251
|
+
const = 1,
|
|
252
|
+
type = _positive_int,
|
|
253
|
+
default = None,
|
|
254
|
+
metavar = "START",
|
|
255
|
+
help = "This option will display the ruler based on the amino acid numbers of the reference sequence, starting at START (defaults to 1)."
|
|
256
|
+
)
|
|
257
|
+
parser_pairwise_protein.add_argument(
|
|
258
|
+
"--save",
|
|
259
|
+
nargs="?",
|
|
260
|
+
const=True,
|
|
261
|
+
metavar = "FILE",
|
|
262
|
+
help = "Save alignment to FILE, or choose a filename interactively."
|
|
263
|
+
)
|
|
264
|
+
parser_pairwise_protein.add_argument(
|
|
265
|
+
"--consensus",
|
|
266
|
+
action = "store_true",
|
|
267
|
+
help = "This option will add a consensus sequence to the bottom of the alignment."
|
|
268
|
+
)
|
|
269
|
+
parser_pairwise_protein.add_argument(
|
|
270
|
+
"--backend",
|
|
271
|
+
choices=("python", "rust"),
|
|
272
|
+
default="rust",
|
|
273
|
+
help = "Choose whether to use the rust implementation (compiled, default) or python (much slower for large numbers of sequences.)"
|
|
274
|
+
)
|
|
275
|
+
parser_pairwise_protein.set_defaults(func=cli_protein_align)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
parser_msa = subparsers.add_parser(
|
|
279
|
+
"msa",
|
|
280
|
+
help = "Aligned multiple protein sequences, provided in a fasta file or a fasta formmated string."
|
|
281
|
+
)
|
|
282
|
+
parser_msa.add_argument(
|
|
283
|
+
"file",
|
|
284
|
+
help = "Input fasta file, or a fasta-formatted string."
|
|
285
|
+
)
|
|
286
|
+
parser_msa.add_argument(
|
|
287
|
+
"--diff",
|
|
288
|
+
action = "store_true",
|
|
289
|
+
help = "This option will output dots for amino acids that are identical to the first sequence."
|
|
290
|
+
)
|
|
291
|
+
parser_msa.add_argument(
|
|
292
|
+
"--consensus",
|
|
293
|
+
action = "store_true",
|
|
294
|
+
help = "This option will add a consensus sequence to the bottom of the alignment."
|
|
295
|
+
)
|
|
296
|
+
parser_msa.add_argument(
|
|
297
|
+
"--wrap",
|
|
298
|
+
metavar = "N",
|
|
299
|
+
type = _positive_int,
|
|
300
|
+
default = 300,
|
|
301
|
+
help = "Specify how many residues of the alignment are rendered per line (image only). Defaults to 300."
|
|
302
|
+
)
|
|
303
|
+
parser_msa.add_argument(
|
|
304
|
+
"--real-ruler",
|
|
305
|
+
nargs="?",
|
|
306
|
+
const = 1,
|
|
307
|
+
type = _positive_int,
|
|
308
|
+
default = None,
|
|
309
|
+
metavar = "START",
|
|
310
|
+
help = "This option will display the ruler based on the amino acid numbers of the reference sequence, starting at START (defaults to 1)."
|
|
311
|
+
)
|
|
312
|
+
parser_msa.add_argument(
|
|
313
|
+
"--save",
|
|
314
|
+
nargs="?",
|
|
315
|
+
const=True,
|
|
316
|
+
metavar = "OUTPUT_FILE",
|
|
317
|
+
help = "Save alignment to FILE, or choose a filename interactively."
|
|
318
|
+
)
|
|
319
|
+
parser_msa.add_argument(
|
|
320
|
+
"--backend",
|
|
321
|
+
choices=("python", "rust"),
|
|
322
|
+
default="rust",
|
|
323
|
+
help = "Choose whether to use the rust implementation (compiled, default) or python (much slower for large numbers of sequences.)"
|
|
324
|
+
)
|
|
325
|
+
parser_msa.add_argument(
|
|
326
|
+
"--gap-extend",
|
|
327
|
+
metavar = "N",
|
|
328
|
+
type = _non_positive_int,
|
|
329
|
+
default = -3,
|
|
330
|
+
help = "Specify the profile merge level affine gap extend penalty. Defaults to -3."
|
|
331
|
+
)
|
|
332
|
+
parser_msa.add_argument(
|
|
333
|
+
"--gap-open",
|
|
334
|
+
metavar = "N",
|
|
335
|
+
type = _non_positive_int,
|
|
336
|
+
default = -10,
|
|
337
|
+
help = "Specify the profile merge level affine gap open penalty. Defaults to -10."
|
|
338
|
+
)
|
|
339
|
+
parser_msa.set_defaults(func=cli_msa)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
parser_identity = subparsers.add_parser(
|
|
343
|
+
"identity",
|
|
344
|
+
help = "Returns all pairwise sequence identities from an input FASTA file or FASTA-formatted string."
|
|
345
|
+
)
|
|
346
|
+
parser_identity.add_argument(
|
|
347
|
+
"file",
|
|
348
|
+
help = "Input fasta file, or a fasta-formatted string."
|
|
349
|
+
)
|
|
350
|
+
parser_identity.set_defaults(func=cli_identity)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
args = parser.parse_args()
|
|
354
|
+
|
|
355
|
+
if args.help:
|
|
356
|
+
print(btc.MY_HELP)
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
if hasattr(args, "func"):
|
|
360
|
+
try:
|
|
361
|
+
args.func(args)
|
|
362
|
+
except ValueError as error:
|
|
363
|
+
print(error, file=sys.stderr)
|
|
364
|
+
sys.exit(1)
|
|
365
|
+
else:
|
|
366
|
+
parser.print_help()
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# CLI Function Dispatchers
|
|
370
|
+
|
|
371
|
+
def cli_transcribe(args):
|
|
372
|
+
print(f"{btc.RED_TERM}{bts.transcribe(args.dna)}{btc.RESET}")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def cli_translate(args):
|
|
376
|
+
f1, f2, f3 = bts.translate(args.dna)
|
|
377
|
+
print(f"{btc.RED_TERM}Frame 1: {f1}")
|
|
378
|
+
print(f"Frame 2: {f2}")
|
|
379
|
+
print(f"Frame 3: {f3}{btc.RESET}")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def cli_reverse_comp(args):
|
|
383
|
+
print(f"{btc.RED_TERM}{bts.reverse_comp(args.dna)}{btc.RESET}")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def cli_gc(args):
|
|
387
|
+
length, contents, at_content, gc_content = bts.gc(args.dna)
|
|
388
|
+
print(f"{btc.RED_TERM}Total bases:{btc.RESET} {length}")
|
|
389
|
+
print(f"{btc.RED_TERM}A:{btc.RESET} {contents["A"]}")
|
|
390
|
+
print(f"{btc.RED_TERM}C:{btc.RESET} {contents["C"]}")
|
|
391
|
+
print(f"{btc.RED_TERM}G:{btc.RESET} {contents["G"]}")
|
|
392
|
+
print(f"{btc.RED_TERM}T:{btc.RESET} {contents["T"]}")
|
|
393
|
+
print(f"{btc.RED_TERM}AT Content:{btc.RESET} {at_content:.2f}")
|
|
394
|
+
print(f"{btc.RED_TERM}GC Content:{btc.RESET} {gc_content:.2f}")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def cli_charge(args):
|
|
398
|
+
protein = btm.Protein(name="", sequence=args.protein)
|
|
399
|
+
print(f"{btc.RED_TERM}Charge = {protein.charge()}{btc.RESET}")
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def cli_mw(args):
|
|
403
|
+
protein = btm.Protein(name="", sequence=args.protein)
|
|
404
|
+
print(f"{btc.RED_TERM}Molecular Weight = {protein.mw():.1f}{btc.RESET}")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def cli_ec(args):
|
|
408
|
+
protein = btm.Protein(name="", sequence=args.protein)
|
|
409
|
+
print(f"{btc.RED_TERM}Extinction Coefficient = {protein.ec()}{btc.RESET}")
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def cli_props(args):
|
|
413
|
+
protein = btm.Protein(name="", sequence=args.protein)
|
|
414
|
+
print(f"{btc.RED_TERM}Molecular Weight = {protein.mw():.1f}{btc.RESET}")
|
|
415
|
+
print(f"{btc.RED_TERM}Charge = {protein.charge()}{btc.RESET}")
|
|
416
|
+
print(f"{btc.RED_TERM}Extinction Coefficient = {protein.ec()}{btc.RESET}")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def cli_glycosylation(args):
|
|
420
|
+
protein = btm.Protein(name="", sequence=args.protein)
|
|
421
|
+
sites = protein.glycosylation()
|
|
422
|
+
if len(sites) == 0:
|
|
423
|
+
print("No N-linked glycosylation sites found.")
|
|
424
|
+
else:
|
|
425
|
+
for i, aa in enumerate(args.protein):
|
|
426
|
+
aa = aa.upper()
|
|
427
|
+
if i not in sites and i-1 not in sites and i-2 not in sites:
|
|
428
|
+
print(aa, end="")
|
|
429
|
+
else:
|
|
430
|
+
print(f"{btc.RED_TERM}{aa}{btc.RESET}", end="")
|
|
431
|
+
print()
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def cli_motifs(args):
|
|
435
|
+
protein1 = btm.Protein(name="", sequence=args.s1)
|
|
436
|
+
protein2 = btm.Protein(name="", sequence=args.s2)
|
|
437
|
+
similars = btcomp.motifs(protein1, protein2)
|
|
438
|
+
if not similars:
|
|
439
|
+
print("No significant similarity found.")
|
|
440
|
+
for key in similars:
|
|
441
|
+
print(f"{btc.RED_TERM}Peptide size = {key[0]}. Threshold of similarity = {key[1]}.{btc.RESET}")
|
|
442
|
+
for ((peptide1, start1, end1), (peptide2, start2, end2), score) in similars[key]:
|
|
443
|
+
print(f"{start1}-{peptide1}-{end1}")
|
|
444
|
+
print(f"{start2}-{peptide2}-{end2}")
|
|
445
|
+
print(f"Score: {score}/{key[0]} ({score/key[0]:.1%})")
|
|
446
|
+
print()
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def cli_length(args):
|
|
450
|
+
print(f"Length: {btc.RED_TERM}{bts.length(args.sequence)}{btc.RESET}")
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def cli_find_pos(args):
|
|
454
|
+
hits = bts.find_pos(args.sequence, args.search_string)
|
|
455
|
+
if not hits:
|
|
456
|
+
print("Not found")
|
|
457
|
+
else:
|
|
458
|
+
print("Found!")
|
|
459
|
+
for hit in hits:
|
|
460
|
+
print(f"At position: {btc.RED_TERM}{hit}{btc.RED_TERM}")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def cli_dna_align(args):
|
|
464
|
+
align1, align2 = bta.pairwise_dna(args.dna1, args.dna2)
|
|
465
|
+
btr.print_dna_pairwise_alignment([align1, align2], args.diff)
|
|
466
|
+
|
|
467
|
+
if args.save:
|
|
468
|
+
btr.save_dna_pairwise_alignment([align1, align2], filename=args.save, start1=args.s1, start2=args.s2, diff=args.diff)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def cli_protein_align(args):
|
|
472
|
+
protein1 = btm.Protein("", args.seq1, allow_x=True)
|
|
473
|
+
protein2 = btm.Protein("", args.seq2, allow_x=True)
|
|
474
|
+
|
|
475
|
+
if args.backend == "rust":
|
|
476
|
+
align1, align2 = btrust._align_protein(protein1.sequence, protein2.sequence)
|
|
477
|
+
align1 = list(align1)
|
|
478
|
+
align2 = list(align2)
|
|
479
|
+
else:
|
|
480
|
+
align1, align2 = btpa.pairwise_protein(protein1, protein2)
|
|
481
|
+
|
|
482
|
+
profile = btm.Profile(
|
|
483
|
+
name = [args.name1 or "1", args.name2 or "2"],
|
|
484
|
+
alignment = [align1, align2]
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
if args.consensus:
|
|
488
|
+
profile = btmsa.add_consensus(profile)
|
|
489
|
+
consensus = True
|
|
490
|
+
else:
|
|
491
|
+
consensus = False
|
|
492
|
+
|
|
493
|
+
_output_protein_alignment(profile, args, consensus)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def cli_msa(args):
|
|
497
|
+
profile = btmsa.msa(args.file, args.backend, int(args.gap_open), int(args.gap_extend))
|
|
498
|
+
if args.consensus:
|
|
499
|
+
profile = btmsa.add_consensus(profile)
|
|
500
|
+
consensus = True
|
|
501
|
+
else:
|
|
502
|
+
consensus = False
|
|
503
|
+
_output_protein_alignment(profile, args, consensus)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def cli_identity(args):
|
|
507
|
+
identities = btcomp.identity(args.file)
|
|
508
|
+
for key in identities:
|
|
509
|
+
print(key, f"{identities[key]:.3f}")
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _output_protein_alignment(profile:btm.Profile, args, consensus:bool):
|
|
513
|
+
if args.save:
|
|
514
|
+
filename = args.save
|
|
515
|
+
|
|
516
|
+
# If no filename provided, use dialog window to choose location and filename.
|
|
517
|
+
if filename is True:
|
|
518
|
+
from . import dialogs as btdi
|
|
519
|
+
filename = btdi.get_save_filename()
|
|
520
|
+
if not filename:
|
|
521
|
+
print("Save cancelled.")
|
|
522
|
+
return
|
|
523
|
+
|
|
524
|
+
# Determine the requested image format
|
|
525
|
+
suffix = Path(filename).suffix.lower()
|
|
526
|
+
if suffix == ".png":
|
|
527
|
+
scale = 1
|
|
528
|
+
elif suffix in {".tif", ".tiff"}:
|
|
529
|
+
scale = 4
|
|
530
|
+
else:
|
|
531
|
+
raise ValueError(f"Unsupported image format {suffix}")
|
|
532
|
+
|
|
533
|
+
# Render the alignment properly, then save it as an image
|
|
534
|
+
image = btr.render_protein_alignment_image(profile, scale, wrap=args.wrap, start=args.real_ruler, consensus=consensus, diff=args.diff)
|
|
535
|
+
btr.save_protein_alignment_image(image, suffix, filename=filename)
|
|
536
|
+
|
|
537
|
+
else:
|
|
538
|
+
|
|
539
|
+
# Terminal output
|
|
540
|
+
btr.print_protein_alignment(profile, consensus, args.diff)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _positive_int(value:str):
|
|
544
|
+
try:
|
|
545
|
+
value = int(value)
|
|
546
|
+
if value < 1:
|
|
547
|
+
raise ValueError
|
|
548
|
+
except ValueError:
|
|
549
|
+
raise argparse.ArgumentTypeError("Ruler start must be a positive integer.")
|
|
550
|
+
return value
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _non_positive_int(value:str):
|
|
554
|
+
try:
|
|
555
|
+
value = int(value)
|
|
556
|
+
if value > 0:
|
|
557
|
+
raise ValueError
|
|
558
|
+
except ValueError:
|
|
559
|
+
raise argparse.ArgumentTypeError("Gap penalty must be a non positive integer.")
|
|
560
|
+
return value
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
if __name__ == "__main__":
|
|
565
|
+
main()
|
buttle/comparisons.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
lazy from collections import defaultdict
|
|
2
|
+
lazy from itertools import combinations
|
|
3
|
+
lazy from pathlib import Path
|
|
4
|
+
lazy from . import constants as btc
|
|
5
|
+
lazy from . import models as btm
|
|
6
|
+
lazy from . import pairwise_align as btpa
|
|
7
|
+
lazy from . import parsers as btp
|
|
8
|
+
lazy from . import buttle_rust as btrust
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def motifs(protein1:btm.Protein, protein2:btm.Protein) -> dict:
|
|
12
|
+
'''
|
|
13
|
+
Given two protein sequences, this method looks for similar stretches fo amino acids, ranging from length 4 to length 40.
|
|
14
|
+
|
|
15
|
+
Matches count as +1, while conservative substitutions count for +0.5. Any two peptides where total_score / length > threshold is considered a match.
|
|
16
|
+
|
|
17
|
+
Threshold is more stringent for shorter peptides, and more lenient for longer peptides. Defined in constant THRESHOLD.
|
|
18
|
+
'''
|
|
19
|
+
|
|
20
|
+
# Determine shortest sequence and set that as seq1
|
|
21
|
+
length1 = len(protein1.sequence)
|
|
22
|
+
length2 = len(protein2.sequence)
|
|
23
|
+
|
|
24
|
+
if length1 > length2:
|
|
25
|
+
seq1 = protein2.sequence
|
|
26
|
+
seq2 = protein1.sequence
|
|
27
|
+
else:
|
|
28
|
+
seq1 = protein1.sequence
|
|
29
|
+
seq2 = protein2.sequence
|
|
30
|
+
|
|
31
|
+
# Initialize the dictionary that will hold matches
|
|
32
|
+
similars = defaultdict(list)
|
|
33
|
+
|
|
34
|
+
# Loop that looks at peptides of length 4 to 40 in the two sequences, compares them, and looks for regions of similarity.
|
|
35
|
+
for size in range(4, min(40, length1, length2)+1):
|
|
36
|
+
|
|
37
|
+
for min_size, max_size, cutoff in btc.THRESHOLD:
|
|
38
|
+
if min_size <= size <= max_size:
|
|
39
|
+
threshold = size * cutoff
|
|
40
|
+
break
|
|
41
|
+
else:
|
|
42
|
+
raise ValueError(f"No threshold defined for motif size {size}.")
|
|
43
|
+
|
|
44
|
+
for n in range(0, len(seq1) - size + 1):
|
|
45
|
+
peptide1 = seq1[n:n+size]
|
|
46
|
+
for x in range(0, len(seq2) - size + 1):
|
|
47
|
+
count = 0
|
|
48
|
+
peptide2 = seq2[x:x+size]
|
|
49
|
+
for z in range(size):
|
|
50
|
+
if peptide1[z] == peptide2[z]:
|
|
51
|
+
count += 1
|
|
52
|
+
continue
|
|
53
|
+
if peptide2[z] in btc.CONSERVATIVES[peptide1[z]]:
|
|
54
|
+
count += 0.5
|
|
55
|
+
if count >= threshold:
|
|
56
|
+
similars[(size, threshold)].append(((peptide1, n+1, n + size), (peptide2, x+1, x + size), count))
|
|
57
|
+
|
|
58
|
+
return similars
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def identity(
|
|
62
|
+
file:str
|
|
63
|
+
) -> dict[tuple[str,str], float]:
|
|
64
|
+
|
|
65
|
+
if Path(file).exists():
|
|
66
|
+
sequences = btp.parse_fasta_file(file)
|
|
67
|
+
else:
|
|
68
|
+
sequences = btp.parse_fasta_string(file)
|
|
69
|
+
|
|
70
|
+
identities = {}
|
|
71
|
+
|
|
72
|
+
for protein1, protein2 in combinations(sequences, 2):
|
|
73
|
+
align1, align2 = btrust._align_protein(protein1.sequence, protein2.sequence)
|
|
74
|
+
length = len(align1)
|
|
75
|
+
identical = sum(1 for aa1, aa2 in zip(align1, align2, strict=True) if aa1==aa2)
|
|
76
|
+
percent_identity = identical/length
|
|
77
|
+
identities[(protein1.name, protein2.name)] = percent_identity
|
|
78
|
+
|
|
79
|
+
return identities
|