njswalig 0.1.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.
- njswalig-0.1.0/.gitignore +1 -0
- njswalig-0.1.0/LICENSE +21 -0
- njswalig-0.1.0/PKG-INFO +117 -0
- njswalig-0.1.0/README.md +94 -0
- njswalig-0.1.0/pyproject.toml +37 -0
- njswalig-0.1.0/src/njswalig/__init__.py +186 -0
- njswalig-0.1.0/tests/test_njswalig.py +137 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dist/
|
njswalig-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jphil101
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
njswalig-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: njswalig
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Needleman-Wunsch and Smith-Waterman pairwise sequence alignment, with the DP table
|
|
5
|
+
Project-URL: Homepage, https://github.com/jphil101/BioinfoPrep/tree/main/njswalig
|
|
6
|
+
Project-URL: Source, https://github.com/jphil101/BioinfoPrep/tree/main/njswalig
|
|
7
|
+
Project-URL: Issues, https://github.com/jphil101/BioinfoPrep/issues
|
|
8
|
+
Author: jphil101
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: bioinformatics,dynamic programming,needleman-wunsch,sequence alignment,smith-waterman
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: biopython; extra == 'test'
|
|
21
|
+
Requires-Dist: pytest; extra == 'test'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# njswalig
|
|
25
|
+
|
|
26
|
+
Needleman–Wunsch (global) and Smith–Waterman (local) pairwise sequence alignment in plain Python,
|
|
27
|
+
with no dependencies. It prints the alignment and, if you ask, the filled-in dynamic-programming score
|
|
28
|
+
table.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install njswalig
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from njswalig import njalign # Needleman-Wunsch (global)
|
|
38
|
+
from njswalig import swalign # Smith-Waterman (local)
|
|
39
|
+
|
|
40
|
+
seqs = ["ATCG", "TTTA"] # any number of sequences, any lengths
|
|
41
|
+
scores = [2, -1, -2] # [match, mismatch, gap] -- always this order
|
|
42
|
+
|
|
43
|
+
alignment = njalign.sequences(seqs, scores) # or njalign.sequence(...) -- same thing
|
|
44
|
+
alignment.show()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Needleman-Wunsch (global): Seq1 vs Seq2, score = -1
|
|
49
|
+
Seq1 ATCG
|
|
50
|
+
.|..
|
|
51
|
+
Seq2 TTTA
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`|` marks a match, `.` a mismatch and a space a gap.
|
|
55
|
+
|
|
56
|
+
### Smith–Waterman
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
swalign.sequences(["TGTTACGG", "GGTTGACTA"], [3, -3, -2]).show()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Smith-Waterman (local): Seq1 vs Seq2, score = 13
|
|
64
|
+
Seq1 2 GTT-AC 6
|
|
65
|
+
||| ||
|
|
66
|
+
Seq2 2 GTTGAC 7
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The numbers show where the aligned region starts and ends in each sequence (1-based).
|
|
70
|
+
|
|
71
|
+
### More than two sequences
|
|
72
|
+
|
|
73
|
+
Every pair is aligned. Sequences are named `Seq1`, `Seq2`, … in list order, or pass your own:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
njalign.sequences(["GATTACA", "GCATGCU", "GATCA"], [1, -1, -1], names=["a", "b", "c"]).show()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### The DP score table
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
swalign.sequences(["ATCG", "TTTA"], [2, -1, -2]).show(matrix=True)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
- T T T A
|
|
87
|
+
- 0 0 0 0 0
|
|
88
|
+
A 0 0 0 0 2
|
|
89
|
+
T 0 2 2 2 0
|
|
90
|
+
C 0 0 1 1 1
|
|
91
|
+
G 0 0 0 0 0
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Using the results in code
|
|
95
|
+
|
|
96
|
+
`sequences(...)` returns a list of pair alignments. Each has `score`, `aligned1`, `aligned2`,
|
|
97
|
+
`match_line`, `name1`, `name2`, `seq1`, `seq2`, `matrix` (the DP table as a list of lists) and
|
|
98
|
+
1-based `start1`, `end1`, `start2`, `end2`.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
best = njalign.sequences(["GATTACA", "GCATGCU"], [1, -1, -1])[0]
|
|
102
|
+
print(best.score, best.aligned1, best.aligned2) # 0 G-ATTACA GCA-TGCU
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Details
|
|
106
|
+
|
|
107
|
+
- **Scoring** is linear: every gap position costs `gap`. `scores` must be a list of exactly three
|
|
108
|
+
numbers, `[match, mismatch, gap]`; anything else raises `ValueError`.
|
|
109
|
+
- **Sequences** are stripped of surrounding spaces and upper-cased, so `"acgt "` equals `"ACGT"`.
|
|
110
|
+
- **Ties:** when several alignments share the best score, one is returned. The traceback prefers a
|
|
111
|
+
diagonal step (match/mismatch), then a gap in the second sequence, then a gap in the first. Smith–Waterman
|
|
112
|
+
starts from the first highest cell, scanning row by row.
|
|
113
|
+
- **No local match:** when nothing scores above 0, Smith–Waterman reports an empty alignment with score 0.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
njswalig-0.1.0/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# njswalig
|
|
2
|
+
|
|
3
|
+
Needleman–Wunsch (global) and Smith–Waterman (local) pairwise sequence alignment in plain Python,
|
|
4
|
+
with no dependencies. It prints the alignment and, if you ask, the filled-in dynamic-programming score
|
|
5
|
+
table.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install njswalig
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from njswalig import njalign # Needleman-Wunsch (global)
|
|
15
|
+
from njswalig import swalign # Smith-Waterman (local)
|
|
16
|
+
|
|
17
|
+
seqs = ["ATCG", "TTTA"] # any number of sequences, any lengths
|
|
18
|
+
scores = [2, -1, -2] # [match, mismatch, gap] -- always this order
|
|
19
|
+
|
|
20
|
+
alignment = njalign.sequences(seqs, scores) # or njalign.sequence(...) -- same thing
|
|
21
|
+
alignment.show()
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Needleman-Wunsch (global): Seq1 vs Seq2, score = -1
|
|
26
|
+
Seq1 ATCG
|
|
27
|
+
.|..
|
|
28
|
+
Seq2 TTTA
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`|` marks a match, `.` a mismatch and a space a gap.
|
|
32
|
+
|
|
33
|
+
### Smith–Waterman
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
swalign.sequences(["TGTTACGG", "GGTTGACTA"], [3, -3, -2]).show()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Smith-Waterman (local): Seq1 vs Seq2, score = 13
|
|
41
|
+
Seq1 2 GTT-AC 6
|
|
42
|
+
||| ||
|
|
43
|
+
Seq2 2 GTTGAC 7
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The numbers show where the aligned region starts and ends in each sequence (1-based).
|
|
47
|
+
|
|
48
|
+
### More than two sequences
|
|
49
|
+
|
|
50
|
+
Every pair is aligned. Sequences are named `Seq1`, `Seq2`, … in list order, or pass your own:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
njalign.sequences(["GATTACA", "GCATGCU", "GATCA"], [1, -1, -1], names=["a", "b", "c"]).show()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### The DP score table
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
swalign.sequences(["ATCG", "TTTA"], [2, -1, -2]).show(matrix=True)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
- T T T A
|
|
64
|
+
- 0 0 0 0 0
|
|
65
|
+
A 0 0 0 0 2
|
|
66
|
+
T 0 2 2 2 0
|
|
67
|
+
C 0 0 1 1 1
|
|
68
|
+
G 0 0 0 0 0
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Using the results in code
|
|
72
|
+
|
|
73
|
+
`sequences(...)` returns a list of pair alignments. Each has `score`, `aligned1`, `aligned2`,
|
|
74
|
+
`match_line`, `name1`, `name2`, `seq1`, `seq2`, `matrix` (the DP table as a list of lists) and
|
|
75
|
+
1-based `start1`, `end1`, `start2`, `end2`.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
best = njalign.sequences(["GATTACA", "GCATGCU"], [1, -1, -1])[0]
|
|
79
|
+
print(best.score, best.aligned1, best.aligned2) # 0 G-ATTACA GCA-TGCU
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Details
|
|
83
|
+
|
|
84
|
+
- **Scoring** is linear: every gap position costs `gap`. `scores` must be a list of exactly three
|
|
85
|
+
numbers, `[match, mismatch, gap]`; anything else raises `ValueError`.
|
|
86
|
+
- **Sequences** are stripped of surrounding spaces and upper-cased, so `"acgt "` equals `"ACGT"`.
|
|
87
|
+
- **Ties:** when several alignments share the best score, one is returned. The traceback prefers a
|
|
88
|
+
diagonal step (match/mismatch), then a gap in the second sequence, then a gap in the first. Smith–Waterman
|
|
89
|
+
starts from the first highest cell, scanning row by row.
|
|
90
|
+
- **No local match:** when nothing scores above 0, Smith–Waterman reports an empty alignment with score 0.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "njswalig"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Needleman-Wunsch and Smith-Waterman pairwise sequence alignment, with the DP table"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [{ name = "jphil101" }]
|
|
14
|
+
keywords = ["sequence alignment", "needleman-wunsch", "smith-waterman", "dynamic programming", "bioinformatics"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Education",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
test = ["pytest", "biopython"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/jphil101/BioinfoPrep/tree/main/njswalig"
|
|
30
|
+
Source = "https://github.com/jphil101/BioinfoPrep/tree/main/njswalig"
|
|
31
|
+
Issues = "https://github.com/jphil101/BioinfoPrep/issues"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.version]
|
|
34
|
+
path = "src/njswalig/__init__.py"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.sdist]
|
|
37
|
+
include = ["src", "tests", "README.md", "LICENSE"]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""njswalig -- Needleman-Wunsch (global) and Smith-Waterman (local) pairwise alignment.
|
|
2
|
+
|
|
3
|
+
>>> from njswalig import njalign, swalign
|
|
4
|
+
>>> seqs = ["ATCG", "TTTA"]
|
|
5
|
+
>>> scores = [2, -1, -2] # [match, mismatch, gap]
|
|
6
|
+
>>> alignment = njalign.sequences(seqs, scores)
|
|
7
|
+
>>> alignment.show()
|
|
8
|
+
Needleman-Wunsch (global): Seq1 vs Seq2, score = -1
|
|
9
|
+
Seq1 ATCG
|
|
10
|
+
.|..
|
|
11
|
+
Seq2 TTTA
|
|
12
|
+
|
|
13
|
+
Every pair of sequences in the list is aligned. ``sequence`` is the same as ``sequences``.
|
|
14
|
+
Scoring is linear: each gap position costs ``gap``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from itertools import combinations
|
|
18
|
+
|
|
19
|
+
__all__ = ["njalign", "swalign", "Aligner", "Alignments", "PairAlignment"]
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
_MODES = {"global": "Needleman-Wunsch (global)", "local": "Smith-Waterman (local)"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PairAlignment:
|
|
26
|
+
"""The best alignment of one pair of sequences.
|
|
27
|
+
|
|
28
|
+
Attributes: ``name1``, ``name2``, ``seq1``, ``seq2``, ``aligned1``, ``aligned2`` (with ``-``
|
|
29
|
+
for gaps), ``score``, ``mode`` ("global"/"local"), ``matrix`` (the filled DP score table,
|
|
30
|
+
``len(seq1)+1`` rows by ``len(seq2)+1`` columns), and 1-based ``start1``/``end1``,
|
|
31
|
+
``start2``/``end2`` giving the aligned region of each sequence.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, mode, name1, name2, seq1, seq2, aligned1, aligned2, score, matrix, start, end):
|
|
35
|
+
self.mode, self.name1, self.name2, self.seq1, self.seq2 = mode, name1, name2, seq1, seq2
|
|
36
|
+
self.aligned1, self.aligned2, self.score, self.matrix = aligned1, aligned2, score, matrix
|
|
37
|
+
self.start1, self.start2 = start[0] + 1, start[1] + 1
|
|
38
|
+
self.end1, self.end2 = end
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def match_line(self):
|
|
42
|
+
"""``|`` match, ``.`` mismatch, space for a gap."""
|
|
43
|
+
return "".join(" " if "-" in (x, y) else "|" if x == y else "." for x, y in zip(self.aligned1, self.aligned2))
|
|
44
|
+
|
|
45
|
+
def __str__(self):
|
|
46
|
+
lines = [f"{_MODES[self.mode]}: {self.name1} vs {self.name2}, score = {_num(self.score)}"]
|
|
47
|
+
if not self.aligned1:
|
|
48
|
+
return lines[0] + "\n(no positive-scoring local alignment)"
|
|
49
|
+
w = max(len(self.name1), len(self.name2))
|
|
50
|
+
if self.mode == "global":
|
|
51
|
+
lines += [f"{self.name1:<{w}} {self.aligned1}",
|
|
52
|
+
f"{'':<{w}} {self.match_line}",
|
|
53
|
+
f"{self.name2:<{w}} {self.aligned2}"]
|
|
54
|
+
else: # local: show where the region sits in each sequence
|
|
55
|
+
p = len(str(max(self.start1, self.start2)))
|
|
56
|
+
lines += [f"{self.name1:<{w}} {self.start1:>{p}} {self.aligned1} {self.end1}",
|
|
57
|
+
f"{'':<{w}} {'':>{p}} {self.match_line}",
|
|
58
|
+
f"{self.name2:<{w}} {self.start2:>{p}} {self.aligned2} {self.end2}"]
|
|
59
|
+
return "\n".join(lines)
|
|
60
|
+
|
|
61
|
+
def matrix_str(self):
|
|
62
|
+
"""The DP score table with sequence letters along the edges."""
|
|
63
|
+
cells = [[_num(v) for v in row] for row in self.matrix]
|
|
64
|
+
w = max(len(c) for row in cells for c in row)
|
|
65
|
+
head = " " + " ".join(f"{c:>{w}}" for c in "-" + self.seq2)
|
|
66
|
+
return "\n".join([head] + [f"{r} " + " ".join(f"{c:>{w}}" for c in row)
|
|
67
|
+
for r, row in zip("-" + self.seq1, cells)])
|
|
68
|
+
|
|
69
|
+
def show(self, matrix=False):
|
|
70
|
+
"""Print the alignment (and the DP score table if ``matrix=True``)."""
|
|
71
|
+
print(self)
|
|
72
|
+
if matrix:
|
|
73
|
+
print(self.matrix_str())
|
|
74
|
+
|
|
75
|
+
def __repr__(self):
|
|
76
|
+
return f"<PairAlignment {self.mode} {self.name1} vs {self.name2} score={_num(self.score)}>"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Alignments(list):
|
|
80
|
+
"""A list of :class:`PairAlignment`, one per pair of input sequences."""
|
|
81
|
+
|
|
82
|
+
def show(self, matrix=False):
|
|
83
|
+
"""Print every alignment (and DP score tables if ``matrix=True``), separated by blank lines."""
|
|
84
|
+
for i, pair in enumerate(self):
|
|
85
|
+
if i:
|
|
86
|
+
print()
|
|
87
|
+
pair.show(matrix=matrix)
|
|
88
|
+
|
|
89
|
+
def __str__(self):
|
|
90
|
+
return "\n\n".join(map(str, self))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Aligner:
|
|
94
|
+
"""``Aligner("global")`` is Needleman-Wunsch; ``Aligner("local")`` is Smith-Waterman."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, mode):
|
|
97
|
+
if mode not in _MODES:
|
|
98
|
+
raise ValueError(f"mode must be 'global' or 'local', got {mode!r}")
|
|
99
|
+
self.mode = mode
|
|
100
|
+
|
|
101
|
+
def sequences(self, seqs, scores, names=None):
|
|
102
|
+
"""Align every pair in ``seqs`` using ``scores = [match, mismatch, gap]``.
|
|
103
|
+
|
|
104
|
+
Sequences are upper-cased and stripped of surrounding whitespace. Names default to
|
|
105
|
+
Seq1, Seq2, ... in list order. Returns :class:`Alignments` (call ``.show()`` to print).
|
|
106
|
+
"""
|
|
107
|
+
seqs, names = _check_seqs(seqs, names)
|
|
108
|
+
match, mismatch, gap = _check_scores(scores)
|
|
109
|
+
return Alignments(_align(self.mode, names[i], names[j], seqs[i], seqs[j], match, mismatch, gap)
|
|
110
|
+
for i, j in combinations(range(len(seqs)), 2))
|
|
111
|
+
|
|
112
|
+
sequence = sequences # same thing, either name works
|
|
113
|
+
|
|
114
|
+
def __repr__(self):
|
|
115
|
+
return f"<Aligner {_MODES[self.mode]}>"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
njalign = Aligner("global")
|
|
119
|
+
swalign = Aligner("local")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _align(mode, name1, name2, a, b, match, mismatch, gap):
|
|
123
|
+
local = mode == "local"
|
|
124
|
+
n, m = len(a), len(b)
|
|
125
|
+
S = [[0] * (m + 1) for _ in range(n + 1)]
|
|
126
|
+
if not local: # global: leading gaps cost gap each
|
|
127
|
+
for i in range(1, n + 1):
|
|
128
|
+
S[i][0] = i * gap
|
|
129
|
+
for j in range(1, m + 1):
|
|
130
|
+
S[0][j] = j * gap
|
|
131
|
+
|
|
132
|
+
def pair_score(i, j):
|
|
133
|
+
return match if a[i - 1] == b[j - 1] else mismatch
|
|
134
|
+
|
|
135
|
+
best = (0, 0, 0) # local: (score, i, j) of the highest cell
|
|
136
|
+
for i in range(1, n + 1):
|
|
137
|
+
for j in range(1, m + 1):
|
|
138
|
+
v = max(S[i - 1][j - 1] + pair_score(i, j), S[i - 1][j] + gap, S[i][j - 1] + gap)
|
|
139
|
+
if local:
|
|
140
|
+
v = max(v, 0)
|
|
141
|
+
if v > best[0]:
|
|
142
|
+
best = (v, i, j)
|
|
143
|
+
S[i][j] = v
|
|
144
|
+
|
|
145
|
+
i, j = (best[1], best[2]) if local else (n, m)
|
|
146
|
+
end, score = (i, j), (best[0] if local else S[n][m])
|
|
147
|
+
out1, out2 = [], []
|
|
148
|
+
while i > 0 or j > 0: # traceback, preferring diagonal, then up, then left
|
|
149
|
+
if local and S[i][j] == 0:
|
|
150
|
+
break
|
|
151
|
+
if i > 0 and j > 0 and _same(S[i][j], S[i - 1][j - 1] + pair_score(i, j)):
|
|
152
|
+
out1.append(a[i - 1]); out2.append(b[j - 1]); i -= 1; j -= 1
|
|
153
|
+
elif i > 0 and _same(S[i][j], S[i - 1][j] + gap):
|
|
154
|
+
out1.append(a[i - 1]); out2.append("-"); i -= 1
|
|
155
|
+
else:
|
|
156
|
+
out1.append("-"); out2.append(b[j - 1]); j -= 1
|
|
157
|
+
return PairAlignment(mode, name1, name2, a, b, "".join(reversed(out1)), "".join(reversed(out2)),
|
|
158
|
+
score, S, (i, j), end)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _same(x, y):
|
|
162
|
+
return abs(x - y) < 1e-9
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _num(x):
|
|
166
|
+
return str(int(x)) if float(x).is_integer() else f"{x:g}"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _check_scores(scores):
|
|
170
|
+
ok = (isinstance(scores, (list, tuple)) and len(scores) == 3
|
|
171
|
+
and all(isinstance(x, (int, float)) and not isinstance(x, bool) for x in scores))
|
|
172
|
+
if not ok:
|
|
173
|
+
raise ValueError(f"scores must be [match, mismatch, gap], e.g. [2, -1, -2]; got {scores!r}")
|
|
174
|
+
return scores
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _check_seqs(seqs, names):
|
|
178
|
+
if isinstance(seqs, str) or not isinstance(seqs, (list, tuple)) or not all(isinstance(s, str) for s in seqs):
|
|
179
|
+
raise ValueError("seqs must be a list of sequences (strings), e.g. ['ATCG', 'TTTA']")
|
|
180
|
+
if len(seqs) < 2:
|
|
181
|
+
raise ValueError("need at least 2 sequences to align")
|
|
182
|
+
seqs = [s.strip().upper() for s in seqs]
|
|
183
|
+
names = list(names) if names is not None else [f"Seq{i + 1}" for i in range(len(seqs))]
|
|
184
|
+
if len(names) != len(seqs) or len(set(names)) != len(names):
|
|
185
|
+
raise ValueError(f"need {len(seqs)} unique names, got {names!r}")
|
|
186
|
+
return seqs, names
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from itertools import combinations
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from Bio.Align import PairwiseAligner
|
|
6
|
+
|
|
7
|
+
from njswalig import Aligner, Alignments, PairAlignment, njalign, swalign
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def biopython_score(a, b, scores, mode):
|
|
11
|
+
match, mismatch, gap = scores
|
|
12
|
+
return PairwiseAligner(mode=mode, match_score=match, mismatch_score=mismatch, gap_score=gap).score(a, b)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def rescore(x, y, scores):
|
|
16
|
+
match, mismatch, gap = scores
|
|
17
|
+
return sum(gap if "-" in (p, q) else match if p == q else mismatch for p, q in zip(x, y))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
random.seed(1)
|
|
21
|
+
CASES = [("".join(random.choices("ACGT", k=random.randint(1, 12))), "".join(random.choices("ACGT", k=random.randint(1, 12))),
|
|
22
|
+
[random.randint(1, 4), random.randint(-4, 0), random.randint(-4, -1)]) for _ in range(150)]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# --- the usage from the brief ------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
def test_brief_usage(capsys):
|
|
28
|
+
seqs = ["ATCG", "TTTA"]
|
|
29
|
+
scores = [2, -1, -2]
|
|
30
|
+
njalign.sequences(seqs, scores).show()
|
|
31
|
+
option1 = capsys.readouterr().out
|
|
32
|
+
njalign.sequence(seqs, scores).show()
|
|
33
|
+
option2 = capsys.readouterr().out
|
|
34
|
+
assert option1 == option2 == (
|
|
35
|
+
"Needleman-Wunsch (global): Seq1 vs Seq2, score = -1\n"
|
|
36
|
+
"Seq1 ATCG\n"
|
|
37
|
+
" .|..\n"
|
|
38
|
+
"Seq2 TTTA\n")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_sequence_is_sequences():
|
|
42
|
+
assert Aligner.sequence is Aligner.sequences
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# --- correctness ---------------------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
@pytest.mark.parametrize("a,b,scores", CASES)
|
|
48
|
+
def test_needleman_wunsch_matches_biopython(a, b, scores):
|
|
49
|
+
(pair,) = njalign.sequences([a, b], scores)
|
|
50
|
+
assert pair.score == biopython_score(a, b, scores, "global")
|
|
51
|
+
assert pair.aligned1.replace("-", "") == a and pair.aligned2.replace("-", "") == b # whole sequences
|
|
52
|
+
assert rescore(pair.aligned1, pair.aligned2, scores) == pair.score # alignment earns its score
|
|
53
|
+
assert pair.matrix[-1][-1] == pair.score
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.parametrize("a,b,scores", CASES)
|
|
57
|
+
def test_smith_waterman_matches_biopython(a, b, scores):
|
|
58
|
+
(pair,) = swalign.sequences([a, b], scores)
|
|
59
|
+
assert pair.score == max(0, biopython_score(a, b, scores, "local"))
|
|
60
|
+
assert pair.score == max(map(max, pair.matrix))
|
|
61
|
+
if pair.score:
|
|
62
|
+
assert a[pair.start1 - 1:pair.end1] == pair.aligned1.replace("-", "") # region positions are right
|
|
63
|
+
assert b[pair.start2 - 1:pair.end2] == pair.aligned2.replace("-", "")
|
|
64
|
+
assert rescore(pair.aligned1, pair.aligned2, scores) == pair.score
|
|
65
|
+
else:
|
|
66
|
+
assert pair.aligned1 == pair.aligned2 == ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_wikipedia_needleman_wunsch():
|
|
70
|
+
(pair,) = njalign.sequences(["GATTACA", "GCATGCU"], [1, -1, -1])
|
|
71
|
+
assert pair.score == 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_wikipedia_smith_waterman():
|
|
75
|
+
(pair,) = swalign.sequences(["TGTTACGG", "GGTTGACTA"], [3, -3, -2])
|
|
76
|
+
assert (pair.aligned1, pair.aligned2, pair.score) == ("GTT-AC", "GTTGAC", 13)
|
|
77
|
+
assert (pair.start1, pair.end1, pair.start2, pair.end2) == (2, 6, 2, 7)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_global_dp_table_edges():
|
|
81
|
+
(pair,) = njalign.sequences(["AC", "GTA"], [1, -1, -2])
|
|
82
|
+
assert [row[0] for row in pair.matrix] == [0, -2, -4]
|
|
83
|
+
assert pair.matrix[0] == [0, -2, -4, -6]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_float_scores():
|
|
87
|
+
(pair,) = njalign.sequences(["ACGT", "AGT"], [1.5, -0.5, -1.25])
|
|
88
|
+
assert pair.score == pytest.approx(biopython_score("ACGT", "AGT", [1.5, -0.5, -1.25], "global"))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# --- many sequences ------------------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
@pytest.mark.parametrize("aligner", [njalign, swalign])
|
|
94
|
+
def test_every_pair_aligned(aligner):
|
|
95
|
+
seqs = ["ACGT", "ACGA", "TCGA", "GGGT"]
|
|
96
|
+
result = aligner.sequences(seqs, [2, -1, -2])
|
|
97
|
+
assert isinstance(result, Alignments) and all(isinstance(p, PairAlignment) for p in result)
|
|
98
|
+
assert [(p.name1, p.name2) for p in result] == [(f"Seq{i + 1}", f"Seq{j + 1}") for i, j in combinations(range(4), 2)]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_custom_names_and_cleanup():
|
|
102
|
+
(pair,) = njalign.sequences([" acgt ", "ACGA"], [1, -1, -1], names=["human", "mouse"])
|
|
103
|
+
assert (pair.name1, pair.name2, pair.seq1) == ("human", "mouse", "ACGT")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_show_all_pairs_and_matrix(capsys):
|
|
107
|
+
swalign.sequences(["ACG", "CG", "AG"], [2, -1, -2]).show(matrix=True)
|
|
108
|
+
out = capsys.readouterr().out
|
|
109
|
+
assert out.count("Smith-Waterman (local)") == 3
|
|
110
|
+
assert " - C G" in out # DP table header
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_no_local_alignment_message(capsys):
|
|
114
|
+
swalign.sequences(["AAAA", "TTTT"], [1, -1, -1]).show()
|
|
115
|
+
assert "no positive-scoring local alignment" in capsys.readouterr().out
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# --- input checking ------------------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
@pytest.mark.parametrize("scores", [[2, -1], [2, -1, -2, 0], "2 -1 -2", [2, "-1", -2], [True, -1, -2], None])
|
|
121
|
+
def test_scores_format_strictly_enforced(scores):
|
|
122
|
+
with pytest.raises(ValueError, match=r"\[match, mismatch, gap\]"):
|
|
123
|
+
njalign.sequences(["AC", "AG"], scores)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@pytest.mark.parametrize("seqs,msg", [("ACGT", "list of sequences"), (["ACGT"], "at least 2"),
|
|
127
|
+
([], "at least 2"), (["AC", 5], "list of sequences")])
|
|
128
|
+
def test_bad_sequences(seqs, msg):
|
|
129
|
+
with pytest.raises(ValueError, match=msg):
|
|
130
|
+
swalign.sequences(seqs, [1, -1, -1])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_bad_names_and_mode():
|
|
134
|
+
with pytest.raises(ValueError, match="unique names"):
|
|
135
|
+
njalign.sequences(["AC", "AG"], [1, -1, -1], names=["x", "x"])
|
|
136
|
+
with pytest.raises(ValueError, match="global' or 'local"):
|
|
137
|
+
Aligner("semi")
|