Assembly

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

BA3B — Reconstruct a String from its Genome Path

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

The path is already ordered, so the string is the first k-mer plus one symbol from each after it. Finding the order is what BA3C and BA3H are for.

# Rosalind: BA3B — Reconstruct a String from its Genome Path
# https://rosalind.info/problems/ba3b/
#
# Given: A sequence of k-mers where each overlaps the next by k-1 symbols.
# Return: The string they spell.

let path = ["ACCGA", "CCGAA", "CGAAG", "GAAGC", "AAGCT"]

# The path is already in order, so the string is the first k-mer plus the last
# symbol of each one after it. No overlap has to be searched for — that is what
# BA3C and BA3H are for.
let text = range(1, len(path))
  |> reduce(|acc, i| acc + substr(path[i], len(path[i]) - 1, 1), path[0])

println("Result:   " + text)
println("Expected: ACCGAAGCT")

fn test_ba3b_genome_path() {
    assert text == "ACCGAAGCT", "BA3B: got " + text
    assert len(text) == len(path[0]) + len(path) - 1, "BA3B: length should be k + n - 1"
}

BA3C — Construct the Overlap Graph of a Collection of k-mers

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

An edge where one k-mer's suffix is another's prefix. Written with `source` and `target` because `from` and `to` are reserved words.

# Rosalind: BA3C — Construct the Overlap Graph of a Collection of k-mers
# https://rosalind.info/problems/ba3c/
#
# Given: A collection of k-mers.
# Return: The overlap graph as an adjacency list.

let patterns = ["ATGCG", "GCATG", "CATGC", "AGGCA", "GGCAT"]

# An edge runs from one k-mer to another when the suffix of the first is the
# prefix of the second — the relation assembly walks. A k-mer is not joined to
# itself unless it genuinely overlaps itself.
fn suffix_of(pattern) { substr(pattern, 1, len(pattern) - 1) }
fn prefix_of(pattern) { substr(pattern, 0, len(pattern) - 1) }

let overlaps = []
for source in patterns {
    for target in patterns {
        if source != target and suffix_of(source) == prefix_of(target) {
            overlaps = push(overlaps, source + " -> " + target)
        }
    }
}

let result = sort(overlaps)

println("Result:")
for edge in result {
    println("  " + edge)
}
println("Expected: AGGCA -> GGCAT / CATGC -> ATGCG / GCATG -> CATGC / GGCAT -> GCATG")

fn test_ba3c_overlap_graph() {
    assert len(result) == 4, "BA3C: expected 4 overlaps, got " + str(len(result))
    for expected in ["AGGCA -> GGCAT", "CATGC -> ATGCG", "GCATG -> CATGC", "GGCAT -> GCATG"] {
        assert contains(result, expected), "BA3C: missing " + expected
    }
}

BA3D — Construct the De Bruijn Graph of a String

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

Nodes are (k-1)-mers and edges are the k-mers, which is the inversion that turns assembly into an Eulerian path problem rather than a Hamiltonian one.

# Rosalind: BA3D — Construct the De Bruijn Graph of a String
# https://rosalind.info/problems/ba3d/
#
# Given: An integer k and a string Text.
# Return: DeBruijn_k(Text) as an adjacency list.

let k = 4
let text = "AAGATTCTCTAC"

# Nodes are (k-1)-mers and edges are the k-mers: each k-mer joins its own prefix
# to its own suffix. That is the inversion that makes assembly an Eulerian path
# problem rather than a Hamiltonian one — edges are what must be used, not nodes.
let adjacency = {}
for i in range(0, len(text) - k + 1) {
    let mer = substr(text, i, k)
    let source = substr(mer, 0, k - 1)
    let target = substr(mer, 1, k - 1)
    if contains(keys(adjacency), source) {
        adjacency[source] = push(adjacency[source], target)
    } else {
        adjacency[source] = [target]
    }
}

# Repeated edges are kept — a k-mer occurring twice is two edges, which is what
# lets the assembly traverse a repeat the right number of times.
let result = sort(keys(adjacency)) |> map(|node| node + " -> " + (sort(adjacency[node]) |> join(",")))

