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.
@@ -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.
@@ -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
@@ -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")