14/03/25
07:05:19

Лаборатория математических методов и моделей в биоинформатике
Института проблем передачи информации им. А.А. Харкевича
Российской академии наук

Выравнивание скрытого палиндрома

Разработаны алгоритмы оптимизации редакционного расстояния между последовательностями, первая из которых известна, а вторая вычисляется как скрытый палиндром произвольной длины. Существенно, что длина искомого палиндрома определяется в результате оптимизации. В другом варианте задачи такой палиндром может быть частичным, когда лишь префикс комплементарен суффиксу. Такой частичный палиндром образует шпильку. Новые алгоритмы работают за квадратичное время, что быстрее, чем полный перебор допустимых палиндромов. Ниже приведён пример их программной реализации на языке Python.

Для двух последовательностей \(x\) и \(y\) программа находит оптимальную длину префикса \(w\) последовательности \(y\), при которой минимально редакционное расстояние между \(x\) и палиндромом \(w\mathrm{c}(w)\) в задаче 1 или частичным палиндромом \(y\mathrm{c}(w)\) в задаче 2, где \(\mathrm{c}(w)\) обозначает последовательность, комплементарную (reverse complement) к \(w\).

Функция palindrome_alignment принимает на вход строки x и y, а также логическое значение loop, позволяющее выбрать один из двух вариантов задачи: задача 1 при loop=False (по умолчанию), задача 2 при loop=True. Функция возвращает минимальное редакционное расстояние выравнивания и список длин префиксов \(w\), при которых оно достигается.

В новой версии реализована функция \(\mathrm{imp}(x)\), равная отношению редакционного расстояния из задачи 1 при \(x=y\) к длине последовательности: $$\mathrm{imp}(x)=\frac{\min\{\mathrm{dist}(x,w\mathrm{c}(w))|x=wz\}}{|x|}.$$ Отношение показывает, насколько несовершенен палиндром.

Пример использования
>>> palindrome_alignment("ACGT", "ACC")
(0, [2])
>>> palindrome_alignment("ACTG", "ACC", loop=True)
(1, [1, 2])
>>> imp("ACTG")
0.5
Реализация: palial.py
Показать код
from typing import List, Tuple

import numpy as np


def dist(x: str, y: str) -> int:
    return palindrome_alignment(x, y)[0]


def imp(x: str) -> float:
    return palindrome_self_alignment(x)[0] / len(x)


def palindrome_alignment(x: str, y: str, loop: bool = False) -> Tuple[int, List[int]]:
    """For a given pair of nucleotide sequences `x`, `y`, find the optimal
    alignment between x and ww' (or yw' if `loop` is `True`) where w is
    some prefix of y and w' is the reverse complement of w.
    Return the edit distance and the list of optimal w lengths.
    >>> palindrome_alignment("ACGT", "ACC")
    (0, [2])
    >>> palindrome_alignment("ACTG", "ACC", loop=True)
    (1, [1, 2])
    """
    x = preprocess_nucleotide_sequence(x)
    y = preprocess_nucleotide_sequence(y)
    forward = prefix_edit_distances(x, y)
    reverse = prefix_edit_distances(reverse_complement(x), y)
    if loop:
        forward = forward[:, -1:]
    total = forward + reverse[::-1]
    minima = total.min(axis=0)
    edit_distance = minima.min()
    prefix_lengths = np.where(minima == edit_distance)[0].tolist()
    return int(edit_distance), prefix_lengths


def palindrome_self_alignment(x: str) -> Tuple[int, List[int]]:
    """A special case of `palindrome_alignment` for self-alignment.
    For a given nucleotide sequence `x`, find the optimal alignment
    between x and ww' where w is some prefix of x and w' is the
    reverse complement of w.
    Return the edit distance and the list of optimal w lengths.
    >>> palindrome_self_alignment("ACGC")
    (1, [2])
    """
    x = preprocess_nucleotide_sequence(x)
    forward = np.abs(np.arange(len(x) + 1) - np.arange(len(x) + 1)[:, None])
    reverse = prefix_edit_distances(reverse_complement(x), x)
    total = forward + reverse[::-1]
    minima = total.min(axis=0)
    edit_distance = minima.min()
    prefix_lengths = np.where(minima == edit_distance)[0].tolist()
    return int(edit_distance), prefix_lengths


def preprocess_nucleotide_sequence(s: str) -> str:
    """Preprocess a nucleotide sequence for the Needleman-Wunsch algorithm."""
    s = s.upper().replace("U", "T").replace("-", "")
    if set(s) - set("ATCG"):
        raise ValueError(f"invalid nucleotide sequence: {s}")
    return s


def reverse_complement(s: str) -> str:
    """Return the reverse complement of a nucleotide sequence."""
    return s[::-1].translate(str.maketrans("ATCGatcg", "TAGCtagc"))


def prefix_edit_distances(s0: str, s1: str) -> np.ndarray:
    """Return the matrix of edit distances between all prefixes of `s0`
    and all prefixes of `s1` indexed by the lengths of the prefixes."""
    n0, n1 = len(s0), len(s1)
    distance = np.zeros((n0 + 1, n1 + 1), np.int32)
    distance[:, 0] = np.arange(n0 + 1)
    distance[0, :] = np.arange(n1 + 1)
    for i in range(1, n0 + 1):
        for j in range(1, n1 + 1):
            distance[i, j] = min(
                distance[i, j - 1] + 1,
                distance[i - 1, j] + 1,
                distance[i - 1, j - 1] + (s0[i - 1] != s1[j - 1]),
            )
    return distance

Более подробная информация доступна в статье О.А. Зверков, А.В. Селиверстов, Г.А. Шиловский. Выравнивание скрытого палиндрома. Математическая биология и биоинформатика, 2024, том 19, № 2, стр. 427–438. DOI: 10.17537/2024.19.427.

Страница обновлена 16.12.2024 (предыдущая версия).