diff --git a/Cargo.toml b/Cargo.toml index 7f93f8550..0e8e28b75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,14 +4,15 @@ version = "0.0.0" edition = "2018" [dependencies] -numpy = "0.27" +numpy = "0.29" ndarray = "0.17" num-traits = "0.2" smallvec = "1" itertools = "0.14" +bitflags = "2" [dependencies.pyo3] -version = "0.27" +version = "0.29" features = ["extension-module"] [lib] @@ -22,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" 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) 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/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", 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/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/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, 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_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 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 000000000..c4f868942 Binary files /dev/null and b/tests/sequence/data/legacy_consistency/legacy_alignments.tar.gz differ 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