Skip to content

Commit 2f4a9fe

Browse files
authored
feat(dynamic_programming): add Needleman-Wunsch global sequence alignment algorithm (#15294)
1 parent 4c8f5dc commit 2f4a9fe

1 file changed

Lines changed: 187 additions & 0 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
"""Needleman-Wunsch algorithm for global sequence alignment.
2+
3+
Reference:
4+
https://en.wikipedia.org/wiki/Needleman%E2%80%93Wunsch_algorithm
5+
6+
The Needleman-Wunsch algorithm (1970) is a dynamic programming algorithm
7+
used in bioinformatics and computational biology to find the optimal global
8+
alignment between two sequences (such as DNA, RNA, or protein sequences).
9+
10+
Unlike local alignment algorithms (e.g., Smith-Waterman), which find the
11+
highest-scoring local sub-regions, Needleman-Wunsch aligns both sequences across
12+
their entire lengths from start to finish.
13+
14+
Algorithm:
15+
1. Initialization:
16+
- Construct a matrix of size (m + 1) x (n + 1) where m and n are sequence lengths.
17+
- Initialize boundary conditions:
18+
score_matrix[i][0] = i * gap_score
19+
score_matrix[0][j] = j * gap_score
20+
21+
2. Matrix Filling (Recurrence Relation):
22+
For each cell (i, j):
23+
diagonal = score_matrix[i - 1][j - 1] + (match_score if seq1[i-1] == seq2[j-1]
24+
else mismatch_score)
25+
deletion = score_matrix[i - 1][j] + gap_score
26+
insertion = score_matrix[i][j - 1] + gap_score
27+
score_matrix[i][j] = max(diagonal, deletion, insertion)
28+
29+
3. Traceback:
30+
- Start from the bottom-right cell (m, n) and trace back to (0, 0).
31+
- At each step, determine which direction (diagonal, up, or left) produced the
32+
maximum score, assembling the aligned sequences in reverse order.
33+
34+
Complexity:
35+
Time Complexity: O(m * n) where m and n are the lengths of the sequences.
36+
Space Complexity: O(m * n) to store the score matrix for traceback.
37+
"""
38+
39+
from __future__ import annotations
40+
41+
42+
def needleman_wunsch(
43+
sequence1: str,
44+
sequence2: str,
45+
match_score: int = 1,
46+
mismatch_score: int = -1,
47+
gap_score: int = -1,
48+
) -> tuple[str, str, int]:
49+
"""Compute the optimal global sequence alignment using Needleman-Wunsch.
50+
51+
Parameters:
52+
sequence1: The first input sequence to align.
53+
sequence2: The second input sequence to align.
54+
match_score: Score awarded when two characters match (default: 1).
55+
mismatch_score: Penalty score when characters do not match (default: -1).
56+
gap_score: Penalty score for introducing a gap '-' (default: -1).
57+
58+
Returns:
59+
A tuple containing:
60+
- aligned_sequence1: The first aligned sequence with inserted gaps.
61+
- aligned_sequence2: The second aligned sequence with inserted gaps.
62+
- alignment_score: The total optimal alignment score.
63+
64+
Raises:
65+
ValueError: If gap_score is positive (gap must be neutral or a penalty).
66+
67+
Examples:
68+
>>> # Wikipedia classic example
69+
>>> needleman_wunsch(
70+
... "GCATGCG", "GATTACA", match_score=1, mismatch_score=-1, gap_score=-1
71+
... )
72+
('GCA-TGCG', 'G-ATTACA', 0)
73+
74+
>>> # Identical sequences
75+
>>> needleman_wunsch(
76+
... "ACGT", "ACGT", match_score=2, mismatch_score=-1, gap_score=-2
77+
... )
78+
('ACGT', 'ACGT', 8)
79+
80+
>>> # Completely mismatched sequences
81+
>>> needleman_wunsch(
82+
... "AAAA", "TTTT", match_score=1, mismatch_score=-1, gap_score=-2
83+
... )
84+
('AAAA', 'TTTT', -4)
85+
86+
>>> # One sequence is empty
87+
>>> needleman_wunsch("AGTC", "", match_score=1, mismatch_score=-1, gap_score=-1)
88+
('AGTC', '----', -4)
89+
90+
>>> # Both sequences are empty
91+
>>> needleman_wunsch("", "")
92+
('', '', 0)
93+
94+
>>> # Protein sequence example
95+
>>> needleman_wunsch(
96+
... "HEAGAWGHEE", "PAWHEAE", match_score=2, mismatch_score=-1, gap_score=-2
97+
... )
98+
('HEAGAWGHE-E', '---PAW-HEAE', -1)
99+
100+
>>> # Invalid gap score
101+
>>> needleman_wunsch("A", "C", gap_score=5)
102+
Traceback (most recent call last):
103+
...
104+
ValueError: gap_score must be non-positive (<= 0)
105+
"""
106+
if gap_score > 0:
107+
msg = "gap_score must be non-positive (<= 0)"
108+
raise ValueError(msg)
109+
110+
first_sequence_length = len(sequence1)
111+
second_sequence_length = len(sequence2)
112+
113+
# Initialize the (m + 1) x (n + 1) dynamic programming score matrix
114+
score_matrix = [
115+
[0] * (second_sequence_length + 1) for _ in range(first_sequence_length + 1)
116+
]
117+
118+
# Fill base-case boundary penalties
119+
for row_index in range(first_sequence_length + 1):
120+
score_matrix[row_index][0] = row_index * gap_score
121+
for col_index in range(second_sequence_length + 1):
122+
score_matrix[0][col_index] = col_index * gap_score
123+
124+
# Populate the score matrix using dynamic programming
125+
for row_index in range(1, first_sequence_length + 1):
126+
for col_index in range(1, second_sequence_length + 1):
127+
char1 = sequence1[row_index - 1]
128+
char2 = sequence2[col_index - 1]
129+
substitution = match_score if char1 == char2 else mismatch_score
130+
131+
diagonal_score = score_matrix[row_index - 1][col_index - 1] + substitution
132+
deletion_score = score_matrix[row_index - 1][col_index] + gap_score
133+
insertion_score = score_matrix[row_index][col_index - 1] + gap_score
134+
135+
score_matrix[row_index][col_index] = max(
136+
diagonal_score, deletion_score, insertion_score
137+
)
138+
139+
# Traceback from bottom-right (m, n) to top-left (0, 0)
140+
aligned_chars_first: list[str] = []
141+
aligned_chars_second: list[str] = []
142+
curr_row = first_sequence_length
143+
curr_col = second_sequence_length
144+
145+
while curr_row > 0 or curr_col > 0:
146+
if curr_row > 0 and curr_col > 0:
147+
char1 = sequence1[curr_row - 1]
148+
char2 = sequence2[curr_col - 1]
149+
substitution = match_score if char1 == char2 else mismatch_score
150+
151+
# Check if diagonal step was optimal
152+
if (
153+
score_matrix[curr_row][curr_col]
154+
== score_matrix[curr_row - 1][curr_col - 1] + substitution
155+
):
156+
aligned_chars_first.append(char1)
157+
aligned_chars_second.append(char2)
158+
curr_row -= 1
159+
curr_col -= 1
160+
continue
161+
162+
# Check if vertical step (gap in second sequence) was optimal
163+
if (
164+
curr_row > 0
165+
and score_matrix[curr_row][curr_col]
166+
== score_matrix[curr_row - 1][curr_col] + gap_score
167+
):
168+
aligned_chars_first.append(sequence1[curr_row - 1])
169+
aligned_chars_second.append("-")
170+
curr_row -= 1
171+
else:
172+
# Horizontal step (gap in first sequence)
173+
aligned_chars_first.append("-")
174+
aligned_chars_second.append(sequence2[curr_col - 1])
175+
curr_col -= 1
176+
177+
aligned_sequence1 = "".join(reversed(aligned_chars_first))
178+
aligned_sequence2 = "".join(reversed(aligned_chars_second))
179+
final_score = score_matrix[first_sequence_length][second_sequence_length]
180+
181+
return aligned_sequence1, aligned_sequence2, final_score
182+
183+
184+
if __name__ == "__main__":
185+
import doctest
186+
187+
doctest.testmod()

0 commit comments

Comments
 (0)