Alignment

9 problems from Rosalind — Bioinformatics Textbook Track. Press Run on any block to execute it in your browser.

BA5G — Compute the Edit Distance Between Two Strings

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: BA5G — Compute the Edit Distance Between Two Strings
# https://rosalind.info/problems/ba5g/
#
# Given: Two amino acid strings.
# Return: The edit distance between them.

let s = "PLEASANTLY"
let t = "MEANLY"

let result = edit_distance(s, t)

println("Result:   " + str(result))
println("Expected: 5")

fn test_ba5g_edit_distance() {
    assert result == 5, "BA5G: got " + str(result)
}

BA5E — Find a Highest-Scoring Alignment of Two Strings

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

One call: global, BLOSUM62, linear gap of 5.

# Rosalind: BA5E — Find a Highest-Scoring Alignment of Two Strings
# https://rosalind.info/problems/ba5e/
#
# Given: Two amino acid strings.
# Return: The maximum global alignment score, and an alignment achieving it.
# BLOSUM62 with indel penalty 5.

let s = "PLEASANTLY"
let t = "MEANLY"

# One call: global mode, a linear gap of 5, scored from BLOSUM62. The match and
# mismatch arguments are ignored for residues the matrix carries, which is all
# of them here.
let result = align(s, t, "global", 0, 0, -5, 0, "blosum62")

println("Result:   " + str(result.score))
println("Expected: 8")
println("Alignment:")
println("  " + result.aligned_a)
println("  " + result.aligned_b)

fn test_ba5e_global_alignment() {
    assert result.score == 8, "BA5E: got " + str(result.score)
    # Both rows of a global alignment cover their whole string.
    assert len(result.aligned_a) == len(result.aligned_b), "BA5E: rows differ in length"
}

BA5F — Find a Highest-Scoring Local Alignment of Two Strings

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

PAM250, not BLOSUM62 — the two disagree enough to change the answer, and the problem says which it wants.

# Rosalind: BA5F — Find a Highest-Scoring Local Alignment of Two Strings
# https://rosalind.info/problems/ba5f/
#
# Given: Two amino acid strings.
# Return: The maximum local alignment score, and an alignment achieving it.
# PAM250 with indel penalty 5.

let s = "MEANLY"
let t = "PENALTY"

# PAM250, not BLOSUM62 — the two matrices disagree enough to change the answer,
# and the problem says which one it wants.
let result = align(s, t, "local", 0, 0, -5, 0, "pam250")

println("Result:   " + str(result.score))
println("Expected: 15")
println("Alignment:")
println("  " + result.aligned_a)
println("  " + result.aligned_b)

fn test_ba5f_local_alignment() {
    assert result.score == 15, "BA5F: got " + str(result.score)
    # A local alignment can only do at least as well as the same pair scored
    # globally, since it may discard the ends.
    assert result.score >= align(s, t, "global", 0, 0, -5, 0, "pam250").score,
        "BA5F: local scored below global"
}

BA5H — Find a Highest-Scoring Fitting Alignment of Two Strings

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

All of w against any window of v. Semiglobal returns the same number on this input for a different reason — it would clip w's ends too — so a fitting mode was added rather than asserting the coincidence.

# Rosalind: BA5H — Find a Highest-Scoring Fitting Alignment of Two Strings
# https://rosalind.info/problems/ba5h/
#
# Given: Two DNA strings v and w, v much the longer.
# Return: The maximum fitting alignment score, and an alignment achieving it.
# Match +1, mismatch and indel both -1.

let v = dna"GTAGGCTTAAGGTTA"
let w = dna"TAGATA"

# Fitting: all of w, any window of v. That is not the same as semiglobal, which
# forgives the end gaps of both sequences and so would let w be clipped too. On
# this input the two happen to agree, which is exactly why the distinction is
# worth making rather than reaching for whichever mode returns the right number.
let result = align(v, w, "fitting", 1, -1, -1, 0)

# w must appear in full: its row of the alignment, gaps removed, is all of w.
fn without_gaps(row) {
    range(0, len(row)) |> map(|i| substr(row, i, 1)) |> filter(|c| c != "-") |> join("")
}

println("Result:   " + str(result.score))
println("Expected: 2")
println("Alignment:")
println("  " + result.aligned_a)
println("  " + result.aligned_b)

