Alignment

3 problems from Rosalind — Bioinformatics Armory. Press Run on any block to execute it in your browser.

NEED — Pairwise Global Alignment

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

Calls NCBI, so it needs a network connection and its answer can change over time.

Reproduces the EMBOSS Needle score of 257 using align() with affine gaps (match 5, mismatch -4, gap_open -9, gap_extend -1). Asserted, but excluded from the hermetic CI gate because it fetches from NCBI.

# Rosalind: NEED — Pairwise Global Alignment
# https://rosalind.info/problems/need/
#
# Given: Two GenBank IDs.
# Return: Maximum global alignment score (DNAfull matrix, gap_open=10, gap_extend=1).

let ids = ["JX205496.1", "JX469991.1"]

# Fetch each record and drop the ">" header line. drop(_, 1) rather than
# tail(): tail() returns the *last* n lines and defaults to 5.
let sequences = ids |> map(|id| drop(split(ncbi_sequence(id), "\n"), 1) |> join(""))

println("Sequences:")
range(0, len(ids)) |> each(|i| println("  " + ids[i] + ": " + str(len(sequences[i])) + " bp"))

# DNAfull scores an ACGT match +5 and a mismatch -4.
#
# EMBOSS charges `gapopen` for the first position of a gap and `gapextend` for
# each further one. This aligner charges `gap_open + gap_extend` to start a gap
# and `gap_extend` to continue it, so gapopen=10 / gapextend=1 becomes
# gap_open = -9 and gap_extend = -1.
let result = align(sequences[0], sequences[1], "global", 5, -4, -1, -9)

println("\nResult:   " + str(result.score))
println("Expected: 257")
println("Match:    " + str(result.score == 257))

println("\nAlignment detail:")
println("  Identity: " + str(result.identity))
println("  Gaps:     " + str(result.gaps))

fn test_need_global_alignment_score() {
    assert result.score == 257, "NEED: expected 257, got " + str(result.score)
}

SUBO — Suboptimal Local Alignment

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

Brute-force O(n^3) window scan; ~25 s. A motif-index approach would be the natural follow-up.

# Rosalind: SUBO — Suboptimal Local Alignment
# https://rosalind.info/problems/subo/
#
# Given: Two DNA strings with a shared inexact repeat (32-40 bp, <=3 changes).
# Return: Total occurrences of the repeat in each string.

let seq1 = "GACTCCTTTGTTTGCCTTAAATAGATACATATTTACTCTTGACTCTTTTGTTGGCCTTAAATAGATACATATTTGTGCGACTCCACGAGTGATTCGTA"
let seq2 = "ATGGACTCCTTTGTTTGCCTTAAATAGATACATATTCAACAAGTGTGCACTTAGCCTTGCCGACTCCTTTGTTTGCCTTAAATAGATACATATTTG"

# Strategy: find all 32-bp substrings of seq1 that appear in seq2 with <=3 mismatches
let motif_len = 33

# Helper: count mismatches between two strings of equal length
# (hamming_distance works on sequences; we use manual counting for strings)
let s1 = seq1
let s2 = seq2

# Find the best shared motif by checking each 33-bp window of s1 against all windows of s2
let best_motif = ""
let best_total = 0

let i = 0
while i <= len(s1) - motif_len {
    let candidate = substr(s1, i, motif_len)

    # Count approximate matches in s2
    let hits_s2 = 0
    let j = 0
    while j <= len(s2) - motif_len {
        let target = substr(s2, j, motif_len)
        let dist = hamming_distance(candidate, target)
        if dist <= 3 then
            hits_s2 = hits_s2 + 1
        j = j + 1
    }

    if hits_s2 > 0 then {
        # Also count in s1
        let hits_s1 = 0
        let k = 0
        while k <= len(s1) - motif_len {
            let target = substr(s1, k, motif_len)
            let dist = hamming_distance(candidate, target)
            if dist <= 3 then
                hits_s1 = hits_s1 + 1
            k = k + 1
        }
        let total = hits_s1 + hits_s2
        if total > best_total then {
            best_total = total
            best_motif = candidate
        }
    }
    i = i + 1
}

# Now count non-overlapping occurrences using the best motif
# Count all approximate matches (overlapping) — Rosalind counts overlapping instances
let count1 = 0
let i = 0
while i <= len(s1) - motif_len {
    let window = substr(s1, i, motif_len)
    if hamming_distance(best_motif, window) <= 3 then
        count1 = count1 + 1
    i = i + 1
}

let count2 = 0
let j = 0
while j <= len(s2) - motif_len {
    let window = substr(s2, j, motif_len)
    if hamming_distance(best_motif, window) <= 3 then
        count2 = count2 + 1
    j = j + 1
}

println("Motif:    " + best_motif)
println("Result:   " + str(count1) + " " + str(count2))
println("Expected: 2 2")
println("Match:    " + str(count1 == 2 && count2 == 2))

fn test_subo_repeat_occurrences() {
    assert count1 == 2 && count2 == 2, "SUBO: expected 2 2, got " + str(count1) + " " + str(count2)
}

CLUS — Global Multiple Alignment

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

# Rosalind: CLUS — Global Multiple Alignment
# https://rosalind.info/problems/clus/
#
# Given: Set of DNA strings in FASTA format.
# Return: ID of the string most different from the others.
#
# We compute pairwise edit distances and find the sequence with
# the highest average distance to all others.

let sequences = [
    {id: "Rosalind_18", seq: dna"GACATGTTTGTTTGCCTTAAACTCGTGGCGGCCTAGCCGTAAGTTAAG"},
    {id: "Rosalind_23", seq: dna"ACTCATGTTTGTTTGCCTTAAACTCTTGGCGGCTTAGCCGTAACTTAAG"},
    {id: "Rosalind_51", seq: dna"TCCTATGTTTGTTTGCCTCAAACTCTTGGCGGCCTAGCCGTAAGGTAAG"},
    {id: "Rosalind_7",  seq: dna"CACGTCTGTTCGCCTAAAACTTTGATTGCCGGCCTACGCTAGTTAGTTA"},
    {id: "Rosalind_28", seq: dna"GGGGTCATGGCTGTTTGCCTTAAACCCTTGGCGGCCTAGCCGTAATGTTT"}
]

# Compute pairwise distances and find the most distant sequence
let most_different_id = ""
let max_avg_dist = 0

for i_seq in sequences {
    let total_dist = 0
    let pair_count = 0
    for j_seq in sequences {
        if i_seq.id != j_seq.id then {
            let dist = edit_distance(str(i_seq.seq), str(j_seq.seq))
            total_dist = total_dist + dist
            pair_count = pair_count + 1
        }
    }
    let avg_dist = total_dist / pair_count
    if avg_dist > max_avg_dist then {
        max_avg_dist = avg_dist
        most_different_id = i_seq.id
    }
}

println("Result:   " + most_different_id)
println("Expected: Rosalind_7")
println("Match:    " + str(most_different_id == "Rosalind_7"))

fn test_clus_most_different_sequence() {
    assert most_different_id == "Rosalind_7", "CLUS: expected Rosalind_7, got " + most_different_id
}