println("Result:")
for line in result {
    println("  " + line)
}
println("Expected: AAG -> AGA / TCT -> CTA,CTC / ... (8 lines)")

fn test_ba3d_de_bruijn_of_a_string() {
    assert len(result) == 8, "BA3D: expected 8 nodes, got " + str(len(result))
    assert contains(result, "TCT -> CTA,CTC"), "BA3D: TCT should branch to both"
    assert contains(result, "AAG -> AGA"), "BA3D: missing AAG -> AGA"
    # Every k-mer of the text is one edge.
    let edge_count = result |> map(|l| len(split(substr(l, index_of(l, "-> ") + 3, len(l)), ","))) |> sum()
    assert edge_count == len(text) - k + 1, "BA3D: edges should equal the number of k-mers"
}

BA3E — Construct the De Bruijn Graph of a Collection of k-mers

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

The same construction from a bag of reads rather than a string. A duplicate k-mer stays duplicated: it is evidence of a repeat, not noise.

# Rosalind: BA3E — Construct the De Bruijn Graph of a Collection of k-mers
# https://rosalind.info/problems/ba3e/
#
# Given: A collection of k-mers.
# Return: The de Bruijn graph as an adjacency list.

let patterns = ["GAGG", "CAGG", "GGGG", "GGGA", "CAGG", "AGGG", "GGAG"]

# Same construction as BA3D, but the k-mers arrive as a bag rather than being
# read out of a string — which is the realistic case, since reads are what a
# sequencer produces. CAGG appears twice in the input and must appear twice in
# the graph: duplicate reads are evidence of a repeat, not noise to discard.
let adjacency = {}
for mer in patterns {
    let source = substr(mer, 0, len(mer) - 1)
    let target = substr(mer, 1, len(mer) - 1)
    if contains(keys(adjacency), source) {
        adjacency[source] = push(adjacency[source], target)
    } else {
        adjacency[source] = [target]
    }
}

let result = sort(keys(adjacency)) |> map(|node| node + " -> " + (sort(adjacency[node]) |> join(",")))

println("Result:")
for line in result {
    println("  " + line)
}
println("Expected: AGG -> GGG / CAG -> AGG,AGG / GAG -> AGG / GGA -> GAG / GGG -> GGA,GGG")

fn test_ba3e_de_bruijn_of_a_collection() {
    assert len(result) == 5, "BA3E: expected 5 nodes, got " + str(len(result))
    assert contains(result, "CAG -> AGG,AGG"), "BA3E: the repeated k-mer must stay repeated"
    assert contains(result, "GGG -> GGA,GGG"), "BA3E: missing GGG's two edges"
}

BA3H — Reconstruct a String from its k-mer Composition

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

Reads become edges, not nodes — which makes assembly an Eulerian path, solvable in linear time. Reads as nodes would give a Hamiltonian path instead: the same data, a different graph, and the difference between tractable and NP-hard.

# Rosalind: BA3H — Reconstruct a String from its k-mer Composition
# https://rosalind.info/problems/ba3h/
#
# Given: An integer k, followed by a list of k-mers Patterns.
# Return: A string Text whose k-mer composition is Patterns.

let k = 4
let patterns = ["CTTA", "ACCA", "TACC", "GGCT", "GCTT", "TTAC"]

# Genome assembly, in miniature. Each k-mer becomes an *edge* from its prefix to
# its suffix — not a node — because then using every read exactly once is
# precisely an Eulerian path, which BA3G already solves in linear time. Making
# reads the nodes instead gives a Hamiltonian path, which is NP-hard: the same
# data, a different graph, and the difference between tractable and not.
let de_bruijn = {}
for pattern in patterns {
    let prefix = substr(pattern, 0, k - 1)
    let suffix = substr(pattern, 1, k - 1)
    if contains(keys(de_bruijn), prefix) {
        de_bruijn[prefix] = push(de_bruijn[prefix], suffix)
    } else {
        de_bruijn[prefix] = [suffix]
    }
}

let path = eulerian_path(de_bruijn)

# Spell the path: the first node in full, then one new character per step.
let text = path[0] + (range(1, len(path))
    |> map(|i| substr(path[i], k - 2, 1))
    |> join(""))