fn test_ba5h_fitting_alignment() {
    assert result.score == 2, "BA5H: got " + str(result.score)
    assert without_gaps(result.aligned_b) == str(w),
        "BA5H: w is not fully aligned — that is what makes this fitting rather than local"
    assert contains(str(v), without_gaps(result.aligned_a)),
        "BA5H: the aligned part of v is not a window of v"
}

BA5I — Find a Highest-Scoring Overlap Alignment of Two Strings

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

The shape read assembly asks for: where the end of one read agrees with the start of the next.

# Rosalind: BA5I — Find a Highest-Scoring Overlap Alignment of Two Strings
# https://rosalind.info/problems/ba5i/
#
# Given: Two protein strings v and w.
# Return: The maximum overlap alignment score, and an alignment of a suffix of v
# with a prefix of w. Match +1, mismatch and indel both -2.

let v = "PAWHEAE"
let w = "HEAGAWGHEE"

# Overlap alignment is the shape read assembly asks for: where does the end of
# one read agree with the start of the next. A prefix of v and a suffix of w are
# both free.
let result = align(v, w, "overlap", 1, -2, -2, 0)

fn without_gaps(row) {
    range(0, len(row)) |> map(|i| substr(row, i, 1)) |> filter(|c| c != "-") |> join("")
}

println("Result:   " + str(result.score))
println("Expected: 1")
println("Alignment:")
println("  " + result.aligned_a)
println("  " + result.aligned_b)

fn test_ba5i_overlap_alignment() {
    assert result.score == 1, "BA5I: got " + str(result.score)
    # The aligned pieces are a suffix of v and a prefix of w.
    let piece_v = without_gaps(result.aligned_a)
    let piece_w = without_gaps(result.aligned_b)
    assert substr(v, len(v) - len(piece_v), len(piece_v)) == piece_v,
        "BA5I: " + piece_v + " is not a suffix of v"
    assert substr(w, 0, len(piece_w)) == piece_w, "BA5I: " + piece_w + " is not a prefix of w"
}

BA5J — Align Two Strings Using Affine Gap Penalties

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

Charging for opening a gap and less per symbol after is what stops one long insertion being priced as a run of unrelated ones.

# Rosalind: BA5J — Align Two Strings Using Affine Gap Penalties
# https://rosalind.info/problems/ba5j/
#
# Given: Two amino acid strings v and w.
# Return: The maximum alignment score, and an alignment achieving it.
# BLOSUM62, gap opening 11, gap extension 1.

let v = "PRTEINS"
let w = "PRTWPSEIN"

# Affine gaps charge for opening a gap and then less per symbol after, which is
# what stops a long insertion being priced as a run of unrelated ones. A gap of
# length L costs 11 + (L-1), so opening is -10 on top of the -1 each symbol pays.
let result = align(v, w, "global", 0, 0, -1, -10, "blosum62")

# The same pair under a linear gap of 11 per symbol, for contrast.
let linear = align(v, w, "global", 0, 0, -11, 0, "blosum62")

println("Result:   " + str(result.score))
println("Expected: 8")
println("Alignment:")
println("  " + result.aligned_a)
println("  " + result.aligned_b)
println("(the same pair with a flat gap of 11 scores " + str(linear.score) + ")")

fn test_ba5j_affine_gap_alignment() {
    assert result.score == 8, "BA5J: got " + str(result.score)
    # Charging less for continuing a gap can only help.
    assert result.score >= linear.score, "BA5J: affine scored below a flat gap"
}

BA5K — Find a Middle Edge in an Alignment Graph in Linear Space

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

A full alignment table costs O(nm) memory; one column costs O(n). Two linear-space sweeps meeting in the middle locate where the best alignment crosses it. Asserted against the full aligner: the middle node's total equals the alignment's own score.

# Rosalind: BA5K — Find a Middle Edge in an Alignment Graph in Linear Space
# https://rosalind.info/problems/ba5k/
#
# Given: Two amino acid strings.
# Return: A middle edge of their alignment graph, scored with BLOSUM62 and a
# linear indel penalty of 5.

let first_string = "PLEASANTLY"
let second_string = "MEASNLY"
let blosum = score_matrix("BLOSUM62")
let gap = 5

