Spaces:
Running
Running
Download app/tools/pairwise_alignment.py from Samad14/bio-nexus-api: direct link, hf CLI and curl.
- Browser
- Download file 6.11 kB
-
https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/app/tools/pairwise_alignment.py
- Command line
-
hf download hf://spaces/Samad14/bio-nexus-api/app/tools/pairwise_alignment.py
-
curl -L -o pairwise_alignment.py https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/app/tools/pairwise_alignment.py
6.11 kB
| """In-process pairwise alignment (Needleman-Wunsch / Smith-Waterman).""" | |
| from __future__ import annotations | |
| from Bio.Align import PairwiseAligner, substitution_matrices | |
| VALID_MODES = ("global", "local") | |
| MATRICES = {"blosum62": "BLOSUM62", "pam250": "PAM250"} | |
| class PairwiseAlignError(ValueError): | |
| pass | |
| def _normalize_sequence(seq: str, label: str) -> str: | |
| body_lines = [line for line in (seq or "").splitlines() if not line.lstrip().startswith(">")] | |
| body = "".join(body_lines) | |
| chars: list[str] = [] | |
| position = 0 | |
| for character in body: | |
| if character.isspace(): | |
| continue | |
| position += 1 | |
| if not character.isalpha(): | |
| raise PairwiseAlignError(f"{label} contains invalid character {character!r} at position {position}") | |
| chars.append(character.upper()) | |
| normalized = "".join(chars) | |
| if not normalized: | |
| raise PairwiseAlignError(f"{label} sequence is empty") | |
| return normalized | |
| def _gap_runs(aligned: str, seq_label: str, residue_offset: int = 0) -> list[dict]: | |
| runs: list[dict] = [] | |
| residues_seen = residue_offset | |
| index = 0 | |
| while index < len(aligned): | |
| if aligned[index] == "-": | |
| end = index | |
| while end < len(aligned) and aligned[end] == "-": | |
| end += 1 | |
| runs.append({"seq": seq_label, "inserted_after": residues_seen, "length": end - index}) | |
| index = end | |
| else: | |
| residues_seen += 1 | |
| index += 1 | |
| return runs | |
| def pairwise_align( | |
| seq_a: str, | |
| seq_b: str, | |
| mode: str = "global", | |
| matrix: str = "blosum62", | |
| open_gap_score: float = -10, | |
| extend_gap_score: float = -1, | |
| ) -> dict: | |
| mode = (mode or "global").lower() | |
| if mode not in VALID_MODES: | |
| raise PairwiseAlignError(f"mode must be one of {VALID_MODES}, got {mode!r}") | |
| matrix = (matrix or "blosum62").lower() | |
| if matrix not in MATRICES: | |
| raise PairwiseAlignError(f"matrix must be one of {list(MATRICES)}, got {matrix!r}") | |
| if open_gap_score > 0 or extend_gap_score > 0: | |
| raise PairwiseAlignError("Gap scores must be penalties (zero or negative), not positive rewards") | |
| seq_a = _normalize_sequence(seq_a, "query") | |
| seq_b = _normalize_sequence(seq_b, "subject") | |
| substitution_matrix = substitution_matrices.load(MATRICES[matrix]) | |
| allowed = set(str(substitution_matrix.alphabet)) - {"*"} | |
| for label, sequence in (("query", seq_a), ("subject", seq_b)): | |
| invalid = sorted(set(sequence) - allowed) | |
| if invalid: | |
| raise PairwiseAlignError(f"{label} contains residues unsupported by {matrix.upper()}: {', '.join(invalid)}") | |
| aligner = PairwiseAligner() | |
| aligner.mode = mode | |
| aligner.substitution_matrix = substitution_matrix | |
| aligner.open_gap_score = open_gap_score | |
| aligner.extend_gap_score = extend_gap_score | |
| alignments = aligner.align(seq_a, seq_b) | |
| common = { | |
| "mode": mode, | |
| "matrix": matrix, | |
| "open_gap_score": float(open_gap_score), | |
| "extend_gap_score": float(extend_gap_score), | |
| "query_length": len(seq_a), | |
| "hit_length": len(seq_b), | |
| } | |
| if len(alignments) == 0: | |
| return { | |
| **common, | |
| "score": 0.0, | |
| "aligned_query": "", | |
| "aligned_hit": "", | |
| "alignment_length": 0, | |
| "identity": 0, | |
| "pct_identity": 0.0, | |
| "similarity": 0, | |
| "pct_similarity": 0.0, | |
| "mismatches": 0, | |
| "gaps_total": 0, | |
| "gap_positions": [], | |
| "query_start": 0, | |
| "query_end": 0, | |
| "hit_start": 0, | |
| "hit_end": 0, | |
| } | |
| best = alignments[0] | |
| aligned_a = str(best[0]) | |
| aligned_b = str(best[1]) | |
| alignment_length = len(aligned_a) | |
| identity = 0 | |
| similarity = 0 | |
| mismatches = 0 | |
| for query_residue, subject_residue in zip(aligned_a, aligned_b): | |
| if query_residue == "-" or subject_residue == "-": | |
| continue | |
| if query_residue == subject_residue: | |
| identity += 1 | |
| else: | |
| mismatches += 1 | |
| try: | |
| if float(substitution_matrix[query_residue, subject_residue]) > 0: | |
| similarity += 1 | |
| except (KeyError, IndexError): | |
| pass | |
| query_blocks, hit_blocks = best.aligned | |
| if mode == "global": | |
| # A global alignment always spans both whole sequences, so coordinates | |
| # are the full 1..len range regardless of where matched blocks start. | |
| query_start = 1 | |
| query_end = len(seq_a) | |
| query_offset = 0 | |
| hit_start = 1 | |
| hit_end = len(seq_b) | |
| hit_offset = 0 | |
| else: | |
| if len(query_blocks): | |
| query_start = int(query_blocks[0][0]) + 1 | |
| query_end = int(query_blocks[-1][1]) | |
| query_offset = int(query_blocks[0][0]) | |
| else: | |
| query_start = query_end = query_offset = 0 | |
| if len(hit_blocks): | |
| hit_start = int(hit_blocks[0][0]) + 1 | |
| hit_end = int(hit_blocks[-1][1]) | |
| hit_offset = int(hit_blocks[0][0]) | |
| else: | |
| hit_start = hit_end = hit_offset = 0 | |
| gap_positions = [ | |
| run for run in _gap_runs(aligned_a, "query", query_offset) + _gap_runs(aligned_b, "subject", hit_offset) | |
| if run["length"] > 0 | |
| ] | |
| gaps_total = sum(run["length"] for run in gap_positions) | |
| return { | |
| **common, | |
| "score": float(best.score), | |
| "aligned_query": aligned_a, | |
| "aligned_hit": aligned_b, | |
| "alignment_length": alignment_length, | |
| "identity": identity, | |
| "pct_identity": round(identity / alignment_length * 100, 1) if alignment_length else 0.0, | |
| "similarity": similarity, | |
| "pct_similarity": round(similarity / alignment_length * 100, 1) if alignment_length else 0.0, | |
| "mismatches": mismatches, | |
| "gaps_total": gaps_total, | |
| "gap_positions": gap_positions, | |
| "query_start": query_start, | |
| "query_end": query_end, | |
| "hit_start": hit_start, | |
| "hit_end": hit_end, | |
| } | |