println("Result:   " + text)
println("Expected: GGCTTACCA")

fn test_ba3h_string_reconstruction() {
    assert text == "GGCTTACCA", "BA3H: got " + text
    assert len(text) == len(patterns) + k - 1, "BA3H: n reads of length k spell n + k - 1"
    # The real requirement: the answer's k-mer composition is the input, as a
    # multiset. Sorting both is enough since every read is used once.
    let composition = range(0, len(text) - k + 1) |> map(|i| substr(text, i, k))
    assert sort(composition) == sort(patterns),
        "BA3H: composition " + str(sort(composition)) + " != " + str(sort(patterns))
}

BA3I — Find a k-Universal Circular String

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

Every (k-1)-mer has two edges in and two out, so the graph is balanced and the walk closes. Returns a different valid string from the published one; the assertion reads around the circle and checks all 2^k k-mers appear exactly once.

# Rosalind: BA3I — Find a k-Universal Circular String
# https://rosalind.info/problems/ba3i/
#
# Given: An integer k.
# Return: A k-universal circular binary string — one containing every binary
# k-mer exactly once when read around the circle.

let k = 4

# Every binary k-mer, in order: 0000, 0001, ... 1111.
fn binary_kmers(width) {
    let patterns = [""]
    for _ in range(0, width) {
        patterns = patterns |> flat_map(|prefix| [prefix + "0", prefix + "1"])
    }
    patterns
}

let patterns = binary_kmers(k)

# The same de Bruijn construction as BA3H, but here every (k-1)-mer has exactly
# two edges in and two out, so the graph is balanced and the walk closes into a
# cycle. A circular string of length 2^k containing all 2^k k-mers is only
# possible because each one overlaps the next by k-1 — the cycle is what makes
# that packing exist at all.
let de_bruijn = {}
for pattern in patterns {
    let prefix = substr(pattern, 0, k - 1)
    let suffix = substr(pattern, 1, k - 1)
    if contains(keys(de_bruijn), prefix) {
        de_bruijn[prefix] = push(de_bruijn[prefix], suffix)
    } else {
        de_bruijn[prefix] = [suffix]
    }
}

let cycle = eulerian_cycle(de_bruijn, patterns[0] |> substr(0, k - 1))

# Spelling a *circular* string drops the last k-1 characters, because they are
# the first k-1 come round again.
let spelled = cycle[0] + (range(1, len(cycle))
    |> map(|i| substr(cycle[i], k - 2, 1))
    |> join(""))
let text = substr(spelled, 0, len(spelled) - (k - 1))

println("Result:   " + text)
println("Expected: 0000110010111101  (any k-universal string is accepted)")

fn test_ba3i_k_universal_circular_string() {
    assert len(text) == pow(2, k), "BA3I: a k-universal binary string has length 2^k"
    # Read around the circle: every binary k-mer exactly once.
    let wrapped = text + substr(text, 0, k - 1)
    let found = range(0, len(text)) |> map(|i| substr(wrapped, i, k))
    assert len(unique(found)) == pow(2, k), "BA3I: a k-mer appears twice"
    assert sort(found) == sort(patterns), "BA3I: not every k-mer appears"
}

BA3J — Reconstruct a String from its Paired Composition

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

Paired reads pin down a repeat that either read alone would be ambiguous inside. The two halves must agree wherever they overlap, and checking that agreement is what makes a wrong assembly detectable.

# Rosalind: BA3J — Reconstruct a String from its Paired Composition
# https://rosalind.info/problems/ba3j/
#
# Given: Integers k and d, followed by a collection of (k,d)-mers.
# Return: A string whose (k,d)-mer composition is the given collection.

let k = 4
let d = 2
let pairs = [
    "GAGA|TTGA", "TCGT|GATG", "CGTG|ATGT", "TGGT|TGAG", "GTGA|TGTT",
    "GTGG|GTGA", "TGAG|GTTG", "GGTC|GAGA", "GTCG|AGAT",
]

# Paired reads carry information plain k-mers do not: two short reads a known
# distance apart pin down a repeat that either one alone would be ambiguous
# inside. The graph is built the same way as BA3H, on pairs rather than single
# k-mers — the prefix of a pair is the prefix of both halves.
fn halves(pair) { split(pair, "|") }