# The idea behind Hirschberg's algorithm. A full alignment table costs O(nm)
# memory, which for two chromosomes is not available. But one *column* of it
# costs O(n), and that is enough to find where the best alignment crosses the
# middle column — from which the problem splits in two and recurses.
#
# The score of the best path through a node is the best path into it plus the
# best path out of it, so two linear-space sweeps meeting in the middle locate
# the crossing without ever holding the table.
let middle = floor(len(second_string) / 2)

# One forward column: best score from (0,0) to each row of column `upto`.
fn forward_column(rows, columns, upto, matrix, indel) {
    let column = range(0, len(rows) + 1) |> map(|i| 0 - i * indel)
    for j in range(1, upto + 1) {
        let next = [0 - j * indel]
        for i in range(1, len(rows) + 1) {
            let diagonal = column[i - 1]
                + substitution_score(matrix, substr(rows, i - 1, 1), substr(columns, j - 1, 1))
            let down = next[i - 1] - indel
            let across = column[i] - indel
            next = push(next, max([diagonal, down, across]))
        }
        column = next
    }
    column
}

# The backward sweep is the forward one on both strings reversed, which is why
# only one direction has to be written.
let from_source = forward_column(first_string, second_string, middle, blosum, gap)
let to_sink_reversed = forward_column(reverse(first_string), reverse(second_string),
                                      len(second_string) - middle, blosum, gap)
let to_sink = reverse(to_sink_reversed)

let totals = range(0, len(first_string) + 1) |> map(|i| from_source[i] + to_sink[i])
let middle_row = argmax(totals)

# Which way the best path leaves the middle node. Three continuations are
# possible; the middle edge is whichever is best, and for this pair it is the
# diagonal one.
let beyond = forward_column(reverse(first_string), reverse(second_string),
                            len(second_string) - middle - 1, blosum, gap) |> reverse()

let at_bottom = middle_row == len(first_string)

let across = { row: middle_row, column: middle + 1, score: beyond[middle_row] - gap }
let down = {
    row: middle_row + 1,
    column: middle,
    score: if at_bottom then 0 - 1000000 else to_sink[middle_row + 1] - gap,
}
let diagonal = {
    row: middle_row + 1,
    column: middle + 1,
    score: if at_bottom then 0 - 1000000 else beyond[middle_row + 1]
        + substitution_score(blosum, substr(first_string, middle_row, 1), substr(second_string, middle, 1)),
}

let ranked = [diagonal, across, down] |> sort_by(|option| 0 - option.score)
let best_edge = ranked[0]

println("Result:   (" + str(middle_row) + ", " + str(middle) + ") ("
        + str(best_edge.row) + ", " + str(best_edge.column) + ")")
println("Expected: (4, 3) (5, 4)")

fn test_ba5k_middle_edge() {
    assert middle == 3, "BA5K: the middle column of a 7-long string is 3"
    assert middle_row == 4, "BA5K: got middle row " + str(middle_row)
    assert best_edge.row == 5 and best_edge.column == 4,
        "BA5K: got end (" + str(best_edge.row) + ", " + str(best_edge.column) + ")"
    # The middle node must lie on a best alignment, so the best path through it
    # scores exactly what the full alignment scores. That is the property the
    # whole linear-space method depends on.
    let full = align(protein(first_string), protein(second_string), "global", 0, 0, 0 - gap, 0, "blosum62")
    assert totals[middle_row] == full.score,
        "BA5K: the middle node scores " + str(totals[middle_row])
            + " but the alignment scores " + str(full.score)
    # And no row beats it.
    for i in range(0, len(totals)) {
        assert totals[i] <= totals[middle_row], "BA5K: row " + str(i) + " scores higher"
    }
}

BA5L — Align Two Strings Using Linear Space

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

Hirschberg's algorithm on BA5K's middle edge. Each half is re-swept, but the halves shrink geometrically so the total stays O(nm) while memory drops from a table to a column — the trade that makes whole-genome alignment possible. Reproduces the published alignment exactly.

# Rosalind: BA5L — Align Two Strings Using Linear Space
# https://rosalind.info/problems/ba5l/
#
# Given: Two amino acid strings.
# Return: Their maximum global alignment score, and an alignment achieving it,
# using BLOSUM62 and a linear indel penalty of 5.

