Spaces:
Running
Running
| """Pairwise sequence alignment — Needleman–Wunsch (global) and Smith–Waterman | |
| (local). numpy + stdlib only; no Biopython dependency so it's trivially testable. | |
| Identity-based scoring (match / mismatch + linear gap) — enough for the | |
| "compare two sequences" use case: visualise percent identity, mismatches, and | |
| gaps for DNA or protein. O(n·m) DP, so we cap |a|·|b| to keep it snappy. | |
| """ | |
| from __future__ import annotations | |
| from typing import Any, Dict | |
| import numpy as np | |
| # |a| * |b| ceiling (~1225 × 1225). Above this we ask the user to align a | |
| # shorter region — the pure-Python traceback + DP fill stays sub-second below it. | |
| MAX_CELLS = 1_500_000 | |
| def _clean(s: str) -> str: | |
| return "".join(c for c in (s or "").upper() if not c.isspace()) | |
| def align(a: str, b: str, *, mode: str = "global", | |
| match: int = 2, mismatch: int = -1, gap: int = -2) -> Dict[str, Any]: | |
| """Align sequences `a` and `b`. | |
| mode: 'global' (Needleman–Wunsch) or 'local' (Smith–Waterman). | |
| Returns aligned strings, a match midline, percent identity, score, gaps. | |
| """ | |
| a = _clean(a) | |
| b = _clean(b) | |
| if not a or not b: | |
| raise ValueError("Both sequences must be non-empty.") | |
| if len(a) * len(b) > MAX_CELLS: | |
| raise ValueError( | |
| f"Sequences too large to align in-browser (|a|×|b| > {MAX_CELLS:,}). " | |
| "Align a shorter region." | |
| ) | |
| local = (mode == "local") | |
| n, m = len(a), len(b) | |
| H = np.zeros((n + 1, m + 1), dtype=np.int32) | |
| # traceback codes: 0 diag, 1 up (gap in b), 2 left (gap in a), 3 stop | |
| T = np.zeros((n + 1, m + 1), dtype=np.int8) | |
| if not local: | |
| H[:, 0] = np.arange(n + 1) * gap | |
| H[0, :] = np.arange(m + 1) * gap | |
| T[1:, 0] = 1 | |
| T[0, 1:] = 2 | |
| best_score, best_i, best_j = 0, 0, 0 | |
| for i in range(1, n + 1): | |
| ai = a[i - 1] | |
| Hi, Hprev, Ti = H[i], H[i - 1], T[i] | |
| for j in range(1, m + 1): | |
| sc = match if ai == b[j - 1] else mismatch | |
| cell = Hprev[j - 1] + sc | |
| t = 0 | |
| up = Hprev[j] + gap | |
| if up > cell: | |
| cell, t = up, 1 | |
| left = Hi[j - 1] + gap | |
| if left > cell: | |
| cell, t = left, 2 | |
| if local and cell < 0: | |
| cell, t = 0, 3 | |
| Hi[j] = cell | |
| Ti[j] = t | |
| if local and cell > best_score: | |
| best_score, best_i, best_j = cell, i, j | |
| if local: | |
| score, i, j = best_score, best_i, best_j | |
| else: | |
| score, i, j = int(H[n, m]), n, m | |
| out_a, out_b = [], [] | |
| while i > 0 or j > 0: | |
| t = int(T[i, j]) | |
| if local and (H[i, j] == 0 or t == 3): | |
| break | |
| if t == 0: | |
| out_a.append(a[i - 1]); out_b.append(b[j - 1]); i -= 1; j -= 1 | |
| elif t == 1: | |
| out_a.append(a[i - 1]); out_b.append("-"); i -= 1 | |
| elif t == 2: | |
| out_a.append("-"); out_b.append(b[j - 1]); j -= 1 | |
| else: | |
| break | |
| out_a.reverse(); out_b.reverse() | |
| aa, bb = "".join(out_a), "".join(out_b) | |
| cols = len(aa) | |
| matches = sum(1 for x, y in zip(aa, bb) if x == y and x != "-") | |
| identity = round(100.0 * matches / cols, 1) if cols else 0.0 | |
| gaps = aa.count("-") + bb.count("-") | |
| midline = "".join( | |
| "|" if (x == y and x != "-") else (" " if (x == "-" or y == "-") else ".") | |
| for x, y in zip(aa, bb) | |
| ) | |
| return { | |
| "mode": "local" if local else "global", | |
| "score": int(score), | |
| "identity": identity, | |
| "length": cols, | |
| "matches": matches, | |
| "gaps": gaps, | |
| "aligned_a": aa, | |
| "aligned_b": bb, | |
| "midline": midline, | |
| } | |