let de_bruijn = {}
for pair in pairs {
    let parts = halves(pair)
    let prefix = substr(parts[0], 0, k - 1) + "|" + substr(parts[1], 0, k - 1)
    let suffix = substr(parts[0], 1, k - 1) + "|" + substr(parts[1], 1, k - 1)
    if contains(keys(de_bruijn), prefix) {
        de_bruijn[prefix] = push(de_bruijn[prefix], suffix)
    } else {
        de_bruijn[prefix] = [suffix]
    }
}

let path = eulerian_path(de_bruijn)

# Spell each half separately, then overlap them. The two spellings must agree
# wherever they overlap — that agreement is the extra constraint the pairing
# buys, and checking it is what makes a wrong assembly detectable.
fn spell(nodes, which, width) {
    let first = halves(nodes[0])[which]
    first + (range(1, len(nodes)) |> map(|i| substr(halves(nodes[i])[which], width - 2, 1)) |> join(""))
}

let first_spelled = spell(path, 0, k)
let second_spelled = spell(path, 1, k)
let gap = k + d

let overlap_disagrees = range(gap, len(first_spelled))
    |> filter(|i| substr(first_spelled, i, 1) != substr(second_spelled, i - gap, 1))

let text = first_spelled + substr(second_spelled, len(second_spelled) - gap, gap)

println("Result:   " + text)
println("Expected: GTGGTCGTGAGATGTTGA")

fn test_ba3j_paired_reconstruction() {
    assert text == "GTGGTCGTGAGATGTTGA", "BA3J: got " + text
    assert len(overlap_disagrees) == 0,
        "BA3J: the two spellings disagree at " + str(overlap_disagrees)
    assert len(text) == len(pairs) + 2 * k + d - 1,
        "BA3J: n pairs spell n + 2k + d - 1 characters"
    # And the answer's own paired composition is the input.
    let composition = range(0, len(text) - (2 * k + d) + 1)
        |> map(|i| substr(text, i, k) + "|" + substr(text, i + k + d, k))
    assert sort(composition) == sort(pairs),
        "BA3J: composition does not match the reads"
}

BA3K — Generate Contigs from a Collection of Reads

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

What assembly actually produces. BA3H asked for the genome, which needs an Eulerian path to exist and be unique — real data gives neither. Where the graph branches the reads genuinely do not say which way the genome went, so the honest output is the unambiguous stretches and no more.

# Rosalind: BA3K — Generate Contigs from a Collection of Reads
# https://rosalind.info/problems/ba3k/
#
# Given: A collection of k-mers Patterns.
# Return: The contigs from the de Bruijn graph of Patterns.

let patterns = ["ATG", "ATG", "TGT", "TGG", "CAT", "GGA", "GAT", "AGA"]
let k = 3

# What assembly actually produces. BA3H asked for *the* genome, which needs an
# Eulerian path to exist and be unique — real data gives neither. Wherever the
# graph branches, the reads genuinely do not say which way the genome went, so
# the honest output is the unambiguous stretches and no more. Those are the
# contigs, and their lengths are the standard measure of how good an assembly is.
let de_bruijn = {}
for pattern in patterns {
    let prefix = substr(pattern, 0, k - 1)
    let suffix = substr(pattern, 1, k - 1)
    if contains(keys(de_bruijn), prefix) {
        de_bruijn[prefix] = push(de_bruijn[prefix], suffix)
    } else {
        de_bruijn[prefix] = [suffix]
    }
}

let vertices = sort(unique(keys(de_bruijn) + (keys(de_bruijn) |> flat_map(|n| de_bruijn[n]))))
fn out_edges(node, graph) { if contains(keys(graph), node) then graph[node] else [] }

let in_degree = {}
for node in vertices { in_degree[node] = 0 }
for node in keys(de_bruijn) {
    for target in de_bruijn[node] { in_degree[target] = in_degree[target] + 1 }
}

fn is_one_in_one_out(node, graph, degrees) {
    degrees[node] == 1 and len(out_edges(node, graph)) == 1
}