let first_string = "PLEASANTLY"
let second_string = "MEANLY"
let blosum = score_matrix("BLOSUM62")
let gap = 5

# Hirschberg's algorithm, built on BA5K's middle edge. The full alignment table
# needs O(nm) memory — for two chromosomes that is terabytes — but a single
# column needs only O(n). Finding where the best alignment crosses the middle
# column costs two linear-space sweeps, and then the problem splits into two
# halves that are solved the same way.
#
# The work roughly doubles (each half is re-swept) but the total stays O(nm),
# because the halves shrink geometrically: nm/2 + nm/4 + ... < nm. Memory drops
# from a table to a column, which is the trade that makes whole-genome alignment
# possible at all.
fn column_scores(rows, columns, matrix, indel) {
    let column = range(0, len(rows) + 1) |> map(|i| 0 - i * indel)
    for j in range(1, len(columns) + 1) {
        let next = [0 - j * indel]
        for i in range(1, len(rows) + 1) {
            let diagonal = column[i - 1]
                + substitution_score(matrix, substr(rows, i - 1, 1), substr(columns, j - 1, 1))
            let down = next[i - 1] - indel
            let across = column[i] - indel
            next = push(next, max([diagonal, down, across]))
        }
        column = next
    }
    column
}

# Returns the two aligned strings for the given pair.
fn hirschberg(rows, columns, matrix, indel) {
    if len(columns) == 0 {
        return [rows, range(0, len(rows)) |> map(|_| "-") |> join("")]
    }
    if len(rows) == 0 {
        return [range(0, len(columns)) |> map(|_| "-") |> join(""), columns]
    }
    if len(rows) == 1 or len(columns) == 1 {
        # Small enough that a full table is a column; fall back to the ordinary
        # alignment rather than recursing further.
        let small = align(protein(rows), protein(columns), "global", 0, 0, 0 - indel, 0, "blosum62")
        return [str(small.aligned_a), str(small.aligned_b)]
    }

    let middle = floor(len(columns) / 2)
    let left = column_scores(rows, substr(columns, 0, middle), matrix, indel)
    let right = column_scores(reverse(rows), reverse(substr(columns, middle, len(columns) - middle)),
                              matrix, indel) |> reverse()
    let split_at = argmax(range(0, len(rows) + 1) |> map(|i| left[i] + right[i]))

    let top = hirschberg(substr(rows, 0, split_at), substr(columns, 0, middle), matrix, indel)
    let bottom = hirschberg(substr(rows, split_at, len(rows) - split_at),
                            substr(columns, middle, len(columns) - middle), matrix, indel)
    [top[0] + bottom[0], top[1] + bottom[1]]
}

let aligned = hirschberg(first_string, second_string, blosum, gap)

fn alignment_score(a, b, matrix, indel) {
    range(0, len(a)) |> map(|i| {
        let x = substr(a, i, 1)
        let y = substr(b, i, 1)
        if x == "-" or y == "-" then 0 - indel else substitution_score(matrix, x, y)
    }) |> sum()
}

let score = alignment_score(aligned[0], aligned[1], blosum, gap)

println("Result:   " + str(int(score)))
println("          " + aligned[0])
println("          " + aligned[1])
println("Expected: 8")
println("          PLEASANTLY")
println("          -MEA--N-LY")

fn test_ba5l_linear_space_alignment() {
    assert score == 8, "BA5L: scored " + str(score)
    # Any alignment achieving the optimum is accepted, so the checks are
    # structural: both rows the same length, gaps never opposite gaps, and each
    # row reduces to its original string.
    assert len(aligned[0]) == len(aligned[1]), "BA5L: the rows must line up"
    let both_gaps = range(0, len(aligned[0]))
        |> count_if(|i| substr(aligned[0], i, 1) == "-" and substr(aligned[1], i, 1) == "-")
    assert both_gaps == 0, "BA5L: a column of two gaps is not an alignment"
    assert replace(aligned[0], "-", "") == first_string, "BA5L: row one is not the first string"
    assert replace(aligned[1], "-", "") == second_string, "BA5L: row two is not the second string"
    # And it matches what the quadratic-space aligner computes.
    let reference = align(protein(first_string), protein(second_string),
                          "global", 0, 0, 0 - gap, 0, "blosum62")
    assert score == reference.score,
        "BA5L: linear space scored " + str(score) + " but the table scored " + str(reference.score)
}

