From 103d6dc008a0c41c80b911e0e387415277588c0c Mon Sep 17 00:00:00 2001 From: Patrick Kunzmann Date: Tue, 7 Apr 2026 14:34:43 +0200 Subject: [PATCH 1/6] Add legacy consistency tests --- src/rust/sequence/align/dispatch.rs | 146 + tests/sequence/align/test_align.py | 254 + tests/sequence/align/test_banded.py | 164 - tests/sequence/align/test_localgapped.py | 81 - tests/sequence/align/test_pairwise.py | 48 +- tests/sequence/align/util.py | 39 + .../data/legacy_consistency/README.rst | 19 + .../create_legacy_alignments.py | 188 + .../legacy_alignments.tar.gz | Bin 0 -> 115416 bytes .../data/legacy_consistency/params.json | 9317 +++++++++++++++++ 10 files changed, 9968 insertions(+), 288 deletions(-) create mode 100644 src/rust/sequence/align/dispatch.rs create mode 100644 tests/sequence/align/test_align.py create mode 100644 tests/sequence/align/util.py create mode 100644 tests/sequence/data/legacy_consistency/README.rst create mode 100644 tests/sequence/data/legacy_consistency/create_legacy_alignments.py create mode 100644 tests/sequence/data/legacy_consistency/legacy_alignments.tar.gz create mode 100644 tests/sequence/data/legacy_consistency/params.json diff --git a/src/rust/sequence/align/dispatch.rs b/src/rust/sequence/align/dispatch.rs new file mode 100644 index 000000000..935239287 --- /dev/null +++ b/src/rust/sequence/align/dispatch.rs @@ -0,0 +1,146 @@ +//! Runtime dispatch from the dynamic Python arguments to the statically +//! monomorphized alignment core. +//! +//! The Python layer passes a scoring scheme and a gap penalty whose concrete +//! types (and the local/terminal/score-only flags) are only known at run time, +//! whereas the alignment core is generic over the [`ScoringScheme`], the +//! [`FillRule`] and the const flags. The macros and [`dispatch_scoring`] here +//! bridge the two by matching each runtime value and calling the core with the +//! corresponding concrete types. + +use super::cell::{AffineGapPenalty, LinearGapPenalty}; +use super::pairwise::align_optimal_generic; +use super::scoring::{Match, Score, SubstitutionMatrix}; +use super::symbol::Symbol; +use numpy::ndarray::Array2; +use pyo3::prelude::*; + +/// The gap penalty configuration passed from the Python layer. +#[derive(FromPyObject)] +pub enum GapPenalty { + /// A single linear gap penalty. + Linear(Score), + /// An affine gap penalty as `(gap_open, gap_extend)`. + Affine(Score, Score), +} + +/// A scoring scheme accepted from the Python layer: either a substitution +/// matrix (a 2D array) or a match/mismatch tuple. +#[derive(FromPyObject)] +pub enum Scoring { + Matrix(SubstitutionMatrix), + Match(Match), +} + +/// Instantiate the fill rule for each `(local, terminal, score_only)` const +/// combination and call the alignment core `$align`. +macro_rules! dispatch_consts { + ( + $align:path, $rule:ident, $scheme:expr, ($($penalty:expr),+), + $local:expr, $terminal:expr, $score_only:expr, + $code1:expr, $code2:expr, $max:expr + ) => { + match ($local, $terminal, $score_only) { + (true, true, true) => $align( + $code1, $code2, <$rule<_, _, true, true, true>>::new($scheme, $($penalty),+), $max, + ), + (true, true, false) => $align( + $code1, $code2, <$rule<_, _, true, true, false>>::new($scheme, $($penalty),+), $max, + ), + (true, false, true) => $align( + $code1, $code2, <$rule<_, _, true, false, true>>::new($scheme, $($penalty),+), $max, + ), + (true, false, false) => $align( + $code1, $code2, <$rule<_, _, true, false, false>>::new($scheme, $($penalty),+), $max, + ), + (false, true, true) => $align( + $code1, $code2, <$rule<_, _, false, true, true>>::new($scheme, $($penalty),+), $max, + ), + (false, true, false) => $align( + $code1, $code2, <$rule<_, _, false, true, false>>::new($scheme, $($penalty),+), $max, + ), + (false, false, true) => $align( + $code1, $code2, <$rule<_, _, false, false, true>>::new($scheme, $($penalty),+), $max, + ), + (false, false, false) => $align( + $code1, $code2, <$rule<_, _, false, false, false>>::new($scheme, $($penalty),+), $max, + ), + } + }; +} + +/// Select the concrete fill rule from the gap penalty, then dispatch the const +/// flags via [`dispatch_consts`]. +macro_rules! dispatch_penalty { + ( + $align:path, $scheme:expr, $gap:expr, + $local:expr, $terminal:expr, $score_only:expr, + $code1:expr, $code2:expr, $max:expr + ) => { + match $gap { + GapPenalty::Linear(gap) => dispatch_consts!( + $align, + LinearGapPenalty, + $scheme, + (gap), + $local, + $terminal, + $score_only, + $code1, + $code2, + $max + ), + GapPenalty::Affine(open, extend) => dispatch_consts!( + $align, + AffineGapPenalty, + $scheme, + (open, extend), + $local, + $terminal, + $score_only, + $code1, + $code2, + $max + ), + } + }; +} + +/// Dispatch over the scoring scheme and run [`align_optimal_generic`] for the +/// resulting concrete scheme, gap penalty and const flags. +#[allow(clippy::too_many_arguments)] +pub fn dispatch_scoring( + scoring: Scoring, + gap_penalty: GapPenalty, + local: bool, + terminal_penalty: bool, + score_only: bool, + code1: &[S], + code2: &[S], + max_number: usize, +) -> (Vec>, Score) { + match scoring { + Scoring::Matrix(scheme) => dispatch_penalty!( + align_optimal_generic, + scheme, + gap_penalty, + local, + terminal_penalty, + score_only, + code1, + code2, + max_number + ), + Scoring::Match(scheme) => dispatch_penalty!( + align_optimal_generic, + scheme, + gap_penalty, + local, + terminal_penalty, + score_only, + code1, + code2, + max_number + ), + } +} diff --git a/tests/sequence/align/test_align.py b/tests/sequence/align/test_align.py new file mode 100644 index 000000000..8a840fc6f --- /dev/null +++ b/tests/sequence/align/test_align.py @@ -0,0 +1,254 @@ +# This source code is part of the Biotite package and is distributed +# under the 3-Clause BSD License. Please see 'LICENSE.rst' for further +# information. + +""" +Tests that apply equally to all three pairwise alignment functions +(:func:`align_optimal()`, :func:`align_banded()` and +:func:`align_local_gapped()`). + +Both tests are driven by the legacy consistency data set, whose entries already +span all three functions and their parameter combinations. Tests specific to a +single function live in the respective ``test_.py``. +""" + +import numpy as np +import pytest +import biotite.sequence.align as align +from tests.sequence.align.util import legacy_alignments + +_LEGACY_ALIGNMENTS = legacy_alignments() +_PARAMS = [param for param, _ in _LEGACY_ALIGNMENTS] +_IDS = [param["file_name"] for param in _PARAMS] + + +def _kwargs(param): + """ + The alignment keyword arguments for a legacy `param` entry. + + Sequence-valued parameters (``gap_penalty``, ``band``, ``seed``) are stored + as lists in JSON, but the functions expect tuples. + """ + return { + key: tuple(value) if isinstance(value, list) else value + for key, value in param["params"].items() + } + + +def _swap_order(kwargs): + """ + Adapt the order-dependent parameters for swapped input sequences: the band + diagonals (``diag = j - i``) flip sign and the seed coordinates swap. + """ + swapped = dict(kwargs) + if "band" in swapped: + lower, upper = swapped["band"] + swapped["band"] = (-upper, -lower) + if "seed" in swapped: + pos1, pos2 = swapped["seed"] + swapped["seed"] = (pos2, pos1) + return swapped + + +# Upper bound on the number of co-optimal alignments retrieved for set-based +# comparisons. If a function returns this many, the set may be truncated and the +# alignment-level comparison is skipped (only the score is compared). +MAX_NUMBER = 100 + + +def _gapped_set(alignments): + """The set of ``(gapped_seq1, gapped_seq2)`` tuples of the given alignments.""" + return {tuple(alignment.get_gapped_sequences()) for alignment in alignments} + + +def _center_seed(alignment): + """The central aligned (non-gap) position of `alignment`, used as a seed.""" + trace = alignment.trace + trace = trace[(trace != -1).all(axis=1)] + return tuple(int(pos) for pos in trace[len(trace) // 2]) + + +@pytest.mark.parametrize("param", _PARAMS, ids=_IDS) +def test_scoring(sequences, param): + """ + The score reported in the returned alignment must be reproduced both by + the standalone :func:`score()` function and by the ``score_only`` mode of + the alignment function itself. + """ + align_function = getattr(align, param["method"]) + i, j = param["seq_indices"] + seq1, seq2 = sequences[i], sequences[j] + kwargs = _kwargs(param) + matrix = align.SubstitutionMatrix.std_protein_matrix() + + alignment = align_function(seq1, seq2, matrix, max_number=1, **kwargs)[0] + + # The standalone score() function reproduces the alignment score + # `terminal_penalty` only applies to align_optimal(); the other functions + # produce no terminal gaps, so the default is irrelevant for them + assert ( + align.score( + alignment, + matrix, + kwargs["gap_penalty"], + terminal_penalty=kwargs.get("terminal_penalty", False), + ) + == alignment.score + ) + # The score-only mode returns the same score as the full alignment + assert ( + align_function(seq1, seq2, matrix, max_number=1, score_only=True, **kwargs) + == alignment.score + ) + + +@pytest.mark.parametrize("param", _PARAMS, ids=_IDS) +def test_match_mismatch(sequences, param): + """ + A ``(match, mismatch)`` scoring scheme must give the same result as a + :class:`SubstitutionMatrix` encoding the same scheme: the match score on the + diagonal and the mismatch score everywhere else. + """ + MATCH = 5 + MISMATCH = -5 + + align_function = getattr(align, param["method"]) + i, j = param["seq_indices"] + seq1, seq2 = sequences[i], sequences[j] + kwargs = _kwargs(param) + + # An equivalent substitution matrix over the (shared) sequence alphabet + alphabet = seq1.get_alphabet() + matrix_data = np.full((len(alphabet), len(alphabet)), MISMATCH, dtype=np.int32) + np.fill_diagonal(matrix_data, MATCH) + matrix = align.SubstitutionMatrix(alphabet, alphabet, matrix_data) + from_matrix = align_function(seq1, seq2, matrix, max_number=1, **kwargs)[0] + + from_tuple = align_function(seq1, seq2, (MATCH, MISMATCH), max_number=1, **kwargs)[ + 0 + ] + + assert from_tuple.score == from_matrix.score + assert from_tuple.get_gapped_sequences() == from_matrix.get_gapped_sequences() + + +@pytest.mark.parametrize("param", _PARAMS, ids=_IDS) +def test_symmetry(sequences, param): + """ + Swapping the two input sequences (and the order-dependent ``band``/``seed`` + parameters) must yield the same alignment, with the two aligned rows + swapped. + """ + align_function = getattr(align, param["method"]) + i, j = param["seq_indices"] + seq1, seq2 = sequences[i], sequences[j] + kwargs = _kwargs(param) + matrix = align.SubstitutionMatrix.std_protein_matrix() + + alignments = align_function(seq1, seq2, matrix, max_number=MAX_NUMBER, **kwargs) + swapped = align_function( + seq2, seq1, matrix, max_number=MAX_NUMBER, **_swap_order(kwargs) + ) + + assert alignments[0].score == swapped[0].score + # The trace cannot be compared directly: with multiple co-optimal alignments + # the two orderings may return different ones. Compare the full set of + # alignments instead, but only if neither was truncated to `MAX_NUMBER`. + if len(alignments) < MAX_NUMBER and len(swapped) < MAX_NUMBER: + swapped_rows = {(row2, row1) for row1, row2 in _gapped_set(swapped)} + assert _gapped_set(alignments) == swapped_rows + + +# align_optimal() is the reference here, so it cannot be compared to itself +_HEURISTIC_PARAMS = [param for param in _PARAMS if param["method"] != "align_optimal"] + + +@pytest.mark.parametrize( + "param", _HEURISTIC_PARAMS, ids=[param["file_name"] for param in _HEURISTIC_PARAMS] +) +def test_comparison_to_optimal_alignment(sequences, param): + """ + Given a sufficiently large search space, align_banded() and + align_local_gapped() must recover the same alignment(s) as align_optimal(). + """ + i, j = param["seq_indices"] + seq1, seq2 = sequences[i], sequences[j] + kwargs = _kwargs(param) + gap_penalty = kwargs["gap_penalty"] + # `align_local_gapped()` is always local; `align_banded()` has `local` as parameter + local = kwargs.get("local", True) + matrix = align.SubstitutionMatrix.std_protein_matrix() + + # Neither heuristic penalizes terminal gaps -> semi-global reference + ref_alignments = align.align_optimal( + seq1, + seq2, + matrix, + gap_penalty=gap_penalty, + local=local, + terminal_penalty=False, + max_number=MAX_NUMBER, + ) + ref_alignments = [align.remove_terminal_gaps(ali) for ali in ref_alignments] + + if param["method"] == "align_banded": + # A band wide enough to contain every diagonal + test_alignments = align.align_banded( + seq1, + seq2, + matrix, + band=(-len(seq1), len(seq2)), + gap_penalty=gap_penalty, + local=local, + max_number=MAX_NUMBER, + ) + else: + # A seed on the optimal local alignment and a threshold large enough to + # not prune it + test_alignments = align.align_local_gapped( + seq1, + seq2, + matrix, + seed=_center_seed(ref_alignments[0]), + threshold=1000, + gap_penalty=gap_penalty, + max_number=MAX_NUMBER, + ) + + assert test_alignments[0].score == ref_alignments[0].score + # Compare the alignments themselves, but only if neither set was truncated + if len(test_alignments) < MAX_NUMBER and len(ref_alignments) < MAX_NUMBER: + ref_gapped = _gapped_set(ref_alignments) + for alignment in test_alignments: + if param["method"] == "align_banded": + assert tuple(alignment.get_gapped_sequences()) in ref_gapped + else: + if tuple(alignment.get_gapped_sequences()) not in ref_gapped: + # align_local_gapped() may legitimately differ: + # it can extend the upstream side slightly further than align_optimal() + # (an equal-scoring, longer alignment) + max_ref_length = max(len(ali) for ali in ref_alignments) + assert len(alignment) > max_ref_length + + +@pytest.mark.parametrize( + "param, ref_alignment", + _LEGACY_ALIGNMENTS, + ids=_IDS, +) +def test_consistency_with_legacy(sequences, param, ref_alignment): + """ + Verify that the current (Rust) implementation of each alignment function + produces the same results as the legacy *Cython* implementation. + """ + align_function = getattr(align, param["method"]) + i, j = param["seq_indices"] + matrix = align.SubstitutionMatrix.std_protein_matrix() + test_alignment = align_function( + sequences[i], sequences[j], matrix, max_number=1, **_kwargs(param) + )[0] + + # Direct alignment comparison is not possible, because the reference + # alignment loaded from FASTA has no score and the trace offset from + # semi-global/local alignment is lost during FASTA round-tripping + assert test_alignment.get_gapped_sequences() == ref_alignment.get_gapped_sequences() diff --git a/tests/sequence/align/test_banded.py b/tests/sequence/align/test_banded.py index 85e297dcb..2b1661ceb 100644 --- a/tests/sequence/align/test_banded.py +++ b/tests/sequence/align/test_banded.py @@ -45,74 +45,6 @@ def test_simple_alignment(gap_penalty, local, band_width): assert alignment in ref_alignments -@pytest.mark.parametrize( - "gap_penalty, local, seq_indices", - itertools.product( - [-10, (-10, -1)], - [False, True], - [(i, j) for i in range(10) for j in range(i + 1)], - ), -) -def test_complex_alignment(sequences, gap_penalty, local, seq_indices): - """ - Test `align_banded()` by comparing the output to `align_optimal()`. - This test uses a set of long sequences, which are pairwise compared. - The band should be chosen sufficiently large so `align_banded()` - can return the optimal alignment(s). - """ - MAX_NUMBER = 100 - - matrix = align.SubstitutionMatrix.std_protein_matrix() - index1, index2 = seq_indices - seq1 = sequences[index1] - seq2 = sequences[index2] - - ref_alignments = align.align_optimal( - seq1, - seq2, - matrix, - gap_penalty=gap_penalty, - local=local, - terminal_penalty=False, - max_number=MAX_NUMBER, - ) - # Remove terminal gaps in reference to obtain a true semi-global - # alignment, as returned by align_banded() - ref_alignments = [align.remove_terminal_gaps(al) for al in ref_alignments] - - identity = align.get_sequence_identity(ref_alignments[0]) - # Use a relatively small band width, if the sequences are similar, - # otherwise use the entire search space - band_width = 100 if identity > 0.5 else len(seq1) + len(seq2) - test_alignments = align.align_banded( - seq1, - seq2, - matrix, - (-band_width, band_width), - gap_penalty=gap_penalty, - local=local, - max_number=MAX_NUMBER, - ) - - try: - assert test_alignments[0].score == ref_alignments[0].score - if len(ref_alignments) < MAX_NUMBER: - # Only test if the exact same alignments were created, - # if the number of traces was not limited by MAX_NUMBER - assert len(test_alignments) == len(ref_alignments) - for alignment in test_alignments: - assert alignment in ref_alignments - except AssertionError: - print("First tested alignment:") - print() - print(test_alignments[0]) - print("\n") - print("First reference alignment:") - print() - print(ref_alignments[0]) - raise - - @pytest.mark.parametrize( "length, excerpt_length, seed", itertools.product([1_000, 1_000_000], [50, 500], range(10)), @@ -133,8 +65,6 @@ def test_large_sequence_mapping(length, excerpt_length, seed): diagonal = np.random.randint(excerpt_pos - BAND_WIDTH, excerpt_pos + BAND_WIDTH) band = (diagonal - BAND_WIDTH, diagonal + BAND_WIDTH) - print(band) - print(len(sequence), len(excerpt)) matrix = align.SubstitutionMatrix.std_nucleotide_matrix() test_alignments = align.align_banded(excerpt, sequence, matrix, band=band) @@ -149,97 +79,3 @@ def test_large_sequence_mapping(length, excerpt_length, seed): axis=1, ) assert np.array_equal(test_trace, ref_trace) - - -@pytest.mark.parametrize( - "gap_penalty, local, seed", - itertools.product([-10, (-10, -1)], [False, True], range(100)), -) -def test_swapping(gap_penalty, local, seed): - """ - Check if `align_banded()` returns a 'swapped' alignment, if - the order of input sequences is swapped. - """ - np.random.seed(seed) - band = (np.random.randint(-30, -10), np.random.randint(10, 30)) - - seq1, seq2 = _create_random_pair(seed) - matrix = align.SubstitutionMatrix.std_protein_matrix() - ref_alignments = align.align_banded( - seq1, seq2, matrix, band=band, local=local, gap_penalty=gap_penalty - ) - - test_alignments = align.align_banded( - seq2, seq1, matrix, band=band, local=local, gap_penalty=gap_penalty - ) - - if len(ref_alignments) != 1 or len(test_alignments) != 1: - # If multiple optimal alignments exist, - # it is not easy to assign a swapped one to an original one - # therefore, simply return in this case - # the number of tested seeds should be large enough to generate - # a reasonable number of suitable test cases - return - ref_alignment = ref_alignments[0] - test_alignment = test_alignments[0] - - assert test_alignment.sequences[0] == ref_alignment.sequences[1] - assert test_alignment.sequences[1] == ref_alignment.sequences[0] - assert np.array_equal(test_alignment.trace, ref_alignment.trace[:, ::-1]) - - -def _create_random_pair( - seed, - length=100, - max_subsitutions=5, - max_insertions=5, - max_deletions=5, - max_truncations=5, -): - """ - generate a pair of protein sequences. - Each pair contains - - 1. a randomly generated sequence - 2. a sequence created by randomly introducing - deletions/insertions/substitutions into the first sequence. - """ - np.random.seed(seed) - - original = seq.ProteinSequence() - original.code = np.random.randint(len(original.alphabet), size=length) - - mutant = original.copy() - - # Random Substitutions - n_subsitutions = np.random.randint(max_subsitutions) - subsitution_indices = np.random.choice( - np.arange(len(mutant)), size=n_subsitutions, replace=False - ) - subsitution_values = np.random.randint(len(original.alphabet), size=n_subsitutions) - mutant.code[subsitution_indices] = subsitution_values - - # Random insertions - n_insertions = np.random.randint(max_insertions) - insertion_indices = np.random.choice( - np.arange(len(mutant)), size=n_insertions, replace=False - ) - insertion_values = np.random.randint(len(original.alphabet), size=n_insertions) - mutant.code = np.insert(mutant.code, insertion_indices, insertion_values) - - # Random deletions - n_deletions = np.random.randint(max_deletions) - deletion_indices = np.random.choice( - np.arange(len(mutant)), size=n_deletions, replace=False - ) - mutant.code = np.delete(mutant.code, deletion_indices) - - # Truncate at both ends of original and mutant - original = original[ - np.random.randint(max_truncations) : -(1 + np.random.randint(max_truncations)) - ] - mutant = mutant[ - np.random.randint(max_truncations) : -(1 + np.random.randint(max_truncations)) - ] - - return original, mutant diff --git a/tests/sequence/align/test_localgapped.py b/tests/sequence/align/test_localgapped.py index 714004118..480a64e43 100644 --- a/tests/sequence/align/test_localgapped.py +++ b/tests/sequence/align/test_localgapped.py @@ -59,87 +59,6 @@ def test_simple_alignment(gap_penalty, seed, threshold, direction, score_only): assert alignment in ref_alignments -@pytest.mark.parametrize( - "gap_penalty, score_only, seq_indices", - itertools.product( - [-10, (-10, -1)], - [False, True], - [(i, j) for i in range(10) for j in range(i + 1)], - ), -) -def test_complex_alignment(sequences, gap_penalty, score_only, seq_indices): - """ - Test `align_local_gapped()` by comparing the output to - `align_optimal()`. - This test uses a set of long sequences, which are pairwise compared. - The threshold should be chosen sufficiently large so - `align_local_gapped()` can return the optimal alignment(s). - """ - MAX_NUMBER = 100 - # The linear gap penalty for longer gaps easily exceeds - # a small threshold -> increase threshold for linear penalty - THRESHOLD = 200 if isinstance(gap_penalty, int) else 50 - - matrix = align.SubstitutionMatrix.std_protein_matrix() - index1, index2 = seq_indices - seq1 = sequences[index1] - seq2 = sequences[index2] - - ref_alignments = align.align_optimal( - seq1, seq2, matrix, gap_penalty=gap_penalty, local=True, max_number=MAX_NUMBER - ) - # Select the center of the alignment as seed - trace = ref_alignments[0].trace - trace = trace[(trace != -1).all(axis=1)] - seed = trace[len(trace) // 2] - - test_result = align.align_local_gapped( - seq1, seq2, matrix, seed, THRESHOLD, gap_penalty, MAX_NUMBER, "both", score_only - ) - - if score_only: - test_score = test_result - # All optimal alignments have the same score - assert test_score == ref_alignments[0].score - else: - try: - test_alignments = test_result - assert test_alignments[0].score == ref_alignments[0].score - # Test if the score is also correctly calculated - assert ( - align.score(test_alignments[0], matrix, gap_penalty) - == ref_alignments[0].score - ) - if len(ref_alignments) < MAX_NUMBER and len(test_alignments) < MAX_NUMBER: - # Only test if the exact same alignments were created, - # if the number of traces was not limited by MAX_NUMBER - for i, alignment in enumerate(test_alignments): - try: - assert alignment in ref_alignments - except AssertionError: - # Edge case: - # In rare case the local alignment may be - # slightly longer on the upstream side for - # 'align_local_ungapped()', since the - # upstream side is handled in an inverted - # manner - # However this does not effect the score - # Consequently, the exception is ignored - # if the alignment is longer than all - # reference alignments - if len(alignment) <= max([len(ali) for ali in ref_alignments]): - raise - except AssertionError: - print(f"Missing test alignment at index {i}:") - print() - print(test_alignments[i]) - print("\n") - print("First reference alignment:") - print() - print(ref_alignments[0]) - raise - - @pytest.mark.parametrize( "gap_penalty, direction, score_only, should_raise", itertools.product( diff --git a/tests/sequence/align/test_pairwise.py b/tests/sequence/align/test_pairwise.py index 712dfb6b8..050d7a345 100644 --- a/tests/sequence/align/test_pairwise.py +++ b/tests/sequence/align/test_pairwise.py @@ -168,48 +168,6 @@ def test_affine_gap_penalty(local, term, gap_penalty, seed): raise -@pytest.mark.parametrize( - "local, term, gap_penalty, seq_indices", - itertools.product( - [True, False], - [True, False], - [-10, (-10, -1)], - [(i, j) for i in range(10) for j in range(i + 1)], - ), -) -def test_align_optimal_symmetry(sequences, local, term, gap_penalty, seq_indices): - """ - Alignments should be indifferent about which sequence comes first. - """ - matrix = align.SubstitutionMatrix.std_protein_matrix() - index1, index2 = seq_indices - seq1 = sequences[index1] - seq2 = sequences[index2] - alignment1 = align.align_optimal( - seq1, - seq2, - matrix, - gap_penalty=gap_penalty, - terminal_penalty=term, - local=local, - max_number=1, - )[0] - # Swap the sequences - alignment2 = align.align_optimal( - seq2, - seq1, - matrix, - gap_penalty=gap_penalty, - terminal_penalty=term, - local=local, - max_number=1, - )[0] - # Comparing all traces of both alignments to each other - # would be unfeasible - # Instead the scores are compared - assert alignment1.score == alignment2.score - - @pytest.mark.parametrize( "gap_penalty, term, seq_indices", itertools.product( @@ -218,10 +176,14 @@ def test_align_optimal_symmetry(sequences, local, term, gap_penalty, seq_indices [(i, j) for i in range(10) for j in range(i + 1)], ), ) -def test_scoring(sequences, gap_penalty, term, seq_indices): +def test_scoring_terminal_penalty(sequences, gap_penalty, term, seq_indices): """ The result of the :func:`score()` function should be equal to the score calculated in :func:`align_optimal()`. + + The general score consistency is already covered by the common + :func:`test_align.test_scoring`; this variant additionally exercises + ``terminal_penalty``, which only :func:`align_optimal()` supports. """ matrix = align.SubstitutionMatrix.std_protein_matrix() index1, index2 = seq_indices diff --git a/tests/sequence/align/util.py b/tests/sequence/align/util.py new file mode 100644 index 000000000..57d862a7f --- /dev/null +++ b/tests/sequence/align/util.py @@ -0,0 +1,39 @@ +# This source code is part of the Biotite package and is distributed +# under the 3-Clause BSD License. Please see 'LICENSE.rst' for further +# information. + +import io +import json +import tarfile +from os.path import join +from tests.util import data_dir + + +def legacy_alignments(): + """ + Iterate over the ``(param, alignment)`` pairs of the legacy consistency data + set (``tests/sequence/data/legacy_consistency``) for all alignment methods. + + `param` is the ``params.json`` entry (including the ``method``), augmented + with the ``file_name`` of the alignment within ``legacy_alignments.tar.gz``. + `alignment` is the reference alignment loaded from that archive member. + The pairs are sorted by ``file_name``. + """ + # Import in function to avoid 'ModuleNotFoundError', + # if a Cython module is not compiled yet + import biotite.sequence.io.fasta as fasta + + base_dir = join(data_dir("sequence"), "legacy_consistency") + with open(join(base_dir, "params.json")) as file: + params = json.load(file) + + pairs = [] + with tarfile.open(join(base_dir, "legacy_alignments.tar.gz"), "r:gz") as tar: + for file_name in sorted(params): + entry = params[file_name] + with io.TextIOWrapper( + tar.extractfile(file_name), encoding="utf-8" + ) as fasta_io: + alignment = fasta.get_alignment(fasta.FastaFile.read(fasta_io)) + pairs.append(({"file_name": file_name, **entry}, alignment)) + return pairs diff --git a/tests/sequence/data/legacy_consistency/README.rst b/tests/sequence/data/legacy_consistency/README.rst new file mode 100644 index 000000000..94eff6983 --- /dev/null +++ b/tests/sequence/data/legacy_consistency/README.rst @@ -0,0 +1,19 @@ +Legacy consistency data +======================= + +This directory contains precomputed pairwise alignments of the Cas9 sequences +from ``tests/sequence/data/cas9.fasta``, generated by the legacy *Cython* +implementations. +These data is used by consistency tests to verify that newer Rust implementations +produce identical results. + +The data were created by ``create_legacy_alignments.py`` in this directory. + +Files +----- + +- ``legacy_alignments.tar.gz`` — all alignments above as FASTA members. +- ``params.json`` — maps each member file name to its + - ``method`` (the alignment function name), + - the ``seq_indices`` of the sequence pair + - and the ``params`` passed to the function as keyword arguments. diff --git a/tests/sequence/data/legacy_consistency/create_legacy_alignments.py b/tests/sequence/data/legacy_consistency/create_legacy_alignments.py new file mode 100644 index 000000000..53a8e41db --- /dev/null +++ b/tests/sequence/data/legacy_consistency/create_legacy_alignments.py @@ -0,0 +1,188 @@ +""" +Create legacy alignment files from the Cython implementations. + +This script must be run while the Cython implementations of +``align_optimal()``, ``align_banded()`` and ``align_local_gapped()`` are still +active. It will refuse to run if any of these functions has already been +replaced by a Rust implementation. + +All alignments are written as FASTA members into a single +``legacy_alignments.tar.gz`` archive. ``params.json`` maps each member file name +to its ``method`` (the alignment function), the ``seq_indices`` of the sequence +pair and the ``params`` that can be passed to the function as keyword arguments. +""" + +import io +import itertools +import json +import tarfile +from pathlib import Path +import biotite.sequence as seq +import biotite.sequence.align as align +import biotite.sequence.io.fasta as fasta + +BAND_WIDTH = 100 +THRESHOLD = 100 +# The gap penalty is part of the Cartesian product: affine and linear. +GAP_PENALTIES = [("affine", (-10, -1)), ("linear", -10)] +# The affine penalty is used to derive the band and seed for the dependent +# `align_banded()` and `align_local_gapped()` reference alignments. +REFERENCE_GAP_PENALTY = (-10, -1) + +DATA_DIR = Path(__file__).resolve().parent +CAS9_PATH = DATA_DIR.parent / "cas9.fasta" +ALIGNMENT_PATH = DATA_DIR / "legacy_alignments.tar.gz" +PARAMS_PATH = DATA_DIR / "params.json" + + +def _assert_cython(func): + """ + Raise if `func` is not a Cython function. + """ + type_name = type(func).__name__ + if type_name != "cython_function_or_method": + raise RuntimeError( + "This script must be run with the legacy Cython implementation." + ) + + +def _middle_aligned_position(alignment): + """ + Return the ``(seq1_pos, seq2_pos)`` tuple from the middle of the aligned + (non-gap) positions in the alignment. + """ + trace = alignment.trace + aligned_mask = (trace != -1).all(axis=1) + aligned_positions = trace[aligned_mask] + mid = aligned_positions[len(aligned_positions) // 2] + return mid[0].item(), mid[1].item() + + +def _add_alignment(tar, alignment, seq_names, file_name): + """ + Write `alignment` as a FASTA member named `file_name` into the open tar + archive `tar`. + """ + fasta_file = fasta.FastaFile() + fasta.set_alignment(fasta_file, alignment, seq_names) + text = io.StringIO() + fasta_file.write(text) + data = text.getvalue().encode() + info = tarfile.TarInfo(file_name) + info.size = len(data) + # Use a fixed timestamp to keep the archive reproducible + info.mtime = 0 + tar.addfile(info, io.BytesIO(data)) + + +def main(): + _assert_cython(align.align_optimal) + _assert_cython(align.align_banded) + _assert_cython(align.align_local_gapped) + + fasta_file = fasta.FastaFile.read(CAS9_PATH) + headers = list(fasta_file.keys()) + sequences = [seq.ProteinSequence(sequence) for sequence in fasta_file.values()] + matrix = align.SubstitutionMatrix.std_protein_matrix() + + params = {} + + with tarfile.open(ALIGNMENT_PATH, "w:gz") as tar: + for i, j in itertools.combinations(range(len(sequences)), 2): + seq1, seq2 = sequences[i], sequences[j] + seq_names = [headers[i], headers[j]] + + # A global optimal alignment is used to derive the band (for + # `align_banded`) and the seed (for `align_local_gapped`). + reference_alignment = align.align_optimal( + seq1, seq2, matrix, gap_penalty=REFERENCE_GAP_PENALTY, max_number=1 + )[0] + mid_pos1, mid_pos2 = _middle_aligned_position(reference_alignment) + diag = mid_pos2 - mid_pos1 + band = (diag - BAND_WIDTH, diag + BAND_WIDTH) + seed = (mid_pos1, mid_pos2) + + # --- align_optimal: local x terminal_penalty x gap_penalty --- + for local, terminal_penalty, (gap_name, gap_penalty) in itertools.product( + (False, True), (False, True), GAP_PENALTIES + ): + alignment = align.align_optimal( + seq1, + seq2, + matrix, + gap_penalty=gap_penalty, + terminal_penalty=terminal_penalty, + local=local, + max_number=1, + )[0] + name = "local" if local else "global" + stem = f"align_optimal_{i}_{j}_{name}_{gap_name}" + if terminal_penalty: + stem += "_terminal" + file_name = f"{stem}.fasta" + _add_alignment(tar, alignment, seq_names, file_name) + params[file_name] = { + "method": "align_optimal", + "seq_indices": [i, j], + "params": { + "gap_penalty": gap_penalty, + "local": local, + "terminal_penalty": terminal_penalty, + }, + } + + # --- align_banded: local x gap_penalty --- + for local, (gap_name, gap_penalty) in itertools.product( + (False, True), GAP_PENALTIES + ): + alignment = align.align_banded( + seq1, + seq2, + matrix, + band=band, + gap_penalty=gap_penalty, + local=local, + max_number=1, + )[0] + name = "local" if local else "global" + file_name = f"align_banded_{i}_{j}_{name}_{gap_name}.fasta" + _add_alignment(tar, alignment, seq_names, file_name) + params[file_name] = { + "method": "align_banded", + "seq_indices": [i, j], + "params": { + "gap_penalty": gap_penalty, + "band": list(band), + "local": local, + }, + } + + # --- align_local_gapped: gap_penalty (always local) --- + for gap_name, gap_penalty in GAP_PENALTIES: + alignment = align.align_local_gapped( + seq1, + seq2, + matrix, + seed=seed, + threshold=THRESHOLD, + gap_penalty=gap_penalty, + max_number=1, + )[0] + file_name = f"align_local_{i}_{j}_{gap_name}.fasta" + _add_alignment(tar, alignment, seq_names, file_name) + params[file_name] = { + "method": "align_local_gapped", + "seq_indices": [i, j], + "params": { + "gap_penalty": gap_penalty, + "seed": list(seed), + "threshold": THRESHOLD, + }, + } + + with open(PARAMS_PATH, "w") as f: + json.dump(params, f, indent=2) + + +if __name__ == "__main__": + main() diff --git a/tests/sequence/data/legacy_consistency/legacy_alignments.tar.gz b/tests/sequence/data/legacy_consistency/legacy_alignments.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..c4f8689424f09de52599b57ca2936eeb6187a6e3 GIT binary patch literal 115416 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'SP>hiILITK)NZj@JChHcy^c4Ep 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 @bkkW@ho^Bctfd;u8v; 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 Hq}l!t3p^V! literal 0 HcmV?d00001 diff --git a/tests/sequence/data/legacy_consistency/params.json b/tests/sequence/data/legacy_consistency/params.json new file mode 100644 index 000000000..6528db39d --- /dev/null +++ b/tests/sequence/data/legacy_consistency/params.json @@ -0,0 +1,9317 @@ +{ + "align_optimal_0_1_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 1 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_1_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 1 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_1_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 1 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_1_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 1 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_1_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 1 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_1_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 1 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_1_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 1 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_0_1_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 1 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_0_1_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 1 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -99, + 101 + ], + "local": false + } + }, + "align_banded_0_1_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 1 + ], + "params": { + "gap_penalty": -10, + "band": [ + -99, + 101 + ], + "local": false + } + }, + "align_banded_0_1_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 1 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -99, + 101 + ], + "local": true + } + }, + "align_banded_0_1_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 1 + ], + "params": { + "gap_penalty": -10, + "band": [ + -99, + 101 + ], + "local": true + } + }, + "align_local_0_1_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 1 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 671, + 672 + ], + "threshold": 100 + } + }, + "align_local_0_1_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 1 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 671, + 672 + ], + "threshold": 100 + } + }, + "align_optimal_0_2_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 2 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_2_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 2 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_2_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 2 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_2_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 2 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_2_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 2 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_2_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 2 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_2_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 2 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_0_2_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 2 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_0_2_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 2 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -101, + 99 + ], + "local": false + } + }, + "align_banded_0_2_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 2 + ], + "params": { + "gap_penalty": -10, + "band": [ + -101, + 99 + ], + "local": false + } + }, + "align_banded_0_2_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 2 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -101, + 99 + ], + "local": true + } + }, + "align_banded_0_2_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 2 + ], + "params": { + "gap_penalty": -10, + "band": [ + -101, + 99 + ], + "local": true + } + }, + "align_local_0_2_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 2 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 677, + 676 + ], + "threshold": 100 + } + }, + "align_local_0_2_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 2 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 677, + 676 + ], + "threshold": 100 + } + }, + "align_optimal_0_3_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_3_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 3 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_3_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_3_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 3 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_3_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_3_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 3 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_3_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_0_3_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 3 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_0_3_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -80, + 120 + ], + "local": false + } + }, + "align_banded_0_3_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 3 + ], + "params": { + "gap_penalty": -10, + "band": [ + -80, + 120 + ], + "local": false + } + }, + "align_banded_0_3_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -80, + 120 + ], + "local": true + } + }, + "align_banded_0_3_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 3 + ], + "params": { + "gap_penalty": -10, + "band": [ + -80, + 120 + ], + "local": true + } + }, + "align_local_0_3_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 677, + 697 + ], + "threshold": 100 + } + }, + "align_local_0_3_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 3 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 677, + 697 + ], + "threshold": 100 + } + }, + "align_optimal_0_4_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_4_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 4 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_4_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_4_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 4 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_4_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_4_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 4 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_4_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_0_4_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 4 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_0_4_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -96, + 104 + ], + "local": false + } + }, + "align_banded_0_4_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 4 + ], + "params": { + "gap_penalty": -10, + "band": [ + -96, + 104 + ], + "local": false + } + }, + "align_banded_0_4_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -96, + 104 + ], + "local": true + } + }, + "align_banded_0_4_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 4 + ], + "params": { + "gap_penalty": -10, + "band": [ + -96, + 104 + ], + "local": true + } + }, + "align_local_0_4_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 666, + 670 + ], + "threshold": 100 + } + }, + "align_local_0_4_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 4 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 666, + 670 + ], + "threshold": 100 + } + }, + "align_optimal_0_5_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_5_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 5 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_5_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_5_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 5 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_5_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_5_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 5 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_5_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_0_5_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 5 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_0_5_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -379, + -179 + ], + "local": false + } + }, + "align_banded_0_5_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 5 + ], + "params": { + "gap_penalty": -10, + "band": [ + -379, + -179 + ], + "local": false + } + }, + "align_banded_0_5_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -379, + -179 + ], + "local": true + } + }, + "align_banded_0_5_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 5 + ], + "params": { + "gap_penalty": -10, + "band": [ + -379, + -179 + ], + "local": true + } + }, + "align_local_0_5_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 785, + 506 + ], + "threshold": 100 + } + }, + "align_local_0_5_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 5 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 785, + 506 + ], + "threshold": 100 + } + }, + "align_optimal_0_6_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_6_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 6 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_6_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_6_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 6 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_6_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_6_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 6 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_6_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_0_6_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 6 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_0_6_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -359, + -159 + ], + "local": false + } + }, + "align_banded_0_6_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 6 + ], + "params": { + "gap_penalty": -10, + "band": [ + -359, + -159 + ], + "local": false + } + }, + "align_banded_0_6_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -359, + -159 + ], + "local": true + } + }, + "align_banded_0_6_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 6 + ], + "params": { + "gap_penalty": -10, + "band": [ + -359, + -159 + ], + "local": true + } + }, + "align_local_0_6_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 782, + 523 + ], + "threshold": 100 + } + }, + "align_local_0_6_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 6 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 782, + 523 + ], + "threshold": 100 + } + }, + "align_optimal_0_7_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_7_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 7 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_7_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_7_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 7 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_7_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_7_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 7 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_7_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_0_7_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 7 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_0_7_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -359, + -159 + ], + "local": false + } + }, + "align_banded_0_7_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 7 + ], + "params": { + "gap_penalty": -10, + "band": [ + -359, + -159 + ], + "local": false + } + }, + "align_banded_0_7_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -359, + -159 + ], + "local": true + } + }, + "align_banded_0_7_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 7 + ], + "params": { + "gap_penalty": -10, + "band": [ + -359, + -159 + ], + "local": true + } + }, + "align_local_0_7_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 792, + 533 + ], + "threshold": 100 + } + }, + "align_local_0_7_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 7 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 792, + 533 + ], + "threshold": 100 + } + }, + "align_optimal_0_8_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_8_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_8_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_8_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_8_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_8_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_8_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_0_8_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_0_8_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -359, + -159 + ], + "local": false + } + }, + "align_banded_0_8_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -359, + -159 + ], + "local": false + } + }, + "align_banded_0_8_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -359, + -159 + ], + "local": true + } + }, + "align_banded_0_8_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -359, + -159 + ], + "local": true + } + }, + "align_local_0_8_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 791, + 532 + ], + "threshold": 100 + } + }, + "align_local_0_8_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 8 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 791, + 532 + ], + "threshold": 100 + } + }, + "align_optimal_0_9_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_9_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_0_9_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_9_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_0_9_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_9_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_0_9_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_0_9_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 0, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_0_9_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -340, + -140 + ], + "local": false + } + }, + "align_banded_0_9_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -340, + -140 + ], + "local": false + } + }, + "align_banded_0_9_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -340, + -140 + ], + "local": true + } + }, + "align_banded_0_9_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 0, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -340, + -140 + ], + "local": true + } + }, + "align_local_0_9_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 789, + 549 + ], + "threshold": 100 + } + }, + "align_local_0_9_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 0, + 9 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 789, + 549 + ], + "threshold": 100 + } + }, + "align_optimal_1_2_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 2 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_2_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 2 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_2_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 2 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_2_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 2 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_2_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 2 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_2_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 2 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_2_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 2 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_1_2_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 2 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_1_2_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 2 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -102, + 98 + ], + "local": false + } + }, + "align_banded_1_2_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 2 + ], + "params": { + "gap_penalty": -10, + "band": [ + -102, + 98 + ], + "local": false + } + }, + "align_banded_1_2_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 2 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -102, + 98 + ], + "local": true + } + }, + "align_banded_1_2_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 2 + ], + "params": { + "gap_penalty": -10, + "band": [ + -102, + 98 + ], + "local": true + } + }, + "align_local_1_2_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 2 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 672, + 670 + ], + "threshold": 100 + } + }, + "align_local_1_2_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 2 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 672, + 670 + ], + "threshold": 100 + } + }, + "align_optimal_1_3_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_3_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 3 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_3_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_3_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 3 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_3_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_3_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 3 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_3_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_1_3_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 3 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_1_3_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -81, + 119 + ], + "local": false + } + }, + "align_banded_1_3_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 3 + ], + "params": { + "gap_penalty": -10, + "band": [ + -81, + 119 + ], + "local": false + } + }, + "align_banded_1_3_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -81, + 119 + ], + "local": true + } + }, + "align_banded_1_3_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 3 + ], + "params": { + "gap_penalty": -10, + "band": [ + -81, + 119 + ], + "local": true + } + }, + "align_local_1_3_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 672, + 691 + ], + "threshold": 100 + } + }, + "align_local_1_3_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 3 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 672, + 691 + ], + "threshold": 100 + } + }, + "align_optimal_1_4_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_4_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 4 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_4_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_4_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 4 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_4_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_4_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 4 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_4_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_1_4_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 4 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_1_4_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -97, + 103 + ], + "local": false + } + }, + "align_banded_1_4_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 4 + ], + "params": { + "gap_penalty": -10, + "band": [ + -97, + 103 + ], + "local": false + } + }, + "align_banded_1_4_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -97, + 103 + ], + "local": true + } + }, + "align_banded_1_4_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 4 + ], + "params": { + "gap_penalty": -10, + "band": [ + -97, + 103 + ], + "local": true + } + }, + "align_local_1_4_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 667, + 670 + ], + "threshold": 100 + } + }, + "align_local_1_4_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 4 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 667, + 670 + ], + "threshold": 100 + } + }, + "align_optimal_1_5_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_5_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 5 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_5_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_5_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 5 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_5_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_5_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 5 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_5_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_1_5_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 5 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_1_5_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -384, + -184 + ], + "local": false + } + }, + "align_banded_1_5_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 5 + ], + "params": { + "gap_penalty": -10, + "band": [ + -384, + -184 + ], + "local": false + } + }, + "align_banded_1_5_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -384, + -184 + ], + "local": true + } + }, + "align_banded_1_5_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 5 + ], + "params": { + "gap_penalty": -10, + "band": [ + -384, + -184 + ], + "local": true + } + }, + "align_local_1_5_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 787, + 503 + ], + "threshold": 100 + } + }, + "align_local_1_5_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 5 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 787, + 503 + ], + "threshold": 100 + } + }, + "align_optimal_1_6_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_6_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 6 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_6_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_6_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 6 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_6_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_6_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 6 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_6_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_1_6_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 6 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_1_6_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -366, + -166 + ], + "local": false + } + }, + "align_banded_1_6_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 6 + ], + "params": { + "gap_penalty": -10, + "band": [ + -366, + -166 + ], + "local": false + } + }, + "align_banded_1_6_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -366, + -166 + ], + "local": true + } + }, + "align_banded_1_6_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 6 + ], + "params": { + "gap_penalty": -10, + "band": [ + -366, + -166 + ], + "local": true + } + }, + "align_local_1_6_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 765, + 499 + ], + "threshold": 100 + } + }, + "align_local_1_6_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 6 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 765, + 499 + ], + "threshold": 100 + } + }, + "align_optimal_1_7_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_7_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 7 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_7_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_7_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 7 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_7_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_7_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 7 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_7_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_1_7_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 7 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_1_7_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -350, + -150 + ], + "local": false + } + }, + "align_banded_1_7_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 7 + ], + "params": { + "gap_penalty": -10, + "band": [ + -350, + -150 + ], + "local": false + } + }, + "align_banded_1_7_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -350, + -150 + ], + "local": true + } + }, + "align_banded_1_7_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 7 + ], + "params": { + "gap_penalty": -10, + "band": [ + -350, + -150 + ], + "local": true + } + }, + "align_local_1_7_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 770, + 520 + ], + "threshold": 100 + } + }, + "align_local_1_7_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 7 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 770, + 520 + ], + "threshold": 100 + } + }, + "align_optimal_1_8_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_8_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_8_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_8_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_8_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_8_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_8_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_1_8_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_1_8_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -350, + -150 + ], + "local": false + } + }, + "align_banded_1_8_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -350, + -150 + ], + "local": false + } + }, + "align_banded_1_8_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -350, + -150 + ], + "local": true + } + }, + "align_banded_1_8_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -350, + -150 + ], + "local": true + } + }, + "align_local_1_8_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 768, + 518 + ], + "threshold": 100 + } + }, + "align_local_1_8_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 8 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 768, + 518 + ], + "threshold": 100 + } + }, + "align_optimal_1_9_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_9_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_1_9_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_9_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_1_9_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_9_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_1_9_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_1_9_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 1, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_1_9_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -346, + -146 + ], + "local": false + } + }, + "align_banded_1_9_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -346, + -146 + ], + "local": false + } + }, + "align_banded_1_9_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -346, + -146 + ], + "local": true + } + }, + "align_banded_1_9_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 1, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -346, + -146 + ], + "local": true + } + }, + "align_local_1_9_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 784, + 538 + ], + "threshold": 100 + } + }, + "align_local_1_9_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 1, + 9 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 784, + 538 + ], + "threshold": 100 + } + }, + "align_optimal_2_3_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_2_3_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 3 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_2_3_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_2_3_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 3 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_2_3_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_2_3_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 3 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_2_3_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_2_3_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 3 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_2_3_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -79, + 121 + ], + "local": false + } + }, + "align_banded_2_3_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 3 + ], + "params": { + "gap_penalty": -10, + "band": [ + -79, + 121 + ], + "local": false + } + }, + "align_banded_2_3_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -79, + 121 + ], + "local": true + } + }, + "align_banded_2_3_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 3 + ], + "params": { + "gap_penalty": -10, + "band": [ + -79, + 121 + ], + "local": true + } + }, + "align_local_2_3_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 2, + 3 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 694, + 715 + ], + "threshold": 100 + } + }, + "align_local_2_3_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 2, + 3 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 694, + 715 + ], + "threshold": 100 + } + }, + "align_optimal_2_4_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_2_4_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 4 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_2_4_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_2_4_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 4 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_2_4_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_2_4_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 4 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_2_4_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_2_4_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 4 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_2_4_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -95, + 105 + ], + "local": false + } + }, + "align_banded_2_4_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 4 + ], + "params": { + "gap_penalty": -10, + "band": [ + -95, + 105 + ], + "local": false + } + }, + "align_banded_2_4_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -95, + 105 + ], + "local": true + } + }, + "align_banded_2_4_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 4 + ], + "params": { + "gap_penalty": -10, + "band": [ + -95, + 105 + ], + "local": true + } + }, + "align_local_2_4_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 2, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 664, + 669 + ], + "threshold": 100 + } + }, + "align_local_2_4_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 2, + 4 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 664, + 669 + ], + "threshold": 100 + } + }, + "align_optimal_2_5_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_2_5_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 5 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_2_5_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_2_5_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 5 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_2_5_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_2_5_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 5 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_2_5_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_2_5_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 5 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_2_5_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -380, + -180 + ], + "local": false + } + }, + "align_banded_2_5_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 5 + ], + "params": { + "gap_penalty": -10, + "band": [ + -380, + -180 + ], + "local": false + } + }, + "align_banded_2_5_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -380, + -180 + ], + "local": true + } + }, + "align_banded_2_5_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 5 + ], + "params": { + "gap_penalty": -10, + "band": [ + -380, + -180 + ], + "local": true + } + }, + "align_local_2_5_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 2, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 787, + 507 + ], + "threshold": 100 + } + }, + "align_local_2_5_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 2, + 5 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 787, + 507 + ], + "threshold": 100 + } + }, + "align_optimal_2_6_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_2_6_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 6 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_2_6_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_2_6_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 6 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_2_6_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_2_6_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 6 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_2_6_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_2_6_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 6 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_2_6_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -372, + -172 + ], + "local": false + } + }, + "align_banded_2_6_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 6 + ], + "params": { + "gap_penalty": -10, + "band": [ + -372, + -172 + ], + "local": false + } + }, + "align_banded_2_6_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -372, + -172 + ], + "local": true + } + }, + "align_banded_2_6_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 6 + ], + "params": { + "gap_penalty": -10, + "band": [ + -372, + -172 + ], + "local": true + } + }, + "align_local_2_6_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 2, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 778, + 506 + ], + "threshold": 100 + } + }, + "align_local_2_6_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 2, + 6 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 778, + 506 + ], + "threshold": 100 + } + }, + "align_optimal_2_7_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_2_7_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 7 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_2_7_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_2_7_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 7 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_2_7_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_2_7_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 7 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_2_7_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_2_7_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 7 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_2_7_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -359, + -159 + ], + "local": false + } + }, + "align_banded_2_7_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 7 + ], + "params": { + "gap_penalty": -10, + "band": [ + -359, + -159 + ], + "local": false + } + }, + "align_banded_2_7_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -359, + -159 + ], + "local": true + } + }, + "align_banded_2_7_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 7 + ], + "params": { + "gap_penalty": -10, + "band": [ + -359, + -159 + ], + "local": true + } + }, + "align_local_2_7_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 2, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 792, + 533 + ], + "threshold": 100 + } + }, + "align_local_2_7_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 2, + 7 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 792, + 533 + ], + "threshold": 100 + } + }, + "align_optimal_2_8_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_2_8_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_2_8_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_2_8_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_2_8_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_2_8_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_2_8_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_2_8_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_2_8_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -359, + -159 + ], + "local": false + } + }, + "align_banded_2_8_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -359, + -159 + ], + "local": false + } + }, + "align_banded_2_8_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -359, + -159 + ], + "local": true + } + }, + "align_banded_2_8_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -359, + -159 + ], + "local": true + } + }, + "align_local_2_8_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 2, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 793, + 534 + ], + "threshold": 100 + } + }, + "align_local_2_8_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 2, + 8 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 793, + 534 + ], + "threshold": 100 + } + }, + "align_optimal_2_9_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_2_9_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_2_9_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_2_9_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_2_9_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_2_9_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_2_9_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_2_9_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 2, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_2_9_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -358, + -158 + ], + "local": false + } + }, + "align_banded_2_9_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -358, + -158 + ], + "local": false + } + }, + "align_banded_2_9_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -358, + -158 + ], + "local": true + } + }, + "align_banded_2_9_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 2, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -358, + -158 + ], + "local": true + } + }, + "align_local_2_9_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 2, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 791, + 533 + ], + "threshold": 100 + } + }, + "align_local_2_9_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 2, + 9 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 791, + 533 + ], + "threshold": 100 + } + }, + "align_optimal_3_4_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_3_4_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 4 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_3_4_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_3_4_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 4 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_3_4_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_3_4_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 4 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_3_4_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_3_4_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 4 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_3_4_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -116, + 84 + ], + "local": false + } + }, + "align_banded_3_4_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 4 + ], + "params": { + "gap_penalty": -10, + "band": [ + -116, + 84 + ], + "local": false + } + }, + "align_banded_3_4_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -116, + 84 + ], + "local": true + } + }, + "align_banded_3_4_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 4 + ], + "params": { + "gap_penalty": -10, + "band": [ + -116, + 84 + ], + "local": true + } + }, + "align_local_3_4_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 3, + 4 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 685, + 669 + ], + "threshold": 100 + } + }, + "align_local_3_4_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 3, + 4 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 685, + 669 + ], + "threshold": 100 + } + }, + "align_optimal_3_5_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_3_5_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 5 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_3_5_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_3_5_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 5 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_3_5_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_3_5_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 5 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_3_5_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_3_5_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 5 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_3_5_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -401, + -201 + ], + "local": false + } + }, + "align_banded_3_5_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 5 + ], + "params": { + "gap_penalty": -10, + "band": [ + -401, + -201 + ], + "local": false + } + }, + "align_banded_3_5_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -401, + -201 + ], + "local": true + } + }, + "align_banded_3_5_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 5 + ], + "params": { + "gap_penalty": -10, + "band": [ + -401, + -201 + ], + "local": true + } + }, + "align_local_3_5_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 3, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 811, + 510 + ], + "threshold": 100 + } + }, + "align_local_3_5_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 3, + 5 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 811, + 510 + ], + "threshold": 100 + } + }, + "align_optimal_3_6_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_3_6_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 6 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_3_6_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_3_6_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 6 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_3_6_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_3_6_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 6 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_3_6_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_3_6_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 6 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_3_6_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -393, + -193 + ], + "local": false + } + }, + "align_banded_3_6_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 6 + ], + "params": { + "gap_penalty": -10, + "band": [ + -393, + -193 + ], + "local": false + } + }, + "align_banded_3_6_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -393, + -193 + ], + "local": true + } + }, + "align_banded_3_6_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 6 + ], + "params": { + "gap_penalty": -10, + "band": [ + -393, + -193 + ], + "local": true + } + }, + "align_local_3_6_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 3, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 799, + 506 + ], + "threshold": 100 + } + }, + "align_local_3_6_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 3, + 6 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 799, + 506 + ], + "threshold": 100 + } + }, + "align_optimal_3_7_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_3_7_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 7 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_3_7_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_3_7_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 7 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_3_7_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_3_7_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 7 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_3_7_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_3_7_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 7 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_3_7_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -380, + -180 + ], + "local": false + } + }, + "align_banded_3_7_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 7 + ], + "params": { + "gap_penalty": -10, + "band": [ + -380, + -180 + ], + "local": false + } + }, + "align_banded_3_7_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -380, + -180 + ], + "local": true + } + }, + "align_banded_3_7_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 7 + ], + "params": { + "gap_penalty": -10, + "band": [ + -380, + -180 + ], + "local": true + } + }, + "align_local_3_7_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 3, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 810, + 530 + ], + "threshold": 100 + } + }, + "align_local_3_7_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 3, + 7 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 810, + 530 + ], + "threshold": 100 + } + }, + "align_optimal_3_8_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_3_8_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_3_8_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_3_8_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_3_8_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_3_8_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_3_8_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_3_8_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_3_8_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -380, + -180 + ], + "local": false + } + }, + "align_banded_3_8_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -380, + -180 + ], + "local": false + } + }, + "align_banded_3_8_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -380, + -180 + ], + "local": true + } + }, + "align_banded_3_8_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -380, + -180 + ], + "local": true + } + }, + "align_local_3_8_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 3, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 811, + 531 + ], + "threshold": 100 + } + }, + "align_local_3_8_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 3, + 8 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 811, + 531 + ], + "threshold": 100 + } + }, + "align_optimal_3_9_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_3_9_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_3_9_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_3_9_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_3_9_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_3_9_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_3_9_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_3_9_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 3, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_3_9_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -379, + -179 + ], + "local": false + } + }, + "align_banded_3_9_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -379, + -179 + ], + "local": false + } + }, + "align_banded_3_9_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -379, + -179 + ], + "local": true + } + }, + "align_banded_3_9_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 3, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -379, + -179 + ], + "local": true + } + }, + "align_local_3_9_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 3, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 813, + 534 + ], + "threshold": 100 + } + }, + "align_local_3_9_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 3, + 9 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 813, + 534 + ], + "threshold": 100 + } + }, + "align_optimal_4_5_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_4_5_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 5 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_4_5_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_4_5_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 5 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_4_5_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_4_5_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 5 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_4_5_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_4_5_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 5 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_4_5_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -386, + -186 + ], + "local": false + } + }, + "align_banded_4_5_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 5 + ], + "params": { + "gap_penalty": -10, + "band": [ + -386, + -186 + ], + "local": false + } + }, + "align_banded_4_5_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -386, + -186 + ], + "local": true + } + }, + "align_banded_4_5_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 5 + ], + "params": { + "gap_penalty": -10, + "band": [ + -386, + -186 + ], + "local": true + } + }, + "align_local_4_5_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 4, + 5 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 778, + 492 + ], + "threshold": 100 + } + }, + "align_local_4_5_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 4, + 5 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 778, + 492 + ], + "threshold": 100 + } + }, + "align_optimal_4_6_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_4_6_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 6 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_4_6_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_4_6_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 6 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_4_6_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_4_6_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 6 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_4_6_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_4_6_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 6 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_4_6_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -362, + -162 + ], + "local": false + } + }, + "align_banded_4_6_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 6 + ], + "params": { + "gap_penalty": -10, + "band": [ + -362, + -162 + ], + "local": false + } + }, + "align_banded_4_6_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -362, + -162 + ], + "local": true + } + }, + "align_banded_4_6_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 6 + ], + "params": { + "gap_penalty": -10, + "band": [ + -362, + -162 + ], + "local": true + } + }, + "align_local_4_6_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 4, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 770, + 508 + ], + "threshold": 100 + } + }, + "align_local_4_6_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 4, + 6 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 770, + 508 + ], + "threshold": 100 + } + }, + "align_optimal_4_7_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_4_7_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 7 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_4_7_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_4_7_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 7 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_4_7_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_4_7_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 7 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_4_7_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_4_7_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 7 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_4_7_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -361, + -161 + ], + "local": false + } + }, + "align_banded_4_7_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 7 + ], + "params": { + "gap_penalty": -10, + "band": [ + -361, + -161 + ], + "local": false + } + }, + "align_banded_4_7_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -361, + -161 + ], + "local": true + } + }, + "align_banded_4_7_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 7 + ], + "params": { + "gap_penalty": -10, + "band": [ + -361, + -161 + ], + "local": true + } + }, + "align_local_4_7_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 4, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 785, + 524 + ], + "threshold": 100 + } + }, + "align_local_4_7_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 4, + 7 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 785, + 524 + ], + "threshold": 100 + } + }, + "align_optimal_4_8_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_4_8_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_4_8_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_4_8_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_4_8_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_4_8_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_4_8_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_4_8_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_4_8_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -361, + -161 + ], + "local": false + } + }, + "align_banded_4_8_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -361, + -161 + ], + "local": false + } + }, + "align_banded_4_8_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -361, + -161 + ], + "local": true + } + }, + "align_banded_4_8_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -361, + -161 + ], + "local": true + } + }, + "align_local_4_8_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 4, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 785, + 524 + ], + "threshold": 100 + } + }, + "align_local_4_8_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 4, + 8 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 785, + 524 + ], + "threshold": 100 + } + }, + "align_optimal_4_9_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_4_9_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_4_9_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_4_9_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_4_9_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_4_9_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_4_9_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_4_9_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 4, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_4_9_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -355, + -155 + ], + "local": false + } + }, + "align_banded_4_9_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -355, + -155 + ], + "local": false + } + }, + "align_banded_4_9_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -355, + -155 + ], + "local": true + } + }, + "align_banded_4_9_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 4, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -355, + -155 + ], + "local": true + } + }, + "align_local_4_9_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 4, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 770, + 515 + ], + "threshold": 100 + } + }, + "align_local_4_9_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 4, + 9 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 770, + 515 + ], + "threshold": 100 + } + }, + "align_optimal_5_6_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_5_6_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 6 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_5_6_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_5_6_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 6 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_5_6_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_5_6_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 6 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_5_6_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_5_6_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 6 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_5_6_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -76, + 124 + ], + "local": false + } + }, + "align_banded_5_6_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 6 + ], + "params": { + "gap_penalty": -10, + "band": [ + -76, + 124 + ], + "local": false + } + }, + "align_banded_5_6_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -76, + 124 + ], + "local": true + } + }, + "align_banded_5_6_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 6 + ], + "params": { + "gap_penalty": -10, + "band": [ + -76, + 124 + ], + "local": true + } + }, + "align_local_5_6_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 5, + 6 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 505, + 529 + ], + "threshold": 100 + } + }, + "align_local_5_6_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 5, + 6 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 505, + 529 + ], + "threshold": 100 + } + }, + "align_optimal_5_7_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_5_7_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 7 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_5_7_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_5_7_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 7 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_5_7_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_5_7_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 7 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_5_7_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_5_7_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 7 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_5_7_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -67, + 133 + ], + "local": false + } + }, + "align_banded_5_7_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 7 + ], + "params": { + "gap_penalty": -10, + "band": [ + -67, + 133 + ], + "local": false + } + }, + "align_banded_5_7_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -67, + 133 + ], + "local": true + } + }, + "align_banded_5_7_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 7 + ], + "params": { + "gap_penalty": -10, + "band": [ + -67, + 133 + ], + "local": true + } + }, + "align_local_5_7_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 5, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 513, + 546 + ], + "threshold": 100 + } + }, + "align_local_5_7_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 5, + 7 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 513, + 546 + ], + "threshold": 100 + } + }, + "align_optimal_5_8_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_5_8_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_5_8_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_5_8_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_5_8_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_5_8_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_5_8_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_5_8_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_5_8_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -67, + 133 + ], + "local": false + } + }, + "align_banded_5_8_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -67, + 133 + ], + "local": false + } + }, + "align_banded_5_8_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -67, + 133 + ], + "local": true + } + }, + "align_banded_5_8_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -67, + 133 + ], + "local": true + } + }, + "align_local_5_8_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 5, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 513, + 546 + ], + "threshold": 100 + } + }, + "align_local_5_8_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 5, + 8 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 513, + 546 + ], + "threshold": 100 + } + }, + "align_optimal_5_9_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_5_9_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_5_9_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_5_9_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_5_9_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_5_9_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_5_9_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_5_9_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 5, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_5_9_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -58, + 142 + ], + "local": false + } + }, + "align_banded_5_9_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -58, + 142 + ], + "local": false + } + }, + "align_banded_5_9_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -58, + 142 + ], + "local": true + } + }, + "align_banded_5_9_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 5, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -58, + 142 + ], + "local": true + } + }, + "align_local_5_9_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 5, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 520, + 562 + ], + "threshold": 100 + } + }, + "align_local_5_9_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 5, + 9 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 520, + 562 + ], + "threshold": 100 + } + }, + "align_optimal_6_7_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_6_7_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 7 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_6_7_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_6_7_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 7 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_6_7_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_6_7_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 7 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_6_7_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_6_7_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 7 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_6_7_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 6, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -92, + 108 + ], + "local": false + } + }, + "align_banded_6_7_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 6, + 7 + ], + "params": { + "gap_penalty": -10, + "band": [ + -92, + 108 + ], + "local": false + } + }, + "align_banded_6_7_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 6, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -92, + 108 + ], + "local": true + } + }, + "align_banded_6_7_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 6, + 7 + ], + "params": { + "gap_penalty": -10, + "band": [ + -92, + 108 + ], + "local": true + } + }, + "align_local_6_7_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 6, + 7 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 526, + 534 + ], + "threshold": 100 + } + }, + "align_local_6_7_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 6, + 7 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 526, + 534 + ], + "threshold": 100 + } + }, + "align_optimal_6_8_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_6_8_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_6_8_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_6_8_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_6_8_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_6_8_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_6_8_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_6_8_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_6_8_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 6, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -92, + 108 + ], + "local": false + } + }, + "align_banded_6_8_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 6, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -92, + 108 + ], + "local": false + } + }, + "align_banded_6_8_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 6, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -92, + 108 + ], + "local": true + } + }, + "align_banded_6_8_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 6, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -92, + 108 + ], + "local": true + } + }, + "align_local_6_8_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 6, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 526, + 534 + ], + "threshold": 100 + } + }, + "align_local_6_8_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 6, + 8 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 526, + 534 + ], + "threshold": 100 + } + }, + "align_optimal_6_9_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_6_9_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_6_9_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_6_9_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_6_9_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_6_9_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_6_9_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_6_9_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 6, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_6_9_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 6, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -88, + 112 + ], + "local": false + } + }, + "align_banded_6_9_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 6, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -88, + 112 + ], + "local": false + } + }, + "align_banded_6_9_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 6, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -88, + 112 + ], + "local": true + } + }, + "align_banded_6_9_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 6, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -88, + 112 + ], + "local": true + } + }, + "align_local_6_9_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 6, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 536, + 548 + ], + "threshold": 100 + } + }, + "align_local_6_9_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 6, + 9 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 536, + 548 + ], + "threshold": 100 + } + }, + "align_optimal_7_8_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_7_8_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_7_8_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_7_8_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 8 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_7_8_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_7_8_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_7_8_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_7_8_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 8 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_7_8_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 7, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -100, + 100 + ], + "local": false + } + }, + "align_banded_7_8_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 7, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -100, + 100 + ], + "local": false + } + }, + "align_banded_7_8_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 7, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -100, + 100 + ], + "local": true + } + }, + "align_banded_7_8_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 7, + 8 + ], + "params": { + "gap_penalty": -10, + "band": [ + -100, + 100 + ], + "local": true + } + }, + "align_local_7_8_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 7, + 8 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 541, + 541 + ], + "threshold": 100 + } + }, + "align_local_7_8_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 7, + 8 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 541, + 541 + ], + "threshold": 100 + } + }, + "align_optimal_7_9_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_7_9_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_7_9_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_7_9_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_7_9_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_7_9_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_7_9_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_7_9_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 7, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_7_9_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 7, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -90, + 110 + ], + "local": false + } + }, + "align_banded_7_9_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 7, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -90, + 110 + ], + "local": false + } + }, + "align_banded_7_9_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 7, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -90, + 110 + ], + "local": true + } + }, + "align_banded_7_9_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 7, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -90, + 110 + ], + "local": true + } + }, + "align_local_7_9_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 7, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 547, + 557 + ], + "threshold": 100 + } + }, + "align_local_7_9_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 7, + 9 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 547, + 557 + ], + "threshold": 100 + } + }, + "align_optimal_8_9_global_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 8, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_8_9_global_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 8, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": false + } + }, + "align_optimal_8_9_global_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 8, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_8_9_global_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 8, + 9 + ], + "params": { + "gap_penalty": -10, + "local": false, + "terminal_penalty": true + } + }, + "align_optimal_8_9_local_affine.fasta": { + "method": "align_optimal", + "seq_indices": [ + 8, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_8_9_local_linear.fasta": { + "method": "align_optimal", + "seq_indices": [ + 8, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": false + } + }, + "align_optimal_8_9_local_affine_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 8, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "local": true, + "terminal_penalty": true + } + }, + "align_optimal_8_9_local_linear_terminal.fasta": { + "method": "align_optimal", + "seq_indices": [ + 8, + 9 + ], + "params": { + "gap_penalty": -10, + "local": true, + "terminal_penalty": true + } + }, + "align_banded_8_9_global_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 8, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -90, + 110 + ], + "local": false + } + }, + "align_banded_8_9_global_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 8, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -90, + 110 + ], + "local": false + } + }, + "align_banded_8_9_local_affine.fasta": { + "method": "align_banded", + "seq_indices": [ + 8, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "band": [ + -90, + 110 + ], + "local": true + } + }, + "align_banded_8_9_local_linear.fasta": { + "method": "align_banded", + "seq_indices": [ + 8, + 9 + ], + "params": { + "gap_penalty": -10, + "band": [ + -90, + 110 + ], + "local": true + } + }, + "align_local_8_9_affine.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 8, + 9 + ], + "params": { + "gap_penalty": [ + -10, + -1 + ], + "seed": [ + 548, + 558 + ], + "threshold": 100 + } + }, + "align_local_8_9_linear.fasta": { + "method": "align_local_gapped", + "seq_indices": [ + 8, + 9 + ], + "params": { + "gap_penalty": -10, + "seed": [ + 548, + 558 + ], + "threshold": 100 + } + } +} \ No newline at end of file From 3c0d325ca3703e9425e0f67ddf3ac5446cd6b8f5 Mon Sep 17 00:00:00 2001 From: Patrick Kunzmann Date: Thu, 16 Apr 2026 10:54:36 +0200 Subject: [PATCH 2/6] Migrate alignment functions to Rust --- Cargo.toml | 1 + doc/apidoc.json | 1 + .../sequence/homology/genome_comparison.py | 8 +- .../structure/alphabet/structure_search.py | 5 +- doc/tutorial/sequence/align_heuristic.rst | 9 +- src/biotite/sequence/align/banded.py | 261 +++++ src/biotite/sequence/align/banded.pyi | 16 - src/biotite/sequence/align/banded.pyx | 652 ------------- src/biotite/sequence/align/localgapped.py | 311 ++++++ src/biotite/sequence/align/localgapped.pyi | 34 - src/biotite/sequence/align/localgapped.pyx | 892 ------------------ src/biotite/sequence/align/localungapped.py | 92 ++ src/biotite/sequence/align/localungapped.pyi | 39 +- src/biotite/sequence/align/localungapped.pyx | 279 ------ src/biotite/sequence/align/pairwise.py | 291 ++++++ src/biotite/sequence/align/pairwise.pyi | 31 - src/biotite/sequence/align/pairwise.pyx | 585 ------------ src/biotite/sequence/align/tracetable.pxd | 64 -- src/biotite/sequence/align/tracetable.pyx | 370 -------- src/rust/sequence/align/banded.rs | 638 +++++++++++++ src/rust/sequence/align/cell.rs | 394 ++++++++ src/rust/sequence/align/dispatch.rs | 146 --- src/rust/sequence/align/localgapped.rs | 642 +++++++++++++ src/rust/sequence/align/localungapped.rs | 290 ++++++ src/rust/sequence/align/mod.rs | 20 + src/rust/sequence/align/pairwise.rs | 882 +++++++++++++++++ src/rust/sequence/align/scoring.rs | 147 +++ src/rust/sequence/align/symbol.rs | 26 + src/rust/sequence/align/table.rs | 150 +++ src/rust/sequence/align/trace.rs | 142 +++ src/rust/sequence/mod.rs | 7 + tests/sequence/align/test_localungapped.py | 13 +- 32 files changed, 4344 insertions(+), 3094 deletions(-) create mode 100644 src/biotite/sequence/align/banded.py delete mode 100644 src/biotite/sequence/align/banded.pyi delete mode 100644 src/biotite/sequence/align/banded.pyx create mode 100644 src/biotite/sequence/align/localgapped.py delete mode 100644 src/biotite/sequence/align/localgapped.pyi delete mode 100644 src/biotite/sequence/align/localgapped.pyx create mode 100644 src/biotite/sequence/align/localungapped.py delete mode 100644 src/biotite/sequence/align/localungapped.pyx create mode 100644 src/biotite/sequence/align/pairwise.py delete mode 100644 src/biotite/sequence/align/pairwise.pyi delete mode 100644 src/biotite/sequence/align/pairwise.pyx delete mode 100644 src/biotite/sequence/align/tracetable.pxd delete mode 100644 src/biotite/sequence/align/tracetable.pyx create mode 100644 src/rust/sequence/align/banded.rs create mode 100644 src/rust/sequence/align/cell.rs delete mode 100644 src/rust/sequence/align/dispatch.rs create mode 100644 src/rust/sequence/align/localgapped.rs create mode 100644 src/rust/sequence/align/localungapped.rs create mode 100644 src/rust/sequence/align/mod.rs create mode 100644 src/rust/sequence/align/pairwise.rs create mode 100644 src/rust/sequence/align/scoring.rs create mode 100644 src/rust/sequence/align/symbol.rs create mode 100644 src/rust/sequence/align/table.rs create mode 100644 src/rust/sequence/align/trace.rs diff --git a/Cargo.toml b/Cargo.toml index 7f93f8550..26ff34e89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ ndarray = "0.17" num-traits = "0.2" smallvec = "1" itertools = "0.14" +bitflags = "2" [dependencies.pyo3] version = "0.27" diff --git a/doc/apidoc.json b/doc/apidoc.json index 3dfdf149a..f7883c136 100644 --- a/doc/apidoc.json +++ b/doc/apidoc.json @@ -151,6 +151,7 @@ "Aligners" : [ "align_ungapped", "align_optimal", + "SeedExtension", "align_local_ungapped", "align_local_gapped", "align_banded", diff --git a/doc/examples/scripts/sequence/homology/genome_comparison.py b/doc/examples/scripts/sequence/homology/genome_comparison.py index 63f1114c0..9195e8219 100644 --- a/doc/examples/scripts/sequence/homology/genome_comparison.py +++ b/doc/examples/scripts/sequence/homology/genome_comparison.py @@ -128,7 +128,7 @@ ######################################################################## # The next step is a local ungapped alignment at the positions of the -# double hits using :func:`align_local_ungapped()`: +# double hits using :class:`SeedExtension`: # For each hit, the alignment is extended into both directions from the # match until the similarity score drops more than a given threshold # below the maximum score found. @@ -139,19 +139,17 @@ # As a result, the ``score_only=True`` parameter increases the # performance drastically. - X_DROP = 20 ACCEPT_THRESHOLD = 100 matrix = align.SubstitutionMatrix.std_nucleotide_matrix() +extension = align.SeedExtension(matrix, threshold=X_DROP) ungapped_scores = np.array( [ - align.align_local_ungapped( + extension.align( chloroplast_seq, bacterium_seqs[strand], - matrix, seed=(i, j), - threshold=X_DROP, score_only=True, ) for i, strand, j in double_hits diff --git a/doc/examples/scripts/structure/alphabet/structure_search.py b/doc/examples/scripts/structure/alphabet/structure_search.py index b69d982c8..5db9c315a 100644 --- a/doc/examples/scripts/structure/alphabet/structure_search.py +++ b/doc/examples/scripts/structure/alphabet/structure_search.py @@ -276,14 +276,13 @@ def filter_undefined_spans(sequence, min_length): # We only keep those hits that meet the given score threshold. substitution_matrix = align.SubstitutionMatrix.std_3di_matrix() +extension = align.SeedExtension(substitution_matrix, threshold=X_DROP) ungapped_scores = np.array( [ - align.align_local_ungapped( + extension.align( query_sequence, db_sequences[db_index], - substitution_matrix, seed=(i, j), - threshold=X_DROP, score_only=True, ) for i, db_index, j in double_hits diff --git a/doc/tutorial/sequence/align_heuristic.rst b/doc/tutorial/sequence/align_heuristic.rst index 5e06883b5..6ceb34cde 100644 --- a/doc/tutorial/sequence/align_heuristic.rst +++ b/doc/tutorial/sequence/align_heuristic.rst @@ -160,12 +160,11 @@ In other words the alignment comprises only a region that is decently similar. THRESHOLD = 20 matrix = align.SubstitutionMatrix.std_protein_matrix() + # The scoring scheme is preprocessed once and reused for each seed + extension = align.SeedExtension(matrix, threshold=THRESHOLD) alignments = [] for query_pos, ref_id, ref_pos in matches: - alignment = align.align_local_ungapped( - reference, query, matrix, - seed=(ref_pos, query_pos), threshold=THRESHOLD - ) + alignment = extension.align(reference, query, seed=(ref_pos, query_pos)) alignments.append(alignment) for alignment in alignments: @@ -190,7 +189,7 @@ The difference between the heuristic gapped sequence alignment methods and :meth:`align_optimal()` is that the former only ideally traverses through a small fraction of the possible alignment search space, allowing them to run much faster. -However, like :meth:`align_local_ungapped()` they need to be informed with a +However, like :class:`SeedExtension` they need to be informed with a match position to start from. Furthermore, in some cases they might not find the optimal alignment, when the assumption of the method does not hold for such alignment. diff --git a/src/biotite/sequence/align/banded.py b/src/biotite/sequence/align/banded.py new file mode 100644 index 000000000..9afe78ccb --- /dev/null +++ b/src/biotite/sequence/align/banded.py @@ -0,0 +1,261 @@ +# This source code is part of the Biotite package and is distributed +# under the 3-Clause BSD License. Please see 'LICENSE.rst' for further +# information. + +from __future__ import annotations + +__name__ = "biotite.sequence.align" +__author__ = "Patrick Kunzmann" +__all__ = ["align_banded"] + +from typing import TYPE_CHECKING, Literal, overload +import numpy as np +from biotite.rust.sequence.align import align_banded as _rust_align_banded +from biotite.sequence.align.alignment import Alignment +from biotite.sequence.align.matrix import SubstitutionMatrix +from biotite.sequence.align.pairwise import prepare_scoring + +if TYPE_CHECKING: + from biotite.sequence.sequence import Sequence + from biotite.typing import S1, S2 + + +@overload +def align_banded( + seq1: Sequence[S1], + seq2: Sequence[S2], + matrix: SubstitutionMatrix[S1, S2] | tuple[int, int], + band: tuple[int, int], + gap_penalty: int | tuple[int, int] = -10, + local: bool = False, + max_number: int = 1000, + score_only: Literal[False] = False, +) -> list[Alignment]: ... +@overload +def align_banded( + seq1: Sequence[S1], + seq2: Sequence[S2], + matrix: SubstitutionMatrix[S1, S2] | tuple[int, int], + band: tuple[int, int], + gap_penalty: int | tuple[int, int] = -10, + local: bool = False, + max_number: int = 1000, + *, + score_only: Literal[True], +) -> int: ... +def align_banded( + seq1: Sequence, + seq2: Sequence, + matrix: SubstitutionMatrix | tuple[int, int], + band: tuple[int, int], + gap_penalty: int | tuple[int, int] = -10, + local: bool = False, + max_number: int = 1000, + score_only: bool = False, +) -> list[Alignment] | int: + r""" + Perform a local or semi-global alignment within a defined diagonal + band. :footcite:`Pearson1988` + + The function requires two diagonals that defines the lower + and upper limit of the alignment band. + A diagonal is an integer defined as :math:`D = j - i`, where *i* and + *j* are sequence positions in the first and second sequence, + respectively. + This means that two symbols at position *i* and *j* can only be + aligned to each other, if :math:`D_L \leq j - i \leq D_U`. + With increasing width of the diagonal band, the probability to find + the optimal alignment, but also the computation time increases. + + Parameters + ---------- + seq1, seq2 : Sequence + The sequences to be aligned. + matrix : SubstitutionMatrix or tuple(int, int) + Either a substitution matrix or a ``(match, mismatch)`` pair of scores. + band : tuple(int, int) + The diagonals that represent the lower and upper limit of the + search space. + A diagonal :math:`D` is defined as :math:`D = j-i`, where + :math:`i` and :math:`j` are positions in `seq1` and `seq2`, + respectively. + An alignment of sequence positions where :math:`D` is lower than + the lower limit or greater than the upper limit is not explored + by the algorithm. + gap_penalty : int or tuple(int, int), optional + If an integer is provided, the value will be interpreted as + linear gap penalty. + If a tuple is provided, an affine gap penalty is used. + The first integer in the tuple is the gap opening penalty, + the second integer is the gap extension penalty. + The values need to be negative. + local : bool, optional + If set to true, a local alignment is performed. + Otherwise (default) a semi-global alignment is performed. + max_number : int, optional + The maximum number of alignments returned. + When the number of branches exceeds this value in the traceback + step, no further branches are created. + score_only : bool, optional + If true, only the similarity score is returned instead of the + list of alignments. + + Returns + ------- + alignments : list of Alignment or int + The generated alignments. + Each alignment in the list has the same similarity score, + which is the maximum score possible within the defined band. + If `score_only` is set to true, only the score is returned. + + See Also + -------- + align_optimal + Guarantees to find the optimal alignment at the cost of greater + compuation time and memory requirements. + + Notes + ----- + The diagonals give the maximum difference between the + number of inserted gaps. + This means for any position in the alignment, the algorithm + will not consider inserting a gap into a sequence, if the first + sequence has already ``-band[0]`` more gaps than the second + sequence or if the second sequence has already ``band[1]`` more gaps + than the first sequence, even if inserting additional gaps would + yield a more optimal alignment. + Considerations on how to find a suitable band width are discussed in + :footcite:`Gibrat2018`. + + The restriction to a limited band is the central difference between + the banded alignment heuristic and the optimal alignment + algorithms :footcite:`Needleman1970, Smith1981`. + Those classical algorithms require :math:`O(m \cdot n)` + memory space and computation time for aligning two sequences with + lengths :math:`m` and :math:`n`, respectively. + The banded alignment algorithm reduces both requirements to + :math:`O(\min(m,n) \cdot (D_U - D_L))`. + + *Implementation details* + + The implementation is very similar to :func:`align_optimal()`. + The most significant difference is that not the complete alignment + table is filled, but only the cells that lie within the diagonal + band. + Furthermore, to reduce also the space requirements the diagnoal band + is 'straightened', i.e. the table's rows are indented to the left. + Hence, this table + + = = = = = = = = = = + . . x x x . . . . . + . . . x x x . . . . + . . . . x x x . . . + . . . . . x x x . . + . . . . . . x x x . + = = = = = = = = = = + + is transformed into this table: + + = = = + x x x + x x x + x x x + x x x + x x x + = = = + + Filled cells, i.e. cells within the band, are indicated by ``x``. + The shorter sequence is always represented by the first dimension + of the table in this implementation. + + References + ---------- + + .. footbibliography:: + + Examples + -------- + + Find a matching diagonal for two sequences: + + >>> sequence1 = NucleotideSequence("GCGCGCTATATTATGCGCGC") + >>> sequence2 = NucleotideSequence("TATAAT") + >>> table = KmerTable.from_sequences(k=4, sequences=[sequence1]) + >>> match = table.match(sequence2)[0] + >>> diagonal = match[0] - match[2] + >>> print(diagonal) + -6 + + Align the sequences centered on the diagonal with buffer in both + directions: + + >>> BUFFER = 5 + >>> matrix = SubstitutionMatrix.std_nucleotide_matrix() + >>> alignment = align_banded( + ... sequence1, sequence2, matrix, + ... band=(diagonal - BUFFER, diagonal + BUFFER), gap_penalty=(-6, -1) + ... )[0] + >>> print(alignment) + TATATTAT + TATA--AT + """ + # Check if gap penalty is linear or affine + if isinstance(gap_penalty, int): + if gap_penalty > 0: + raise ValueError("Gap penalty must be negative") + elif isinstance(gap_penalty, tuple): + if gap_penalty[0] > 0 or gap_penalty[1] > 0: + raise ValueError("Gap penalty must be negative") + else: + raise TypeError("Gap penalty must be either integer or tuple") + # Check if max_number is reasonable + if max_number < 1: + raise ValueError("Maximum number of returned alignments must be at least 1") + + # The shorter sequence is the one on the left of the matrix + # -> shorter sequence is 'seq1' + if len(seq2) < len(seq1): + seq1, seq2 = seq2, seq1 + band = (-band[0], -band[1]) + if isinstance(matrix, SubstitutionMatrix): + matrix = matrix.transpose() + is_swapped = True + else: + is_swapped = False + lower_diag, upper_diag = min(band), max(band) + if len(seq1) + upper_diag <= 0 or lower_diag >= len(seq2): + raise ValueError( + "Alignment band is out of range, the band allows no overlap " + "between both sequences" + ) + # Crop band diagonals to reasonable size, so that it at maximum + # covers the search space of an unbanded alignment + lower_diag = max(lower_diag, -len(seq1) + 1) + upper_diag = min(upper_diag, len(seq2) - 1) + if upper_diag - lower_diag + 1 < 1: + raise ValueError("The width of the band is 0") + + # The two sequences may use different code dtypes + # -> widen both to a common type so the Rust layer dispatches once + common_dtype = np.promote_types(seq1.code.dtype, seq2.code.dtype) + + traces, score = _rust_align_banded( + np.ascontiguousarray(seq1.code, common_dtype), + np.ascontiguousarray(seq2.code, common_dtype), + prepare_scoring(matrix, seq1, seq2), + gap_penalty, + lower_diag, + upper_diag, + local, + score_only, + max_number, + ) + + if score_only: + return score + if is_swapped: + return [ + Alignment([seq2, seq1], np.flip(trace, axis=1), score) for trace in traces + ] + else: + return [Alignment([seq1, seq2], trace, score) for trace in traces] diff --git a/src/biotite/sequence/align/banded.pyi b/src/biotite/sequence/align/banded.pyi deleted file mode 100644 index 60d68b964..000000000 --- a/src/biotite/sequence/align/banded.pyi +++ /dev/null @@ -1,16 +0,0 @@ -__all__ = ["align_banded"] - -from biotite.sequence.align.alignment import Alignment -from biotite.sequence.align.matrix import SubstitutionMatrix -from biotite.sequence.sequence import Sequence -from biotite.typing import S1, S2 - -def align_banded( - seq1: Sequence[S1], - seq2: Sequence[S2], - matrix: SubstitutionMatrix[S1, S2], - band: tuple[int, int], - gap_penalty: int | tuple[int, int] = -10, - local: bool = False, - max_number: int = 1000, -) -> list[Alignment]: ... diff --git a/src/biotite/sequence/align/banded.pyx b/src/biotite/sequence/align/banded.pyx deleted file mode 100644 index 2d1dca205..000000000 --- a/src/biotite/sequence/align/banded.pyx +++ /dev/null @@ -1,652 +0,0 @@ -# This source code is part of the Biotite package and is distributed -# under the 3-Clause BSD License. Please see 'LICENSE.rst' for further -# information. - -__name__ = "biotite.sequence.align" -__author__ = "Patrick Kunzmann" -__all__ = ["align_banded"] - -cimport cython -cimport numpy as np -from .tracetable cimport follow_trace, get_trace_linear, get_trace_affine, \ - TraceDirectionAffine, TraceState - -from .matrix import SubstitutionMatrix -from ..sequence import Sequence -from .alignment import Alignment -import numpy as np - - -ctypedef np.int32_t int32 -ctypedef np.int64_t int64 -ctypedef np.uint8_t uint8 -ctypedef np.uint16_t uint16 -ctypedef np.uint32_t uint32 -ctypedef np.uint64_t uint64 - -ctypedef fused CodeType1: - uint8 - uint16 - uint32 - uint64 -ctypedef fused CodeType2: - uint8 - uint16 - uint32 - uint64 - - -def align_banded(seq1, seq2, matrix, band, gap_penalty=-10, local=False, - max_number=1000): - """ - align_banded(seq1, seq2, matrix, band, gap_penalty=-10, local=False, - max_number=1000) - - Perform a local or semi-global alignment within a defined diagonal - band. :footcite:`Pearson1988` - - The function requires two diagonals that defines the lower - and upper limit of the alignment band. - A diagonal is an integer defined as :math:`D = j - i`, where *i* and - *j* are sequence positions in the first and second sequence, - respectively. - This means that two symbols at position *i* and *j* can only be - aligned to each other, if :math:`D_L \leq j - i \leq D_U`. - With increasing width of the diagonal band, the probability to find - the optimal alignment, but also the computation time increases. - - Parameters - ---------- - seq1, seq2 : Sequence - The sequences to be aligned. - matrix : SubstitutionMatrix - The substitution matrix used for scoring. - band : tuple(int, int) - The diagonals that represent the lower and upper limit of the - search space. - A diagonal :math:`D` is defined as :math:`D = j-i`, where - :math:`i` and :math:`j` are positions in `seq1` and `seq2`, - respectively. - An alignment of sequence positions where :math:`D` is lower than - the lower limit or greater than the upper limit is not explored - by the algorithm. - gap_penalty : int or tuple(int, int), optional - If an integer is provided, the value will be interpreted as - linear gap penalty. - If a tuple is provided, an affine gap penalty is used. - The first integer in the tuple is the gap opening penalty, - the second integer is the gap extension penalty. - The values need to be negative. - local : bool, optional - If set to true, a local alignment is performed. - Otherwise (default) a semi-global alignment is performed. - max_number : int, optional - The maximum number of alignments returned. - When the number of branches exceeds this value in the traceback - step, no further branches are created. - - Returns - ------- - alignments : list of Alignment - The generated alignments. - Each alignment in the list has the same similarity score, - which is the maximum score possible within the defined band. - - See Also - -------- - align_optimal - Guarantees to find the optimal alignment at the cost of greater - compuation time and memory requirements. - - Notes - ----- - The diagonals give the maximum difference between the - number of inserted gaps. - This means for any position in the alignment, the algorithm - will not consider inserting a gap into a sequence, if the first - sequence has already ``-band[0]`` more gaps than the second - sequence or if the second sequence has already ``band[1]`` more gaps - than the first sequence, even if inserting additional gaps would - yield a more optimal alignment. - Considerations on how to find a suitable band width are discussed in - :footcite:`Gibrat2018`. - - The restriction to a limited band is the central difference between - the banded alignment heuristic and the optimal alignment - algorithms :footcite:`Needleman1970, Smith1981`. - Those classical algorithms require :math:`O(m \cdot n)` - memory space and computation time for aligning two sequences with - lengths :math:`m` and :math:`n`, respectively. - The banded alignment algorithm reduces both requirements to - :math:`O(\min(m,n) \cdot (D_U - D_L))`. - - *Implementation details* - - The implementation is very similar to :func:`align_optimal()`. - The most significant difference is that not the complete alignment - table is filled, but only the cells that lie within the diagonal - band. - Furthermore, to reduce also the space requirements the diagnoal band - is 'straightened', i.e. the table's rows are indented to the left. - Hence, this table - - = = = = = = = = = = - . . x x x . . . . . - . . . x x x . . . . - . . . . x x x . . . - . . . . . x x x . . - . . . . . . x x x . - = = = = = = = = = = - - is transformed into this table: - - = = = - x x x - x x x - x x x - x x x - x x x - = = = - - Filled cells, i.e. cells within the band, are indicated by ``x``. - The shorter sequence is always represented by the first dimension - of the table in this implementation. - - References - ---------- - - .. footbibliography:: - - Examples - -------- - - Find a matching diagonal for two sequences: - - >>> sequence1 = NucleotideSequence("GCGCGCTATATTATGCGCGC") - >>> sequence2 = NucleotideSequence("TATAAT") - >>> table = KmerTable.from_sequences(k=4, sequences=[sequence1]) - >>> match = table.match(sequence2)[0] - >>> diagonal = match[0] - match[2] - >>> print(diagonal) - -6 - - Align the sequences centered on the diagonal with buffer in both - directions: - - >>> BUFFER = 5 - >>> matrix = SubstitutionMatrix.std_nucleotide_matrix() - >>> alignment = align_banded( - ... sequence1, sequence2, matrix, - ... band=(diagonal - BUFFER, diagonal + BUFFER), gap_penalty=(-6, -1) - ... )[0] - >>> print(alignment) - TATATTAT - TATA--AT - """ - # Check matrix alphabets - if not matrix.get_alphabet1().extends(seq1.get_alphabet()) \ - or not matrix.get_alphabet2().extends(seq2.get_alphabet()): - raise ValueError("The sequences' alphabets do not fit the matrix") - # Check if gap penalty is linear or affine - if type(gap_penalty) == int: - if gap_penalty > 0: - raise ValueError("Gap penalty must be negative") - affine_penalty = False - elif type(gap_penalty) == tuple: - if gap_penalty[0] > 0 or gap_penalty[1] > 0: - raise ValueError("Gap penalty must be negative") - affine_penalty = True - else: - raise TypeError("Gap penalty must be either integer or tuple") - # Check if max_number is reasonable - if max_number < 1: - raise ValueError( - "Maximum number of returned alignments must be at least 1" - ) - - # The shorter sequence is the one on the left of the matrix - # -> shorter sequence is 'seq1' - if len(seq2) < len(seq1): - seq1, seq2 = seq2, seq1 - band = [-diag for diag in band] - matrix = matrix.transpose() - is_swapped = True - else: - is_swapped = False - lower_diag, upper_diag = min(band), max(band) - if len(seq1) + upper_diag <= 0 or lower_diag >= len(seq2): - raise ValueError( - "Alignment band is out of range, the band allows no overlap " - "between both sequences" - ) - # Crop band diagonals to reasonable size, so that it at maximum - # covers the search space of an unbanded alignment - lower_diag = max(lower_diag, -len(seq1)+1) - upper_diag = min(upper_diag, len(seq2)-1) - band_width = upper_diag - lower_diag + 1 - if band_width < 1: - raise ValueError("The width of the band is 0") - - # This implementation uses transposed tables in comparison - # to the common visualization - # This means, the first sequence is one the left - # and the second sequence is at the top - - # Terminal gap column on the left can be omitted in this algorithm, - # as terminal gaps are not part of the alignment - # This is not possible for the top row, as the dynamic programming - # algorithm requires these initial values - # On the left and right side an additional column is inserted - # representing the invalid boundaries of the band - # This prevents unnecessary bound checks when filling the dynamic - # programming matrix (score and trace) - trace_table = np.zeros((len(seq1)+1, band_width+2), dtype=np.uint8) - code1 = seq1.code - code2 = seq2.code - - - # Table filling - ############### - - # A score value that signals that the respective direction in the - # dynamic programming matrix should not be used, since it would be - # outside the band - # It is the 'worst' score available, so the trace table will never - # include such a direction - neg_inf = np.iinfo(np.int32).min - # Correct the 'negative infinity' integer, by making it more positive - # This prevents an integer underflow when the gap penalty or - # match score is added to this value - neg_inf -= min(gap_penalty) if affine_penalty else gap_penalty - min_score = np.min(matrix.score_matrix()) - if min_score < 0: - neg_inf -= min_score - - if affine_penalty: - # Affine gap penalty - gap_open = gap_penalty[0] - gap_ext = gap_penalty[1] - # m_table, g1_table and g2_table are the 3 score tables - m_table = np.zeros((len(seq1)+1, band_width+2), dtype=np.int32) - # Fill with negative infinity values to prevent that an - # alignment trace starts with a gap extension - # instead of a gap opening - g1_table = np.full((len(seq1)+1, band_width+2), neg_inf, np.int32) - g2_table = np.full((len(seq1)+1, band_width+2), neg_inf, np.int32) - # As explained for the trace table (see above), - # the score table is filled with with netagive infinty values - # on the left and right column to prevent the trace leaving the - # alignment band - m_table[:, 0] = neg_inf - m_table[:, -1] = neg_inf - # Initialize first row and column for global alignments - _fill_align_table_affine(code1, code2, - matrix.score_matrix(), trace_table, - m_table, g1_table, g2_table, - lower_diag, upper_diag, - gap_open, gap_ext, local) - else: - # Linear gap penalty - score_table = np.zeros((len(seq1)+1, band_width+2), dtype=np.int32) - score_table[:, 0] = neg_inf - score_table[:, -1] = neg_inf - _fill_align_table( - code1, code2, matrix.score_matrix(), trace_table, score_table, - lower_diag, upper_diag, gap_penalty, local - ) - - - # Traceback - ########### - - # Stores all possible traces (= possible alignments) - # A trace stores the indices of the aligned symbols - # in both sequences - trace_list = [] - # Lists of trace starting indices - i_list = np.zeros(0, dtype=int) - j_list = np.zeros(0, dtype=int) - # `state_list` lists of start states - # State specifies the table the trace starts in - if local: - # The start point is the maximal score in the table - # Multiple starting points possible, - # when duplicates of maximum score exist - if affine_penalty: - # The maximum score in the gap score tables do not need to - # be considered, as these starting positions would indicate - # that the local alignment starts with a gap - # Hence the maximum score value in these tables is always - # less than in the match table - max_score = np.max(m_table) - i_list, j_list = np.where((m_table == max_score)) - state_list = np.full( - len(i_list), TraceState.MATCH_STATE, dtype=int - ) - else: - max_score = np.max(score_table) - i_list, j_list = np.where((score_table == max_score)) - # State is always 0 for linear gap penalty - # since there is only one table - state_list = np.full( - len(i_list), TraceState.NO_STATE, dtype=int - ) - else: - # Get all allowed trace start indices - possible_i_start, possible_j_start = get_global_trace_starts( - len(seq1), len(seq2), lower_diag, upper_diag - ) - if affine_penalty: - state_list = np.zeros(0, dtype=int) - m_scores = m_table[possible_i_start, possible_j_start] - g1_scores = g1_table[possible_i_start, possible_j_start] - g2_scores = g2_table[possible_i_start, possible_j_start] - m_max_score = np.max(m_scores) - g1_max_score = np.max(g1_scores) - g2_max_score = np.max(g2_scores) - max_score = max(m_max_score, g1_max_score, g2_max_score) - if m_max_score == max_score: - best_indices = np.where(m_scores == max_score)[0] - i_list = np.append(i_list, possible_i_start[best_indices]) - j_list = np.append(j_list, possible_j_start[best_indices]) - state_list = np.append( - state_list, - np.full(len(best_indices), - TraceState.MATCH_STATE, dtype=int) - ) - if g1_max_score == max_score: - best_indices = np.where(g1_scores == max_score)[0] - i_list = np.append(i_list, possible_i_start[best_indices]) - j_list = np.append(j_list, possible_j_start[best_indices]) - state_list = np.append( - state_list, - np.full(len(best_indices), - TraceState.GAP_LEFT_STATE, dtype=int) - ) - if g2_max_score == max_score: - best_indices = np.where(g2_scores == max_score)[0] - i_list = np.append(i_list, possible_i_start[best_indices]) - j_list = np.append(j_list, possible_j_start[best_indices]) - state_list = np.append( - state_list, - np.full(len(best_indices), - TraceState.GAP_TOP_STATE, dtype=int) - ) - else: - # Choose the trace start index with the highest score - # in the score table - scores = score_table[possible_i_start, possible_j_start] - max_score = np.max(scores) - best_indices = np.where(scores == max_score) - i_list = possible_i_start[best_indices] - j_list = possible_j_start[best_indices] - state_list = np.full( - len(i_list), TraceState.NO_STATE, dtype=int - ) - - # Follow the traces specified in state and indices lists - cdef int curr_trace_count - for k in range(len(i_list)): - i_start = i_list[k] - j_start = j_list[k] - state_start = state_list[k] - # Pessimistic array allocation: - # The maximum trace length arises from an alignment, where each - # symbol is aligned to a gap - trace = np.full((len(seq1) + len(seq2), 2), -1, dtype=np.int64) - curr_trace_count = 1 - follow_trace( - trace_table, True, i_start, j_start, 0, - trace, trace_list, state=state_start, - curr_trace_count=&curr_trace_count, max_trace_count=max_number, - lower_diag=lower_diag, upper_diag=upper_diag - ) - - # Replace gap entries in trace with -1 - for i, trace in enumerate(trace_list): - trace = np.flip(trace, axis=0) - gap_filter = np.zeros(trace.shape, dtype=bool) - gap_filter[np.unique(trace[:,0], return_index=True)[1], 0] = True - gap_filter[np.unique(trace[:,1], return_index=True)[1], 1] = True - trace[~gap_filter] = -1 - trace_list[i] = trace - - # Limit the number of generated alignments to `max_number`: - # In most cases this is achieved by discarding branches in - # 'follow_trace()', however, if multiple alignment starts - # are used, the number of created traces are the number of - # starts times `max_number` - trace_list = trace_list[:max_number] - if is_swapped: - return [Alignment([seq2, seq1], np.flip(trace, axis=1), max_score) - for trace in trace_list] - else: - return [Alignment([seq1, seq2], trace, max_score) - for trace in trace_list] - - -@cython.boundscheck(False) -@cython.wraparound(False) -def _fill_align_table(CodeType1[:] code1 not None, - CodeType2[:] code2 not None, - const int32[:,:] mat not None, - uint8[:,:] trace_table not None, - int32[:,:] score_table not None, - int lower_diag, - int upper_diag, - int gap_penalty, - bint local): - """ - Fill an alignment table with linear gap penalty using dynamic - programming. - - Parameters - ---------- - code1, code2 - The sequence code of each sequence to be aligned. - mat - The score matrix obtained from the :class:`SubstitutionMatrix` - object. - trace_table - A matrix containing values indicating the direction for the - traceback step. - The matrix is filled in this function - score_table - The alignment table. - The matrix is filled in this function. - gap_penalty - The linear gap penalty. - local - Indicates, whether a local alignment should be performed. - """ - - cdef int i, j - cdef int seq_i, seq_j - cdef int32 from_diag, from_left, from_top - cdef uint8 trace - cdef int32 score - - # Starts at 1 since the first row and column are already filled - for seq_i in range(0, code1.shape[0]): - # Transform sequence index into table index - i = seq_i + 1 - for seq_j in range( - max(0, seq_i + lower_diag), - min(code2.shape[0], seq_i + upper_diag+1) - ): - # Transform sequence index into table index - # Due to the diagonal band and its 'straightening' - # seq_j must be transformed to obtain the table index - j = seq_j - seq_i - lower_diag + 1 - - # Evaluate score from available directions: - # Due the 'straightening' of the the diagonal band, - # the 'upper left' and 'upper' direction from the classic - # matrix become 'upper' and 'upper right', respectively - from_diag = score_table[i-1, j ] + mat[code1[seq_i], code2[seq_j]] - from_left = score_table[i, j-1] + gap_penalty - from_top = score_table[i-1, j+1] + gap_penalty - - trace = get_trace_linear(from_diag, from_left, from_top, &score) - - # Local alignment specialty: - # If score is less than or equal to 0, - # then 0 is saved on the field and the trace ends here - if local == True and score <= 0: - score_table[i,j] = 0 - else: - score_table[i,j] = score - trace_table[i,j] = trace - - -@cython.boundscheck(False) -@cython.wraparound(False) -def _fill_align_table_affine(CodeType1[:] code1 not None, - CodeType2[:] code2 not None, - const int32[:,:] mat not None, - uint8[:,:] trace_table not None, - int32[:,:] m_table not None, - int32[:,:] g1_table not None, - int32[:,:] g2_table not None, - int lower_diag, - int upper_diag, - int gap_open, - int gap_ext, - bint local): - """ - Fill an alignment table with affine gap penalty using dynamic - programming. - - Parameters - ---------- - code1, code2 - The sequence code of each sequence to be aligned. - matrix - The score matrix obtained from the class:`SubstitutionMatrix` - object. - trace_table - A matrix containing values indicating the direction for the - traceback step. - The matrix is filled in this function. - m_table, g1_table, g2_table - The alignment tables containing the scores. - `m_table` contains values for matches. - `g1_table` contains values for gaps in the first sequence. - `g2_table` contains values for gaps in the second sequence. - The matrix is filled in this function. - gap_open - The gap opening penalty. - gap_ext - The gap extension penalty. - local - Indicates, whether a local alignment should be performed. - """ - - cdef int i, j - cdef int seq_i, seq_j - cdef int32 mm_score, g1m_score, g2m_score - cdef int32 mg1_score, g1g1_score - cdef int32 mg2_score, g2g2_score - cdef uint8 trace - cdef int32 m_score, g1_score, g2_score - cdef int32 similarity_score - - # Starts at 1 since the first row and column are already fil - for seq_i in range(0, code1.shape[0]): - i = seq_i + 1 - for seq_j in range( - max(0, seq_i + lower_diag), - min(code2.shape[0], seq_i + upper_diag+1) - ): - j = seq_j - seq_i - lower_diag + 1 - # Calculate the scores for possible transitions - # into the current cell - similarity_score = mat[code1[seq_i], code2[seq_j]] - mm_score = m_table[i-1, j] + similarity_score - g1m_score = g1_table[i-1, j] + similarity_score - g2m_score = g2_table[i-1, j] + similarity_score - # No transition from g1_table to g2_table and vice versa - # Since this would mean adjacent gaps in both sequences - # A substitution makes more sense in this case - mg1_score = m_table[i, j-1] + gap_open - g1g1_score = g1_table[i, j-1] + gap_ext - mg2_score = m_table[i-1, j+1] + gap_open - g2g2_score = g2_table[i-1, j+1] + gap_ext - - trace = get_trace_affine( - mm_score, g1m_score, g2m_score, - mg1_score, g1g1_score, - mg2_score, g2g2_score, - # The max score values to be written - &m_score, &g1_score, &g2_score - ) - - # Fill values into tables - # Local alignment specialty: - # If score is less than or equal to 0, - # then the score of the cell remains 0 - # and the trace ends here - if local == True: - if m_score <= 0: - # End trace in specific table - # by filtering out the respective bits - trace &= ~( - TraceDirectionAffine.MATCH_TO_MATCH | - TraceDirectionAffine.GAP_LEFT_TO_MATCH | - TraceDirectionAffine.GAP_TOP_TO_MATCH - ) - # m_table[i,j] remains 0 - else: - m_table[i,j] = m_score - if g1_score <= 0: - trace &= ~( - TraceDirectionAffine.MATCH_TO_GAP_LEFT | - TraceDirectionAffine.GAP_LEFT_TO_GAP_LEFT - ) - # g1_table[i,j] remains negative infinity - else: - g1_table[i,j] = g1_score - if g2_score <= 0: - trace &= ~( - TraceDirectionAffine.MATCH_TO_GAP_TOP | - TraceDirectionAffine.GAP_TOP_TO_GAP_TOP - ) - # g2_table[i,j] remains negative infinity - else: - g2_table[i,j] = g2_score - else: - m_table[i,j] = m_score - g1_table[i,j] = g1_score - g2_table[i,j] = g2_score - trace_table[i,j] = trace - - -def get_global_trace_starts(seq1_len, seq2_len, lower_diag, upper_diag): - band_width = upper_diag - lower_diag + 1 - - j = np.arange(1, band_width + 1) - seq_j = j + (seq1_len-1) + lower_diag - 1 - # Start from the end from the first (shorter) sequence, - # if the table cell is in bounds of the second (longer) sequence, - # otherwise start from the end of the second sequence - i = np.where( - seq_j < seq2_len, - np.full(len(j), (seq1_len-1) + 1, dtype=int), - # Take: - # - # seq_j = j + (seq1_len-1) + lower_diag - 1 - # - # Replace seq_j with last sequence position of second sequence - # and last sequence position of first sequence with seq_i: - # - # (seq2_len-1) = j + seq_i + lower_diag - 1 - # - # Replace seq_i with corresponding i in trace table: - # - # (seq2_len-1) = j + (i - 1) + lower_diag - 1 - # - # Resolve to i: - # - (seq2_len-1) - j - lower_diag + 2 - ) - return i, j \ No newline at end of file diff --git a/src/biotite/sequence/align/localgapped.py b/src/biotite/sequence/align/localgapped.py new file mode 100644 index 000000000..4decf5bfa --- /dev/null +++ b/src/biotite/sequence/align/localgapped.py @@ -0,0 +1,311 @@ +# This source code is part of the Biotite package and is distributed +# under the 3-Clause BSD License. Please see 'LICENSE.rst' for further +# information. + +from __future__ import annotations + +__name__ = "biotite.sequence.align" +__author__ = "Patrick Kunzmann" +__all__ = ["align_local_gapped"] + +import itertools +from typing import TYPE_CHECKING, Literal, overload +import numpy as np +from biotite.rust.sequence.align import align_region as _rust_align_region +from biotite.sequence.align.alignment import Alignment +from biotite.sequence.align.pairwise import prepare_scoring + +if TYPE_CHECKING: + from biotite.sequence.align.matrix import SubstitutionMatrix + from biotite.sequence.sequence import Sequence + from biotite.typing import S1, S2 + + +@overload +def align_local_gapped( + seq1: Sequence[S1], + seq2: Sequence[S2], + matrix: SubstitutionMatrix[S1, S2] | tuple[int, int], + seed: tuple[int, int], + threshold: int, + gap_penalty: int | tuple[int, int] = -10, + max_number: int = 1, + direction: Literal["both", "upstream", "downstream"] = "both", + score_only: Literal[False] = False, + max_table_size: int | None = None, +) -> list[Alignment]: ... +@overload +def align_local_gapped( + seq1: Sequence[S1], + seq2: Sequence[S2], + matrix: SubstitutionMatrix[S1, S2] | tuple[int, int], + seed: tuple[int, int], + threshold: int, + gap_penalty: int | tuple[int, int] = -10, + max_number: int = 1, + direction: Literal["both", "upstream", "downstream"] = "both", + *, + score_only: Literal[True], + max_table_size: int | None = None, +) -> int: ... +def align_local_gapped( + seq1: Sequence, + seq2: Sequence, + matrix: SubstitutionMatrix | tuple[int, int], + seed: tuple[int, int], + threshold: int, + gap_penalty: int | tuple[int, int] = -10, + max_number: int = 1, + direction: str = "both", + score_only: bool = False, + max_table_size: int | None = None, +) -> list[Alignment] | int: + """ + Perform a local gapped alignment extending from a given `seed` + position. + + The alignment extends into one or both directions (controlled by + `direction`) until the total alignment score falls more than + `threshold` below the maximum score found (*X-Drop*). + :footcite:`Zhang2000` + The returned alignment contains the range that yielded the maximum + score. + + Parameters + ---------- + seq1, seq2 : Sequence + The sequences to be aligned. + matrix : SubstitutionMatrix or tuple(int, int) + Either a substitution matrix or a ``(match, mismatch)`` pair of scores. + seed : tuple(int, int) + The indices in `seq1` and `seq2` where the local alignment + starts. + The indices must be non-negative. + threshold : int + If the current score falls this value below the maximum score + found, the alignment terminates. + gap_penalty : int or tuple(int, int), optional + If an integer is provided, the value will be interpreted as + linear gap penalty. + If a tuple is provided, an affine gap penalty is used + :footcite:`Gotoh1982`. + The first integer in the tuple is the gap opening penalty, + the second integer is the gap extension penalty. + The values need to be negative. + max_number : int, optional + The maximum number of alignments returned. + When the number of branches exceeds this value in the traceback + step, no further branches are created. + By default, only a single alignment is returned. + direction : {'both', 'upstream', 'downstream'}, optional + Controls in which direction the alignment extends starting + from the seed. + If ``'upstream'``, the alignment starts before the `seed` and + ends at the `seed`. + If ``'downstream'``, the alignment starts at the `seed` and + ends behind the `seed`. + If ``'both'`` (default) the alignment starts before the `seed` + and ends behind the `seed`. + The `seed` position itself is always included in the alignment. + score_only : bool, optional + If set to ``True``, only the similarity score is returned + instead of the :class:`Alignment`, decreasing the runtime + substantially. + max_table_size : int, optional + A :class:`MemoryError` is raised, if the number of cells + in the internal dynamic programming table, i.e. approximately + the product of the lengths of the aligned regions, would exceed + the given value. + + Returns + ------- + alignments : list of Alignment + A list of found alignments. + Each alignment in the list has the same similarity + score. + Only returned, if `score_only` is ``False``. + score : int + The alignment similarity score. + Only returned, if `score_only` is ``True``. + + See Also + -------- + align_optimal + For the optimal (non-heuristic) alignment of two sequences. + align_banded + For a heuristic alignment constrained to a diagonal band. + + Notes + ----- + Unlike :func:`align_optimal()`, this function does not allocate + memory proportional to the length of both sequences, but only + approximately proportional to lengths of the aligned regions. + In principle, this makes this function viable for local alignments + of sequences of any length. + However, if the product of the lengths of the homologous regions + is too large to fit into memory, a :class:`MemoryError` or even a + crash may occur. + This may also happen in spurious long alignments due to poor choice + of substitution matrix or gap penalty. + You may set `max_table_size` to avoid excessive memory use and + crashes. + + References + ---------- + + .. footbibliography:: + + Examples + -------- + + >>> seq1 = NucleotideSequence("CGTAGCTATCGCCTGTACGGTT") + >>> seq2 = NucleotideSequence("TATATGCCTTACGGAATTGCTTTTT") + >>> matrix = SubstitutionMatrix.std_nucleotide_matrix() + >>> alignment = align_local_gapped( + ... seq1, seq2, matrix, seed=(16, 10), threshold=20 + ... )[0] + >>> print(alignment) + TATCGCCTGTACGG + TAT-GCCT-TACGG + """ + # Resolve the scoring scheme (substitution matrix or match/mismatch tuple) + scoring = prepare_scoring(matrix, seq1, seq2) + + # Check if gap penalty is linear or affine + if isinstance(gap_penalty, int): + if gap_penalty >= 0: + raise ValueError("Gap penalty must be negative") + elif isinstance(gap_penalty, tuple): + if gap_penalty[0] >= 0 or gap_penalty[1] >= 0: + raise ValueError("Gap penalty must be negative") + else: + raise TypeError("Gap penalty must be either integer or tuple") + + # Check if max_number is reasonable + if max_number < 1: + raise ValueError("Maximum number of returned alignments must be at least 1") + + # Check maximum table size + if max_table_size is None: + max_table_size = np.iinfo(np.int64).max + elif max_table_size <= 0: + raise ValueError("Maximum table size must be a positve value") + + seq1_start, seq2_start = seed + if seq1_start < 0 or seq2_start < 0: + raise IndexError("Seed must contain positive indices") + if seq1_start >= len(seq1) or seq2_start >= len(seq2): + raise IndexError( + f"Seed {(seq1_start, seq2_start)} is out of bounds " + f"for the sequences of length {len(seq1)} and {len(seq2)}" + ) + + if direction == "both": + upstream = True + downstream = True + elif direction == "upstream": + upstream = True + downstream = False + elif direction == "downstream": + upstream = False + downstream = True + else: + raise ValueError(f"Direction '{direction}' is invalid") + # Range check to avoid negative indices + if seq1_start == 0 or seq2_start == 0: + upstream = False + + if threshold < 0: + raise ValueError("The threshold value must be a non-negative integer") + + # The two sequences may use different code dtypes + # -> widen both to a common type so the Rust layer dispatches once + common_dtype = np.promote_types(seq1.code.dtype, seq2.code.dtype) + code1 = np.ascontiguousarray(seq1.code, common_dtype) + code2 = np.ascontiguousarray(seq2.code, common_dtype) + + total_score = 0 + upstream_traces = [] + downstream_traces = [] + # Separate alignment into two parts: + # the regions upstream and downstream from the seed position + if upstream: + # For the upstream region the respective part of the sequence + # must be reversed + traces, score = _rust_align_region( + np.ascontiguousarray(code1[seq1_start - 1 :: -1]), + np.ascontiguousarray(code2[seq2_start - 1 :: -1]), + scoring, + gap_penalty, + threshold, + max_number, + score_only, + int(max_table_size), + ) + total_score += score + if not score_only: + # Undo the sequence reversing + upstream_traces = [trace[::-1] for trace in traces] + offset = np.array(seed) - 1 + for trace in upstream_traces: + # Gap values (-1) are not transformed, + # as gaps are not indices + non_gap_mask = trace != -1 + # Second part of sequence reversing + trace[non_gap_mask] *= -1 + # Add seed offset to trace indices + trace[non_gap_mask[:, 0], 0] += offset[0] + trace[non_gap_mask[:, 1], 1] += offset[1] + + if downstream: + traces, score = _rust_align_region( + np.ascontiguousarray(code1[seq1_start + 1 :]), + np.ascontiguousarray(code2[seq2_start + 1 :]), + scoring, + gap_penalty, + threshold, + max_number, + score_only, + int(max_table_size), + ) + total_score += score + if not score_only: + downstream_traces = traces + offset = np.array(seed) + 1 + for trace in downstream_traces: + trace[trace[:, 0] != -1, 0] += offset[0] + trace[trace[:, 1] != -1, 1] += offset[1] + + # Add the score of the seed position itself + if isinstance(scoring, tuple): + match_score, mismatch_score = scoring + total_score += ( + match_score if code1[seq1_start] == code2[seq2_start] else mismatch_score + ) + else: + total_score += int(scoring[code1[seq1_start], code2[seq2_start]]) + + if score_only: + return total_score + + if upstream and downstream: + # Create cartesian product of upstream and downstream traces + # Only consider max_number alignments + traces = [ + np.concatenate([upstream_trace, [seed], downstream_trace]) + for _, (upstream_trace, downstream_trace) in zip( + range(max_number), + itertools.product(upstream_traces, downstream_traces), + ) + ] + elif upstream: + traces = [np.concatenate([trace, [seed]]) for trace in upstream_traces] + elif downstream: + traces = [np.concatenate([[seed], trace]) for trace in downstream_traces] + else: + # 'direction == "upstream"', but the start index is 0 so no + # upstream alignment is performed + # -> the trace includes only the seed + traces = [np.array(seed)[np.newaxis, :]] + + return [Alignment([seq1, seq2], trace, total_score) for trace in traces] diff --git a/src/biotite/sequence/align/localgapped.pyi b/src/biotite/sequence/align/localgapped.pyi deleted file mode 100644 index d9ef79f78..000000000 --- a/src/biotite/sequence/align/localgapped.pyi +++ /dev/null @@ -1,34 +0,0 @@ -__all__ = ["align_local_gapped"] - -from typing import Literal, overload -from biotite.sequence.align.alignment import Alignment -from biotite.sequence.align.matrix import SubstitutionMatrix -from biotite.sequence.sequence import Sequence -from biotite.typing import S1, S2 - -@overload -def align_local_gapped( - seq1: Sequence[S1], - seq2: Sequence[S2], - matrix: SubstitutionMatrix[S1, S2], - seed: tuple[int, int], - threshold: int, - gap_penalty: int | tuple[int, int] = -10, - max_number: int = 1, - direction: Literal["both", "upstream", "downstream"] = "both", - score_only: Literal[False] = False, - max_table_size: int | None = None, -) -> list[Alignment]: ... -@overload -def align_local_gapped( - seq1: Sequence[S1], - seq2: Sequence[S2], - matrix: SubstitutionMatrix[S1, S2], - seed: tuple[int, int], - threshold: int, - gap_penalty: int | tuple[int, int] = -10, - max_number: int = 1, - direction: Literal["both", "upstream", "downstream"] = "both", - score_only: Literal[True] = ..., - max_table_size: int | None = None, -) -> int: ... diff --git a/src/biotite/sequence/align/localgapped.pyx b/src/biotite/sequence/align/localgapped.pyx deleted file mode 100644 index 1926e0bce..000000000 --- a/src/biotite/sequence/align/localgapped.pyx +++ /dev/null @@ -1,892 +0,0 @@ -# This source code is part of the Biotite package and is distributed -# under the 3-Clause BSD License. Please see 'LICENSE.rst' for further -# information. - -__name__ = "biotite.sequence.align" -__author__ = "Patrick Kunzmann" -__all__ = ["align_local_gapped"] - -cimport cython -cimport numpy as np -from .tracetable cimport follow_trace, get_trace_linear, get_trace_affine - -import itertools -import numpy as np -from .alignment import Alignment - - -ctypedef np.int32_t int32 -ctypedef np.int64_t int64 -ctypedef np.uint8_t uint8 -ctypedef np.uint16_t uint16 -ctypedef np.uint32_t uint32 -ctypedef np.uint64_t uint64 - -ctypedef fused CodeType1: - uint8 - uint16 - uint32 - uint64 -ctypedef fused CodeType2: - uint8 - uint16 - uint32 - uint64 - - -cdef int INIT_SIZE = 100 - - -def align_local_gapped(seq1, seq2, matrix, seed, int32 threshold, - gap_penalty=-10, max_number=1, - direction="both", score_only=False, - max_table_size=None): - """ - align_local_gapped(seq1, seq2, matrix, seed, threshold, - gap_penalty=-10, max_number=1, - direction="both", score_only=False, - max_table_size=None) - - Perform a local gapped alignment extending from a given `seed` - position. - - The alignment extends into one or both directions (controlled by - `direction`) until the total alignment score falls more than - `threshold` below the maximum score found (*X-Drop*). - :footcite:`Zhang2000` - The returned alignment contains the range that yielded the maximum - score. - - Parameters - ---------- - seq1, seq2 : Sequence - The sequences to be aligned. - matrix : SubstitutionMatrix - The substitution matrix used for scoring. - seed : tuple(int, int) - The indices in `seq1` and `seq2` where the local alignment - starts. - The indices must be non-negative. - threshold : int - If the current score falls this value below the maximum score - found, the alignment terminates. - gap_penalty : int or tuple(int, int), optional - If an integer is provided, the value will be interpreted as - linear gap penalty. - If a tuple is provided, an affine gap penalty is used - :footcite:`Gotoh1982`. - The first integer in the tuple is the gap opening penalty, - the second integer is the gap extension penalty. - threshold : int - If the current score falls this value below the maximum score - found, the alignment terminates. - max_number : int, optional - The maximum number of alignments returned. - When the number of branches exceeds this value in the traceback - step, no further branches are created. - By default, only a single alignment is returned. - direction : {'both', 'upstream', 'downstream'}, optional - Controls in which direction the alignment extends starting - from the seed. - If ``'upstream'``, the alignment starts before the `seed` and - ends at the `seed`. - If ``'downstream'``, the alignment starts at the `seed` and - ends behind the `seed`. - If ``'both'`` (default) the alignment starts before the `seed` - and ends behind the `seed`. - The `seed` position itself is always included in the alignment. - score_only : bool, optional - If set to ``True``, only the similarity score is returned - instead of the :class:`Alignment`, decreasing the runtime - substantially. - max_table_size : int, optional - A :class:`MemoryError` is raised, if the number of cells - in the internal dynamic programming table, i.e. approximately - the product of the lengths of the aligned regions, would exceed - the given value. - - Returns - ------- - alignments : list of Alignment - A list of found alignments. - Each alignment in the list has the same similarity - score. - Only returned, if `score_only` is ``False``. - score : int - The alignment similarity score. - Only returned, if `score_only` is ``True``. - - See Also - -------- - align_ungapped - For ungapped local alignments with the same *X-Drop* technique. - - Notes - ----- - Unilke :func:`align_optimal()`, this function does not allocate - memory proportional to the length of both sequences, but only - approximately proportional to lengths of the aligned regions. - In principle, this makes this function viable for local alignments - of sequences of any length. - However, if the product of the lengths of the homologous regions - is too large to fit into memory, a :class:`MemoryError` or even a - crash may occur. - This may also happen in spurious long alignments due to poor choice - of substitution matrix or gap penalty. - You may set `max_table_size` to avoid excessive memory use and - crashes. - - References - ---------- - - .. footbibliography:: - - Examples - -------- - - >>> seq1 = NucleotideSequence("CGTAGCTATCGCCTGTACGGTT") - >>> seq2 = NucleotideSequence("TATATGCCTTACGGAATTGCTTTTT") - >>> matrix = SubstitutionMatrix.std_nucleotide_matrix() - >>> alignment = align_local_gapped( - ... seq1, seq2, matrix, seed=(16, 10), threshold=20 - ... )[0] - >>> print(alignment) - TATCGCCTGTACGG - TAT-GCCT-TACGG - >>> alignment = align_local_gapped( - ... seq1, seq2, matrix, seed=(16, 10), threshold=20, direction="upstream" - ... )[0] - >>> print(alignment) - TATCGCCTGTA - TAT-GCCT-TA - >>> alignment = align_local_gapped( - ... seq1, seq2, matrix, seed=(16, 10), threshold=20, direction="downstream" - ... )[0] - >>> print(alignment) - ACGG - ACGG - >>> score = align_local_gapped( - ... seq1, seq2, matrix, seed=(16, 10), threshold=20, score_only=True - ... ) - >>> print(score) - 40 - """ - # Check matrix alphabets - if not matrix.get_alphabet1().extends(seq1.get_alphabet()) \ - or not matrix.get_alphabet2().extends(seq2.get_alphabet()): - raise ValueError("The sequences' alphabets do not fit the matrix") - score_matrix = matrix.score_matrix() - - # Check if gap penalty is linear or affine - if type(gap_penalty) == int: - if gap_penalty >= 0: - raise ValueError("Gap penalty must be negative") - elif type(gap_penalty) == tuple: - if gap_penalty[0] >= 0 or gap_penalty[1] >= 0: - raise ValueError("Gap penalty must be negative") - else: - raise TypeError("Gap penalty must be either integer or tuple") - - # Check if max_number is reasonable - if max_number < 1: - raise ValueError( - "Maximum number of returned alignments must be at least 1" - ) - - # Check maximum table size - if max_table_size is None: - max_table_size = np.iinfo(np.int64).max - elif max_table_size <= 0: - raise ValueError("Maximum table size must be a positve value") - - - code1 = seq1.code - code2 = seq2.code - - cdef int seq1_start, seq2_start - seq1_start, seq2_start = seed - if seq1_start < 0 or seq2_start < 0: - raise IndexError("Seed must contain positive indices") - if seq1_start >= len(code1) or seq2_start >= len(code2): - raise IndexError( - f"Seed {(seq1_start, seq2_start)} is out of bounds " - f"for the sequences of length {len(code1)} and {len(code2)}" - ) - - - cdef bint upstream - cdef bint downstream - if direction == "both": - upstream = True - downstream = True - elif direction == "upstream": - upstream = True - downstream = False - elif direction == "downstream": - upstream = False - downstream = True - else: - raise ValueError(f"Direction '{direction}' is invalid") - # Range check to avoid negative indices - if seq1_start == 0 or seq2_start == 0: - upstream = False - - if threshold < 0: - raise ValueError("The threshold value must be a non-negative integer") - - - cdef int32 score - cdef int32 total_score = 0 - # Separate alignment into two parts: - # the regions upstream and downstream from the seed position - if upstream: - # For the upstream region the respective part of the sequence - # must be reversed - score, upstream_traces = _align_region( - code1[seq1_start-1::-1], code2[seq2_start-1::-1], - score_matrix, threshold, gap_penalty, - max_number, score_only, max_table_size - ) - total_score += score - if upstream_traces is not None: - # Undo the sequence reversing - upstream_traces = [trace[::-1] for trace in upstream_traces] - offset = np.array(seed) - 1 - for trace in upstream_traces: - # Gap values (-1) are not transformed, - # as gaps are not indices - non_gap_mask = (trace != -1) - # Second part of sequence reversing - trace[non_gap_mask] *= -1 - # Add seed offset to trace indices - trace[non_gap_mask[:, 0], 0] += offset[0] - trace[non_gap_mask[:, 1], 1] += offset[1] - - if downstream: - score, downstream_traces = _align_region( - code1[seq1_start+1:], code2[seq2_start+1:], - score_matrix, threshold, gap_penalty, - max_number, score_only, max_table_size - ) - total_score += score - if downstream_traces is not None: - offset = np.array(seed) + 1 - for trace in downstream_traces: - trace[trace[:, 0] != -1, 0] += offset[0] - trace[trace[:, 1] != -1, 1] += offset[1] - - total_score += score_matrix[code1[seq1_start], code2[seq2_start]] - - - if score_only: - return total_score - else: - if upstream and downstream: - # Create cartesian product of upstream and downstream traces - # Only consider max_number alignments - traces = [ - np.concatenate([upstream_trace, [seed], downstream_trace]) - for _, (upstream_trace, downstream_trace) in zip( - range(max_number), - itertools.product(upstream_traces, downstream_traces) - ) - ] - elif upstream: - traces = [ - np.concatenate([trace, [seed]]) for trace in upstream_traces - ] - elif downstream: - traces = [ - np.concatenate([[seed], trace]) for trace in downstream_traces - ] - else: - # 'direction == "upstream"', but the start index is 0 so no - # upstream alignment is performed - # -> the trace includes only the seed - traces = [np.array(seed)[np.newaxis, :]] - - return [Alignment([seq1, seq2], trace, total_score) - for trace in traces] - - -def _align_region(code1, code2, matrix, threshold, gap_penalty, - max_number, score_only, max_table_size): - """ - Perfrom a local *X-Drop* alignment extending from the start of the - given sequences - - Parameters - ---------- - code1, code2 : ndarray, dtype={np.uint8, np.uint16, np.uint32, np.uint64} - The code of the sequences to be aligned. - matrix : ndarray, shape(k, k), dtype=np.int32 - The score matrix. - threshold : int - If the current score falls this value below the maximum score - found, the alignment terminates. - gap_penalty : int or tuple(int, int) - If an integer is provided, the value will be interpreted as - linear gap penalty. - If a tuple is provided, an affine gap penalty is used [2]_. - The first integer in the tuple is the gap opening penalty, - the second integer is the gap extension penalty. - threshold : int - If the current score falls this value below the maximum score - found, the alignment terminates. - max_number : int - The maximum number of alignments returned. - When the number of branches exceeds this value in the traceback - step, no further branches are created. - score_only : bool - If set to ``True``, only the similarity score is calculated and - the traceback is not conducted. - max_table_size : int - Raise a :class:`MemoryError`, if a dynamic programming table - exceeds this size. - - Returns - ------- - score : int or None - The alignment similarity score. - trace : list of (ndarray, shape=(n,2), dtype=int) or None - A list of alignment traces, where each trace corresponds to an - alignment with the maximum similarity score found. - This list has only multiple elements if there are multiple - traces, that correspond to the same maximum similarity score. - ``None``, if `score_only` is ``False``. - """ - if type(gap_penalty) == int: - affine_penalty = False - else: - affine_penalty = True - - - - init_size = ( - _min(len(code1)+1, INIT_SIZE), - _min(len(code2)+1, INIT_SIZE) - ) - trace_table = np.zeros(init_size, dtype=np.uint8) - - - # Table filling - ############### - # Set the initial (upper left) score value to 'threshold + 1', - # to be able to use '0' as minus infinity value - init_score = threshold + 1 - if affine_penalty: - m_table = np.zeros(init_size, dtype=np.int32) - g1_table = np.zeros(init_size, dtype=np.int32) - g2_table = np.zeros(init_size, dtype=np.int32) - # This implementation does not initialize the entire first - # row/column to avoid issues with premature pruning in the table - # filling process - m_table[0,0] = init_score - trace_table, m_table, g1_table, g2_table = _fill_align_table_affine( - code1, code2, matrix, trace_table, m_table, g1_table, g2_table, - threshold, gap_penalty[0], gap_penalty[1], score_only, - max_table_size - ) - else: - score_table = np.zeros(init_size, dtype=np.int32) - score_table[0,0] = init_score - trace_table, score_table = _fill_align_table( - code1, code2, matrix, trace_table, score_table, threshold, - gap_penalty, score_only, max_table_size - ) - - # If only the score is desired, the traceback is not necessary - if score_only: - if affine_penalty: - # The maximum score in the gap score tables do not need to - # be considered, as these starting positions would indicate - # that the alignment starts with a gap - # Hence the maximum score value in these tables is always - # less than in the match table - max_score = np.max(m_table) - else: - max_score = np.max(score_table) - # The initial score needs to be subtracted again, - # since it was artificially added for convenience resaons - return max_score - init_score, None - - - # Traceback - ########### - # Stores all possible traces (= possible alignments) - # A trace stores the indices of the aligned symbols - # in both sequences - trace_list = [] - # Lists of trace starting indices - i_list = np.zeros(0, dtype=int) - j_list = np.zeros(0, dtype=int) - # List of start states - # State specifies the table the trace starts in - state_list = np.zeros(0, dtype=int) - # The start point is the maximal score in the table - # Multiple starting points possible, - # when duplicates of maximal score exist - if affine_penalty: - # Only consicder match table (see reason above) - max_score = np.max(m_table) - i_list, j_list = np.where((m_table == max_score)) - state_list = np.append(state_list, np.full(len(i_list), 1)) - else: - max_score = np.max(score_table) - i_list, j_list = np.where((score_table == max_score)) - # State is always 0 for linear gap penalty - # since there is only one table - state_list = np.zeros(len(i_list), dtype=int) - - # Follow the traces specified in state and indices lists - cdef int curr_trace_count - for k in range(len(i_list)): - i_start = i_list[k] - j_start = j_list[k] - state_start = state_list[k] - # Pessimistic array allocation: - # The maximum trace length arises from an alignment, where each - # symbol is aligned to a gap - trace = np.full(( i_start+1 + j_start+1, 2 ), -1, dtype=np.int64) - curr_trace_count = 1 - follow_trace( - trace_table, False, i_start, j_start, 0, trace, trace_list, - state=state_start, curr_trace_count=&curr_trace_count, - max_trace_count=max_number, - # Diagonals are only needed for banded alignments - lower_diag=0, upper_diag=0 - ) - - # Replace gap entries in trace with -1 - for i, trace in enumerate(trace_list): - trace = np.flip(trace, axis=0) - gap_filter = np.zeros(trace.shape, dtype=bool) - gap_filter[np.unique(trace[:,0], return_index=True)[1], 0] = True - gap_filter[np.unique(trace[:,1], return_index=True)[1], 1] = True - trace[~gap_filter] = -1 - trace_list[i] = trace - - # Limit the number of generated alignments to `max_number`: - # In most cases this is achieved by discarding branches in - # 'follow_trace()', however, if multiple local alignment starts - # are used, the number of created traces are the number of - # starts times `max_number` - trace_list = trace_list[:max_number] - - return max_score - init_score, trace_list - - -@cython.boundscheck(False) -@cython.wraparound(False) -def _fill_align_table(CodeType1[:] code1 not None, - CodeType2[:] code2 not None, - const int32[:,:] matrix not None, - uint8[:,:] trace_table not None, - int32[:,:] score_table not None, - int32 threshold, - int32 gap_penalty, - bint score_only, - int64 max_table_size): - """ - Fill an alignment table with linear gap penalty using dynamic - programming. - - Parameters - ---------- - code1, code2 - The sequence code of each sequence to be aligned. - matrix - The score matrix obtained from the :class:`SubstitutionMatrix` - object. - trace_table - The initial matrix containing values indicating the direction - for the traceback step. - score_table - The initial score table. - threshold - An alignment cell is invalidated if the total similarity score - is this threshold below the maximum similarity score found so - far. - gap_penalty - The linear gap penalty. - score_only - If true, the trace table is not filled. - max_table_size : int64 - Raise a :class:`MemoryError`, if a dynamic programming table - exceeds this size. - - Returns - ------- - trace_table - The filled trace table. - score_table - The filled score table. - """ - cdef int i, j, k=0 - # The ranges for i in the current (k=0) - # and previous (k=1, k=2) antidiagonals, that point to valid cells - cdef int i_min_k_0=0, i_max_k_0=0 - cdef int i_min_k_1=0, i_max_k_1=0 - cdef int i_min_k_2=0, i_max_k_2=0 - # The pruned range for i and j in the current antidiagonal, - # calculated from the previous antidiagonals - cdef int i_min, i_max - cdef int j_max - # The maximum values for i and j ever encountered while iterating - # over the antidiagonals -> used for final trimming of tables - cdef int i_max_total=0, j_max_total=0 - - cdef int32 from_diag, from_left, from_top - cdef uint8 trace = 0 - cdef int32 score = 0 - cdef int32 max_score = score_table[0, 0] - cdef int32 req_score = max_score - threshold - - # Instead of iteration over row and column, - # iterate over antidiagonals and diagonals to achieve symmetric - # treatment of both sequences - for k in range(1, code1.shape[0] + code2.shape[0] + 1): - # Prepare values for iteration - i_min_k_2 = i_min_k_1 - i_max_k_2 = i_max_k_1 - i_min_k_1 = i_min_k_0 - i_max_k_1 = i_max_k_0 - # Reset values for iteration to most 'restrictive' values - # These restrictive values are overwritten in the next iteration - # if valid cells are present - i_min_k_0 = k - i_max_k_0 = 0 - - # Prune index range for antidiagonal - # to range where valid cells exist - i_min = _min(i_min_k_1, i_min_k_2 + 1) - i_max = _max(i_max_k_1 + 1, i_max_k_2 + 1) - # The index must also not be out of sequence range - i_min = _max(i_min, k - code2.shape[0]) - i_max = _min(i_max, code1.shape[0]) - # The algorithm has finished, - # if the calculated antidiagonal has no range of valid cells - if i_min > i_max: - break - - j_max = k - i_min - # Expand ndarrays - # if their size would be exceeded in the following iteration - if i_max >= score_table.shape[0]: - score_table = _extend_table( - np.asarray(score_table), 0, max_table_size - ) - if not score_only: - trace_table = _extend_table( - np.asarray(trace_table), 0, max_table_size - ) - if j_max >= score_table.shape[1]: - score_table = _extend_table( - np.asarray(score_table), 1, max_table_size - ) - if not score_only: - trace_table = _extend_table( - np.asarray(trace_table), 1, max_table_size - ) - i_max_total = _max(i_max_total, i_max) - j_max_total = _max(j_max_total, j_max) - - for i in range(i_min, i_max+1): - j = k - i - - # Evaluate score from diagonal direction - if i != 0 and j != 0: - from_diag = score_table[i-1, j-1] - # Check if score stems from cells that are valid - if from_diag != 0: - # -1 in sequence index is necessary - # due to the shift of the sequences - # to the bottom/right in the table - from_diag += matrix[code1[i-1], code2[j-1]] - else: - from_diag = 0 - else: - from_diag = 0 - # Evaluate score through gap insertion - if i != 0: - from_top = score_table[i-1, j] + gap_penalty - else: - from_top = 0 - if j != 0: - from_left = score_table[i, j-1] + gap_penalty - else: - from_left = 0 - - if score_only: - score = _max(from_diag, _max(from_left, from_top)) - else: - trace = get_trace_linear( - from_diag, from_left, from_top, &score - ) - - # Check if the obtained score reaches the required threshold - # and if they even exceed the maximum score - if score >= req_score: - if i_min_k_0 == k: - # 'i_min_k_0 == k' - # -> i_min_k_0 has not been set in this iteration, yet - i_min_k_0 = i - i_max_k_0 = i - score_table[i,j] = score - if not score_only: - trace_table[i,j] = trace - if score > max_score: - max_score = score - req_score = max_score - threshold - - - return np.asarray(trace_table)[:i_max_total+1, :j_max_total+1], \ - np.asarray(score_table)[:i_max_total+1, :j_max_total+1] - - -@cython.boundscheck(False) -@cython.wraparound(False) -def _fill_align_table_affine(CodeType1[:] code1 not None, - CodeType2[:] code2 not None, - const int32[:,:] matrix not None, - uint8[:,:] trace_table not None, - int32[:,:] m_table not None, - int32[:,:] g1_table not None, - int32[:,:] g2_table not None, - int32 threshold, - int32 gap_open, - int32 gap_ext, - bint score_only, - int64 max_table_size): - """ - Fill an alignment table with affines gap penalty using dynamic - programming. - - Parameters - ---------- - code1, code2 - The sequence code of each sequence to be aligned. - matrix - The score matrix obtained from the :class:`SubstitutionMatrix` - object. - trace_table - The initial matrix containing values indicating the direction - for the traceback step. - m_table, g1_table, g2_table - The alignment tables containing the scores. - `m_table` contains values for matches. - `g1_table` contains values for gaps in the first sequence. - `g2_table` contains values for gaps in the second sequence. - threshold - An alignment cell is invalidated if the total similarity score - is this threshold below the maximum similarity score found so - far. - gap_open - The gap opening penalty. - gap_ext - The gap extension penalty. - score_only - If true, the trace table is not filled. - max_table_size : int64 - Raise a :class:`MemoryError`, if a dynamic programming table - exceeds this size. - - Returns - ------- - trace_table - The filled trace table. - m_table, g1_table, g2_table - The filled score tables. - """ - cdef int i, j, k=0 - # The ranges for i in the current (k=0) - # and previous (k=1, k=2) antidiagonals, that point to valid cells - cdef int i_min_k_0=0, i_max_k_0=0 - cdef int i_min_k_1=0, i_max_k_1=0 - cdef int i_min_k_2=0, i_max_k_2=0 - # The pruned range for i and j in the current antidiagonal, - # calculated from the previous antidiagonals - cdef int i_min, i_max - cdef int j_max - # The maximum values for i and j ever encountered while iterating - # over the antidiagonals -> used for final trimming of tables - cdef int i_max_total=0, j_max_total=0 - - cdef uint8 trace = 0 - cdef bint is_valid_cell - - cdef int32 mm_score, g1m_score, g2m_score - cdef int32 mg1_score, g1g1_score - cdef int32 mg2_score, g2g2_score - cdef int32 m_score, g1_score, g2_score - cdef int32 similarity_score - cdef int32 max_score = m_table[0, 0] - cdef int32 req_score = max_score - threshold - - # Instead of iteration over row and column, - # iterate over antidiagonals and diagonals to achieve symmetric - # treatment of both sequences - for k in range(1, code1.shape[0] + code2.shape[0] + 1): - # Prepare values for iteration - i_min_k_2 = i_min_k_1 - i_max_k_2 = i_max_k_1 - i_min_k_1 = i_min_k_0 - i_max_k_1 = i_max_k_0 - # Reset values for iteration to most 'restrictive' values - # These restrictive values are overwritten in the next iteration - # if valid cells are present - i_min_k_0 = k - i_max_k_0 = 0 - - # Prune index range for antidiagonal - # to range where valid cells exist - i_min = _min(i_min_k_1, i_min_k_2 + 1) - i_max = _max(i_max_k_1 + 1, i_max_k_2 + 1) - # The index must also not be out of sequence range - i_min = _max(i_min, k - code2.shape[0]) - i_max = _min(i_max, code1.shape[0]) - # The algorithm has finished, - # if the calculated antidiagonal has no range of valid cells - if i_min > i_max: - break - - j_max = k - i_min - # Expand ndarrays - # if their size would be exceeded in the following iteration - if i_max >= m_table.shape[0]: - m_table = _extend_table(np.asarray(m_table), 0, max_table_size) - g1_table = _extend_table(np.asarray(g1_table), 0, max_table_size) - g2_table = _extend_table(np.asarray(g2_table), 0, max_table_size) - if not score_only: - trace_table = _extend_table( - np.asarray(trace_table), 0, max_table_size - ) - if j_max >= m_table.shape[1]: - m_table = _extend_table(np.asarray(m_table), 1, max_table_size) - g1_table = _extend_table(np.asarray(g1_table), 1, max_table_size) - g2_table = _extend_table(np.asarray(g2_table), 1, max_table_size) - if not score_only: - trace_table = _extend_table( - np.asarray(trace_table), 1, max_table_size - ) - i_max_total = _max(i_max_total, i_max) - j_max_total = _max(j_max_total, j_max) - - for i in range(i_min, i_max+1): - j = k - i - - # Evaluate score from diagonal direction - if i != 0 and j != 0: - # -1 in sequence index is necessary - # due to the shift of the sequences - # to the bottom/right in the table - similarity_score = matrix[code1[i-1], code2[j-1]] - mm_score = m_table[i-1,j-1] - g1m_score = g1_table[i-1,j-1] - g2m_score = g2_table[i-1,j-1] - # Check if scores stem from cells that are valid - if mm_score != 0: - mm_score += similarity_score - if g1m_score != 0: - g1m_score += similarity_score - if g2m_score != 0: - g2m_score += similarity_score - else: - mm_score = 0 - g1m_score = 0 - g2m_score = 0 - # Evaluate score through gap insertion - # No transition from g1_table to g2_table and vice versa, - # since this would mean adjacent gaps in both sequences; - # a substitution makes more sense in this case - if j != 0: - mg1_score = m_table[i,j-1] + gap_open - g1g1_score = g1_table[i,j-1] + gap_ext - else: - mg1_score = 0 - g1g1_score = 0 - if i != 0: - mg2_score = m_table[i-1,j] + gap_open - g2g2_score = g2_table[i-1,j] + gap_ext - else: - mg2_score = 0 - g2g2_score = 0 - - - - if score_only: - m_score = _max(mm_score, _max(g1m_score, g2m_score)) - g1_score = _max(mg1_score, g1g1_score) - g2_score = _max(mg2_score, g2g2_score) - else: - trace = get_trace_affine( - mm_score, g1m_score, g2m_score, - mg1_score, g1g1_score, - mg2_score, g2g2_score, - # The max score values to be written - &m_score, &g1_score, &g2_score - ) - - - # Check if the obtained scores reach the required threshold - # and if they even exceed the maximum score - is_valid_cell = False - - if m_score >= req_score: - if i_min_k_0 == k: - i_min_k_0 = i - i_max_k_0 = i - m_table[i,j] = m_score - is_valid_cell = True - if m_score > max_score: - max_score = m_score - req_score = max_score - threshold - - if g1_score >= req_score: - if i_min_k_0 == k: - i_min_k_0 = i - i_max_k_0 = i - g1_table[i,j] = g1_score - is_valid_cell = True - if g1_score > max_score: - max_score = g1_score - req_score = max_score - threshold - - if g2_score >= req_score: - if i_min_k_0 == k: - i_min_k_0 = i - i_max_k_0 = i - g2_table[i,j] = g2_score - is_valid_cell = True - if g2_score > max_score: - max_score = g2_score - req_score = max_score - threshold - - if is_valid_cell and not score_only: - trace_table[i,j] = trace - - - return np.asarray(trace_table)[:i_max_total+1, :j_max_total+1], \ - np.asarray(m_table )[:i_max_total+1, :j_max_total+1], \ - np.asarray(g1_table )[:i_max_total+1, :j_max_total+1], \ - np.asarray(g2_table )[:i_max_total+1, :j_max_total+1] - - -def _extend_table(table, int dimension, int64 max_size): - if dimension == 0: - new_shape = (table.shape[0] * 2, table.shape[1]) - else: - new_shape = (table.shape[0], table.shape[1] * 2) - if new_shape[0] * new_shape[1] > max_size: - raise MemoryError("Maximum table size exceeded") - new_table = np.zeros(new_shape, dtype=table.dtype) - # Fill in exiisting data - new_table[:table.shape[0], :table.shape[1]] = table - return new_table - - -cdef inline int _min(int32 a, int32 b): - return a if a < b else b - -cdef inline int _max(int32 a, int32 b): - return a if a > b else b diff --git a/src/biotite/sequence/align/localungapped.py b/src/biotite/sequence/align/localungapped.py new file mode 100644 index 000000000..a7c70cca7 --- /dev/null +++ b/src/biotite/sequence/align/localungapped.py @@ -0,0 +1,92 @@ +# This source code is part of the Biotite package and is distributed +# under the 3-Clause BSD License. Please see 'LICENSE.rst' for further +# information. + +from __future__ import annotations + +__name__ = "biotite.sequence.align" +__author__ = "Patrick Kunzmann" +__all__ = ["SeedExtension", "align_local_ungapped"] + +import warnings +from typing import TYPE_CHECKING +from biotite.rust.sequence.align import SeedExtension + +if TYPE_CHECKING: + from biotite.sequence.align.alignment import Alignment + from biotite.sequence.align.matrix import SubstitutionMatrix + from biotite.sequence.sequence import Sequence + +# The class is implemented in Rust and therefore is not generic at runtime; +# allow ``SeedExtension[S1, S2]`` subscription so the annotated stub works +SeedExtension.__class_getitem__ = classmethod(lambda cls, params: cls) + + +def align_local_ungapped( + seq1: Sequence, + seq2: Sequence, + matrix: SubstitutionMatrix | tuple[int, int], + seed: tuple[int, int], + threshold: int, + direction: str = "both", + score_only: bool = False, + check_matrix: bool = True, +) -> Alignment | int: + """ + Perform a local alignment extending from given `seed` position + without inserting gaps. + + .. deprecated:: + Use :class:`SeedExtension` instead. + + Parameters + ---------- + seq1, seq2 : Sequence + The sequences to be aligned. + matrix : SubstitutionMatrix or tuple(int, int) + Either a substitution matrix or a ``(match, mismatch)`` pair of scores. + seed : tuple(int, int) + The indices in `seq1` and `seq2` where the local alignment + starts. + The indices must be non-negative. + threshold : int + If the current score falls this value below the maximum score + found, the alignment terminates. + direction : {'both', 'upstream', 'downstream'}, optional + Controls in which direction the alignment extends starting + from the seed (see :class:`SeedExtension`). + score_only : bool, optional + If set to ``True``, only the similarity score is returned + instead of the :class:`Alignment`. + check_matrix : bool, optional + If set to False, the `matrix` is not checked for compatibility + with the alphabets of the sequences. + + Returns + ------- + alignment : Alignment + The resulting ungapped alignment. + Only returned, if `score_only` is ``False``. + score : int + The alignment similarity score. + Only returned, if `score_only` is ``True``. + + See Also + -------- + SeedExtension + The non-deprecated, reusable replacement for this function. + align_local_gapped + For gapped local alignments with the same *X-Drop* technique. + """ + warnings.warn( + "'align_local_ungapped()' is deprecated, use 'SeedExtension' instead", + DeprecationWarning, + ) + # Preserve the historical alphabet check of this function + if check_matrix and not isinstance(matrix, tuple): + if not matrix.get_alphabet1().extends( + seq1.get_alphabet() + ) or not matrix.get_alphabet2().extends(seq2.get_alphabet()): + raise ValueError("The sequences' alphabets do not fit the matrix") + extension = SeedExtension(matrix, threshold, direction) + return extension.align(seq1, seq2, seed, score_only=score_only) diff --git a/src/biotite/sequence/align/localungapped.pyi b/src/biotite/sequence/align/localungapped.pyi index e9d7dbf6b..b8e554bd5 100644 --- a/src/biotite/sequence/align/localungapped.pyi +++ b/src/biotite/sequence/align/localungapped.pyi @@ -1,16 +1,44 @@ -__all__ = ["align_local_ungapped"] +# This source code is part of the Biotite package and is distributed +# under the 3-Clause BSD License. Please see 'LICENSE.rst' for further +# information. -from typing import Literal, overload +from typing import Generic, Literal, overload from biotite.sequence.align.alignment import Alignment from biotite.sequence.align.matrix import SubstitutionMatrix from biotite.sequence.sequence import Sequence from biotite.typing import S1, S2 +__all__ = ["SeedExtension", "align_local_ungapped"] + +class SeedExtension(Generic[S1, S2]): + def __init__( + self, + matrix: SubstitutionMatrix[S1, S2] | tuple[int, int], + threshold: int, + direction: Literal["both", "upstream", "downstream"] = "both", + ) -> None: ... + @overload + def align( + self, + seq1: Sequence[S1], + seq2: Sequence[S2], + seed: tuple[int, int], + score_only: Literal[False] = False, + ) -> Alignment: ... + @overload + def align( + self, + seq1: Sequence[S1], + seq2: Sequence[S2], + seed: tuple[int, int], + score_only: Literal[True], + ) -> int: ... + @overload def align_local_ungapped( seq1: Sequence[S1], seq2: Sequence[S2], - matrix: SubstitutionMatrix[S1, S2], + matrix: SubstitutionMatrix[S1, S2] | tuple[int, int], seed: tuple[int, int], threshold: int, direction: Literal["both", "upstream", "downstream"] = "both", @@ -21,10 +49,11 @@ def align_local_ungapped( def align_local_ungapped( seq1: Sequence[S1], seq2: Sequence[S2], - matrix: SubstitutionMatrix[S1, S2], + matrix: SubstitutionMatrix[S1, S2] | tuple[int, int], seed: tuple[int, int], threshold: int, direction: Literal["both", "upstream", "downstream"] = "both", - score_only: Literal[True] = ..., + *, + score_only: Literal[True], check_matrix: bool = True, ) -> int: ... diff --git a/src/biotite/sequence/align/localungapped.pyx b/src/biotite/sequence/align/localungapped.pyx deleted file mode 100644 index 649972c91..000000000 --- a/src/biotite/sequence/align/localungapped.pyx +++ /dev/null @@ -1,279 +0,0 @@ -# This source code is part of the Biotite package and is distributed -# under the 3-Clause BSD License. Please see 'LICENSE.rst' for further -# information. - -__name__ = "biotite.sequence.align" -__author__ = "Patrick Kunzmann" -__all__ = ["align_local_ungapped"] - -cimport cython -cimport numpy as np - -import numpy as np -from .alignment import Alignment - - -ctypedef np.int32_t int32 -ctypedef np.int64_t int64 -ctypedef np.uint8_t uint8 -ctypedef np.uint16_t uint16 -ctypedef np.uint32_t uint32 -ctypedef np.uint64_t uint64 - -ctypedef fused CodeType1: - uint8 - uint16 - uint32 - uint64 -ctypedef fused CodeType2: - uint8 - uint16 - uint32 - uint64 - - -def align_local_ungapped(seq1, seq2, matrix, seed, int32 threshold, - str direction="both", bint score_only=False, - bint check_matrix=True): - """ - align_local_ungapped(seq1, seq2, matrix, seed, threshold, - direction="both", score_only=False, check_matrix=True) - - Perform a local alignment extending from given `seed` position - without inserting gaps. - - The alignment extends into one or both directions (controlled by - `direction`) until the total alignment score falls more than - `threshold` below the maximum score found (*X-Drop*). - The returned alignment contains the range that yielded the maximum - score. - - Parameters - ---------- - seq1, seq2 : Sequence - The sequences to be aligned. - The sequences do not need to have the same alphabets, as long as - the two alphabets of `matrix` extend the alphabets of the two - sequences. - matrix : SubstitutionMatrix - The substitution matrix used for scoring. - seed : tuple(int, int) - The indices in `seq1` and `seq2` where the local alignment - starts. - The indices must be non-negative. - threshold : int - If the current score falls this value below the maximum score - found, the alignment terminates. - direction : {'both', 'upstream', 'downstream'}, optional - Controls in which direction the alignment extends starting - from the seed. - If ``'upstream'``, the alignment starts before the `seed` and - ends at the `seed`. - If ``'downstream'``, the alignment starts at the `seed` and - ends behind the `seed`. - If ``'both'`` (default) the alignment starts before the `seed` - and ends behind the `seed`. - The `seed` position itself is always included in the alignment. - score_only : bool, optional - If set to ``True``, only the similarity score is returned - instead of the :class:`Alignment`, decreasing the runtime - substantially. - check_matrix : bool, optional - If set to False, the `matrix` is not checked for compatibility - with the alphabets of the sequences. - Due to the small overall runtime of the function, this can increase - performance substantially. - However, unexpected results or crashes may occur, if an - incompatible `matrix` is given. - - - Returns - ------- - alignment : Alignment - The resulting ungapped alignment. - Only returned, if `score_only` is ``False``. - score : int - The alignment similarity score. - Only returned, if `score_only` is ``True``. - - See Also - -------- - align_gapped - For gapped local alignments with the same *X-Drop* technique. - - Examples - -------- - - >>> seq1 = ProteinSequence("BIQTITE") - >>> seq2 = ProteinSequence("PYRRHQTITE") - >>> matrix = SubstitutionMatrix.std_protein_matrix() - >>> alignment = align_local_ungapped(seq1, seq2, matrix, seed=(4,7), threshold=10) - >>> print(alignment) - QTITE - QTITE - >>> alignment = align_local_ungapped(seq1, seq2, matrix, (4,7), 10, direction="upstream") - >>> print(alignment) - QTI - QTI - >>> alignment = align_local_ungapped(seq1, seq2, matrix, (4,7), 10, direction="downstream") - >>> print(alignment) - ITE - ITE - >>> score = align_local_ungapped(seq1, seq2, matrix, (4,7), 10, score_only=True) - >>> print(score) - 24 - """ - if check_matrix: - if not matrix.get_alphabet1().extends(seq1.get_alphabet()) \ - or not matrix.get_alphabet2().extends(seq2.get_alphabet()): - raise ValueError( - "The sequences' alphabets do not fit the matrix" - ) - cdef const int32[:,:] score_matrix = matrix.score_matrix() - - cdef bint upstream - cdef bint downstream - if direction == "both": - upstream = True - downstream = True - elif direction == "upstream": - upstream = True - downstream = False - elif direction == "downstream": - upstream = False - downstream = True - else: - raise ValueError(f"Direction '{direction}' is invalid") - - if threshold < 0: - raise ValueError("The threshold value must be a non-negative integer") - - cdef int seq1_start, seq2_start - seq1_start, seq2_start = seed - if seq1_start < 0 or seq2_start < 0: - raise IndexError("Seed must contain positive indices") - - cdef np.ndarray code1 = seq1.code - cdef np.ndarray code2 = seq2.code - # For C- function call of the '_seed_extend_uint8()' function - # for the common case - # This gives significant performance increase since the - # seed extend itself runs fast - cdef bint both_uint8 = (code1.dtype == np.uint8) \ - & (code2.dtype == np.uint8) - - cdef int32 length - cdef int start_offset = 0 - cdef int stop_offset = 1 - cdef int32 score = 0 - cdef int32 total_score = 0 - - # Separate alignment into two parts: - # the regions upstream and downstream from the seed position - # Range check to avoid negative indices - if upstream and seq1_start > 0 and seq2_start > 0: - # For the upstream region the respective part of the sequence - # must be reversed - if both_uint8: - length = _seed_extend_uint8( - code1[seq1_start-1::-1], code2[seq2_start-1::-1], - score_matrix, threshold, &score - ) - else: - score, length = _seed_extend_generic( - code1[seq1_start-1::-1], code2[seq2_start-1::-1], - score_matrix, threshold - ) - total_score += score - start_offset -= length - if downstream: - if both_uint8: - length = _seed_extend_uint8( - code1[seq1_start+1:], code2[seq2_start+1:], - score_matrix, threshold, &score - ) - else: - score, length = _seed_extend_generic( - code1[seq1_start+1:], code2[seq2_start+1:], - score_matrix, threshold - ) - total_score += score - stop_offset += length - total_score += score_matrix[code1[seq1_start], code2[seq2_start]] - - if score_only: - return total_score - else: - trace = np.stack([ - np.arange(seq1_start + start_offset, seq1_start + stop_offset), - np.arange(seq2_start + start_offset, seq2_start + stop_offset) - ], axis=-1) - return Alignment([seq1, seq2], trace, total_score) - - -@cython.boundscheck(False) -@cython.wraparound(False) -def _seed_extend_generic(CodeType1[:] code1 not None, - CodeType2[:] code2 not None, - const int32[:,:] matrix not None, - int32 threshold): - """ - Align two sequences without insertion of gaps beginning from - start of the given sequences. - If the score drops too low, terminate the alignment. - Return the similarity score and the number of aligned symbols. - """ - cdef int i - cdef int32 total_score = 0, max_score = 0 - cdef int i_max_score = -1 - - # Iterate over the symbols in both sequences - # The alignment automatically terminates, - # if the the end of either sequence is reached - for i in range(_min(code1.shape[0], code2.shape[0])): - total_score += matrix[code1[i], code2[i]] - if total_score >= max_score: - max_score = total_score - i_max_score = i - elif max_score - total_score > threshold: - # Score drops too low -> terminate alignment - break - - # Return the total score and the number of aligned symbols at the - # point with maximum total score - return max_score, i_max_score + 1 - -@cython.boundscheck(False) -@cython.wraparound(False) -cdef int _seed_extend_uint8(uint8[:] code1, uint8[:] code2, - const int32[:,:] matrix, - int32 threshold, int32* score): - """ - The same functionality as :func:`_seed_extend_generic()` but as - C-function tailored for the common ``uint8`` sequence code *dtype*. - This increases the performance for this common case. - """ - cdef int i - cdef int32 total_score = 0, max_score = 0 - cdef int i_max_score = -1 - - # Iterate over the symbols in both sequences - # The alignment automatically terminates, - # if the the end of either sequence is reached - for i in range(_min(code1.shape[0], code2.shape[0])): - total_score += matrix[code1[i], code2[i]] - if total_score >= max_score: - max_score = total_score - i_max_score = i - elif max_score - total_score > threshold: - # Score drops too low -> terminate alignment - break - - # Return the total score and the number of aligned symbols at the - # point with maximum total score - score[0] = max_score - return i_max_score + 1 - - -cdef inline int _min(int a, int b): - return a if a < b else b \ No newline at end of file diff --git a/src/biotite/sequence/align/pairwise.py b/src/biotite/sequence/align/pairwise.py new file mode 100644 index 000000000..a7fc08d02 --- /dev/null +++ b/src/biotite/sequence/align/pairwise.py @@ -0,0 +1,291 @@ +# This source code is part of the Biotite package and is distributed +# under the 3-Clause BSD License. Please see 'LICENSE.rst' for further +# information. + +from __future__ import annotations + +__name__ = "biotite.sequence.align" +__author__ = "Patrick Kunzmann" +__all__ = ["align_ungapped", "align_optimal"] + +from typing import TYPE_CHECKING, Literal, overload +import numpy as np +from biotite.rust.sequence.align import align_optimal as _rust_align_optimal +from biotite.sequence.align.alignment import Alignment +from biotite.sequence.align.matrix import SubstitutionMatrix + +if TYPE_CHECKING: + from biotite.sequence.sequence import Sequence + from biotite.typing import S1, S2 + + +@overload +def align_ungapped( + seq1: Sequence[S1], + seq2: Sequence[S2], + matrix: SubstitutionMatrix[S1, S2] | tuple[int, int], + score_only: Literal[False] = False, +) -> Alignment: ... +@overload +def align_ungapped( + seq1: Sequence[S1], + seq2: Sequence[S2], + matrix: SubstitutionMatrix[S1, S2] | tuple[int, int], + score_only: Literal[True], +) -> int: ... +def align_ungapped( + seq1: Sequence, + seq2: Sequence, + matrix: SubstitutionMatrix | tuple[int, int], + score_only: bool = False, +) -> Alignment | int: + """ + Align two sequences without insertion of gaps. + + Both sequences need to have the same length. + + Parameters + ---------- + seq1, seq2 : Sequence + The sequences, whose similarity should be scored. + matrix : SubstitutionMatrix or tuple(int, int) + Either a substitution matrix or a ``(match, mismatch)`` pair of scores. + score_only : bool, optional + If true return only the score instead of an alignment. + + Returns + ------- + score : Alignment or int + The resulting trivial alignment. If `score_only` is set to true, + only the score is returned. + """ + if len(seq1) != len(seq2): + raise ValueError( + f"Different sequence lengths ({len(seq1):d} and {len(seq2):d})" + ) + if isinstance(matrix, SubstitutionMatrix): + if not matrix.get_alphabet1().extends( + seq1.get_alphabet() + ) or not matrix.get_alphabet2().extends(seq2.get_alphabet()): + raise ValueError("The sequences' alphabets do not fit the matrix") + # Sum the substitution scores of the aligned symbols via fancy indexing + score = int(matrix.score_matrix()[seq1.code, seq2.code].sum()) + else: + # Match/mismatch scoring + match_score, mismatch_score = matrix + score = int(np.where(seq1.code == seq2.code, match_score, mismatch_score).sum()) + if score_only: + return score + else: + # Sequences do not need to be actually aligned + # -> Create alignment with trivial trace + # [[0 0] + # [1 1] + # [2 2] + # ... ] + seq_length = len(seq1) + return Alignment( + sequences=[seq1, seq2], + trace=np.tile(np.arange(seq_length), 2).reshape(2, seq_length).transpose(), + score=score, + ) + + +@overload +def align_optimal( + seq1: Sequence[S1], + seq2: Sequence[S2], + matrix: SubstitutionMatrix[S1, S2] | tuple[int, int], + gap_penalty: int | tuple[int, int] = -10, + terminal_penalty: bool = True, + local: bool = False, + max_number: int = 1000, + score_only: Literal[False] = False, +) -> list[Alignment]: ... +@overload +def align_optimal( + seq1: Sequence[S1], + seq2: Sequence[S2], + matrix: SubstitutionMatrix[S1, S2] | tuple[int, int], + gap_penalty: int | tuple[int, int] = -10, + terminal_penalty: bool = True, + local: bool = False, + max_number: int = 1000, + *, + score_only: Literal[True], +) -> int: ... +def align_optimal( + seq1: Sequence, + seq2: Sequence, + matrix: SubstitutionMatrix | tuple[int, int], + gap_penalty: int | tuple[int, int] = -10, + terminal_penalty: bool = True, + local: bool = False, + max_number: int = 1000, + score_only: bool = False, +) -> list[Alignment] | int: + """ + Perform an optimal alignment of two sequences based on a + dynamic programming algorithm. + + This algorithm yields an optimal alignment, i.e. the sequences + are aligned in the way that results in the highest similarity + score. This operation can be very time and space consuming, + because both scale linearly with each sequence length. + + Parameters + ---------- + seq1, seq2 : Sequence + The sequences to be aligned. + matrix : SubstitutionMatrix or tuple(int, int) + The substitution matrix used for scoring. + gap_penalty : int or tuple(int, int), optional + If an integer is provided, the value will be interpreted as + linear gap penalty. + If a tuple is provided, an affine gap penalty is used. + The first integer in the tuple is the gap opening penalty, + the second integer is the gap extension penalty. + The values need to be negative. + terminal_penalty : bool, optional + If true, gap penalties are applied to terminal gaps. + If `local` is true, this parameter has no effect. + local : bool, optional + If false, a global alignment is performed, otherwise a local + alignment is performed. + max_number : int, optional + The maximum number of alignments returned. + When the number of branches exceeds this value in the traceback + step, no further branches are created. + score_only : bool, optional + If true, only the similarity score is returned instead of the + list of alignments. + + Returns + ------- + alignments : list, type=Alignment or int + A list of alignments. + Each alignment in the list has the same maximum similarity + score. + If `score_only` is set to true, only the score is returned. + + See Also + -------- + align_banded + For a faster heuristic alignment constrained to a diagonal band. + + Notes + ----- + The aligned sequences do not need to be instances from the same + :class:`Sequence` subclass, since they do not need to have the same + alphabet. The only requirement is that the + :class:`SubstitutionMatrix`' alphabets extend the alphabets of the + two sequences. + + This function can either perform a global alignment, based on the + Needleman-Wunsch algorithm :footcite:`Needleman1970` or a local + alignment, based on the Smith–Waterman algorithm + :footcite:`Smith1981`. + + Furthermore this function supports affine gap penalties using the + Gotoh algorithm :footcite:`Gotoh1982`, however, this requires + approximately 4 times the RAM space and execution time. + + References + ---------- + + .. footbibliography:: + + Examples + -------- + + >>> seq1 = NucleotideSequence("ATACGCTTGCT") + >>> seq2 = NucleotideSequence("AGGCGCAGCT") + >>> matrix = SubstitutionMatrix.std_nucleotide_matrix() + >>> ali = align_optimal(seq1, seq2, matrix, gap_penalty=-6) + >>> for a in ali: + ... print(a, "\\n") + ATACGCTTGCT + AGGCGCA-GCT + + ATACGCTTGCT + AGGCGC-AGCT + + """ + # Check if gap penalty is linear or affine + if isinstance(gap_penalty, int): + if gap_penalty > 0: + raise ValueError("Gap penalty must be negative") + elif isinstance(gap_penalty, tuple): + if gap_penalty[0] > 0 or gap_penalty[1] > 0: + raise ValueError("Gap penalty must be negative") + else: + raise TypeError("Gap penalty must be either integer or tuple") + # Check if max_number is reasonable + if max_number < 1: + raise ValueError("Maximum number of returned alignments must be at least 1") + + # The two sequences may use different code dtypes + # -> widen both to a common type so the Rust layer dispatches once + common_dtype = np.promote_types(seq1.code.dtype, seq2.code.dtype) + + traces, score = _rust_align_optimal( + np.ascontiguousarray(seq1.code, common_dtype), + np.ascontiguousarray(seq2.code, common_dtype), + prepare_scoring(matrix, seq1, seq2), + gap_penalty, + terminal_penalty, + local, + score_only, + max_number, + ) + + if score_only: + return score + return [Alignment([seq1, seq2], trace, score) for trace in traces] + + +def prepare_scoring( + scoring: SubstitutionMatrix | tuple[int, int], + seq1: Sequence | None, + seq2: Sequence | None, + check_matrix: bool = True, +) -> np.ndarray | tuple[int, int]: + """ + Resolve the `scoring` argument of the alignment functions into the value + handed to the Rust layer. + + `scoring` is either a :class:`SubstitutionMatrix` (whose 2D score matrix is + returned) or a ``(match, mismatch)`` tuple of scores (returned as a tuple of + plain integers). In the matrix case the matrix alphabets are checked against + the sequences, unless `check_matrix` is false. + + This helper is shared across the alignment modules, but is intentionally + kept out of ``__all__`` as it is not part of the public API. + + Parameters + ---------- + scoring : SubstitutionMatrix or tuple(int, int) + The scoring scheme to resolve. + seq1, seq2 : Sequence or None + The sequences to check the matrix alphabets against. + May be ``None`` if `check_matrix` is false. + check_matrix : bool, optional + Whether to check the matrix alphabets against the sequences. + + Returns + ------- + scoring : ndarray or tuple(int, int) + The 2D score matrix or the ``(match, mismatch)`` tuple. + """ + if isinstance(scoring, tuple): + match_score, mismatch_score = scoring + return (int(match_score), int(mismatch_score)) + if check_matrix: + # If the matrix is checked, the sequences are required + if seq1 is None or seq2 is None: + raise TypeError("Sequences are required to check the matrix") + if not scoring.get_alphabet1().extends( + seq1.get_alphabet() + ) or not scoring.get_alphabet2().extends(seq2.get_alphabet()): + raise ValueError("The sequences' alphabets do not fit the matrix") + return scoring.score_matrix() diff --git a/src/biotite/sequence/align/pairwise.pyi b/src/biotite/sequence/align/pairwise.pyi deleted file mode 100644 index 5f7b413ee..000000000 --- a/src/biotite/sequence/align/pairwise.pyi +++ /dev/null @@ -1,31 +0,0 @@ -__all__ = ["align_ungapped", "align_optimal"] - -from typing import Literal, overload -from biotite.sequence.align.alignment import Alignment -from biotite.sequence.align.matrix import SubstitutionMatrix -from biotite.sequence.sequence import Sequence -from biotite.typing import S1, S2 - -@overload -def align_ungapped( - seq1: Sequence[S1], - seq2: Sequence[S2], - matrix: SubstitutionMatrix[S1, S2], - score_only: Literal[False] = False, -) -> Alignment: ... -@overload -def align_ungapped( - seq1: Sequence[S1], - seq2: Sequence[S2], - matrix: SubstitutionMatrix[S1, S2], - score_only: Literal[True], -) -> int: ... -def align_optimal( - seq1: Sequence[S1], - seq2: Sequence[S2], - matrix: SubstitutionMatrix[S1, S2], - gap_penalty: int | tuple[int, int] = -10, - terminal_penalty: bool = True, - local: bool = False, - max_number: int = 1000, -) -> list[Alignment]: ... diff --git a/src/biotite/sequence/align/pairwise.pyx b/src/biotite/sequence/align/pairwise.pyx deleted file mode 100644 index 167ffae68..000000000 --- a/src/biotite/sequence/align/pairwise.pyx +++ /dev/null @@ -1,585 +0,0 @@ -# This source code is part of the Biotite package and is distributed -# under the 3-Clause BSD License. Please see 'LICENSE.rst' for further -# information. - -__name__ = "biotite.sequence.align" -__author__ = "Patrick Kunzmann" -__all__ = ["align_ungapped", "align_optimal"] - -cimport cython -cimport numpy as np -from .tracetable cimport follow_trace, get_trace_linear, get_trace_affine, \ - TraceDirectionLinear, TraceDirectionAffine - -from .alignment import Alignment -import numpy as np - - -ctypedef np.int32_t int32 -ctypedef np.int64_t int64 -ctypedef np.uint8_t uint8 -ctypedef np.uint16_t uint16 -ctypedef np.uint32_t uint32 -ctypedef np.uint64_t uint64 - -ctypedef fused CodeType1: - uint8 - uint16 - uint32 - uint64 -ctypedef fused CodeType2: - uint8 - uint16 - uint32 - uint64 - - -def align_ungapped(seq1, seq2, matrix, score_only=False): - """ - align_ungapped(seq1, seq2, matrix, score_only=False) - - Align two sequences without insertion of gaps. - - Both sequences need to have the same length. - - Parameters - ---------- - seq1, seq2 : Sequence - The sequences, whose similarity should be scored. - matrix : SubstitutionMatrix - The substitution matrix used for scoring. - score_only : bool, optional - If true return only the score instead of an alignment. - - Returns - ------- - score : Alignment or int - The resulting trivial alignment. If `score_only` is set to true, - only the score is returned. - """ - if len(seq1) != len(seq2): - raise ValueError( - f"Different sequence lengths ({len(seq1):d} and {len(seq2):d})" - ) - if not matrix.get_alphabet1().extends(seq1.get_alphabet()) \ - or not matrix.get_alphabet2().extends(seq2.get_alphabet()): - raise ValueError("The sequences' alphabets do not fit the matrix") - score = _add_scores(seq1.code, seq2.code, matrix.score_matrix()) - if score_only: - return score - else: - # Sequences do not need to be actually aligned - # -> Create alignment with trivial trace - # [[0 0] - # [1 1] - # [2 2] - # ... ] - seq_length = len(seq1) - return Alignment( - sequences = [seq1, seq2], - trace = np.tile(np.arange(seq_length), 2) - .reshape(2, seq_length) - .transpose(), - score = score - ) - - -@cython.boundscheck(False) -@cython.wraparound(False) -def _add_scores(CodeType1[:] code1 not None, - CodeType2[:] code2 not None, - const int32[:,:] matrix not None): - cdef int32 score = 0 - cdef int i - for i in range(code1.shape[0]): - score += matrix[code1[i], code2[i]] - return score - - -def align_optimal(seq1, seq2, matrix, gap_penalty=-10, - terminal_penalty=True, local=False, - max_number=1000): - """ - align_optimal(seq1, seq2, matrix, gap_penalty=-10, - terminal_penalty=True, local=False, max_number=1000) - - Perform an optimal alignment of two sequences based on a - dynamic programming algorithm. - - This algorithm yields an optimal alignment, i.e. the sequences - are aligned in the way that results in the highest similarity - score. This operation can be very time and space consuming, - because both scale linearly with each sequence length. - - The aligned sequences do not need to be instances from the same - :class:`Sequence` subclass, since they do not need to have the same - alphabet. The only requirement is that the - :class:`SubstitutionMatrix`' alphabets extend the alphabets of the - two sequences. - - This function can either perform a global alignment, based on the - Needleman-Wunsch algorithm :footcite:`Needleman1970` or a local - alignment, based on the Smith–Waterman algorithm - :footcite:`Smith1981`. - - Furthermore this function supports affine gap penalties using the - Gotoh algorithm :footcite:`Gotoh1982`, however, this requires - approximately 4 times the RAM space and execution time. - - Parameters - ---------- - seq1, seq2 : Sequence - The sequences to be aligned. - matrix : SubstitutionMatrix - The substitution matrix used for scoring. - gap_penalty : int or tuple(int, int), optional - If an integer is provided, the value will be interpreted as - linear gap penalty. - If a tuple is provided, an affine gap penalty is used. - The first integer in the tuple is the gap opening penalty, - the second integer is the gap extension penalty. - The values need to be negative. - terminal_penalty : bool, optional - If true, gap penalties are applied to terminal gaps. - If `local` is true, this parameter has no effect. - local : bool, optional - If false, a global alignment is performed, otherwise a local - alignment is performed. - max_number : int, optional - The maximum number of alignments returned. - When the number of branches exceeds this value in the traceback - step, no further branches are created. - - Returns - ------- - alignments : list, type=Alignment - A list of alignments. - Each alignment in the list has the same maximum similarity - score. - - See Also - -------- - align_banded - - References - ---------- - - .. footbibliography:: - - Examples - -------- - - >>> seq1 = NucleotideSequence("ATACGCTTGCT") - >>> seq2 = NucleotideSequence("AGGCGCAGCT") - >>> matrix = SubstitutionMatrix.std_nucleotide_matrix() - >>> ali = align_optimal(seq1, seq2, matrix, gap_penalty=-6) - >>> for a in ali: - ... print(a, "\\n") - ATACGCTTGCT - AGGCGCA-GCT - - ATACGCTTGCT - AGGCGC-AGCT - - """ - # Check matrix alphabets - if not matrix.get_alphabet1().extends(seq1.get_alphabet()) \ - or not matrix.get_alphabet2().extends(seq2.get_alphabet()): - raise ValueError("The sequences' alphabets do not fit the matrix") - # Check if gap penalty is linear or affine - if type(gap_penalty) == int: - if gap_penalty > 0: - raise ValueError("Gap penalty must be negative") - affine_penalty = False - elif type(gap_penalty) == tuple: - if gap_penalty[0] > 0 or gap_penalty[1] > 0: - raise ValueError("Gap penalty must be negative") - affine_penalty = True - else: - raise TypeError("Gap penalty must be either integer or tuple") - # Check if max_number is reasonable - if max_number < 1: - raise ValueError( - "Maximum number of returned alignments must be at least 1" - ) - - - # This implementation uses transposed tables in comparison - # to the common visualization - # This means the first sequence is one the left - # and the second sequence is at the top - trace_table = np.zeros(( len(seq1)+1, len(seq2)+1 ), dtype=np.uint8) - code1 = seq1.code - code2 = seq2.code - - # Table filling - ############### - if affine_penalty: - # Affine gap penalty - gap_open = gap_penalty[0] - gap_ext = gap_penalty[1] - # Value for negative infinity - # Used to prevent unallowed state transitions - # Subtraction of gap_open, gap_ext and lowest score value - # to prevent integer overflow - neg_inf = np.iinfo(np.int32).min - gap_open - gap_ext - min_score = np.min(matrix.score_matrix()) - if min_score < 0: - neg_inf -= min_score - # m_table, g1_table and g2_table are the 3 score tables - m_table = np.zeros((len(seq1)+1, len(seq2)+1), dtype=np.int32) - # Fill with negative infinity values to prevent that an - # alignment trace starts with a gap extension - # instead of a gap opening - g1_table = np.full((len(seq1)+1, len(seq2)+1), neg_inf, dtype=np.int32) - g2_table = np.full((len(seq1)+1, len(seq2)+1), neg_inf, dtype=np.int32) - # Disallow trace coming from the match table on the - # left column/top row, as these represent terminal gaps - m_table [0, 1:] = neg_inf - m_table [1:, 0] = neg_inf - # Initialize first row and column for global alignments - if not local: - if terminal_penalty: - # Terminal gaps are penalized - # -> Penalties in first row/column - g1_table[0, 1:] = (np.arange(len(seq2)) * gap_ext) + gap_open - g2_table[1:, 0] = (np.arange(len(seq1)) * gap_ext) + gap_open - else: - g1_table[0, 1:] = np.zeros(len(seq2)) - g2_table[1:, 0] = np.zeros(len(seq1)) - trace_table[0, 1] = TraceDirectionAffine.MATCH_TO_GAP_LEFT - trace_table[0, 2:] = TraceDirectionAffine.GAP_LEFT_TO_GAP_LEFT - trace_table[1, 0] = TraceDirectionAffine.MATCH_TO_GAP_TOP - trace_table[2: ,0] = TraceDirectionAffine.GAP_TOP_TO_GAP_TOP - else: - g1_table[0, 1:] = np.zeros(len(seq2)) - g2_table[1:, 0] = np.zeros(len(seq1)) - _fill_align_table_affine(code1, code2, - matrix.score_matrix(), trace_table, - m_table, g1_table, g2_table, - gap_open, gap_ext, terminal_penalty, local) - else: - # Linear gap penalty - # The table for saving the scores - score_table = np.zeros(( len(seq1)+1, len(seq2)+1 ), dtype=np.int32) - # Initialize first row and column for global alignments - if not local: - if terminal_penalty: - # Terminal gaps are penalized - # -> Penalties in first row/column - score_table[:,0] = np.arange(len(seq1)+1) * gap_penalty - score_table[0,:] = np.arange(len(seq2)+1) * gap_penalty - trace_table[1:,0] = TraceDirectionLinear.GAP_TOP - trace_table[0,1:] = TraceDirectionLinear.GAP_LEFT - _fill_align_table(code1, code2, matrix.score_matrix(), trace_table, - score_table, gap_penalty, terminal_penalty, local) - - - # Traceback - ########### - # Stores all possible traces (= possible alignments) - # A trace stores the indices of the aligned symbols - # in both sequences - trace_list = [] - # Lists of trace starting indices - i_list = np.zeros(0, dtype=int) - j_list = np.zeros(0, dtype=int) - # List of start states - # State specifies the table the trace starts in - state_list = np.zeros(0, dtype=int) - if local: - # The start point is the maximal score in the table - # Multiple starting points possible, - # when duplicates of maximal score exist - if affine_penalty: - # The maximum score in the gap score tables do not need to - # be considered, as these starting positions would indicate - # that the local alignment starts with a gap - # Hence the maximum score value in these tables is always - # less than in the match table - max_score = np.max(m_table) - i_list, j_list = np.where((m_table == max_score)) - state_list = np.append(state_list, np.full(len(i_list), 1)) - else: - max_score = np.max(score_table) - i_list, j_list = np.where((score_table == max_score)) - # State is always 0 for linear gap penalty - # since there is only one table - state_list = np.zeros(len(i_list), dtype=int) - else: - # The start point is the last element in the table - # -1 in start indices due to sequence offset mentioned before - i_start = trace_table.shape[0] -1 - j_start = trace_table.shape[1] -1 - if affine_penalty: - max_score = max(m_table[i_start,j_start], - g1_table[i_start,j_start], - g2_table[i_start,j_start]) - if m_table[i_start,j_start] == max_score: - i_list = np.append(i_list, i_start) - j_list = np.append(j_list, j_start) - state_list = np.append(state_list, 1) - if g1_table[i_start,j_start] == max_score: - i_list = np.append(i_list, i_start) - j_list = np.append(j_list, j_start) - state_list = np.append(state_list, 2) - if g2_table[i_start,j_start] == max_score: - i_list = np.append(i_list, i_start) - j_list = np.append(j_list, j_start) - state_list = np.append(state_list, 3) - else: - i_list = np.append(i_list, i_start) - j_list = np.append(j_list, j_start) - state_list = np.append(state_list, 0) - max_score = score_table[i_start,j_start] - # Follow the traces specified in state and indices lists - cdef int curr_trace_count - for k in range(len(i_list)): - i_start = i_list[k] - j_start = j_list[k] - state_start = state_list[k] - # Pessimistic array allocation: - # The maximum trace length arises from an alignment, where each - # symbol is aligned to a gap - trace = np.full(( i_start+1 + j_start+1, 2 ), -1, dtype=np.int64) - curr_trace_count = 1 - follow_trace( - trace_table, False, i_start, j_start, 0, trace, trace_list, - state=state_start, curr_trace_count=&curr_trace_count, - max_trace_count=max_number, - # Diagonals are only needed for banded alignments - lower_diag=0, upper_diag=0 - ) - - # Replace gap entries in trace with -1 - for i, trace in enumerate(trace_list): - trace = np.flip(trace, axis=0) - gap_filter = np.zeros(trace.shape, dtype=bool) - gap_filter[np.unique(trace[:,0], return_index=True)[1], 0] = True - gap_filter[np.unique(trace[:,1], return_index=True)[1], 1] = True - trace[~gap_filter] = -1 - trace_list[i] = trace - - # Limit the number of generated alignments to `max_number`: - # In most cases this is achieved by discarding branches in - # 'follow_trace()', however, if multiple local alignment starts - # are used, the number of created traces are the number of - # starts times `max_number` - trace_list = trace_list[:max_number] - return [Alignment([seq1, seq2], trace, max_score) for trace in trace_list] - - -@cython.boundscheck(False) -@cython.wraparound(False) -def _fill_align_table(CodeType1[:] code1 not None, - CodeType2[:] code2 not None, - const int32[:,:] matrix not None, - uint8[:,:] trace_table not None, - int32[:,:] score_table not None, - int gap_penalty, - bint term_penalty, - bint local): - """ - Fill an alignment table with linear gap penalty using dynamic - programming. - - Parameters - ---------- - code1, code2 - The sequence code of each sequence to be aligned. - matrix - The score matrix obtained from the :class:`SubstitutionMatrix` - object. - trace_table - A matrix containing values indicating the direction for the - traceback step. - The matrix is filled in this function - score_table - The alignment table. - The matrix is filled in this function. - gap_penalty - The linear gap penalty. - term_penalty - Indicates, whether terminal gaps should be penalized. - local - Indicates, whether a local alignment should be performed. - """ - - cdef int i, j - cdef int max_i, max_j - cdef int32 from_diag, from_left, from_top - cdef uint8 trace - cdef int32 score - - # For local alignments terminal gaps on the right side are ignored - # anyway, as the alignment should stop before - if local: - term_penalty = True - # Used in case terminal gaps are not penalized - i_max = score_table.shape[0] -1 - j_max = score_table.shape[1] -1 - - # Starts at 1 since the first row and column are already filled - for i in range(1, score_table.shape[0]): - for j in range(1, score_table.shape[1]): - # Evaluate score from diagonal direction - # -1 in sequence index is necessary - # due to the shift of the sequences - # to the bottom/right in the table - from_diag = score_table[i-1, j-1] + matrix[code1[i-1], code2[j-1]] - # Evaluate score from left direction - if not term_penalty and i == i_max: - from_left = score_table[i, j-1] - else: - from_left = score_table[i, j-1] + gap_penalty - # Evaluate score from top direction - if not term_penalty and j == j_max: - from_top = score_table[i-1, j] - else: - from_top = score_table[i-1, j] + gap_penalty - - trace = get_trace_linear(from_diag, from_left, from_top, &score) - - # Local alignment specialty: - # If score is less than or equal to 0, - # then the score of the cell remains 0 - # and the trace ends here - if local == True and score <= 0: - continue - - score_table[i,j] = score - trace_table[i,j] = trace - - -@cython.boundscheck(False) -@cython.wraparound(False) -def _fill_align_table_affine(CodeType1[:] code1 not None, - CodeType2[:] code2 not None, - const int32[:,:] matrix not None, - uint8[:,:] trace_table not None, - int32[:,:] m_table not None, - int32[:,:] g1_table not None, - int32[:,:] g2_table not None, - int gap_open, - int gap_ext, - bint term_penalty, - bint local): - """ - Fill an alignment table with affine gap penalty using dynamic - programming. - - Parameters - ---------- - code1, code2 - The sequence code of each sequence to be aligned. - matrix - The score matrix obtained from the class:`SubstitutionMatrix` - object. - trace_table - A matrix containing values indicating the direction for the - traceback step. - The matrix is filled in this function. - m_table, g1_table, g2_table - The alignment tables containing the scores. - `m_table` contains values for matches. - `g1_table` contains values for gaps in the first sequence. - `g2_table` contains values for gaps in the second sequence. - The matrix is filled in this function. - gap_open - The gap opening penalty. - gap_ext - The gap extension penalty. - term_penalty - Indicates, whether terminal gaps should be penalized. - local - Indicates, whether a local alignment should be performed. - """ - - cdef int i, j - cdef int max_i, max_j - cdef int32 mm_score, g1m_score, g2m_score - cdef int32 mg1_score, g1g1_score - cdef int32 mg2_score, g2g2_score - cdef int32 m_score, g1_score, g2_score - cdef int32 similarity_score - cdef uint8 trace - - # For local alignments terminal gaps on the right and the bottom are - # ignored anyway, as the alignment should stop before - if local: - term_penalty = True - # Used in case terminal gaps are not penalized - i_max = trace_table.shape[0] -1 - j_max = trace_table.shape[1] -1 - - # Starts at 1 since the first row and column are already filled - for i in range(1, trace_table.shape[0]): - for j in range(1, trace_table.shape[1]): - # Calculate the scores for possible transitions - # into the current cell - similarity_score = matrix[code1[i-1], code2[j-1]] - mm_score = m_table[i-1,j-1] + similarity_score - g1m_score = g1_table[i-1,j-1] + similarity_score - g2m_score = g2_table[i-1,j-1] + similarity_score - # No transition from g1_table to g2_table and vice versa - # Since this would mean adjacent gaps in both sequences - # A substitution makes more sense in this case - if not term_penalty and i == i_max: - mg1_score = m_table[i,j-1] - g1g1_score = g1_table[i,j-1] - else: - mg1_score = m_table[i,j-1] + gap_open - g1g1_score = g1_table[i,j-1] + gap_ext - if not term_penalty and j == j_max: - mg2_score = m_table[i-1,j] - g2g2_score = g2_table[i-1,j] - else: - mg2_score = m_table[i-1,j] + gap_open - g2g2_score = g2_table[i-1,j] + gap_ext - - trace = get_trace_affine( - mm_score, g1m_score, g2m_score, - mg1_score, g1g1_score, - mg2_score, g2g2_score, - # The max score values to be written - &m_score, &g1_score, &g2_score - ) - - # Fill values into tables - # Local alignment specialty: - # If score is less than or equal to 0, - # then the score of the cell remains 0 - # and the trace ends here - if local == True: - if m_score <= 0: - # End trace in specific table - # by filtering out the respective bits - trace &= ~( - TraceDirectionAffine.MATCH_TO_MATCH | - TraceDirectionAffine.GAP_LEFT_TO_MATCH | - TraceDirectionAffine.GAP_TOP_TO_MATCH - ) - # m_table[i,j] remains 0 - else: - m_table[i,j] = m_score - if g1_score <= 0: - trace &= ~( - TraceDirectionAffine.MATCH_TO_GAP_LEFT | - TraceDirectionAffine.GAP_LEFT_TO_GAP_LEFT - ) - # g1_table[i,j] remains negative infinity - else: - g1_table[i,j] = g1_score - if g2_score <= 0: - trace &= ~( - TraceDirectionAffine.MATCH_TO_GAP_TOP | - TraceDirectionAffine.GAP_TOP_TO_GAP_TOP - ) - # g2_table[i,j] remains negative infinity - else: - g2_table[i,j] = g2_score - else: - m_table[i,j] = m_score - g1_table[i,j] = g1_score - g2_table[i,j] = g2_score - trace_table[i,j] = trace \ No newline at end of file diff --git a/src/biotite/sequence/align/tracetable.pxd b/src/biotite/sequence/align/tracetable.pxd deleted file mode 100644 index fe36ead59..000000000 --- a/src/biotite/sequence/align/tracetable.pxd +++ /dev/null @@ -1,64 +0,0 @@ -# This source code is part of the Biotite package and is distributed -# under the 3-Clause BSD License. Please see 'LICENSE.rst' for further -# information. - -cimport cython -cimport numpy as np - - -# A trace table saves the directions a cell came from -# A "1" in the corresponding bit in the trace table means -# the cell came from this direction - -cdef enum TraceDirectionLinear: - # Values for linear gap penalty (one score table) - MATCH = 1 # bit 1 -> diagonal -> alignment of symbols - GAP_LEFT = 2 # bit 2 -> left -> gap in first sequence - GAP_TOP = 4 # bit 3 -> top -> gap in second sequence - -cdef enum TraceDirectionAffine: - # Values for affine gap penalty (three score tables) - MATCH_TO_MATCH = 1 # bit 1 -> match - match transition - GAP_LEFT_TO_MATCH = 2 # bit 2 -> seq 1 gap - match transition - GAP_TOP_TO_MATCH = 4 # bit 3 -> seq 2 gap - match transition - MATCH_TO_GAP_LEFT = 8 # bit 4 -> match - seq 1 gap transition - GAP_LEFT_TO_GAP_LEFT = 16 # bit 5 -> seq 1 gap - seq 1 gap transition - MATCH_TO_GAP_TOP = 32 # bit 6 -> match - seq 2 gap transition - GAP_TOP_TO_GAP_TOP = 64 # bit 7 -> seq 2 gap - seq 2 gap transition - - -cdef enum TraceState: - # The state specifies the table the traceback is currently in - # For linear gap penalty (only one table/state exists): - NO_STATE = 0 - # For affine gap penalty (three tables/states exists): - MATCH_STATE = 1 - GAP_LEFT_STATE = 2 - GAP_TOP_STATE = 3 - - -cdef np.uint8_t get_trace_linear(np.int32_t match_score, - np.int32_t gap_left_score, - np.int32_t gap_top_score, - np.int32_t *max_score) - -cdef np.uint8_t get_trace_affine(np.int32_t match_to_match_score, - np.int32_t gap_left_to_match_score, - np.int32_t gap_top_to_match_score, - np.int32_t match_to_gap_left_score, - np.int32_t gap_left_to_gap_left_score, - np.int32_t match_to_gap_top_score, - np.int32_t gap_top_to_gap_top_score, - np.int32_t *max_match_score, - np.int32_t *max_gap_left_score, - np.int32_t *max_gap_top_score) - -cdef int follow_trace(np.uint8_t[:,:] trace_table, - bint banded, - int i, int j, int pos, - np.int64_t[:,:] trace, - list trace_list, - int state, - int* curr_trace_count, - int max_trace_count, - int lower_diag, int upper_diag) except -1 \ No newline at end of file diff --git a/src/biotite/sequence/align/tracetable.pyx b/src/biotite/sequence/align/tracetable.pyx deleted file mode 100644 index 5ef22e257..000000000 --- a/src/biotite/sequence/align/tracetable.pyx +++ /dev/null @@ -1,370 +0,0 @@ -# This source code is part of the Biotite package and is distributed -# under the 3-Clause BSD License. Please see 'LICENSE.rst' for further -# information. - -""" -A module for Biotite's internal use only. -Contains C-functions for handling trace tables in a reuasable way for -different alignment functions. -""" - -__name__ = "biotite.sequence.align" -__author__ = "Patrick Kunzmann" -__all__ = [] - -cimport cython -cimport numpy as np - -import numpy as np - - -cdef inline np.uint8_t get_trace_linear(np.int32_t match_score, - np.int32_t gap_left_score, - np.int32_t gap_top_score, - np.int32_t *max_score): - """ - Find maximum score from the input scores and return corresponding - trace direction for linear gap penalty. - """ - if match_score > gap_left_score: - if match_score > gap_top_score: - trace = TraceDirectionLinear.MATCH - max_score[0] = match_score - elif match_score == gap_top_score: - trace = ( - TraceDirectionLinear.MATCH | - TraceDirectionLinear.GAP_TOP - ) - max_score[0] = match_score - else: - trace = TraceDirectionLinear.GAP_TOP - max_score[0] = gap_top_score - elif match_score == gap_left_score: - if match_score > gap_top_score: - trace = ( - TraceDirectionLinear.MATCH | - TraceDirectionLinear.GAP_LEFT - ) - max_score[0] = match_score - elif match_score == gap_top_score: - trace = ( - TraceDirectionLinear.MATCH | - TraceDirectionLinear.GAP_LEFT | - TraceDirectionLinear.GAP_TOP - ) - max_score[0] = match_score - else: - trace = TraceDirectionLinear.GAP_TOP - max_score[0] = gap_top_score - else: - if gap_left_score > gap_top_score: - trace = TraceDirectionLinear.GAP_LEFT - max_score[0] = gap_left_score - elif gap_left_score == gap_top_score: - trace = ( - TraceDirectionLinear.GAP_LEFT | - TraceDirectionLinear.GAP_TOP - ) - max_score[0] = gap_left_score - else: - trace = TraceDirectionLinear.GAP_TOP - max_score[0] = gap_top_score - - return trace - - -cdef inline np.uint8_t get_trace_affine(np.int32_t match_to_match_score, - np.int32_t gap_left_to_match_score, - np.int32_t gap_top_to_match_score, - np.int32_t match_to_gap_left_score, - np.int32_t gap_left_to_gap_left_score, - np.int32_t match_to_gap_top_score, - np.int32_t gap_top_to_gap_top_score, - np.int32_t *max_match_score, - np.int32_t *max_gap_left_score, - np.int32_t *max_gap_top_score): - """ - Find maximum scores from the input scores and return corresponding - trace direction for affine gap penalty. - """ - # Match Table - if match_to_match_score > gap_left_to_match_score: - if match_to_match_score > gap_top_to_match_score: - trace = TraceDirectionAffine.MATCH_TO_MATCH - max_match_score[0] = match_to_match_score - elif match_to_match_score == gap_top_to_match_score: - trace = ( - TraceDirectionAffine.MATCH_TO_MATCH | - TraceDirectionAffine.GAP_TOP_TO_MATCH - ) - max_match_score[0] = match_to_match_score - else: - trace = TraceDirectionAffine.GAP_TOP_TO_MATCH - max_match_score[0] = gap_top_to_match_score - elif match_to_match_score == gap_left_to_match_score: - if match_to_match_score > gap_top_to_match_score: - trace = ( - TraceDirectionAffine.MATCH_TO_MATCH | - TraceDirectionAffine.GAP_LEFT_TO_MATCH - ) - max_match_score[0] = match_to_match_score - elif match_to_match_score == gap_top_to_match_score: - trace = ( - TraceDirectionAffine.MATCH_TO_MATCH | - TraceDirectionAffine.GAP_LEFT_TO_MATCH | - TraceDirectionAffine.GAP_TOP_TO_MATCH - ) - max_match_score[0] = match_to_match_score - else: - trace = TraceDirectionAffine.GAP_TOP_TO_MATCH - max_match_score[0] = gap_top_to_match_score - else: - if gap_left_to_match_score > gap_top_to_match_score: - trace = TraceDirectionAffine.GAP_LEFT_TO_MATCH - max_match_score[0] = gap_left_to_match_score - elif gap_left_to_match_score == gap_top_to_match_score: - trace = ( - TraceDirectionAffine.GAP_LEFT_TO_MATCH | - TraceDirectionAffine.GAP_TOP_TO_MATCH - ) - max_match_score[0] = gap_left_to_match_score - else: - trace = TraceDirectionAffine.GAP_TOP_TO_MATCH - max_match_score[0] = gap_top_to_match_score - - # 'Gap left' table - if match_to_gap_left_score > gap_left_to_gap_left_score: - trace |= TraceDirectionAffine.MATCH_TO_GAP_LEFT - max_gap_left_score[0] = match_to_gap_left_score - elif match_to_gap_left_score < gap_left_to_gap_left_score: - trace |= TraceDirectionAffine.GAP_LEFT_TO_GAP_LEFT - max_gap_left_score[0] = gap_left_to_gap_left_score - else: - trace |= ( - TraceDirectionAffine.MATCH_TO_GAP_LEFT | - TraceDirectionAffine.GAP_LEFT_TO_GAP_LEFT - ) - max_gap_left_score[0] = match_to_gap_left_score - - # 'Gap right' table - if match_to_gap_top_score > gap_top_to_gap_top_score: - trace |= TraceDirectionAffine.MATCH_TO_GAP_TOP - max_gap_top_score[0] = match_to_gap_top_score - elif match_to_gap_top_score < gap_top_to_gap_top_score: - trace |= TraceDirectionAffine.GAP_TOP_TO_GAP_TOP - max_gap_top_score[0] = gap_top_to_gap_top_score - else: - trace |= ( - TraceDirectionAffine.MATCH_TO_GAP_TOP | - TraceDirectionAffine.GAP_TOP_TO_GAP_TOP - ) - max_gap_top_score[0] = gap_top_to_gap_top_score - - return trace - - -cdef int follow_trace(np.uint8_t[:,:] trace_table, - bint banded, - int i, int j, int pos, - np.int64_t[:,:] trace, - list trace_list, - int state, - int* curr_trace_count, - int max_trace_count, - int lower_diag, int upper_diag) except -1: - """ - Follow and return traces from a trace table. - - Parameters - ---------- - trace_table - A matrix containing values indicating the direction for the - traceback. - banded - Whether the trace table belongs to a banded alignment - i, j - The current position in the trace table. - For the first branch, this is the start of the traceback. - For additional branches this is the start of the respective - branch. - pos - The current position inthe trace array to be created. - For the first branch, this is 0. - For additional branches the value of the parent branch is taken. - trace - The alignment trace array to be filled. - trace_list - When a trace is finished, it is appened to this list - state - The current score table (*match*, *gap left*, *gap top*) - the traceback is in, taken from parent branch. - Always 0 when a linear gap penalty is used. - curr_trace_count - The current number of branches. The value is a pointer, so that - updating this value propagates the value to all other branches - max_trace_count - The maximum number of branches created. When the number of - branches reaches this value, no new branches are created. - lower_diag, upper_diag - The lower and upper diagonal for a banded alignment. - Unused, if `banded` is false. - - Returns - ------- - int - ``0`` if, no exception is raised, otherwisw ``-1``. - """ - - cdef list next_indices - cdef list next_states - cdef int trace_value - cdef int k - cdef int seq_i, seq_j - cdef int i_match, i_gap_left, i_gap_top - cdef int j_match, j_gap_left, j_gap_top - - if state == TraceState.NO_STATE: - # Linear gap penalty - # Trace table has a 0 -> no trace direction -> break loop - # The '0'-cell itself is also not included in the traceback - while trace_table[i,j] != 0: - if banded: - seq_i = i - 1 - seq_j = j + seq_i + lower_diag - 1 - i_match, i_gap_left, i_gap_top = i-1, i, i-1 - j_match, j_gap_left, j_gap_top = j , j-1, j+1 - else: - # -1 is necessary due to the shift of the sequences - # to the bottom/right in the table - seq_i = i - 1 - seq_j = j - 1 - i_match, i_gap_left, i_gap_top = i-1, i, i-1 - j_match, j_gap_left, j_gap_top = j-1, j-1, j - trace[pos, 0] = seq_i - trace[pos, 1] = seq_j - pos += 1 - # Traces may split - next_indices = [] - trace_value = trace_table[i,j] - if trace_value & TraceDirectionLinear.MATCH: - next_indices.append((i_match, j_match)) - if trace_value & TraceDirectionLinear.GAP_LEFT: - next_indices.append((i_gap_left, j_gap_left)) - if trace_value & TraceDirectionLinear.GAP_TOP: - next_indices.append((i_gap_top, j_gap_top)) - # Trace branching - # -> Recursive call of _follow_trace() for indices[1:] - for k in range(1, len(next_indices)): - if curr_trace_count[0] < max_trace_count: - curr_trace_count[0] += 1 - new_i, new_j = next_indices[k] - follow_trace( - trace_table, banded, new_i, new_j, pos, - np.copy(trace), trace_list, 0, - curr_trace_count, max_trace_count, - lower_diag, upper_diag - ) - # Continue in this method with indices[0] - i, j = next_indices[0] - else: - # Affine gap penalty - # -> check only for the current state whether the trace ends - while ( - ( - state == TraceState.MATCH_STATE and trace_table[i,j] & ( - TraceDirectionAffine.MATCH_TO_MATCH | - TraceDirectionAffine.GAP_LEFT_TO_MATCH | - TraceDirectionAffine.GAP_TOP_TO_MATCH - ) != 0 - ) or ( - state == TraceState.GAP_LEFT_STATE and trace_table[i,j] & ( - TraceDirectionAffine.MATCH_TO_GAP_LEFT | - TraceDirectionAffine.GAP_LEFT_TO_GAP_LEFT - ) != 0 - ) or ( - state == TraceState.GAP_TOP_STATE and trace_table[i,j] & ( - TraceDirectionAffine.MATCH_TO_GAP_TOP | - TraceDirectionAffine.GAP_TOP_TO_GAP_TOP - ) != 0 - ) - ): - if banded: - seq_i = i - 1 - seq_j = j + seq_i + lower_diag - 1 - i_match, i_gap_left, i_gap_top = i-1, i, i-1 - j_match, j_gap_left, j_gap_top = j , j-1, j+1 - else: - # -1 is necessary due to the shift of the sequences - # to the bottom/right in the table - seq_i = i - 1 - seq_j = j - 1 - i_match, i_gap_left, i_gap_top = i-1, i, i-1 - j_match, j_gap_left, j_gap_top = j-1, j-1, j - trace[pos, 0] = seq_i - trace[pos, 1] = seq_j - pos += 1 - next_indices = [] - next_states = [] - - # Get value of trace corresponding to current state - # = table trace is currently in - if state == TraceState.MATCH_STATE: - trace_value = trace_table[i,j] & ( - TraceDirectionAffine.MATCH_TO_MATCH | - TraceDirectionAffine.GAP_LEFT_TO_MATCH | - TraceDirectionAffine.GAP_TOP_TO_MATCH - ) - elif state == TraceState.GAP_LEFT_STATE: - trace_value = trace_table[i,j] & ( - TraceDirectionAffine.MATCH_TO_GAP_LEFT | - TraceDirectionAffine.GAP_LEFT_TO_GAP_LEFT - ) - else: # state == TraceState.GAP_TOP_STATE: - trace_value = trace_table[i,j] & ( - TraceDirectionAffine.MATCH_TO_GAP_TOP | - TraceDirectionAffine.GAP_TOP_TO_GAP_TOP - ) - - # Determine indices and state of next trace step - if trace_value & TraceDirectionAffine.MATCH_TO_MATCH: - next_indices.append((i_match, j_match)) - next_states.append(TraceState.MATCH_STATE) - if trace_value & TraceDirectionAffine.GAP_LEFT_TO_MATCH: - next_indices.append((i_match, j_match)) - next_states.append(TraceState.GAP_LEFT_STATE) - if trace_value & TraceDirectionAffine.GAP_TOP_TO_MATCH: - next_indices.append((i_match, j_match)) - next_states.append(TraceState.GAP_TOP_STATE) - if trace_value & TraceDirectionAffine.MATCH_TO_GAP_LEFT: - next_indices.append((i_gap_left, j_gap_left)) - next_states.append(TraceState.MATCH_STATE) - if trace_value & TraceDirectionAffine.GAP_LEFT_TO_GAP_LEFT: - next_indices.append((i_gap_left, j_gap_left)) - next_states.append(TraceState.GAP_LEFT_STATE) - if trace_value & TraceDirectionAffine.MATCH_TO_GAP_TOP: - next_indices.append((i_gap_top, j_gap_top)) - next_states.append(TraceState.MATCH_STATE) - if trace_value & TraceDirectionAffine.GAP_TOP_TO_GAP_TOP: - next_indices.append((i_gap_top, j_gap_top)) - next_states.append(TraceState.GAP_TOP_STATE) - # Trace branching - # -> Recursive call of _follow_trace() for indices[1:] - for k in range(1, len(next_indices)): - if curr_trace_count[0] < max_trace_count: - curr_trace_count[0] += 1 - new_i, new_j = next_indices[k] - new_state = next_states[k] - follow_trace( - trace_table, banded, new_i, new_j, pos, - np.copy(trace), trace_list, new_state, - curr_trace_count, max_trace_count, - lower_diag, upper_diag - ) - # Continue in this method with indices[0] and states[0] - i, j = next_indices[0] - state = next_states[0] - # Trim trace to correct size (delete all pure -1 entries) - # and append to trace_list - tr_arr = np.asarray(trace) - trace_list.append(tr_arr[(tr_arr[:,0] != -1) | (tr_arr[:,1] != -1)]) - return 0 \ No newline at end of file diff --git a/src/rust/sequence/align/banded.rs b/src/rust/sequence/align/banded.rs new file mode 100644 index 000000000..e48b67261 --- /dev/null +++ b/src/rust/sequence/align/banded.rs @@ -0,0 +1,638 @@ +//! Banded pairwise alignment (`align_banded`). +//! +//! Only the cells within a diagonal band `lower_diag..=upper_diag` (with +//! `diag = j - i`) are filled. The band is 'straightened' into a table of shape +//! `(seq1_len + 1, band_width + 2)`, where the two extra columns are +//! out-of-band sentinels. +//! +//! Banded alignment is always either local or semi-global (terminal gaps are +//! never penalized) and fills every in-band cell with the same interior +//! recurrence, so its [`FillRule`] is simpler than the rectangular one in +//! [`pairwise`](super::pairwise): no first-row/column or terminal-edge methods, +//! and only the `LOCAL` and `SCORE_ONLY` const generics. +//! +//! The Python layer guarantees `seq1` is the shorter sequence and supplies the +//! cropped band. + +use super::cell::{ + get_trace_affine, get_trace_linear, AffineCell, AffineDirection, AffineDirectionStore, + AffineScore, Cell, LinearCell, LinearDirection, LinearDirectionStore, TraceDirection, + TraceState, +}; +use super::scoring::{GapPenalty, Score, Scoring, ScoringScheme}; +use super::symbol::Symbol; +use super::table::{StridedTable, TableIndex}; +use super::trace::{follow_trace, path_to_trace}; +use crate::dispatch_dtype; +use numpy::ndarray::Array2; +use numpy::{ + IntoPyArray, PyArray1, PyArrayDescrMethods, PyArrayMethods, PyUntypedArray, + PyUntypedArrayMethods, +}; +use pyo3::prelude::*; +use pyo3::types::{PyList, PyTuple}; +use std::cmp::Ordering; +use std::marker::PhantomData; + +/// Perform a local or semi-global banded pairwise alignment. +/// +/// Parameters +/// ---------- +/// code1, code2 : ndarray +/// The sequence codes, sharing the same unsigned integer dtype. `code1` +/// must be the shorter sequence. +/// scoring : ndarray, dtype=int32 or tuple(int, int) +/// A substitution matrix or a `(match, mismatch)` score pair. +/// gap_penalty : int or tuple(int, int) +/// A linear or affine gap penalty. +/// lower_diag, upper_diag : int +/// The (already cropped) lower and upper band diagonals. +/// local : bool +/// Whether to perform a local (instead of semi-global) alignment. +/// score_only : bool +/// If true, only the score is computed and the trace list is empty. +/// max_number : int +/// The maximum number of (co-optimal) alignments to return. +/// +/// Returns +/// ------- +/// traces : list of ndarray +/// The alignment traces, each of shape `(length, 2)`. Empty if `score_only`. +/// score : int +/// The optimal alignment score within the band. +#[pyfunction] +#[pyo3(signature = (code1, code2, scoring, gap_penalty, lower_diag, upper_diag, local, score_only, max_number))] +#[allow(clippy::too_many_arguments)] +pub fn align_banded<'py>( + py: Python<'py>, + code1: &Bound<'py, PyUntypedArray>, + code2: &Bound<'py, PyUntypedArray>, + scoring: Scoring, + gap_penalty: GapPenalty, + lower_diag: isize, + upper_diag: isize, + local: bool, + score_only: bool, + max_number: usize, +) -> PyResult> { + let dtype = code1.dtype(); + dispatch_dtype!( + py, + &dtype, + [u8, u16, u32, u64], + align_banded_dtype( + py, + code1, + code2, + scoring, + gap_penalty, + lower_diag, + upper_diag, + local, + score_only, + max_number + ) + ) +} + +#[allow(clippy::too_many_arguments)] +fn align_banded_dtype<'py, S: Symbol + numpy::Element>( + py: Python<'py>, + code1: &Bound<'py, PyUntypedArray>, + code2: &Bound<'py, PyUntypedArray>, + scoring: Scoring, + gap_penalty: GapPenalty, + lower_diag: isize, + upper_diag: isize, + local: bool, + score_only: bool, + max_number: usize, +) -> PyResult> { + let code1_array = code1.cast::>()?.readonly(); + let code2_array = code2.cast::>()?.readonly(); + let code1 = code1_array.as_slice()?; + let code2 = code2_array.as_slice()?; + + // Instantiate the fill rule for the runtime scoring scheme, gap penalty, + // `local` and `score_only` flags, then run the generic core. The + // direction-storage type (`$dir` for a full alignment, `()` for score-only) + // selects the cell size. + macro_rules! run { + ($rule:ident, $dir:ty, $local:literal, $scheme:expr, ($($penalty:expr),+)) => { + align_banded_generic( + code1, code2, + <$rule<_, _, $dir, $local>>::new($scheme, $($penalty),+), + lower_diag, upper_diag, max_number, + ) + }; + } + macro_rules! by_consts { + ($rule:ident, $dir:ty, $scheme:expr, ($($penalty:expr),+)) => { + match (local, score_only) { + (true, true) => run!($rule, (), true, $scheme, ($($penalty),+)), + (true, false) => run!($rule, $dir, true, $scheme, ($($penalty),+)), + (false, true) => run!($rule, (), false, $scheme, ($($penalty),+)), + (false, false) => run!($rule, $dir, false, $scheme, ($($penalty),+)), + } + }; + } + macro_rules! by_penalty { + ($scheme:expr) => { + match gap_penalty { + GapPenalty::Linear(gap) => { + by_consts!(LinearGapPenalty, LinearDirection, $scheme, (gap)) + } + GapPenalty::Affine(open, extend) => { + by_consts!(AffineGapPenalty, AffineDirection, $scheme, (open, extend)) + } + } + }; + } + let (traces, score) = match scoring { + Scoring::Matrix(scheme) => by_penalty!(scheme), + Scoring::Match(scheme) => by_penalty!(scheme), + }; + + let list = PyList::empty(py); + for trace in traces { + list.append(trace.into_pyarray(py))?; + } + PyTuple::new(py, [list.into_any(), score.into_pyobject(py)?.into_any()]) +} + +/// Core banded alignment, generic over the symbol type and fill rule. +pub(crate) fn align_banded_generic( + code1: &[S], + code2: &[S], + rule: F, + lower_diag: isize, + upper_diag: isize, + max_number: usize, +) -> (Vec>, Score) +where + S: Symbol, + F: FillRule, +{ + let n = code1.len(); + let m = code2.len(); + let band_width = (upper_diag - lower_diag + 1) as usize; + + // The 'straightened' band: rows are the first sequence, the `band_width` + // interior columns are the band, flanked by two out-of-band sentinel + // columns (0 and `band_width + 1`) + // Initialize the interior with the edge value, then overwrite the two sentinel columns + let mut table = StridedTable::filled((n + 1, band_width + 2), rule.fill_edges()); + for i in 0..=n { + let left = table.index((i, 0)); + let right = table.index((i, band_width + 1)); + table[left] = rule.fill_sentinel(); + table[right] = rule.fill_sentinel(); + } + + // Constant offsets to the three predecessors in band coordinates: + // Due to the straightening, the diagonal/left/top neighbors become these: + let match_offset = table.index_offset((-1, 0)); + let gap1_offset = table.index_offset((0, -1)); + let gap2_offset = table.index_offset((-1, 1)); + let column_step = table.index_offset((0, 1)); + + for (seq_i, &s1) in code1.iter().enumerate() { + let i = seq_i + 1; + let seq_j_start = (seq_i as isize + lower_diag).max(0) as usize; + let seq_j_end = ((seq_i as isize + upper_diag + 1).min(m as isize)).max(0) as usize; + // Consecutive `seq_j` map to consecutive band columns, so walk the index + // instead of recomputing it per cell + let j_start = seq_j_start.wrapping_add_signed(1 - seq_i as isize - lower_diag); + let mut current = table.index((i, j_start)); + for &s2 in code2.iter().take(seq_j_end).skip(seq_j_start) { + let from_match = table[current + match_offset]; + let from_gap1 = table[current + gap1_offset]; + let from_gap2 = table[current + gap2_offset]; + table[current] = rule.fill(s1, s2, &from_match, &from_gap1, &from_gap2); + current += column_step; + } + } + + let (score, starts) = rule.find_traceback_starts(&table, n, m, lower_diag, upper_diag); + + if F::SCORE_ONLY { + return (Vec::new(), score); + } + + let trace_offset = |direction| match direction { + TraceDirection::Diag => match_offset, + TraceDirection::To1 => gap1_offset, + TraceDirection::To2 => gap2_offset, + }; + let mut paths = Vec::new(); + for (start, state) in starts { + let mut count: usize = 1; + follow_trace( + &table, + start, + state, + &trace_offset, + &mut count, + max_number, + &mut paths, + ); + } + paths.truncate(max_number); + + let traces = paths + .into_iter() + .map(|path| { + path_to_trace(&path, |index| { + let (i, j) = table.unindex(index); + // 'Unstraighthen' the table indices to sequence indices + let seq_i = i as i64 - 1; + let seq_j = j as i64 + seq_i + lower_diag as i64 - 1; + (seq_i, seq_j) + }) + }) + .collect(); + (traces, score) +} + +/// Analogous to the [`FillRule`](super::pairwise::FillRule) in `pairwise.rs` +pub trait FillRule { + type Cell: Cell; + + const SCORE_ONLY: bool; + + /// The initial value of the in-band cells. + /// + /// Most of the cells are overwritten by [`fill`] + /// But the out-of-band triangles are read from this state. + fn fill_edges(&self) -> Self::Cell; + + fn fill_sentinel(&self) -> Self::Cell; + + fn fill( + &self, + symbol1: S, + symbol2: S, + from_match: &Self::Cell, + from_gap1: &Self::Cell, + from_gap2: &Self::Cell, + ) -> Self::Cell; + + #[allow(clippy::type_complexity)] + fn find_traceback_starts( + &self, + table: &StridedTable, + seq1_len: usize, + seq2_len: usize, + lower_diag: isize, + upper_diag: isize, + ) -> (Score, Vec<(TableIndex, ::StateInfo)>); +} + +pub struct LinearGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: LinearDirectionStore, +{ + scoring_scheme: V, + gap_penalty: Score, + neg_inf: Score, + _phantom: PhantomData<(S, D)>, +} + +impl LinearGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: LinearDirectionStore, +{ + pub fn new(scoring_scheme: V, gap_penalty: Score) -> Self { + let mut neg_inf = Score::MIN - gap_penalty; + let min_score = scoring_scheme.min(); + if min_score < 0 { + neg_inf -= min_score; + } + LinearGapPenalty { + scoring_scheme, + gap_penalty, + neg_inf, + _phantom: PhantomData, + } + } +} + +impl FillRule for LinearGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: LinearDirectionStore, +{ + type Cell = LinearCell; + + const SCORE_ONLY: bool = D::SCORE_ONLY; + + #[inline] + fn fill_edges(&self) -> LinearCell { + LinearCell::default() + } + + #[inline] + fn fill_sentinel(&self) -> LinearCell { + LinearCell { + score: self.neg_inf, + direction: D::default(), + } + } + + #[inline(always)] + fn fill( + &self, + symbol1: S, + symbol2: S, + from_match: &LinearCell, + from_gap1: &LinearCell, + from_gap2: &LinearCell, + ) -> LinearCell { + let similarity = self.scoring_scheme.score(symbol1, symbol2); + let score_from_match = from_match.score + similarity; + let score_from_gap1 = from_gap1.score + self.gap_penalty; + let score_from_gap2 = from_gap2.score + self.gap_penalty; + let (mut score, mut direction) = if D::SCORE_ONLY { + ( + score_from_match.max(score_from_gap1).max(score_from_gap2), + D::default(), + ) + } else { + let (score, direction) = + get_trace_linear(score_from_match, score_from_gap1, score_from_gap2); + (score, D::wrap(direction)) + }; + if LOCAL && score <= 0 { + score = 0; + direction = D::default(); + } + LinearCell { score, direction } + } + + fn find_traceback_starts( + &self, + table: &StridedTable>, + seq1_len: usize, + seq2_len: usize, + lower_diag: isize, + upper_diag: isize, + ) -> (Score, Vec<(TableIndex, ())>) { + if LOCAL { + let mut max = Score::MIN; + let mut starts = Vec::new(); + for (index, cell) in table.indexed_iter() { + match cell.score.cmp(&max) { + Ordering::Greater => { + max = cell.score; + starts.clear(); + starts.push((index, ())); + } + Ordering::Equal => starts.push((index, ())), + Ordering::Less => {} + } + } + (max, starts) + } else { + let potential_starts = get_potential_starts(seq1_len, seq2_len, lower_diag, upper_diag); + let mut max = Score::MIN; + for &(i, j) in &potential_starts { + max = max.max(table[table.index((i, j))].score); + } + let starts = potential_starts + .iter() + .map(|&(i, j)| table.index((i, j))) + .filter(|&index| table[index].score == max) + .map(|index| (index, ())) + .collect(); + (max, starts) + } + } +} + +pub struct AffineGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: AffineDirectionStore, +{ + scoring_scheme: V, + gap_open: Score, + gap_extend: Score, + neg_inf: Score, + _phantom: PhantomData<(S, D)>, +} + +impl AffineGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: AffineDirectionStore, +{ + pub fn new(scoring_scheme: V, gap_open: Score, gap_extend: Score) -> Self { + let mut neg_inf = Score::MIN - gap_open - gap_extend; + let min_score = scoring_scheme.min(); + if min_score < 0 { + neg_inf -= min_score; + } + AffineGapPenalty { + scoring_scheme, + gap_open, + gap_extend, + neg_inf, + _phantom: PhantomData, + } + } +} + +impl FillRule for AffineGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: AffineDirectionStore, +{ + type Cell = AffineCell; + + const SCORE_ONLY: bool = D::SCORE_ONLY; + + #[inline] + fn fill_edges(&self) -> AffineCell { + AffineCell { + score: AffineScore { + m: 0, + g1: self.neg_inf, + g2: self.neg_inf, + }, + direction: D::default(), + } + } + + #[inline] + fn fill_sentinel(&self) -> AffineCell { + AffineCell { + score: AffineScore { + m: self.neg_inf, + g1: self.neg_inf, + g2: self.neg_inf, + }, + direction: D::default(), + } + } + + #[inline(always)] + fn fill( + &self, + symbol1: S, + symbol2: S, + from_match: &AffineCell, + from_gap1: &AffineCell, + from_gap2: &AffineCell, + ) -> AffineCell { + let similarity = self.scoring_scheme.score(symbol1, symbol2); + let mm = from_match.score.m + similarity; + let g1m = from_match.score.g1 + similarity; + let g2m = from_match.score.g2 + similarity; + let mg1 = from_gap1.score.m + self.gap_open; + let g1g1 = from_gap1.score.g1 + self.gap_extend; + let mg2 = from_gap2.score.m + self.gap_open; + let g2g2 = from_gap2.score.g2 + self.gap_extend; + + // `direction` stays a raw `AffineDirection` (computed only when traced); + // it is wrapped into the cell's `D` at the end, so the local masking + // below is plain bit arithmetic that the score-only build discards. + let (m_score, g1_score, g2_score, mut direction) = if D::SCORE_ONLY { + ( + mm.max(g1m).max(g2m), + mg1.max(g1g1), + mg2.max(g2g2), + AffineDirection::empty(), + ) + } else { + get_trace_affine(mm, g1m, g2m, mg1, g1g1, mg2, g2g2) + }; + let mut score = AffineScore { + m: m_score, + g1: g1_score, + g2: g2_score, + }; + + if LOCAL { + // Local alignment specialty: + // If score is less than or equal to 0, then the score of the cell remains 0 + // and the trace ends here + if m_score <= 0 { + // End trace by filtering out the respective bits + direction &= !(AffineDirection::MATCH_TO_MATCH + | AffineDirection::GAP1_TO_MATCH + | AffineDirection::GAP2_TO_MATCH); + score.m = 0; + } + if g1_score <= 0 { + direction &= !(AffineDirection::MATCH_TO_GAP1 | AffineDirection::GAP1_TO_GAP1); + score.g1 = self.neg_inf; + } + if g2_score <= 0 { + direction &= !(AffineDirection::MATCH_TO_GAP2 | AffineDirection::GAP2_TO_GAP2); + score.g2 = self.neg_inf; + } + } + + AffineCell { + score, + direction: D::wrap(direction), + } + } + + fn find_traceback_starts( + &self, + table: &StridedTable>, + seq1_len: usize, + seq2_len: usize, + lower_diag: isize, + upper_diag: isize, + ) -> (Score, Vec<(TableIndex, TraceState)>) { + if LOCAL { + // Only the match table is considered: + // a local alignment cannot start or end with a gap + let mut max = Score::MIN; + let mut starts = Vec::new(); + for (index, cell) in table.indexed_iter() { + match cell.score.m.cmp(&max) { + Ordering::Greater => { + max = cell.score.m; + starts.clear(); + starts.push((index, TraceState::Match)); + } + Ordering::Equal => starts.push((index, TraceState::Match)), + Ordering::Less => {} + } + } + (max, starts) + } else { + let potential_starts = get_potential_starts(seq1_len, seq2_len, lower_diag, upper_diag); + let mut max = Score::MIN; + for &(i, j) in &potential_starts { + let cell = table[table.index((i, j))]; + max = max.max(cell.score.m).max(cell.score.g1).max(cell.score.g2); + } + let mut starts = Vec::new(); + for state in [TraceState::Match, TraceState::Gap1, TraceState::Gap2] { + for &(i, j) in &potential_starts { + let index = table.index((i, j)); + if table[index].score.get(state) == max { + starts.push((index, state)); + } + } + } + (max, starts) + } + } +} + +/// Potential starting points for a semi-global banded alignment. +/// These are cells that touch the end of either sequence. +fn get_potential_starts( + seq1_len: usize, + seq2_len: usize, + lower_diag: isize, + upper_diag: isize, +) -> Vec<(usize, usize)> { + let band_width = (upper_diag - lower_diag + 1) as usize; + let mut starts = Vec::with_capacity(band_width); + // Start from the end from the first (shorter) sequence, + // if the table cell is in bounds of the second (longer) sequence, + // otherwise start from the end of the second sequence + for j in 1..=band_width as isize { + let seq_j = j + (seq1_len as isize - 1) + lower_diag - 1; + let i = if seq_j < seq2_len as isize { + // The cell touches the end of the first (shorter) sequence. + seq1_len as isize + } else { + // The cell touches the end of the second sequence; solve + // `seq2_len - 1 = j + (i - 1) + lower_diag - 1` for `i`. + // + // Take: + // + // seq_j = j + (seq1_len-1) + lower_diag - 1 + // + // Replace seq_j with last sequence position of second sequence + // and last sequence position of first sequence with seq_i: + // + // (seq2_len-1) = j + seq_i + lower_diag - 1 + // + // Replace seq_i with corresponding i in table: + // + // (seq2_len-1) = j + (i - 1) + lower_diag - 1 + // + // Solve for i: + // + seq2_len as isize - j - lower_diag + 1 + }; + starts.push((i as usize, j as usize)); + } + starts +} diff --git a/src/rust/sequence/align/cell.rs b/src/rust/sequence/align/cell.rs new file mode 100644 index 000000000..161935bd3 --- /dev/null +++ b/src/rust/sequence/align/cell.rs @@ -0,0 +1,394 @@ +//! Dynamic programming cell types shared by all alignment algorithms. +//! +//! A [`Cell`] is a single element of a Needleman-Wunsch-like DP table, holding +//! the score(s) and the trace information. The per-cell recurrence (the +//! `FillRule`) lives in each alignment module, since each algorithm has +//! different requirements; the only computation shared here is the +//! max-selection primitive ([`get_trace_linear`] / [`get_trace_affine`]). + +use super::scoring::Score; +use bitflags::bitflags; +use smallvec::SmallVec; + +/// Traceback steps returned by [`Cell::traceback`], parameterized by the cell's +/// [`StateInfo`](Cell::StateInfo). +/// +/// At most three co-optimal directions are produced per cell (match / gap-in-1 / +/// gap-in-2; the affine recurrence likewise yields at most three transitions for +/// any single state), so they are kept inline without heap allocation. +pub type TraceVec = SmallVec<[(TraceDirection, S); 3]>; + +/// A direction the traceback moves from a cell. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TraceDirection { + /// Diagonal: alignment of two symbols. + Diag, + /// Gap in the first sequence (move along a row). + To1, + /// Gap in the second sequence (move along a column). + To2, +} + +/// The score table the traceback is currently in. +/// Required for affine gap penalty. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TraceState { + /// The match table. + Match, + /// The table for gaps in the first sequence. + Gap1, + /// The table for gaps in the second sequence. + Gap2, +} + +bitflags! { + /// Traceback directions for linear gap penalty (one score table). + /// + /// A set bit means the cell can be reached from that direction. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] + pub struct LinearDirection: u8 { + /// Diagonal: alignment of two symbols. + const MATCH = 1; + /// Gap in the first sequence. + const GAP1 = 2; + /// Gap in the second sequence. + const GAP2 = 4; + } +} + +bitflags! { + /// Traceback transitions for affine gap penalty (three score tables). + /// + /// Each bit encodes a transition between two tables/states. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] + pub struct AffineDirection: u8 { + /// Match -> match transition. + const MATCH_TO_MATCH = 1; + /// Gap in sequence 1 -> match transition. + const GAP1_TO_MATCH = 2; + /// Gap in sequence 2 -> match transition. + const GAP2_TO_MATCH = 4; + /// Match -> gap in sequence 1 transition. + const MATCH_TO_GAP1 = 8; + /// Gap in sequence 1 -> gap in sequence 1 transition (extension). + const GAP1_TO_GAP1 = 16; + /// Match -> gap in sequence 2 transition. + const MATCH_TO_GAP2 = 32; + /// Gap in sequence 2 -> gap in sequence 2 transition (extension). + const GAP2_TO_GAP2 = 64; + } +} + +/// A single element of a dynamic programming table. +/// +/// It tracks both the score(s) needed to fill the table and the trace +/// information needed to reconstruct the alignment. +pub trait Cell: Copy + Default { + /// Extra information required to resume the traceback from this cell. + type StateInfo: Copy; + + /// The predecessor steps to follow during traceback, given the state the + /// traceback arrived in. + /// + /// Each step is the direction to move and the state to continue in. + /// An empty result terminates the (branch of the) traceback. + /// The order matters: the first step is the one followed by the main trace, + /// further steps spawn additional alignment branches. + fn traceback(&self, state_info: Self::StateInfo) -> TraceVec; +} + +/// The trace-direction storage of a [`LinearCell`]. +/// +/// This is what lets a score-only table cell shrink to just its score: a full +/// alignment stores the real [`LinearDirection`] bitflags, while a score-only +/// alignment stores the zero-sized `()` placeholder, halving the cell size. +pub trait LinearDirectionStore: Copy + Default { + /// Whether the cell is score-only, storing no trace directions (and thus + /// not computing them). + const SCORE_ONLY: bool; + + /// Wrap a freshly computed direction, discarding it in score-only mode. + fn wrap(direction: LinearDirection) -> Self; + + /// The traceback steps encoded by the stored direction. + fn traceback(&self) -> TraceVec<()>; +} + +impl LinearDirectionStore for LinearDirection { + const SCORE_ONLY: bool = false; + + #[inline(always)] + fn wrap(direction: LinearDirection) -> Self { + direction + } + + #[inline] + fn traceback(&self) -> TraceVec<()> { + let mut steps = TraceVec::new(); + if self.contains(LinearDirection::MATCH) { + steps.push((TraceDirection::Diag, ())); + } + if self.contains(LinearDirection::GAP1) { + steps.push((TraceDirection::To1, ())); + } + if self.contains(LinearDirection::GAP2) { + steps.push((TraceDirection::To2, ())); + } + steps + } +} + +impl LinearDirectionStore for () { + const SCORE_ONLY: bool = true; + + #[inline(always)] + fn wrap(_direction: LinearDirection) -> Self {} + + fn traceback(&self) -> TraceVec<()> { + unreachable!("score-only cells are never traced back") + } +} + +/// A DP cell for linear gap penalty: a single score plus trace directions. +/// +/// `D` is the [`LinearDirectionStore`]: [`LinearDirection`] for a full +/// alignment, or `()` in score-only mode (shrinking the cell to its score). +#[derive(Debug, Clone, Copy, Default)] +pub struct LinearCell { + pub score: Score, + pub direction: D, +} + +impl Cell for LinearCell { + type StateInfo = (); + + #[inline] + fn traceback(&self, _state_info: Self::StateInfo) -> TraceVec { + self.direction.traceback() + } +} + +/// The three scores of an affine [`AffineCell`], one per score table. +#[derive(Debug, Clone, Copy, Default)] +pub struct AffineScore { + /// Score in the match table. + pub m: Score, + /// Score in the gap-in-sequence-1 table. + pub g1: Score, + /// Score in the gap-in-sequence-2 table. + pub g2: Score, +} + +impl AffineScore { + /// The score of the table corresponding to the given traceback state. + #[inline(always)] + pub fn get(&self, state: TraceState) -> Score { + match state { + TraceState::Match => self.m, + TraceState::Gap1 => self.g1, + TraceState::Gap2 => self.g2, + } + } +} + +/// The trace-transition storage of an [`AffineCell`]. +/// +/// The affine analogue of [`LinearDirectionStore`]: [`AffineDirection`] for a +/// full alignment, or `()` in score-only mode. +pub trait AffineDirectionStore: Copy + Default { + /// Whether the cell is score-only, storing no trace transitions (and thus + /// not computing them). + const SCORE_ONLY: bool; + + /// Wrap a freshly computed transition, discarding it in score-only mode. + fn wrap(direction: AffineDirection) -> Self; + + /// The traceback steps encoded by the stored transition for `state`. + fn traceback(&self, state: TraceState) -> TraceVec; +} + +impl AffineDirectionStore for AffineDirection { + const SCORE_ONLY: bool = false; + + #[inline(always)] + fn wrap(direction: AffineDirection) -> Self { + direction + } + + #[inline] + fn traceback(&self, state: TraceState) -> TraceVec { + let d = *self; + let mut steps = TraceVec::new(); + // Only the transitions valid for the current state are followed; within + // a state, every step moves in the same direction. + match state { + TraceState::Match => { + if d.contains(AffineDirection::MATCH_TO_MATCH) { + steps.push((TraceDirection::Diag, TraceState::Match)); + } + if d.contains(AffineDirection::GAP1_TO_MATCH) { + steps.push((TraceDirection::Diag, TraceState::Gap1)); + } + if d.contains(AffineDirection::GAP2_TO_MATCH) { + steps.push((TraceDirection::Diag, TraceState::Gap2)); + } + } + TraceState::Gap1 => { + if d.contains(AffineDirection::MATCH_TO_GAP1) { + steps.push((TraceDirection::To1, TraceState::Match)); + } + if d.contains(AffineDirection::GAP1_TO_GAP1) { + steps.push((TraceDirection::To1, TraceState::Gap1)); + } + } + TraceState::Gap2 => { + if d.contains(AffineDirection::MATCH_TO_GAP2) { + steps.push((TraceDirection::To2, TraceState::Match)); + } + if d.contains(AffineDirection::GAP2_TO_GAP2) { + steps.push((TraceDirection::To2, TraceState::Gap2)); + } + } + } + steps + } +} + +impl AffineDirectionStore for () { + const SCORE_ONLY: bool = true; + + #[inline(always)] + fn wrap(_direction: AffineDirection) -> Self {} + + fn traceback(&self, _state: TraceState) -> TraceVec { + unreachable!("score-only cells are never traced back") + } +} + +/// A DP cell for affine gap penalty: three scores (match, gap-in-1, gap-in-2) +/// plus trace transitions. +/// +/// `D` is the [`AffineDirectionStore`]: [`AffineDirection`] for a full +/// alignment, or `()` in score-only mode (shrinking the cell to its scores). +#[derive(Debug, Clone, Copy, Default)] +pub struct AffineCell { + pub score: AffineScore, + pub direction: D, +} + +impl Cell for AffineCell { + type StateInfo = TraceState; + + #[inline] + fn traceback(&self, state_info: Self::StateInfo) -> TraceVec { + self.direction.traceback(state_info) + } +} + +/// Pick the maximum of three scores for linear gap penalty and return the +/// corresponding set of trace directions (ties yield multiple directions). +#[inline(always)] +pub(crate) fn get_trace_linear( + match_score: Score, + gap1_score: Score, + gap2_score: Score, +) -> (Score, LinearDirection) { + use LinearDirection as D; + // NOTE: `if`/`else` is deliberately used instead of `match score.cmp(...)`: + // Matching on `Ordering` materializes a three-valued result and reorders the + // comparisons, which the optimizer turns into measurably slower code (rust-lang/rust#86511) + if match_score > gap1_score { + if match_score > gap2_score { + (match_score, D::MATCH) + } else if match_score == gap2_score { + (match_score, D::MATCH | D::GAP2) + } else { + (gap2_score, D::GAP2) + } + } else if match_score == gap1_score { + if match_score > gap2_score { + (match_score, D::MATCH | D::GAP1) + } else if match_score == gap2_score { + (match_score, D::MATCH | D::GAP1 | D::GAP2) + } else { + (gap2_score, D::GAP2) + } + } else if gap1_score > gap2_score { + (gap1_score, D::GAP1) + } else if gap1_score == gap2_score { + (gap1_score, D::GAP1 | D::GAP2) + } else { + (gap2_score, D::GAP2) + } +} + +/// Pick the maximum scores for the three affine tables and return the +/// corresponding trace transitions. +/// +/// Returns `(match_score, gap1_score, gap2_score, directions)`. +#[inline(always)] +#[allow(clippy::too_many_arguments)] +pub(crate) fn get_trace_affine( + mm: Score, + g1m: Score, + g2m: Score, + mg1: Score, + g1g1: Score, + mg2: Score, + g2g2: Score, +) -> (Score, Score, Score, AffineDirection) { + use AffineDirection as D; + // NOTE: `if`/`else` is deliberately used instead of `match score.cmp(...)`: + // Matching on `Ordering` materializes a three-valued result and reorders the + // comparisons, which the optimizer turns into measurably slower code (rust-lang/rust#86511) + // Match table + let (m_score, mut trace) = if mm > g1m { + if mm > g2m { + (mm, D::MATCH_TO_MATCH) + } else if mm == g2m { + (mm, D::MATCH_TO_MATCH | D::GAP2_TO_MATCH) + } else { + (g2m, D::GAP2_TO_MATCH) + } + } else if mm == g1m { + if mm > g2m { + (mm, D::MATCH_TO_MATCH | D::GAP1_TO_MATCH) + } else if mm == g2m { + (mm, D::MATCH_TO_MATCH | D::GAP1_TO_MATCH | D::GAP2_TO_MATCH) + } else { + (g2m, D::GAP2_TO_MATCH) + } + } else if g1m > g2m { + (g1m, D::GAP1_TO_MATCH) + } else if g1m == g2m { + (g1m, D::GAP1_TO_MATCH | D::GAP2_TO_MATCH) + } else { + (g2m, D::GAP2_TO_MATCH) + }; + + // 'Gap left' table + let g1_score = if mg1 > g1g1 { + trace |= D::MATCH_TO_GAP1; + mg1 + } else if mg1 < g1g1 { + trace |= D::GAP1_TO_GAP1; + g1g1 + } else { + trace |= D::MATCH_TO_GAP1 | D::GAP1_TO_GAP1; + mg1 + }; + + // 'Gap top' table + let g2_score = if mg2 > g2g2 { + trace |= D::MATCH_TO_GAP2; + mg2 + } else if mg2 < g2g2 { + trace |= D::GAP2_TO_GAP2; + g2g2 + } else { + trace |= D::MATCH_TO_GAP2 | D::GAP2_TO_GAP2; + mg2 + }; + + (m_score, g1_score, g2_score, trace) +} diff --git a/src/rust/sequence/align/dispatch.rs b/src/rust/sequence/align/dispatch.rs deleted file mode 100644 index 935239287..000000000 --- a/src/rust/sequence/align/dispatch.rs +++ /dev/null @@ -1,146 +0,0 @@ -//! Runtime dispatch from the dynamic Python arguments to the statically -//! monomorphized alignment core. -//! -//! The Python layer passes a scoring scheme and a gap penalty whose concrete -//! types (and the local/terminal/score-only flags) are only known at run time, -//! whereas the alignment core is generic over the [`ScoringScheme`], the -//! [`FillRule`] and the const flags. The macros and [`dispatch_scoring`] here -//! bridge the two by matching each runtime value and calling the core with the -//! corresponding concrete types. - -use super::cell::{AffineGapPenalty, LinearGapPenalty}; -use super::pairwise::align_optimal_generic; -use super::scoring::{Match, Score, SubstitutionMatrix}; -use super::symbol::Symbol; -use numpy::ndarray::Array2; -use pyo3::prelude::*; - -/// The gap penalty configuration passed from the Python layer. -#[derive(FromPyObject)] -pub enum GapPenalty { - /// A single linear gap penalty. - Linear(Score), - /// An affine gap penalty as `(gap_open, gap_extend)`. - Affine(Score, Score), -} - -/// A scoring scheme accepted from the Python layer: either a substitution -/// matrix (a 2D array) or a match/mismatch tuple. -#[derive(FromPyObject)] -pub enum Scoring { - Matrix(SubstitutionMatrix), - Match(Match), -} - -/// Instantiate the fill rule for each `(local, terminal, score_only)` const -/// combination and call the alignment core `$align`. -macro_rules! dispatch_consts { - ( - $align:path, $rule:ident, $scheme:expr, ($($penalty:expr),+), - $local:expr, $terminal:expr, $score_only:expr, - $code1:expr, $code2:expr, $max:expr - ) => { - match ($local, $terminal, $score_only) { - (true, true, true) => $align( - $code1, $code2, <$rule<_, _, true, true, true>>::new($scheme, $($penalty),+), $max, - ), - (true, true, false) => $align( - $code1, $code2, <$rule<_, _, true, true, false>>::new($scheme, $($penalty),+), $max, - ), - (true, false, true) => $align( - $code1, $code2, <$rule<_, _, true, false, true>>::new($scheme, $($penalty),+), $max, - ), - (true, false, false) => $align( - $code1, $code2, <$rule<_, _, true, false, false>>::new($scheme, $($penalty),+), $max, - ), - (false, true, true) => $align( - $code1, $code2, <$rule<_, _, false, true, true>>::new($scheme, $($penalty),+), $max, - ), - (false, true, false) => $align( - $code1, $code2, <$rule<_, _, false, true, false>>::new($scheme, $($penalty),+), $max, - ), - (false, false, true) => $align( - $code1, $code2, <$rule<_, _, false, false, true>>::new($scheme, $($penalty),+), $max, - ), - (false, false, false) => $align( - $code1, $code2, <$rule<_, _, false, false, false>>::new($scheme, $($penalty),+), $max, - ), - } - }; -} - -/// Select the concrete fill rule from the gap penalty, then dispatch the const -/// flags via [`dispatch_consts`]. -macro_rules! dispatch_penalty { - ( - $align:path, $scheme:expr, $gap:expr, - $local:expr, $terminal:expr, $score_only:expr, - $code1:expr, $code2:expr, $max:expr - ) => { - match $gap { - GapPenalty::Linear(gap) => dispatch_consts!( - $align, - LinearGapPenalty, - $scheme, - (gap), - $local, - $terminal, - $score_only, - $code1, - $code2, - $max - ), - GapPenalty::Affine(open, extend) => dispatch_consts!( - $align, - AffineGapPenalty, - $scheme, - (open, extend), - $local, - $terminal, - $score_only, - $code1, - $code2, - $max - ), - } - }; -} - -/// Dispatch over the scoring scheme and run [`align_optimal_generic`] for the -/// resulting concrete scheme, gap penalty and const flags. -#[allow(clippy::too_many_arguments)] -pub fn dispatch_scoring( - scoring: Scoring, - gap_penalty: GapPenalty, - local: bool, - terminal_penalty: bool, - score_only: bool, - code1: &[S], - code2: &[S], - max_number: usize, -) -> (Vec>, Score) { - match scoring { - Scoring::Matrix(scheme) => dispatch_penalty!( - align_optimal_generic, - scheme, - gap_penalty, - local, - terminal_penalty, - score_only, - code1, - code2, - max_number - ), - Scoring::Match(scheme) => dispatch_penalty!( - align_optimal_generic, - scheme, - gap_penalty, - local, - terminal_penalty, - score_only, - code1, - code2, - max_number - ), - } -} diff --git a/src/rust/sequence/align/localgapped.rs b/src/rust/sequence/align/localgapped.rs new file mode 100644 index 000000000..51b32f9dc --- /dev/null +++ b/src/rust/sequence/align/localgapped.rs @@ -0,0 +1,642 @@ +//! Local *X-Drop* gapped alignment of a single region (`align_region`). +//! +//! This module aligns one region extending from the *start* of the two given +//! (sub)sequences, using the X-Drop heuristic :footcite:`Zhang2000`: the +//! dynamic programming table is filled antidiagonal by antidiagonal and a cell +//! is only kept while its score stays within `threshold` of the best score seen +//! so far. The [`StridedTable`] grows on demand (via +//! [`StridedTable::from_data`]), so memory is proportional to the aligned region +//! rather than the full sequences. +//! +//! The upstream/downstream split around the seed, the sequence reversal for the +//! upstream region and the trace combination are handled by the Python layer; +//! this module only aligns a single forward region. +//! +//! The X-Drop [`FillRule`] differs from the rectangular one in +//! [`pairwise`](super::pairwise): a score of `0` marks an unreached (invalid) +//! cell, the diagonal predecessor only contributes if it is itself valid, the +//! `(0, 0)` anchor is seeded with `threshold + 1` (subtracted from the final +//! score), and [`fill`](FillRule::fill) prunes a cell against the running +//! maximum instead of always producing a value. Only the max-selection +//! primitive ([`get_trace_linear`]/[`get_trace_affine`]) and the local +//! traceback-start search are shared with the other algorithms. + +use super::cell::{ + get_trace_affine, get_trace_linear, AffineCell, AffineDirection, AffineDirectionStore, + AffineScore, Cell, LinearCell, LinearDirection, LinearDirectionStore, TraceDirection, + TraceState, +}; +use super::scoring::{GapPenalty, Score, Scoring, ScoringScheme}; +use super::symbol::Symbol; +use super::table::{StridedTable, TableIndex}; +use super::trace::{follow_trace, path_to_trace}; +use crate::dispatch_dtype; +use numpy::ndarray::Array2; +use numpy::{ + IntoPyArray, PyArray1, PyArrayDescrMethods, PyArrayMethods, PyUntypedArray, + PyUntypedArrayMethods, +}; +use pyo3::exceptions::PyMemoryError; +use pyo3::prelude::*; +use pyo3::types::{PyList, PyTuple}; +use std::cmp::Ordering; +use std::marker::PhantomData; + +/// The initial side length of the dynamic programming table. +const INIT_SIZE: usize = 100; + +/// Signals that a table growth would exceed the user-provided maximum size. +struct TableSizeExceeded; + +/// Align a single region extending from the start of `code1`/`code2`. +/// +/// Parameters +/// ---------- +/// code1, code2 : ndarray +/// The region sequence codes, sharing the same unsigned integer dtype. +/// scoring : ndarray, dtype=int32 or tuple(int, int) +/// A substitution matrix or a `(match, mismatch)` score pair. +/// gap_penalty : int or tuple(int, int) +/// A linear or affine gap penalty. +/// threshold : int +/// The X-Drop threshold. +/// max_number : int +/// The maximum number of (co-optimal) alignments to return. +/// score_only : bool +/// If true, only the score is computed and the trace list is empty. +/// max_table_size : int +/// Raise `MemoryError` if the dynamic programming table would exceed this +/// number of cells. +/// +/// Returns +/// ------- +/// traces : list of ndarray +/// The alignment traces, each of shape `(length, 2)`. Empty if `score_only`. +/// score : int +/// The local alignment score of the region. +#[pyfunction] +#[pyo3(signature = (code1, code2, scoring, gap_penalty, threshold, max_number, score_only, max_table_size))] +#[allow(clippy::too_many_arguments)] +pub fn align_region<'py>( + py: Python<'py>, + code1: &Bound<'py, PyUntypedArray>, + code2: &Bound<'py, PyUntypedArray>, + scoring: Scoring, + gap_penalty: GapPenalty, + threshold: Score, + max_number: usize, + score_only: bool, + max_table_size: usize, +) -> PyResult> { + let dtype = code1.dtype(); + dispatch_dtype!( + py, + &dtype, + [u8, u16, u32, u64], + align_region_dtype( + py, + code1, + code2, + scoring, + gap_penalty, + threshold, + max_number, + score_only, + max_table_size + ) + ) +} + +#[allow(clippy::too_many_arguments)] +fn align_region_dtype<'py, S: Symbol + numpy::Element>( + py: Python<'py>, + code1: &Bound<'py, PyUntypedArray>, + code2: &Bound<'py, PyUntypedArray>, + scoring: Scoring, + gap_penalty: GapPenalty, + threshold: Score, + max_number: usize, + score_only: bool, + max_table_size: usize, +) -> PyResult> { + let code1_array = code1.cast::>()?.readonly(); + let code2_array = code2.cast::>()?.readonly(); + let code1 = code1_array.as_slice()?; + let code2 = code2_array.as_slice()?; + + // Instantiate the fill rule for the runtime scoring scheme, gap penalty and + // `score_only` flag, then run the generic core. The direction-storage type + // (`$dir` for a full alignment, `()` for score-only) selects the cell size. + macro_rules! run { + ($rule:ident, $dir:ty, $scheme:expr, ($($penalty:expr),+)) => { + align_region_generic( + code1, code2, + <$rule<_, _, $dir>>::new($scheme, $($penalty),+, threshold), + max_number, max_table_size, + ) + }; + } + macro_rules! by_consts { + ($rule:ident, $dir:ty, $scheme:expr, ($($penalty:expr),+)) => { + if score_only { + run!($rule, (), $scheme, ($($penalty),+)) + } else { + run!($rule, $dir, $scheme, ($($penalty),+)) + } + }; + } + macro_rules! by_penalty { + ($scheme:expr) => { + match gap_penalty { + GapPenalty::Linear(gap) => { + by_consts!(LinearGapPenalty, LinearDirection, $scheme, (gap)) + } + GapPenalty::Affine(open, extend) => { + by_consts!(AffineGapPenalty, AffineDirection, $scheme, (open, extend)) + } + } + }; + } + let result = match scoring { + Scoring::Matrix(scheme) => by_penalty!(scheme), + Scoring::Match(scheme) => by_penalty!(scheme), + }; + let (traces, score) = + result.map_err(|_| PyMemoryError::new_err("Maximum table size exceeded"))?; + + let list = PyList::empty(py); + for trace in traces { + list.append(trace.into_pyarray(py))?; + } + PyTuple::new(py, [list.into_any(), score.into_pyobject(py)?.into_any()]) +} + +/// Core local X-Drop alignment of a single region, generic over the symbol type +/// and fill rule. +fn align_region_generic( + code1: &[S], + code2: &[S], + rule: F, + max_number: usize, + max_table_size: usize, +) -> Result<(Vec>, Score), TableSizeExceeded> +where + S: Symbol, + F: FillRule, +{ + let (table, max_score) = fill_xdrop(code1, code2, &rule, max_table_size)?; + // The fill already tracks the maximum score; only the traceback needs the + // start cells, so `score_only` avoids that extra table scan. + let traces = if F::SCORE_ONLY { + Vec::new() + } else { + let (_, starts) = rule.find_traceback_starts(&table); + traceback(&table, starts, max_number) + }; + Ok((traces, max_score - rule.init_score())) +} + +/// Fill the X-Drop table antidiagonal by antidiagonal. +/// +/// The table grows on demand and the fill stops once an antidiagonal has no +/// surviving cell. Returns the filled table and the maximum score reached. +fn fill_xdrop( + code1: &[S], + code2: &[S], + rule: &F, + max_table_size: usize, +) -> Result<(StridedTable, Score), TableSizeExceeded> +where + S: Symbol, + F: FillRule, +{ + let n = code1.len(); + let m = code2.len(); + let mut table = StridedTable::::new(((n + 1).min(INIT_SIZE), (m + 1).min(INIT_SIZE))); + let origin = table.index((0, 0)); + table[origin] = rule.fill_origin(); + + let mut max_score = rule.init_score(); + + // The valid `i` range of the current (`k0`) and two preceding antidiagonals (`k1` and `k2`) + // The `k2` pair is always assigned (from `k1`) before it is read. + let (mut i_min_k0, mut i_max_k0) = (0usize, 0usize); + let (mut i_min_k1, mut i_max_k1) = (0usize, 0usize); + let (mut i_min_k2, mut i_max_k2): (usize, usize); + + // Instead of iteration over rows and columns, iterate over antidiagonals and diagonals to + // achieve symmetric treatment of both sequences + for k in 1..=(n + m) { + // Move up the antidiagonal chain + i_min_k2 = i_min_k1; + i_max_k2 = i_max_k1; + i_min_k1 = i_min_k0; + i_max_k1 = i_max_k0; + // Reset to the most restrictive range; `k` is the 'unset' sentinel for + // `i_min`, since every valid `i` in this antidiagonal is `< k`. + i_min_k0 = k; + i_max_k0 = 0; + + // Prune the `i` range to where valid cells can exist, + // then clamp it to the sequence bounds + let i_min = i_min_k1.min(i_min_k2 + 1).max(k.saturating_sub(m)); + let i_max = (i_max_k1 + 1).max(i_max_k2 + 1).min(n); + if i_min > i_max { + // The calculated antidiagonal has no range of valid cells -> the alignment has finished + break; + } + let j_max = k - i_min; + + // Grow the table if the current antidiagonal would exceed it. + let (rows, cols) = table.shape(); + if i_max >= rows { + let new_rows = rows * 2; + if new_rows + .checked_mul(cols) + .is_none_or(|size| size > max_table_size) + { + return Err(TableSizeExceeded); + } + table = StridedTable::from_data((new_rows, cols), table); + } + let (rows, cols) = table.shape(); + if j_max >= cols { + let new_cols = cols * 2; + if rows + .checked_mul(new_cols) + .is_none_or(|size| size > max_table_size) + { + return Err(TableSizeExceeded); + } + table = StridedTable::from_data((rows, new_cols), table); + } + + // Constant offsets to the three predecessors. Consecutive cells of the + // antidiagonal move by `diag_step` (down one row, left one column), so + // the index is walked instead of recomputed per cell. The offsets are + // determined here, after any table growth, where the stride is final. + let diag_offset = table.index_offset((-1, -1)); + let gap1_offset = table.index_offset((0, -1)); + let gap2_offset = table.index_offset((-1, 0)); + let diag_step = table.index_offset((1, -1)); + let mut current = table.index((i_min, k - i_min)); + + for i in i_min..=i_max { + let j = k - i; + // The diagonal predecessor (and its symbols) only exist away from + // the top row and left column. + let diagonal = if i > 0 && j > 0 { + Some((code1[i - 1], code2[j - 1], table[current + diag_offset])) + } else { + None + }; + let from_gap1 = if j > 0 { + table[current + gap1_offset] + } else { + F::Cell::default() + }; + let from_gap2 = if i > 0 { + table[current + gap2_offset] + } else { + F::Cell::default() + }; + if let Some(cell) = rule.fill(diagonal, &from_gap1, &from_gap2, &mut max_score) { + if i_min_k0 == k { + i_min_k0 = i; + } + i_max_k0 = i; + table[current] = cell; + } + current += diag_step; + } + } + + Ok((table, max_score)) +} + +/// Run the traceback over a filled table and convert the paths to trace arrays. +fn traceback( + table: &StridedTable, + starts: Vec<(TableIndex, ::StateInfo)>, + max_number: usize, +) -> Vec> { + let match_offset = table.index_offset((-1, -1)); + let gap1_offset = table.index_offset((0, -1)); + let gap2_offset = table.index_offset((-1, 0)); + let trace_offset = |direction| match direction { + TraceDirection::Diag => match_offset, + TraceDirection::To1 => gap1_offset, + TraceDirection::To2 => gap2_offset, + }; + let mut paths = Vec::new(); + for (start, state) in starts { + let mut count: usize = 1; + follow_trace( + table, + start, + state, + &trace_offset, + &mut count, + max_number, + &mut paths, + ); + } + paths.truncate(max_number); + paths + .into_iter() + .map(|path| { + path_to_trace(&path, |index| { + let (i, j) = table.unindex(index); + (i as i64 - 1, j as i64 - 1) + }) + }) + .collect() +} + +/// Analogous to the [`FillRule`](super::pairwise::FillRule) in `pairwise.rs`. +pub trait FillRule { + type Cell: Cell; + + const SCORE_ONLY: bool; + + fn fill_origin(&self) -> Self::Cell; + + fn init_score(&self) -> Score; + + fn fill( + &self, + diagonal: Option<(S, S, Self::Cell)>, + from_gap1: &Self::Cell, + from_gap2: &Self::Cell, + max_observed_score: &mut Score, + ) -> Option; + + #[allow(clippy::type_complexity)] + fn find_traceback_starts( + &self, + table: &StridedTable, + ) -> (Score, Vec<(TableIndex, ::StateInfo)>); +} + +pub struct LinearGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: LinearDirectionStore, +{ + scoring_scheme: V, + gap_penalty: Score, + threshold: Score, + _phantom: PhantomData<(S, D)>, +} + +impl LinearGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: LinearDirectionStore, +{ + pub fn new(scoring_scheme: V, gap_penalty: Score, threshold: Score) -> Self { + LinearGapPenalty { + scoring_scheme, + gap_penalty, + threshold, + _phantom: PhantomData, + } + } +} + +impl FillRule for LinearGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: LinearDirectionStore, +{ + type Cell = LinearCell; + + const SCORE_ONLY: bool = D::SCORE_ONLY; + + #[inline] + fn fill_origin(&self) -> LinearCell { + LinearCell { + score: self.init_score(), + direction: D::default(), + } + } + + #[inline] + fn init_score(&self) -> Score { + // One above the threshold, so that a score of `0` can mark an invalid + // cell while the anchor itself stays valid. + self.threshold + 1 + } + + #[inline(always)] + fn fill( + &self, + diagonal: Option<(S, S, LinearCell)>, + from_gap1: &LinearCell, + from_gap2: &LinearCell, + max_observed_score: &mut Score, + ) -> Option> { + // The diagonal only contributes if its predecessor is itself valid. + let from_match = match diagonal { + Some((symbol1, symbol2, predecessor)) if predecessor.score != 0 => { + predecessor.score + self.scoring_scheme.score(symbol1, symbol2) + } + _ => 0, + }; + let from_gap1 = from_gap1.score + self.gap_penalty; + let from_gap2 = from_gap2.score + self.gap_penalty; + let (score, direction) = if D::SCORE_ONLY { + (from_match.max(from_gap1).max(from_gap2), D::default()) + } else { + let (score, direction) = get_trace_linear(from_match, from_gap1, from_gap2); + (score, D::wrap(direction)) + }; + if score >= *max_observed_score - self.threshold { + if score > *max_observed_score { + *max_observed_score = score; + } + Some(LinearCell { score, direction }) + } else { + None + } + } + + fn find_traceback_starts( + &self, + table: &StridedTable>, + ) -> (Score, Vec<(TableIndex, ())>) { + let mut max = Score::MIN; + let mut starts = Vec::new(); + for (index, cell) in table.indexed_iter() { + match cell.score.cmp(&max) { + Ordering::Greater => { + max = cell.score; + starts.clear(); + starts.push((index, ())); + } + Ordering::Equal => starts.push((index, ())), + Ordering::Less => {} + } + } + (max, starts) + } +} + +pub struct AffineGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: AffineDirectionStore, +{ + scoring_scheme: V, + gap_open: Score, + gap_extend: Score, + threshold: Score, + _phantom: PhantomData<(S, D)>, +} + +impl AffineGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: AffineDirectionStore, +{ + pub fn new(scoring_scheme: V, gap_open: Score, gap_extend: Score, threshold: Score) -> Self { + AffineGapPenalty { + scoring_scheme, + gap_open, + gap_extend, + threshold, + _phantom: PhantomData, + } + } +} + +impl FillRule for AffineGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: AffineDirectionStore, +{ + type Cell = AffineCell; + + const SCORE_ONLY: bool = D::SCORE_ONLY; + + #[inline] + fn fill_origin(&self) -> AffineCell { + AffineCell { + score: AffineScore { + m: self.init_score(), + g1: 0, + g2: 0, + }, + direction: D::default(), + } + } + + #[inline] + fn init_score(&self) -> Score { + self.threshold + 1 + } + + #[inline(always)] + fn fill( + &self, + diagonal: Option<(S, S, AffineCell)>, + from_gap1: &AffineCell, + from_gap2: &AffineCell, + max_observed_score: &mut Score, + ) -> Option> { + // Transitions into the match table from the diagonal predecessor; each + // contributes only if the originating table was valid. + let (mm, g1m, g2m) = match diagonal { + Some((symbol1, symbol2, predecessor)) => { + let similarity = self.scoring_scheme.score(symbol1, symbol2); + let gated = |score: Score| if score != 0 { score + similarity } else { 0 }; + ( + gated(predecessor.score.m), + gated(predecessor.score.g1), + gated(predecessor.score.g2), + ) + } + None => (0, 0, 0), + }; + // Transitions into the gap-in-1 (left) and gap-in-2 (top) tables. + let mg1 = from_gap1.score.m + self.gap_open; + let g1g1 = from_gap1.score.g1 + self.gap_extend; + let mg2 = from_gap2.score.m + self.gap_open; + let g2g2 = from_gap2.score.g2 + self.gap_extend; + + let (m_score, g1_score, g2_score, direction) = if D::SCORE_ONLY { + ( + mm.max(g1m).max(g2m), + mg1.max(g1g1), + mg2.max(g2g2), + D::default(), + ) + } else { + let (m_score, g1_score, g2_score, direction) = + get_trace_affine(mm, g1m, g2m, mg1, g1g1, mg2, g2g2); + (m_score, g1_score, g2_score, D::wrap(direction)) + }; + + // Each table is independently kept if it reaches the threshold; the cell + // is valid (and the full trace is stored) if any of them is. + let mut score = AffineScore::default(); + let mut valid = false; + let mut req_score = *max_observed_score - self.threshold; + if m_score >= req_score { + score.m = m_score; + valid = true; + if m_score > *max_observed_score { + *max_observed_score = m_score; + req_score = *max_observed_score - self.threshold; + } + } + if g1_score >= req_score { + score.g1 = g1_score; + valid = true; + if g1_score > *max_observed_score { + *max_observed_score = g1_score; + req_score = *max_observed_score - self.threshold; + } + } + if g2_score >= req_score { + score.g2 = g2_score; + valid = true; + if g2_score > *max_observed_score { + *max_observed_score = g2_score; + } + } + if valid { + Some(AffineCell { score, direction }) + } else { + None + } + } + + fn find_traceback_starts( + &self, + table: &StridedTable>, + ) -> (Score, Vec<(TableIndex, TraceState)>) { + // Only the match table is considered: a local alignment cannot start or + // end with a gap. + let mut max = Score::MIN; + let mut starts = Vec::new(); + for (index, cell) in table.indexed_iter() { + match cell.score.m.cmp(&max) { + Ordering::Greater => { + max = cell.score.m; + starts.clear(); + starts.push((index, TraceState::Match)); + } + Ordering::Equal => starts.push((index, TraceState::Match)), + Ordering::Less => {} + } + } + (max, starts) + } +} diff --git a/src/rust/sequence/align/localungapped.rs b/src/rust/sequence/align/localungapped.rs new file mode 100644 index 000000000..f6c0813c6 --- /dev/null +++ b/src/rust/sequence/align/localungapped.rs @@ -0,0 +1,290 @@ +//! Local ungapped seed extension ([`SeedExtension`]). +//! +//! Extends an ungapped alignment outward from a seed position in both +//! directions, accumulating the substitution score symbol by symbol and +//! stopping in each direction once the running score falls more than +//! `threshold` below the best score seen so far (*X-Drop* :footcite:`Zhang2000`). +//! +//! The scoring scheme, threshold and direction are preprocessed once when the +//! [`SeedExtension`] is constructed (in particular the substitution matrix is +//! extracted only once), so repeated [`align`](SeedExtension::align) calls — the +//! typical seeding workload — avoid that per-call overhead. Each `align` takes +//! the two [`Sequence`](biotite) objects directly, widens their codes to a +//! common dtype, walks the upstream/downstream regions via zero-copy slice +//! iterators and returns the resulting [`Alignment`](biotite) (or just the +//! score). As the alignment is ungapped (a single diagonal), no dynamic +//! programming table or [`Cell`](super::cell::Cell) is required. + +use super::scoring::{Score, Scoring, ScoringScheme}; +use super::symbol::Symbol; +use crate::dispatch_dtype; +use numpy::ndarray::Array2; +use numpy::{ + IntoPyArray, PyArray1, PyArrayDescrMethods, PyArrayMethods, PyUntypedArray, + PyUntypedArrayMethods, +}; +use pyo3::exceptions::{PyIndexError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::PyList; + +/// A reusable local ungapped seed extension (*X-Drop*). +/// +/// The scoring scheme, threshold and direction are preprocessed once on +/// construction, so the same :class:`SeedExtension` can run :meth:`align` on +/// multiple sequence pairs. +/// +/// Parameters +/// ---------- +/// matrix : SubstitutionMatrix or tuple(int, int) +/// Either a substitution matrix or a ``(match, mismatch)`` pair of scores. +/// threshold : int +/// If the current score falls this value below the maximum score +/// found, the alignment terminates. +/// direction : {'both', 'upstream', 'downstream'}, optional +/// Controls in which direction the alignment extends starting from the +/// seed. +/// If ``'upstream'``, the alignment ends at the seed; if ``'downstream'``, +/// it starts at the seed; if ``'both'`` (default) it extends in both +/// directions. +/// The seed position itself is always included in the alignment. +/// +/// See Also +/// -------- +/// align_local_gapped +/// For gapped local alignments with the same *X-Drop* technique. +/// +/// Examples +/// -------- +/// +/// >>> seq1 = ProteinSequence("BIQTITE") +/// >>> seq2 = ProteinSequence("PYRRHQTITE") +/// >>> matrix = SubstitutionMatrix.std_protein_matrix() +/// >>> extension = SeedExtension(matrix, threshold=10) +/// >>> print(extension.align(seq1, seq2, seed=(4, 7))) +/// QTITE +/// QTITE +/// >>> print(extension.align(seq1, seq2, seed=(4, 7), score_only=True)) +/// 24 +#[pyclass] +pub struct SeedExtension { + scoring: Scoring, + threshold: Score, + upstream: bool, + downstream: bool, +} + +#[pymethods] +impl SeedExtension { + #[new] + #[pyo3(signature = (matrix, threshold, direction = "both"))] + fn new(matrix: Scoring, threshold: Score, direction: &str) -> PyResult { + if threshold < 0 { + return Err(PyValueError::new_err( + "The threshold value must be a non-negative integer", + )); + } + let (upstream, downstream) = match direction { + "both" => (true, true), + "upstream" => (true, false), + "downstream" => (false, true), + _ => { + return Err(PyValueError::new_err(format!( + "Direction '{direction}' is invalid" + ))); + } + }; + Ok(SeedExtension { + scoring: matrix, + threshold, + upstream, + downstream, + }) + } + + /// Extend the alignment from `seed` between `seq1` and `seq2`. + /// + /// Parameters + /// ---------- + /// seq1, seq2 : Sequence + /// The sequences to be aligned. + /// seed : tuple(int, int) + /// The indices in `seq1` and `seq2` where the alignment starts. + /// The indices must be non-negative and in bounds. + /// score_only : bool, optional + /// If set to ``True``, only the similarity score is returned instead + /// of the :class:`Alignment`. + /// + /// Returns + /// ------- + /// alignment : Alignment + /// The resulting ungapped alignment. + /// Only returned, if `score_only` is ``False``. + /// score : int + /// The alignment similarity score. + /// Only returned, if `score_only` is ``True``. + #[pyo3(signature = (seq1, seq2, seed, score_only = false))] + fn align<'py>( + &self, + py: Python<'py>, + seq1: &Bound<'py, PyAny>, + seq2: &Bound<'py, PyAny>, + seed: &Bound<'py, PyAny>, + score_only: bool, + ) -> PyResult> { + // Accept any indexable seed (e.g. a tuple or a NumPy array) + let i: i64 = seed.get_item(0)?.extract()?; + let j: i64 = seed.get_item(1)?.extract()?; + if i < 0 || j < 0 { + return Err(PyIndexError::new_err("Seed must contain positive indices")); + } + let (i, j) = (i as usize, j as usize); + let len1 = seq1.len()?; + let len2 = seq2.len()?; + if i >= len1 || j >= len2 { + return Err(PyIndexError::new_err(format!( + "Seed {:?} is out of bounds for the sequences of length {len1} and {len2}", + (i, j) + ))); + } + + // The two sequences may use different code dtypes + // -> widen both to a common type so the kernel is dispatched once + let np = py.import("numpy")?; + let code1_obj = seq1.getattr("code")?; + let code2_obj = seq2.getattr("code")?; + let common_dtype = np.call_method1( + "promote_types", + (code1_obj.getattr("dtype")?, code2_obj.getattr("dtype")?), + )?; + let code1 = np.call_method1("ascontiguousarray", (code1_obj, &common_dtype))?; + let code2 = np.call_method1("ascontiguousarray", (code2_obj, &common_dtype))?; + let code1 = code1.cast::()?; + let code2 = code2.cast::()?; + + let dtype = code1.dtype(); + dispatch_dtype!( + py, + &dtype, + [u8, u16, u32, u64], + align_dtype(py, self, seq1, seq2, code1, code2, (i, j), score_only) + ) + } +} + +#[allow(clippy::too_many_arguments)] +fn align_dtype<'py, S: Symbol + numpy::Element>( + py: Python<'py>, + extension: &SeedExtension, + seq1: &Bound<'py, PyAny>, + seq2: &Bound<'py, PyAny>, + code1: &Bound<'py, PyUntypedArray>, + code2: &Bound<'py, PyUntypedArray>, + seed: (usize, usize), + score_only: bool, +) -> PyResult> { + let code1_array = code1.cast::>()?.readonly(); + let code2_array = code2.cast::>()?.readonly(); + let code1_slice = code1_array.as_slice()?; + let code2_slice = code2_array.as_slice()?; + + let (total_score, start_offset, stop_offset) = match &extension.scoring { + Scoring::Matrix(scheme) => seed_extend(extension, code1_slice, code2_slice, scheme, seed), + Scoring::Match(scheme) => seed_extend(extension, code1_slice, code2_slice, scheme, seed), + }; + + if score_only { + return Ok(total_score.into_pyobject(py)?.into_any()); + } + + // The alignment is ungapped, so the trace is a contiguous diagonal + let (i, j) = seed; + let length = (stop_offset - start_offset) as usize; + let mut trace = Array2::::zeros((length, 2)); + for k in 0..length { + let offset = start_offset + k as i64; + trace[[k, 0]] = i as i64 + offset; + trace[[k, 1]] = j as i64 + offset; + } + let trace = trace.into_pyarray(py); + + let sequences = PyList::new(py, [seq1, seq2])?; + let alignment = py + .import("biotite.sequence.align.alignment")? + .getattr("Alignment")? + .call1((sequences, trace, total_score))?; + Ok(alignment) +} + +/// Core seed extension, generic over the symbol type and scoring scheme. +/// +/// Returns the total score (including the seed) and the seed-relative +/// `start_offset`/`stop_offset` of the aligned diagonal. +fn seed_extend( + extension: &SeedExtension, + code1: &[S], + code2: &[S], + scoring: &V, + seed: (usize, usize), +) -> (Score, i64, i64) +where + S: Symbol, + V: ScoringScheme, +{ + let (i, j) = seed; + // The seed position itself is always part of the alignment + let mut total_score = scoring.score(code1[i], code2[j]); + let mut start_offset: i64 = 0; + let mut stop_offset: i64 = 1; + + if extension.upstream { + // Walk the prefixes before the seed in reverse (zero-copy) + let pairs = code1[..i] + .iter() + .rev() + .zip(code2[..j].iter().rev()) + .map(|(&s1, &s2)| (s1, s2)); + let (score, length) = extend(scoring, extension.threshold, pairs); + total_score += score; + start_offset = -length; + } + if extension.downstream { + // Walk the suffixes after the seed + let pairs = code1[i + 1..] + .iter() + .zip(code2[j + 1..].iter()) + .map(|(&s1, &s2)| (s1, s2)); + let (score, length) = extend(scoring, extension.threshold, pairs); + total_score += score; + stop_offset = 1 + length; + } + + (total_score, start_offset, stop_offset) +} + +/// Run the X-Drop extension over a stream of aligned symbol pairs. +/// +/// Returns the maximum score reached and the number of pairs up to (and +/// including) that maximum. +#[inline] +fn extend(scoring: &V, threshold: Score, pairs: I) -> (Score, i64) +where + S: Symbol, + V: ScoringScheme, + I: Iterator, +{ + let mut total_score: Score = 0; + let mut max_score: Score = 0; + let mut length: i64 = 0; + for (offset, (s1, s2)) in pairs.enumerate() { + total_score += scoring.score(s1, s2); + if total_score >= max_score { + // A new maximum (ties extend the alignment over zero-score columns) + max_score = total_score; + length = (offset + 1) as i64; + } else if max_score - total_score > threshold { + // Score dropped too far below the maximum -> terminate + break; + } + } + (max_score, length) +} diff --git a/src/rust/sequence/align/mod.rs b/src/rust/sequence/align/mod.rs new file mode 100644 index 000000000..ca495d20a --- /dev/null +++ b/src/rust/sequence/align/mod.rs @@ -0,0 +1,20 @@ +use pyo3::prelude::*; + +pub mod banded; +pub mod cell; +pub mod localgapped; +pub mod localungapped; +pub mod pairwise; +pub mod scoring; +pub mod symbol; +pub mod table; +pub mod trace; + +pub fn module<'py>(parent_module: &Bound<'py, PyModule>) -> PyResult> { + let module = PyModule::new(parent_module.py(), "align")?; + module.add_function(wrap_pyfunction!(pairwise::align_optimal, &module)?)?; + module.add_function(wrap_pyfunction!(banded::align_banded, &module)?)?; + module.add_function(wrap_pyfunction!(localgapped::align_region, &module)?)?; + module.add_class::()?; + Ok(module) +} diff --git a/src/rust/sequence/align/pairwise.rs b/src/rust/sequence/align/pairwise.rs new file mode 100644 index 000000000..e92f4c7ab --- /dev/null +++ b/src/rust/sequence/align/pairwise.rs @@ -0,0 +1,882 @@ +//! Optimal pairwise alignment (`align_optimal`). +//! +//! The Python layer widens both sequence codes to a common unsigned integer +//! type and passes the raw code arrays plus the scoring scheme; this module +//! Fills the dynamic programming table, runs the traceback and returns the +//! resulting trace arrays and score. +//! +//! The [`FillRule`] for the full Needleman-Wunsch/Smith-Waterman/Gotoh table is +//! defined here, specialized at compile time via const generics for +//! local/global alignment, terminal gap penalty handling and score-only mode. + +use super::cell::{ + get_trace_affine, get_trace_linear, AffineCell, AffineDirection, AffineDirectionStore, + AffineScore, Cell, LinearCell, LinearDirection, LinearDirectionStore, TraceDirection, + TraceState, +}; +use super::scoring::{GapPenalty, Score, Scoring, ScoringScheme}; +use super::symbol::Symbol; +use super::table::{StridedTable, TableIndex}; +use super::trace::{follow_trace, path_to_trace}; +use crate::dispatch_dtype; +use numpy::ndarray::Array2; +use numpy::{ + IntoPyArray, PyArray1, PyArrayDescrMethods, PyArrayMethods, PyUntypedArray, + PyUntypedArrayMethods, +}; +use pyo3::prelude::*; +use pyo3::types::{PyList, PyTuple}; +use std::cmp::Ordering; +use std::marker::PhantomData; + +/// Perform an optimal global or local pairwise alignment. +/// +/// Parameters +/// ---------- +/// code1, code2 : ndarray +/// The sequence codes. Both must share the same unsigned integer dtype. +/// scoring : ndarray, dtype=int32 or tuple(int, int) +/// A substitution matrix or a `(match, mismatch)` score pair. +/// gap_penalty : int or tuple(int, int) +/// A linear or affine gap penalty. +/// terminal_penalty : bool +/// Whether terminal gaps are penalized. +/// local : bool +/// Whether to perform a local (instead of global) alignment. +/// score_only : bool +/// If true, only the score is computed and the trace list is empty. +/// max_number : int +/// The maximum number of (co-optimal) alignments to return. +/// +/// Returns +/// ------- +/// traces : list of ndarray +/// The alignment traces, each of shape `(length, 2)`. Empty if `score_only`. +/// score : int +/// The optimal alignment score. +#[pyfunction] +#[pyo3(signature = (code1, code2, scoring, gap_penalty, terminal_penalty, local, score_only, max_number))] +#[allow(clippy::too_many_arguments)] +pub fn align_optimal<'py>( + py: Python<'py>, + code1: &Bound<'py, PyUntypedArray>, + code2: &Bound<'py, PyUntypedArray>, + scoring: Scoring, + gap_penalty: GapPenalty, + terminal_penalty: bool, + local: bool, + score_only: bool, + max_number: usize, +) -> PyResult> { + let dtype = code1.dtype(); + dispatch_dtype!( + py, + &dtype, + [u8, u16, u32, u64], + align_optimal_dtype( + py, + code1, + code2, + scoring, + gap_penalty, + terminal_penalty, + local, + score_only, + max_number + ) + ) +} + +#[allow(clippy::too_many_arguments)] +fn align_optimal_dtype<'py, S: Symbol + numpy::Element>( + py: Python<'py>, + code1: &Bound<'py, PyUntypedArray>, + code2: &Bound<'py, PyUntypedArray>, + scoring: Scoring, + gap_penalty: GapPenalty, + terminal_penalty: bool, + local: bool, + score_only: bool, + max_number: usize, +) -> PyResult> { + let code1_array = code1.cast::>()?.readonly(); + let code2_array = code2.cast::>()?.readonly(); + let code1 = code1_array.as_slice()?; + let code2 = code2_array.as_slice()?; + + // Instantiate the fill rule for the runtime scoring scheme, gap penalty and + // `local`/`terminal_penalty`/`score_only` flags, then run the generic core. + // The direction-storage type (`$dir` for a full alignment, `()` for + // score-only) selects the cell size. + macro_rules! run { + ($rule:ident, $dir:ty, $local:literal, $terminal:literal, $scheme:expr, ($($penalty:expr),+)) => { + align_optimal_generic( + code1, code2, + <$rule<_, _, $dir, $local, $terminal>>::new($scheme, $($penalty),+), + max_number, + ) + }; + } + macro_rules! by_consts { + ($rule:ident, $dir:ty, $scheme:expr, ($($penalty:expr),+)) => { + match (local, terminal_penalty, score_only) { + (true, true, true) => run!($rule, (), true, true, $scheme, ($($penalty),+)), + (true, true, false) => run!($rule, $dir, true, true, $scheme, ($($penalty),+)), + (true, false, true) => run!($rule, (), true, false, $scheme, ($($penalty),+)), + (true, false, false) => run!($rule, $dir, true, false, $scheme, ($($penalty),+)), + (false, true, true) => run!($rule, (), false, true, $scheme, ($($penalty),+)), + (false, true, false) => run!($rule, $dir, false, true, $scheme, ($($penalty),+)), + (false, false, true) => run!($rule, (), false, false, $scheme, ($($penalty),+)), + (false, false, false) => run!($rule, $dir, false, false, $scheme, ($($penalty),+)), + } + }; + } + macro_rules! by_penalty { + ($scheme:expr) => { + match gap_penalty { + GapPenalty::Linear(gap) => { + by_consts!(LinearGapPenalty, LinearDirection, $scheme, (gap)) + } + GapPenalty::Affine(open, extend) => { + by_consts!(AffineGapPenalty, AffineDirection, $scheme, (open, extend)) + } + } + }; + } + let (traces, score) = match scoring { + Scoring::Matrix(scheme) => by_penalty!(scheme), + Scoring::Match(scheme) => by_penalty!(scheme), + }; + + let list = PyList::empty(py); + for trace in traces { + list.append(trace.into_pyarray(py))?; + } + PyTuple::new(py, [list.into_any(), score.into_pyobject(py)?.into_any()]) +} + +/// Core optimal pairwise alignment, generic over the symbol type and fill rule. +/// +/// Returns the list of optimal traces and their shared score. +pub(crate) fn align_optimal_generic( + code1: &[S], + code2: &[S], + rule: F, + max_number: usize, +) -> (Vec>, Score) +where + S: Symbol, + F: FillRule, +{ + let n = code1.len(); + let m = code2.len(); + + // The table is transposed relative to the usual visualization: the first + // sequence runs down the rows, the second along the columns + let mut table = StridedTable::::new((n + 1, m + 1)); + + // The flat index is affine in `(row, column)`, so the offset from a cell to + // each of its predecessors is constant + // -> Compute these offsets once instead of building an index per cell + let match_offset = table.index_offset((-1, -1)); + let gap1_offset = table.index_offset((0, -1)); + let gap2_offset = table.index_offset((-1, 0)); + // Steps to the next cell along a row and down a column + let column_step = table.index_offset((0, 1)); + let row_step = table.index_offset((1, 0)); + + let origin = table.index((0, 0)); + table[origin] = rule.fill_origin(); + // First column: walk downward, each cell filled from the one above it. + let mut current = origin; + for _ in 0..n { + let predecessor = table[current]; + current += row_step; + table[current] = rule.fill_start1(&predecessor); + } + // First row: walk rightward, each cell filled from the one to its left. + let mut current = origin; + for _ in 0..m { + let predecessor = table[current]; + current += column_step; + table[current] = rule.fill_start2(&predecessor); + } + + for (i_offset, &s1) in code1.iter().enumerate() { + let i = i_offset + 1; + let last_row = i == n; + // Index of the first cell in this row + // Consecutive cells along the row are reached by adding `column_step` + let mut current = table.index((i, 1)); + for (j_offset, &s2) in code2.iter().enumerate() { + let last_col = j_offset + 1 == m; + let from_match = table[current + match_offset]; + let from_gap1 = table[current + gap1_offset]; + let from_gap2 = table[current + gap2_offset]; + table[current] = if last_row && last_col { + rule.fill_corner(s1, s2, &from_match, &from_gap1, &from_gap2) + } else if last_row { + rule.fill_end2(s1, s2, &from_match, &from_gap1, &from_gap2) + } else if last_col { + rule.fill_end1(s1, s2, &from_match, &from_gap1, &from_gap2) + } else { + rule.fill(s1, s2, &from_match, &from_gap1, &from_gap2) + }; + current += column_step; + } + } + + let (score, starts) = rule.find_traceback_starts(&table); + + if F::SCORE_ONLY { + return (Vec::new(), score); + } + + let trace_offset = |direction| match direction { + TraceDirection::Diag => match_offset, + TraceDirection::To1 => gap1_offset, + TraceDirection::To2 => gap2_offset, + }; + let mut paths = Vec::new(); + for (start, state) in starts { + // The branch counter is reset per start + // The total number of traces is capped afterwards + let mut count: usize = 1; + follow_trace( + &table, + start, + state, + &trace_offset, + &mut count, + max_number, + &mut paths, + ); + } + paths.truncate(max_number); + + let traces = paths + .into_iter() + .map(|path| { + path_to_trace(&path, |index| { + let (i, j) = table.unindex(index); + (i as i64 - 1, j as i64 - 1) + }) + }) + .collect(); + (traces, score) +} + +/// Encapsulates how a single DP cell of the full (rectangular) table is computed +/// from its predecessors. +/// +/// Implementors use const generic parameters to specialize for local/global +/// alignment, terminal gap penalty handling and score-only mode, so these +/// branches are resolved at compile time rather than per cell. +pub trait FillRule { + /// The cell type this rule produces. + type Cell: Cell; + + /// Whether only the score is required, so the trace can be omitted. + const SCORE_ONLY: bool; + + /// Fill the origin cell `(0, 0)`. + fn fill_origin(&self) -> Self::Cell; + + /// Fill a cell in the first column from the cell above it (a leading gap in + /// the second sequence). + fn fill_start1(&self, from_gap2: &Self::Cell) -> Self::Cell; + + /// Fill a cell in the first row from the cell to its left (a leading gap in + /// the first sequence). + fn fill_start2(&self, from_gap1: &Self::Cell) -> Self::Cell; + + /// Fill an interior cell from its diagonal, left and top predecessors. + fn fill( + &self, + symbol1: S, + symbol2: S, + from_match: &Self::Cell, + from_gap1: &Self::Cell, + from_gap2: &Self::Cell, + ) -> Self::Cell; + + /// Fill a cell in the last column, where a trailing gap in the second + /// sequence is exempt from penalty (unless terminal gaps are penalized). + fn fill_end1( + &self, + symbol1: S, + symbol2: S, + from_match: &Self::Cell, + from_gap1: &Self::Cell, + from_gap2: &Self::Cell, + ) -> Self::Cell; + + /// Fill a cell in the last row, where a trailing gap in the first sequence + /// is exempt from penalty (unless terminal gaps are penalized). + fn fill_end2( + &self, + symbol1: S, + symbol2: S, + from_match: &Self::Cell, + from_gap1: &Self::Cell, + from_gap2: &Self::Cell, + ) -> Self::Cell; + + /// Fill the bottom-right corner cell, where trailing gaps in both sequences + /// are exempt from penalty (unless terminal gaps are penalized). + fn fill_corner( + &self, + symbol1: S, + symbol2: S, + from_match: &Self::Cell, + from_gap1: &Self::Cell, + from_gap2: &Self::Cell, + ) -> Self::Cell; + + /// Find the traceback start position(s) and the optimal score. + /// + /// For global alignment this is the bottom-right corner; for local + /// alignment it is the cell(s) with the maximum score. + #[allow(clippy::type_complexity)] + fn find_traceback_starts( + &self, + table: &StridedTable, + ) -> (Score, Vec<(TableIndex, ::StateInfo)>); +} + +/// A [`FillRule`] using a linear gap penalty. +pub struct LinearGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: LinearDirectionStore, +{ + scoring_scheme: V, + gap_penalty: Score, + _phantom: PhantomData<(S, D)>, +} + +impl + LinearGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: LinearDirectionStore, +{ + pub fn new(scoring_scheme: V, gap_penalty: Score) -> Self { + LinearGapPenalty { + scoring_scheme, + gap_penalty, + _phantom: PhantomData, + } + } + + /// The shared recurrence. `WAIVE_GAP1`/`WAIVE_GAP2` drop the gap penalty for + /// a trailing gap in the first/second sequence (used in the last row/column + /// when terminal gaps are not penalized). + #[inline(always)] + fn recurrence( + &self, + symbol1: S, + symbol2: S, + from_match: &LinearCell, + from_gap1: &LinearCell, + from_gap2: &LinearCell, + ) -> LinearCell { + let similarity = self.scoring_scheme.score(symbol1, symbol2); + let score_from_match = from_match.score + similarity; + let score_from_gap1 = if WAIVE_GAP1 { + from_gap1.score + } else { + from_gap1.score + self.gap_penalty + }; + let score_from_gap2 = if WAIVE_GAP2 { + from_gap2.score + } else { + from_gap2.score + self.gap_penalty + }; + // Computing the trace directions is only worthwhile if they are needed. + let (mut score, mut direction) = if D::SCORE_ONLY { + ( + score_from_match.max(score_from_gap1).max(score_from_gap2), + D::default(), + ) + } else { + let (score, direction) = + get_trace_linear(score_from_match, score_from_gap1, score_from_gap2); + (score, D::wrap(direction)) + }; + // Local alignment: a non-positive cell ends the trace and resets to 0. + if LOCAL && score <= 0 { + score = 0; + direction = D::default(); + } + LinearCell { score, direction } + } +} + +impl FillRule + for LinearGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: LinearDirectionStore, +{ + type Cell = LinearCell; + + const SCORE_ONLY: bool = D::SCORE_ONLY; + + #[inline] + fn fill_origin(&self) -> LinearCell { + LinearCell::default() + } + + #[inline] + fn fill_start1(&self, from_gap2: &LinearCell) -> LinearCell { + // Local alignments must not extend into the leading gap region. + if LOCAL { + LinearCell::default() + } else { + let score = if TERMINAL_PENALTY { + from_gap2.score + self.gap_penalty + } else { + 0 + }; + LinearCell { + score, + direction: D::wrap(LinearDirection::GAP2), + } + } + } + + #[inline] + fn fill_start2(&self, from_gap1: &LinearCell) -> LinearCell { + if LOCAL { + LinearCell::default() + } else { + let score = if TERMINAL_PENALTY { + from_gap1.score + self.gap_penalty + } else { + 0 + }; + LinearCell { + score, + direction: D::wrap(LinearDirection::GAP1), + } + } + } + + #[inline(always)] + fn fill( + &self, + symbol1: S, + symbol2: S, + from_match: &LinearCell, + from_gap1: &LinearCell, + from_gap2: &LinearCell, + ) -> LinearCell { + self.recurrence::(symbol1, symbol2, from_match, from_gap1, from_gap2) + } + + #[inline(always)] + fn fill_end1( + &self, + symbol1: S, + symbol2: S, + from_match: &LinearCell, + from_gap1: &LinearCell, + from_gap2: &LinearCell, + ) -> LinearCell { + // A terminal-penalty or local alignment fills this like an ordinary + // interior cell; only a global alignment without terminal penalty + // exempts the trailing gap. + if TERMINAL_PENALTY || LOCAL { + self.recurrence::(symbol1, symbol2, from_match, from_gap1, from_gap2) + } else { + self.recurrence::(symbol1, symbol2, from_match, from_gap1, from_gap2) + } + } + + #[inline(always)] + fn fill_end2( + &self, + symbol1: S, + symbol2: S, + from_match: &LinearCell, + from_gap1: &LinearCell, + from_gap2: &LinearCell, + ) -> LinearCell { + if TERMINAL_PENALTY || LOCAL { + self.recurrence::(symbol1, symbol2, from_match, from_gap1, from_gap2) + } else { + self.recurrence::(symbol1, symbol2, from_match, from_gap1, from_gap2) + } + } + + #[inline(always)] + fn fill_corner( + &self, + symbol1: S, + symbol2: S, + from_match: &LinearCell, + from_gap1: &LinearCell, + from_gap2: &LinearCell, + ) -> LinearCell { + if TERMINAL_PENALTY || LOCAL { + self.recurrence::(symbol1, symbol2, from_match, from_gap1, from_gap2) + } else { + self.recurrence::(symbol1, symbol2, from_match, from_gap1, from_gap2) + } + } + + fn find_traceback_starts( + &self, + table: &StridedTable>, + ) -> (Score, Vec<(TableIndex, ())>) { + if LOCAL { + // The start is the highest-scoring cell. + let mut max = Score::MIN; + let mut starts = Vec::new(); + for (index, cell) in table.indexed_iter() { + match cell.score.cmp(&max) { + Ordering::Greater => { + max = cell.score; + starts.clear(); + starts.push((index, ())); + } + Ordering::Equal => starts.push((index, ())), + Ordering::Less => {} + } + } + (max, starts) + } else { + // The start is the bottom-right corner + let shape = table.shape(); + let index = table.index((shape.0 - 1, shape.1 - 1)); + let score = table[index].score; + (score, vec![(index, ())]) + } + } +} + +/// A [`FillRule`] using an affine gap penalty. +pub struct AffineGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: AffineDirectionStore, +{ + scoring_scheme: V, + gap_open: Score, + gap_extend: Score, + /// Value representing negative infinity, chosen so that adding a gap penalty + /// or similarity score during the recurrence can never underflow. + neg_inf: Score, + _phantom: PhantomData<(S, D)>, +} + +impl + AffineGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: AffineDirectionStore, +{ + pub fn new(scoring_scheme: V, gap_open: Score, gap_extend: Score) -> Self { + // The sentinel is offset away from `Score::MIN` by the magnitudes of + // both gap penalties and the minimum similarity, so that any single + // addition in the recurrence stays representable. The penalties are + // guaranteed non-positive by the Python layer. + let mut neg_inf = Score::MIN - gap_open - gap_extend; + let min_score = scoring_scheme.min(); + if min_score < 0 { + neg_inf -= min_score; + } + AffineGapPenalty { + scoring_scheme, + gap_open, + gap_extend, + neg_inf, + _phantom: PhantomData, + } + } + + /// The shared recurrence. `WAIVE_GAP1`/`WAIVE_GAP2` drop the gap penalty for + /// a trailing gap in the first/second sequence (used in the last row/column + /// when terminal gaps are not penalized). + #[inline(always)] + fn recurrence( + &self, + symbol1: S, + symbol2: S, + from_match: &AffineCell, + from_gap1: &AffineCell, + from_gap2: &AffineCell, + ) -> AffineCell { + let similarity = self.scoring_scheme.score(symbol1, symbol2); + // Transitions into the match table come from the diagonal predecessor + let mm = from_match.score.m + similarity; + let g1m = from_match.score.g1 + similarity; + let g2m = from_match.score.g2 + similarity; + // Transitions into the gap-in-1 table come from the left predecessor + let (mg1, g1g1) = if WAIVE_GAP1 { + (from_gap1.score.m, from_gap1.score.g1) + } else { + ( + from_gap1.score.m + self.gap_open, + from_gap1.score.g1 + self.gap_extend, + ) + }; + // Transitions into the gap-in-2 table come from the top predecessor + let (mg2, g2g2) = if WAIVE_GAP2 { + (from_gap2.score.m, from_gap2.score.g2) + } else { + ( + from_gap2.score.m + self.gap_open, + from_gap2.score.g2 + self.gap_extend, + ) + }; + + // `direction` stays a raw `AffineDirection` (computed only when traced); + // it is wrapped into the cell's `D` at the end, so the local masking + // below is plain bit arithmetic that the score-only build discards. + let (m_score, g1_score, g2_score, mut direction) = if D::SCORE_ONLY { + ( + mm.max(g1m).max(g2m), + mg1.max(g1g1), + mg2.max(g2g2), + AffineDirection::empty(), + ) + } else { + get_trace_affine(mm, g1m, g2m, mg1, g1g1, mg2, g2g2) + }; + let mut score = AffineScore { + m: m_score, + g1: g1_score, + g2: g2_score, + }; + + if LOCAL { + // A non-positive cell ends the trace in the respective table + if m_score <= 0 { + direction &= !(AffineDirection::MATCH_TO_MATCH + | AffineDirection::GAP1_TO_MATCH + | AffineDirection::GAP2_TO_MATCH); + score.m = 0; + } + if g1_score <= 0 { + direction &= !(AffineDirection::MATCH_TO_GAP1 | AffineDirection::GAP1_TO_GAP1); + score.g1 = self.neg_inf; + } + if g2_score <= 0 { + direction &= !(AffineDirection::MATCH_TO_GAP2 | AffineDirection::GAP2_TO_GAP2); + score.g2 = self.neg_inf; + } + } + + AffineCell { + score, + direction: D::wrap(direction), + } + } +} + +impl FillRule + for AffineGapPenalty +where + S: Symbol, + V: ScoringScheme, + D: AffineDirectionStore, +{ + type Cell = AffineCell; + + const SCORE_ONLY: bool = D::SCORE_ONLY; + + #[inline] + fn fill_origin(&self) -> AffineCell { + AffineCell { + score: AffineScore { + m: 0, + g1: self.neg_inf, + g2: self.neg_inf, + }, + direction: D::default(), + } + } + + #[inline] + fn fill_start1(&self, from_gap2: &AffineCell) -> AffineCell { + if LOCAL { + // Fill with negative infinity values to prevent that an + // alignment trace starts with a gap extension + // instead of a gap opening + return AffineCell { + score: AffineScore { + m: self.neg_inf, + g1: self.neg_inf, + g2: 0, + }, + direction: D::default(), + }; + } + // Open the gap from the predecessor's match table, or extend its + // gap-in-2 table; the larger one wins and dictates the direction + let (open, extend) = if TERMINAL_PENALTY { + ( + from_gap2.score.m + self.gap_open, + from_gap2.score.g2 + self.gap_extend, + ) + } else { + (from_gap2.score.m, from_gap2.score.g2) + }; + let (g2, direction) = if open >= extend { + (open, AffineDirection::MATCH_TO_GAP2) + } else { + (extend, AffineDirection::GAP2_TO_GAP2) + }; + AffineCell { + score: AffineScore { + m: self.neg_inf, + g1: self.neg_inf, + g2, + }, + direction: D::wrap(direction), + } + } + + #[inline] + fn fill_start2(&self, from_gap1: &AffineCell) -> AffineCell { + if LOCAL { + return AffineCell { + score: AffineScore { + m: self.neg_inf, + g1: 0, + g2: self.neg_inf, + }, + direction: D::default(), + }; + } + let (open, extend) = if TERMINAL_PENALTY { + ( + from_gap1.score.m + self.gap_open, + from_gap1.score.g1 + self.gap_extend, + ) + } else { + (from_gap1.score.m, from_gap1.score.g1) + }; + let (g1, direction) = if open >= extend { + (open, AffineDirection::MATCH_TO_GAP1) + } else { + (extend, AffineDirection::GAP1_TO_GAP1) + }; + AffineCell { + score: AffineScore { + m: self.neg_inf, + g1, + g2: self.neg_inf, + }, + direction: D::wrap(direction), + } + } + + #[inline(always)] + fn fill( + &self, + symbol1: S, + symbol2: S, + from_match: &AffineCell, + from_gap1: &AffineCell, + from_gap2: &AffineCell, + ) -> AffineCell { + self.recurrence::(symbol1, symbol2, from_match, from_gap1, from_gap2) + } + + #[inline(always)] + fn fill_end1( + &self, + symbol1: S, + symbol2: S, + from_match: &AffineCell, + from_gap1: &AffineCell, + from_gap2: &AffineCell, + ) -> AffineCell { + // A terminal-penalty or local alignment fills this like an ordinary + // interior cell; only a global alignment without terminal penalty + // exempts the trailing gap + if TERMINAL_PENALTY || LOCAL { + self.recurrence::(symbol1, symbol2, from_match, from_gap1, from_gap2) + } else { + self.recurrence::(symbol1, symbol2, from_match, from_gap1, from_gap2) + } + } + + #[inline(always)] + fn fill_end2( + &self, + symbol1: S, + symbol2: S, + from_match: &AffineCell, + from_gap1: &AffineCell, + from_gap2: &AffineCell, + ) -> AffineCell { + if TERMINAL_PENALTY || LOCAL { + self.recurrence::(symbol1, symbol2, from_match, from_gap1, from_gap2) + } else { + self.recurrence::(symbol1, symbol2, from_match, from_gap1, from_gap2) + } + } + + #[inline(always)] + fn fill_corner( + &self, + symbol1: S, + symbol2: S, + from_match: &AffineCell, + from_gap1: &AffineCell, + from_gap2: &AffineCell, + ) -> AffineCell { + if TERMINAL_PENALTY || LOCAL { + self.recurrence::(symbol1, symbol2, from_match, from_gap1, from_gap2) + } else { + self.recurrence::(symbol1, symbol2, from_match, from_gap1, from_gap2) + } + } + + fn find_traceback_starts( + &self, + table: &StridedTable>, + ) -> (Score, Vec<(TableIndex, TraceState)>) { + if LOCAL { + // Only the match table is considered: + // a local alignment cannot start or end with a gap + let mut max = Score::MIN; + let mut starts = Vec::new(); + for (index, cell) in table.indexed_iter() { + match cell.score.m.cmp(&max) { + Ordering::Greater => { + max = cell.score.m; + starts.clear(); + starts.push((index, TraceState::Match)); + } + Ordering::Equal => starts.push((index, TraceState::Match)), + Ordering::Less => {} + } + } + (max, starts) + } else { + // The start is the bottom-right corner; the trace may start in any + // of the three tables that reaches the maximum score. + let shape = table.shape(); + let index = table.index((shape.0 - 1, shape.1 - 1)); + let cell = table[index]; + let max = cell.score.m.max(cell.score.g1).max(cell.score.g2); + let mut starts = Vec::new(); + for state in [TraceState::Match, TraceState::Gap1, TraceState::Gap2] { + if table[index].score.get(state) == max { + starts.push((index, state)); + } + } + (max, starts) + } + } +} diff --git a/src/rust/sequence/align/scoring.rs b/src/rust/sequence/align/scoring.rs new file mode 100644 index 000000000..19cf1eb20 --- /dev/null +++ b/src/rust/sequence/align/scoring.rs @@ -0,0 +1,147 @@ +//! Scoring schemes for sequence alignment. +//! +//! A [`ScoringScheme`] assigns a similarity score to a pair of symbols. +//! Two implementations are provided: [`SubstitutionMatrix`] for general +//! matrix-based scoring and [`Match`] for simple binary match/mismatch scoring. + +use super::symbol::Symbol; +use numpy::ndarray::ArrayView2; +use numpy::PyReadonlyArray2; +use pyo3::prelude::*; +use pyo3::Borrowed; + +/// The universal type used to represent alignment scores. +pub type Score = i32; + +/// A scheme that assigns a similarity score to a pair of symbols. +pub trait ScoringScheme { + /// The similarity score for aligning `symbol1` with `symbol2`. + fn score(&self, symbol1: S, symbol2: S) -> Score; + + /// The minimum score this scheme can return. + /// + /// This is used to compute a safe 'negative-infinity' sentinel score + /// without risking integer underflow. + fn min(&self) -> Score; +} + +/// Scoring by lookup in a substitution matrix. +/// +/// The matrix is stored in row-major (strided) format for efficient +/// lookup. +pub struct SubstitutionMatrix { + /// Flat row-major score buffer of length `n_rows * n_cols`. + data: Vec, + /// Number of columns, i.e. the size of the second alphabet. + n_cols: usize, + /// Precomputed minimum entry, returned by [`ScoringScheme::min`]. + min: Score, +} + +impl SubstitutionMatrix { + /// Create a matrix from a 2D score array. + /// + /// The number of rows is the size of the first sequence's alphabet, the + /// number of columns the size of the second. + pub fn new(matrix: ArrayView2) -> Self { + let n_rows = matrix.shape()[0]; + let n_cols = matrix.shape()[1]; + let mut data = Vec::with_capacity(n_rows * n_cols); + for row in 0..n_rows { + for col in 0..n_cols { + data.push(matrix[[row, col]]); + } + } + let min = data.iter().copied().min().unwrap_or(0); + SubstitutionMatrix { data, n_cols, min } + } +} + +impl ScoringScheme for SubstitutionMatrix { + #[inline(always)] + fn score(&self, symbol1: S, symbol2: S) -> Score { + self.data[symbol1.index() * self.n_cols + symbol2.index()] + } + + #[inline(always)] + fn min(&self) -> Score { + self.min + } +} + +impl<'a, 'py> FromPyObject<'a, 'py> for SubstitutionMatrix { + type Error = PyErr; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> PyResult { + // Accept either a raw 2D score array (as produced by the Python layer) + // or a `SubstitutionMatrix` object exposing `score_matrix()` + if let Ok(matrix) = obj.extract::>() { + return Ok(SubstitutionMatrix::new(matrix.as_array())); + } + let matrix: PyReadonlyArray2 = obj.getattr("score_matrix")?.call0()?.extract()?; + Ok(SubstitutionMatrix::new(matrix.as_array())) + } +} + +/// A simple binary match/mismatch scoring scheme. +pub struct Match { + match_score: Score, + mismatch_score: Score, +} + +impl Match { + pub fn new(match_score: Score, mismatch_score: Score) -> Self { + Match { + match_score, + mismatch_score, + } + } +} + +impl ScoringScheme for Match { + #[inline(always)] + fn score(&self, symbol1: S, symbol2: S) -> Score { + if symbol1 == symbol2 { + self.match_score + } else { + self.mismatch_score + } + } + + #[inline(always)] + fn min(&self) -> Score { + self.match_score.min(self.mismatch_score) + } +} + +impl<'a, 'py> FromPyObject<'a, 'py> for Match { + type Error = PyErr; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> PyResult { + let (match_score, mismatch_score): (Score, Score) = obj.extract()?; + Ok(Match::new(match_score, mismatch_score)) + } +} + +/// The gap penalty configuration passed from the Python layer. +/// +/// The concrete variant is only known at run time, so each alignment module +/// matches on it to dispatch to its own statically monomorphized fill rule. +#[derive(FromPyObject)] +pub enum GapPenalty { + /// A single linear gap penalty. + Linear(Score), + /// An affine gap penalty as `(gap_open, gap_extend)`. + Affine(Score, Score), +} + +/// A scoring scheme accepted from the Python layer: either a [`SubstitutionMatrix`] +/// (a 2D array) or a [`Match`]/mismatch tuple. +/// +/// Like [`GapPenalty`], the variant is resolved at run time and drives the +/// dispatch to a monomorphized fill rule. +#[derive(FromPyObject)] +pub enum Scoring { + Matrix(SubstitutionMatrix), + Match(Match), +} diff --git a/src/rust/sequence/align/symbol.rs b/src/rust/sequence/align/symbol.rs new file mode 100644 index 000000000..df805b1e8 --- /dev/null +++ b/src/rust/sequence/align/symbol.rs @@ -0,0 +1,26 @@ +//! The [`Symbol`] trait abstracts over the unsigned integer types used to +//! represent sequence codes, so the alignment algorithms can support different +//! alphabet sizes (`uint8`/`uint16`/`uint32`/`uint64` on the Python side). + +/// A symbol code. +/// +/// Implemented for the unsigned integer types. +pub trait Symbol: Copy + Eq { + /// Convert the symbol code into an index usable for matrix lookup. + fn index(self) -> usize; +} + +macro_rules! impl_symbol { + ($($t:ty),+ $(,)?) => { + $( + impl Symbol for $t { + #[inline(always)] + fn index(self) -> usize { + self as usize + } + } + )+ + }; +} + +impl_symbol!(u8, u16, u32, u64); diff --git a/src/rust/sequence/align/table.rs b/src/rust/sequence/align/table.rs new file mode 100644 index 000000000..84f6b13a1 --- /dev/null +++ b/src/rust/sequence/align/table.rs @@ -0,0 +1,150 @@ +//! The [`StridedTable`] dynamic programming table. + +use std::ops::{Add, AddAssign, Index, IndexMut, Sub}; + +/// An index into a [`StridedTable`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct TableIndex(usize); + +/// An offset between two [`TableIndex`] values. +/// +/// Moving to a neighboring cell is a cheap addition of a precomputed offset +/// (e.g. `+ 1` along a row, `+ stride` along a column), without recomputing +/// strides. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TableOffset(isize); + +impl Add for TableIndex { + type Output = TableIndex; + + #[inline(always)] + fn add(self, offset: TableOffset) -> TableIndex { + // `wrapping_add_signed` keeps the arithmetic in the `usize` domain; a + // `usize -> isize -> usize` round-trip would defeat the compiler's + // pointer-induction optimization of the table-fill loop. + TableIndex(self.0.wrapping_add_signed(offset.0)) + } +} + +impl AddAssign for TableIndex { + #[inline(always)] + fn add_assign(&mut self, offset: TableOffset) { + *self = *self + offset; + } +} + +impl Sub for TableIndex { + type Output = TableOffset; + + #[inline(always)] + fn sub(self, other: TableIndex) -> TableOffset { + TableOffset(self.0 as isize - other.0 as isize) + } +} + +/// A dense, row-major dynamic programming table holding one cell per position. +/// +/// It is a container for the [`Cell`](super::cell::Cell) objects that make up +/// both the score and trace tables of an alignment. +pub struct StridedTable { + shape: (usize, usize), + stride: usize, + data: Vec, +} + +impl StridedTable { + /// Create a zero-initialized table of the given `(rows, columns)` shape. + pub fn new(shape: (usize, usize)) -> Self { + let stride = shape.1; + let data = vec![T::default(); shape.0 * shape.1]; + StridedTable { + shape, + stride, + data, + } + } + + /// Create a table of the given `(rows, columns)` shape with every cell set + /// to a copy of `value`. + pub fn filled(shape: (usize, usize), value: T) -> Self { + let stride = shape.1; + let data = vec![value; shape.0 * shape.1]; + StridedTable { + shape, + stride, + data, + } + } + + /// Create a new (zero-initialized) table of the given `shape` and copy the + /// cells of `table` into it, keeping their `(row, column)` positions. + /// + /// Can be used to grow the dynamic programming table. + pub fn from_data(shape: (usize, usize), table: StridedTable) -> Self { + assert!( + shape.0 >= table.shape.0 && shape.1 >= table.shape.1, + "The new shape must not be smaller than the existing table" + ); + let mut new_table = StridedTable::new(shape); + let width = table.shape.1; + for row in 0..table.shape.0 { + let src = row * table.stride; + let dst = row * new_table.stride; + new_table.data[dst..dst + width].clone_from_slice(&table.data[src..src + width]); + } + new_table + } +} + +impl StridedTable { + /// The `(rows, columns)` shape of the table. + #[inline(always)] + pub fn shape(&self) -> (usize, usize) { + self.shape + } + + /// The index of `position = (row, column)`. + #[inline(always)] + pub fn index(&self, position: (usize, usize)) -> TableIndex { + TableIndex(position.0 * self.stride + position.1) + } + + /// The offset corresponding to moving by `offset = (rows, columns)`. + #[inline(always)] + pub fn index_offset(&self, offset: (isize, isize)) -> TableOffset { + TableOffset(offset.0 * self.stride as isize + offset.1) + } + + /// The `(row, column)` position of `index`, i.e. the inverse of + /// [`StridedTable::index`]. + #[inline(always)] + pub fn unindex(&self, index: TableIndex) -> (usize, usize) { + (index.0 / self.stride, index.0 % self.stride) + } + + /// Iterate over all cells in row-major (memory) order, paired with their + /// index. Iterating the backing buffer avoids per-cell bounds checks. + #[inline] + pub fn indexed_iter(&self) -> impl Iterator { + self.data + .iter() + .enumerate() + .map(|(i, cell)| (TableIndex(i), cell)) + } +} + +impl Index for StridedTable { + type Output = T; + + #[inline(always)] + fn index(&self, index: TableIndex) -> &T { + &self.data[index.0] + } +} + +impl IndexMut for StridedTable { + #[inline(always)] + fn index_mut(&mut self, index: TableIndex) -> &mut T { + &mut self.data[index.0] + } +} diff --git a/src/rust/sequence/align/trace.rs b/src/rust/sequence/align/trace.rs new file mode 100644 index 000000000..1fd1e8302 --- /dev/null +++ b/src/rust/sequence/align/trace.rs @@ -0,0 +1,142 @@ +//! Traceback over a filled [`StridedTable`] to reconstruct alignments. + +use super::cell::{Cell, TraceDirection}; +use super::table::{StridedTable, TableIndex, TableOffset}; +use numpy::ndarray::Array2; +use std::collections::HashSet; + +/// Walk the trace table from a start index, collecting one or more optimal +/// traces (each a path of table indices, in end-to-start order). +/// +/// `offset` maps a [`TraceDirection`] to the [`TableOffset`] step to take in the +/// table. For an ordinary rectangular table the diagonal moves by +/// `index_offset((-1, -1))`, etc.; banded alignments supply a different +/// mapping. This keeps the diagonal-to-rectangle index logic out of +/// `follow_trace`. +/// +/// `count` is the shared branch counter and `max_count` the branch limit +/// (`max_number`): once `count` reaches `max_count`, no further branches are +/// spawned. +#[allow(clippy::too_many_arguments)] +pub fn follow_trace( + table: &StridedTable, + start: TableIndex, + start_state: C::StateInfo, + offset: &Off, + count: &mut usize, + max_count: usize, + out: &mut Vec>, +) where + C: Cell, + Off: Fn(TraceDirection) -> TableOffset, +{ + follow_trace_inner( + table, + start, + start_state, + offset, + count, + max_count, + out, + Vec::new(), + ); +} + +#[allow(clippy::too_many_arguments)] +fn follow_trace_inner( + table: &StridedTable, + start: TableIndex, + start_state: C::StateInfo, + offset: &Off, + count: &mut usize, + max_count: usize, + out: &mut Vec>, + mut path: Vec, +) where + C: Cell, + Off: Fn(TraceDirection) -> TableOffset, +{ + let mut index = start; + let mut state = start_state; + loop { + let cell = table[index]; + let steps = cell.traceback(state); + if steps.is_empty() { + // No further direction -> the trace ends here (this cell is not + // part of the alignment). + break; + } + path.push(index); + // Spawn a branch for every additional direction (a tie). + for &(dir, next_state) in steps.iter().skip(1) { + if *count < max_count { + *count += 1; + follow_trace_inner( + table, + index + offset(dir), + next_state, + offset, + count, + max_count, + out, + path.clone(), + ); + } + } + // Continue the main trace with the first (highest-priority) direction. + let (dir, next_state) = steps[0]; + index += offset(dir); + state = next_state; + } + out.push(path); +} + +/// Convert a trace path (table indices, end-to-start order) into an alignment +/// trace array of shape `(length, 2)` with `-1` marking gaps. +/// +/// `to_seq` maps a table index to the `(seq1_index, seq2_index)` it represents +/// (before gap filtering). For non-banded alignment this is `(i - 1, j - 1)`. +/// +/// After reversing the path to start-to-end order, the first occurrence of each +/// sequence index is kept and any repeats become `-1` (a gap): a run of equal +/// indices in one sequence corresponds to a stretch of gaps in that sequence. +pub fn path_to_trace(path: &[TableIndex], to_seq: F) -> Array2 +where + F: Fn(TableIndex) -> (i64, i64), +{ + let len = path.len(); + let mut col0: Vec = Vec::with_capacity(len); + let mut col1: Vec = Vec::with_capacity(len); + // The path is in end-to-start order; reverse it to start-to-end. + for &index in path.iter().rev() { + let (s0, s1) = to_seq(index); + col0.push(s0); + col1.push(s1); + } + gap_filter(&mut col0); + gap_filter(&mut col1); + + // Drop rows that became a gap in both sequences (defensive; should not + // normally happen, as every step advances at least one sequence). + let mut rows: Vec<[i64; 2]> = Vec::with_capacity(len); + for k in 0..len { + if col0[k] == -1 && col1[k] == -1 { + continue; + } + rows.push([col0[k], col1[k]]); + } + + Array2::from_shape_fn((rows.len(), 2), |(r, c)| rows[r][c]) +} + +/// Replace every repeated value in a column with `-1`, keeping only the first +/// occurrence of each value. +fn gap_filter(column: &mut [i64]) { + let mut seen: HashSet = HashSet::new(); + for value in column.iter_mut() { + // `insert` returns false if the value was already present. + if !seen.insert(*value) { + *value = -1; + } + } +} diff --git a/src/rust/sequence/mod.rs b/src/rust/sequence/mod.rs index 66a250b46..99a02d583 100644 --- a/src/rust/sequence/mod.rs +++ b/src/rust/sequence/mod.rs @@ -1,9 +1,16 @@ +use crate::add_subpackage; use pyo3::prelude::*; +pub mod align; pub mod codec; pub fn module<'py>(parent_module: &Bound<'py, PyModule>) -> PyResult> { let module = PyModule::new(parent_module.py(), "sequence")?; module.add_class::()?; + add_subpackage( + &module, + &align::module(&module)?, + "biotite.rust.sequence.align", + )?; Ok(module) } diff --git a/tests/sequence/align/test_localungapped.py b/tests/sequence/align/test_localungapped.py index 11105a11a..066a2b77b 100644 --- a/tests/sequence/align/test_localungapped.py +++ b/tests/sequence/align/test_localungapped.py @@ -85,7 +85,7 @@ def test_simple_alignments( uint8_code, ): """ - Check if `algin_local_ungapped()` produces correct alignments based on + Check if `SeedExtension` produces correct alignments based on simple known examples. """ # Limit start or stop reference alignment range to seed @@ -115,9 +115,8 @@ def test_simple_alignments( ref_score = align.score(ref_alignment, matrix) ref_alignment.score = ref_score - test_result = align.align_local_ungapped( - seq1, seq2, matrix, seed, threshold, direction, score_only - ) + extension = align.SeedExtension(matrix, threshold, direction) + test_result = extension.align(seq1, seq2, seed, score_only=score_only) if score_only: assert test_result == ref_score @@ -134,7 +133,7 @@ def test_random_alignment(seed, uint8_code): each sequence, where both conserved regions are similar to each other. The conserved regions only contain point mutations and no indels. - Expect that the alignment score found by `align_local_ungapped()` is + Expect that the alignment score found by `SeedExtension` is equal to the alignment score found by `align_optimal()`. """ MIN_SIZE = 200 @@ -196,11 +195,11 @@ def test_random_alignment(seed, uint8_code): local=True, max_number=1, # High gap penalty to prevent introduction of gaps, - # since 'align_local_ungapped()' is also no able to place gaps + # since 'SeedExtension' is also not able to place gaps gap_penalty=-1000, )[0].score - test_alignment = align.align_local_ungapped(seq1, seq2, matrix, seed, THRESHOLD) + test_alignment = align.SeedExtension(matrix, THRESHOLD).align(seq1, seq2, seed) assert test_alignment.score == ref_score # Test if the score is also correctly calculated From 5c182abff4a57d76edaf6cc9617e6bd350436a55 Mon Sep 17 00:00:00 2001 From: Patrick Kunzmann Date: Mon, 15 Jun 2026 13:30:15 +0200 Subject: [PATCH 3/6] Require puccinialin only if `cargo` is not installed --- setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 4f0e1eae0..2d56a37bd 100644 --- a/setup.py +++ b/setup.py @@ -2,8 +2,6 @@ import shutil import sys import numpy as np -from Cython.Build import cythonize -from puccinialin import setup_rust from setuptools import setup from setuptools_rust import RustExtension @@ -19,6 +17,9 @@ def _should_build_wheel(): if not os.environ.get("BIOTITE_OMIT_RUST", False): if not shutil.which("cargo"): # Rust compiler is not installed -> Install it temporarily + # `puccinialin` is only required in this fallback case + from puccinialin import setup_rust + extra_env = setup_rust() env = {**os.environ, **extra_env} else: @@ -34,6 +35,8 @@ def _should_build_wheel(): rust_extensions = None if not os.environ.get("BIOTITE_OMIT_CYTHON", False): + from Cython.Build import cythonize + # Only build C files and compile them when building a wheel cython_extensions = cythonize( "src/**/*.pyx", From 7c6d9ea7837d8221beecea64e451bed6ce656b4d Mon Sep 17 00:00:00 2001 From: Patrick Kunzmann Date: Mon, 15 Jun 2026 21:08:58 +0200 Subject: [PATCH 4/6] Raise linter warnings as error --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 26ff34e89..cf8235abb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,6 @@ path = "src/rust/lib.rs" # Enable optimizations in dev mode for better performance during development [profile.dev] opt-level = 3 + +[lints.rust] +warnings = "deny" From 7baa2cd2ffd609c602a0f8236999c1bbed15fcdc Mon Sep 17 00:00:00 2001 From: Patrick Kunzmann Date: Tue, 16 Jun 2026 12:37:14 +0200 Subject: [PATCH 5/6] Update pyo3 version --- Cargo.toml | 4 +-- src/rust/structure/bonds.rs | 62 +++++++++++++++++----------------- src/rust/structure/celllist.rs | 2 +- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cf8235abb..0e8e28b75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.0.0" edition = "2018" [dependencies] -numpy = "0.27" +numpy = "0.29" ndarray = "0.17" num-traits = "0.2" smallvec = "1" @@ -12,7 +12,7 @@ itertools = "0.14" bitflags = "2" [dependencies.pyo3] -version = "0.27" +version = "0.29" features = ["extension-module"] [lib] diff --git a/src/rust/structure/bonds.rs b/src/rust/structure/bonds.rs index f2ece0f47..9bda8a81d 100644 --- a/src/rust/structure/bonds.rs +++ b/src/rust/structure/bonds.rs @@ -304,7 +304,7 @@ impl Bond { /// SINGLE bond between C4 and H4 /// SINGLE bond between C5 and H5 /// SINGLE bond between C6 and H6 -#[pyclass(module = "biotite.structure", subclass)] +#[pyclass(module = "biotite.structure", subclass, skip_from_py_object)] #[derive(Clone)] pub struct BondList { atom_count: usize, @@ -413,35 +413,11 @@ impl BondList { /// [2 3] /// [2 4]] #[staticmethod] - pub fn concatenate(bond_lists: Vec) -> PyResult { - if bond_lists.is_empty() { - return Ok(BondList { - atom_count: 0, - bonds: Vec::new(), - }); - } - - let total_bonds: usize = bond_lists.iter().map(|bl| bl.bonds.len()).sum(); - let mut merged_bonds: Vec = Vec::with_capacity(total_bonds); - let mut cum_atom_count: usize = 0; - - for bond_list in &bond_lists { - for bond in &bond_list.bonds { - merged_bonds.push(Bond { - // As bot atom indices are offset by the same amount, - // the order of the atoms is guaranteed to be consistent - atom1: bond.atom1 + cum_atom_count, - atom2: bond.atom2 + cum_atom_count, - bond_type: bond.bond_type, - }); - } - cum_atom_count += bond_list.atom_count; - } - - Ok(BondList { - atom_count: cum_atom_count, - bonds: merged_bonds, - }) + pub fn concatenate(bond_lists: Vec>) -> BondList { + let guards: Vec> = + bond_lists.iter().map(|bound| bound.borrow()).collect(); + let refs: Vec<&BondList> = guards.iter().map(|guard| &**guard).collect(); + BondList::concatenate_lists(&refs) } /// offset_indices(offset) @@ -1112,7 +1088,7 @@ impl BondList { } pub fn __add__(&self, other: &BondList) -> BondList { - BondList::concatenate(vec![self.clone(), other.clone()]).unwrap() + BondList::concatenate_lists(&[self, other]) } pub fn __getitem__<'py>( @@ -1266,6 +1242,30 @@ impl BondList { &self.bonds } + /// Concatenate multiple bond lists into one, offsetting the atom indices of + /// each list by the cumulative atom count of the preceding lists. + fn concatenate_lists(bond_lists: &[&BondList]) -> BondList { + let total_bonds: usize = bond_lists.iter().map(|bl| bl.bonds.len()).sum(); + let mut merged_bonds: Vec = Vec::with_capacity(total_bonds); + let mut cum_atom_count: usize = 0; + for bond_list in bond_lists { + for bond in &bond_list.bonds { + merged_bonds.push(Bond { + // As both atom indices are offset by the same amount, + // the order of the atoms is guaranteed to be consistent + atom1: bond.atom1 + cum_atom_count, + atom2: bond.atom2 + cum_atom_count, + bond_type: bond.bond_type, + }); + } + cum_atom_count += bond_list.atom_count; + } + BondList { + atom_count: cum_atom_count, + bonds: merged_bonds, + } + } + /// Create an empty bond list for internal use from other Rust modules. pub fn empty(atom_count: usize) -> Self { BondList { diff --git a/src/rust/structure/celllist.rs b/src/rust/structure/celllist.rs index 73291d02c..bfdd855b5 100644 --- a/src/rust/structure/celllist.rs +++ b/src/rust/structure/celllist.rs @@ -76,7 +76,7 @@ mod biotite { /// [1 8]] #[allow(clippy::upper_case_acronyms)] #[derive(Clone, Copy, PartialEq)] -#[pyclass(module = "biotite.structure")] +#[pyclass(module = "biotite.structure", from_py_object)] pub enum CellListResult { MAPPING, MASK, From fe1601a0475d7566bd7e9e64e1ed9be4b55fdc2c Mon Sep 17 00:00:00 2001 From: Patrick Kunzmann Date: Wed, 24 Jun 2026 16:12:52 +0200 Subject: [PATCH 6/6] Benchmarks `score_only` alignemnts --- benchmarks/sequence/align/benchmark_pairwise.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/benchmarks/sequence/align/benchmark_pairwise.py b/benchmarks/sequence/align/benchmark_pairwise.py index 0d3d29ea5..a9b43feac 100644 --- a/benchmarks/sequence/align/benchmark_pairwise.py +++ b/benchmarks/sequence/align/benchmark_pairwise.py @@ -31,6 +31,7 @@ def seed(seq_pair): @pytest.mark.benchmark +@pytest.mark.parametrize("score_only", [False, True], ids=["traceback", "score_only"]) @pytest.mark.parametrize( "method", [ @@ -40,9 +41,9 @@ def seed(seq_pair): ], ids=lambda x: x.func.__name__, ) -def benchmark_align_pairwise(seq_pair, matrix, seed, method): +def benchmark_align_pairwise(seq_pair, matrix, seed, method, score_only): """ Perform pairwise sequence alignment using different algorithms. """ kwargs = {"seed": seed} if method.func is align.align_local_gapped else {} - method(seq_pair[0], seq_pair[1], matrix, **kwargs) + method(seq_pair[0], seq_pair[1], matrix, score_only=score_only, **kwargs)