# Maximal non-branching paths, exactly as in BA3M.
let paths = []
for node in vertices {
    if is_one_in_one_out(node, de_bruijn, in_degree) == false {
        for target in out_edges(node, de_bruijn) {
            let walk = [node, target]
            let at = target
            while is_one_in_one_out(at, de_bruijn, in_degree) {
                let onward = out_edges(at, de_bruijn)[0]
                walk = push(walk, onward)
                at = onward
            }
            paths = push(paths, walk)
        }
    }
}

# Spell each path, as in BA3H.
let contigs = sort(paths |> map(|walk|
    walk[0] + (range(1, len(walk)) |> map(|i| substr(walk[i], k - 2, 1)) |> join(""))))

println("Result:   " + join(contigs, " "))
println("Expected: AGA ATG ATG CAT GAT TGGA TGT")

fn test_ba3k_contig_generation() {
    assert join(contigs, " ") == "AGA ATG ATG CAT GAT TGGA TGT",
        "BA3K: got " + join(contigs, " ")
    # ATG appears twice, because two separate branches spell it — contigs are
    # listed per path, not deduplicated.
    assert (contigs |> count_if(|c| c == "ATG")) == 2, "BA3K: ATG is spelled by two paths"
    # TGGA is the only contig longer than k, which is exactly where the graph did
    # not branch. Everything else stops immediately.
    assert (contigs |> count_if(|c| len(c) > k)) == 1, "BA3K: only one contig extends past k"
    assert contains(contigs, "TGGA"), "BA3K: TGGA is the unambiguous stretch"
    # Every contig is spellable from the reads it came from.
    for contig in contigs {
        let pieces = range(0, len(contig) - k + 1) |> map(|i| substr(contig, i, k))
        for piece in pieces {
            assert contains(patterns, piece), "BA3K: " + contig + " uses a read that is not there"
        }
    }
}

BA3L — Construct a String Spelled by a Gapped Genome Path

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

BA3J had to find the path; here it is given, so what remains is the overlap check — a path whose two halves disagree spells nothing at all, however valid it looked in the graph.

# Rosalind: BA3L — Construct a String Spelled by a Gapped Genome Path
# https://rosalind.info/problems/ba3l/
#
# Given: A sequence of (k,d)-mers already in path order.
# Return: The string they spell, if one exists.

let k = 4
let d = 2
let path = [
    "GACC|GCGC", "ACCG|CGCC", "CCGA|GCCG", "CGAG|CCGG", "GAGC|CGGA",
]

# BA3J had to find the path first; here it is given, so what remains is the
# spelling — and the check that the path is consistent at all. The two halves
# overlap by k+d characters once laid out, and if they disagree anywhere then no
# string has this paired composition, however valid the path looked in the graph.
fn halves(pair) { split(pair, "|") }

fn spell(nodes, which, width) {
    let head = halves(nodes[0])[which]
    head + (range(1, len(nodes)) |> map(|i| substr(halves(nodes[i])[which], width - 1, 1)) |> join(""))
}

let first_spelled = spell(path, 0, k)
let second_spelled = spell(path, 1, k)
let gap = k + d

let disagreements = range(gap, len(first_spelled))
    |> filter(|i| substr(first_spelled, i, 1) != substr(second_spelled, i - gap, 1))

let text = first_spelled + substr(second_spelled, len(second_spelled) - gap, gap)

println("Result:   " + text)
println("Expected: GACCGAGCGCCGGA")

fn test_ba3l_gapped_genome_path() {
    assert text == "GACCGAGCGCCGGA", "BA3L: got " + text
    # The overlap check is the substance of this problem, not a formality: a path
    # whose halves disagree spells nothing at all.
    assert len(disagreements) == 0,
        "BA3L: the two spellings disagree at " + str(disagreements)
    assert len(text) == len(path) + 2 * k + d - 1,
        "BA3L: n pairs spell n + 2k + d - 1 characters"
    # And the answer's own paired composition is the path it came from.
    let composition = range(0, len(text) - (2 * k + d) + 1)
        |> map(|i| substr(text, i, k) + "|" + substr(text, i + k + d, k))
    assert composition == path, "BA3L: the composition does not match the given path"
}