BA5M — Find a Highest-Scoring Multiple Sequence Alignment

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

Three sequences need a cube, and each cell has seven predecessors rather than three. The cost is O(n^k), which is why exact multiple alignment stops being possible at a handful of sequences and real tools use heuristics. Returns a different optimal alignment from the published one; the assertion recounts the agreeing columns.

# Rosalind: BA5M — Find a Highest-Scoring Multiple Sequence Alignment
# https://rosalind.info/problems/ba5m/
#
# Given: Three DNA strings.
# Return: The maximum score of a three-way alignment — one point per column where
# all three symbols agree — and an alignment achieving it.

let one = "ATATCCG"
let two = "TCCGA"
let three = "ATGTACTG"

# Two strings need a table; three need a cube, and each cell has seven
# predecessors rather than three — every non-empty choice of which sequences
# advance. That is the point this problem makes: the cost is O(n^k) in the number
# of sequences, so aligning ten sequences exactly is out of reach and real
# multiple alignment is done by heuristics instead.
let moves = [
    [1, 1, 1], [1, 1, 0], [1, 0, 1], [0, 1, 1], [1, 0, 0], [0, 1, 0], [0, 0, 1],
]

let best = {}
let came_from = {}
fn key(i, j, k) { str(i) + "," + str(j) + "," + str(k) }

best[key(0, 0, 0)] = 0
for i in range(0, len(one) + 1) {
    for j in range(0, len(two) + 1) {
        for k in range(0, len(three) + 1) {
            if i + j + k > 0 {
                let here = key(i, j, k)
                best[here] = 0 - 1000000
                for move in moves {
                    let pi = i - move[0]
                    let pj = j - move[1]
                    let pk = k - move[2]
                    if pi >= 0 and pj >= 0 and pk >= 0 {
                        # A column scores only when all three advance onto the
                        # same symbol; every other move scores nothing.
                        let matched = move[0] == 1 and move[1] == 1 and move[2] == 1
                            and substr(one, pi, 1) == substr(two, pj, 1)
                            and substr(two, pj, 1) == substr(three, pk, 1)
                        let candidate = best[key(pi, pj, pk)] + (if matched then 1 else 0)
                        if candidate > best[here] {
                            best[here] = candidate
                            came_from[here] = move
                        }
                    }
                }
            }
        }
    }
}

let score = best[key(len(one), len(two), len(three))]

let rows = ["", "", ""]
let at = [len(one), len(two), len(three)]
while at[0] + at[1] + at[2] > 0 {
    let move = came_from[key(at[0], at[1], at[2])]
    let sources = [one, two, three]
    for s in range(0, 3) {
        if move[s] == 1 {
            rows[s] = substr(sources[s], at[s] - 1, 1) + rows[s]
            at[s] = at[s] - 1
        } else {
            rows[s] = "-" + rows[s]
        }
    }
}

println("Result:   " + str(score))
for line in rows { println("          " + line) }
println("Expected: 3")
println("          ATATCC-G-")
println("          ---TCC-GA")
println("          ATGTACTG-")

fn test_ba5m_multiple_alignment() {
    assert score == 3, "BA5M: scored " + str(score)
    # Any alignment reaching 3 is accepted, so the checks are structural.
    assert len(rows[0]) == len(rows[1]) and len(rows[1]) == len(rows[2]),
        "BA5M: all three rows must be the same length"
    assert replace(rows[0], "-", "") == one, "BA5M: line 1 is not the first string"
    assert replace(rows[1], "-", "") == two, "BA5M: line 2 is not the second string"
    assert replace(rows[2], "-", "") == three, "BA5M: line 3 is not the third string"
    # And the alignment shown really scores what was claimed.
    let agreeing = range(0, len(rows[0])) |> count_if(|i| {
        let a = substr(rows[0], i, 1)
        a != "-" and a == substr(rows[1], i, 1) and a == substr(rows[2], i, 1)
    })
    assert agreeing == score, "BA5M: " + str(agreeing) + " columns agree, not " + str(score)
}