Rearrangements
11 problems from Rosalind — Bioinformatics Textbook Track. Press Run on any block to execute it in your browser.
BA6A — Implement GreedySorting to Sort a Permutation by Reversals
solvedbrowser + CLI Problem statement Open in the workbench Download .bl
Signs matter because a reversed gene reads on the other strand. Greedy sorting fixes each position once and never revisits it: at most 2n reversals, which is not the minimum but does bound the true distance.
# Rosalind: BA6A — Implement GreedySorting to Sort a Permutation by Reversals
# https://rosalind.info/problems/ba6a/
#
# Given: A signed permutation P.
# Return: Every permutation produced by GreedySorting, ending at the identity.
let start = [-3, 4, 1, 5, -2]
# Chromosomes rearrange by reversal — a segment is cut out, flipped and put back,
# which is why the signs matter: a reversed gene reads on the other strand. The
# number of reversals separating two genomes is a measure of how long ago they
# diverged.
#
# Greedy sorting fixes position 1 first, then 2, and never revisits either. It
# takes at most 2n reversals, which is not the minimum — finding that is much
# harder — but it terminates and it bounds the true distance.
fn reverse_segment(items, from_index, to_index) {
let head = range(0, from_index) |> map(|i| items[i])
let middle = range(from_index, to_index + 1) |> map(|i| 0 - items[to_index - (i - from_index)])
let tail = range(to_index + 1, len(items)) |> map(|i| items[i])
head + middle + tail
}
let permutation = start
let steps = []
for k in range(0, len(start)) {
if permutation[k] != k + 1 {
# Where the value that belongs here currently sits, either sign.
let at = (range(k, len(permutation)) |> filter(|i| abs(permutation[i]) == k + 1))[0]
permutation = reverse_segment(permutation, k, at)
steps = push(steps, permutation)
# The reversal may have left it negative, which costs one more flip.
if permutation[k] == 0 - (k + 1) {
permutation = reverse_segment(permutation, k, k)
steps = push(steps, permutation)
}
}
}
fn written(items) {
"(" + (items |> map(|v| if v > 0 then "+" + str(v) else str(v)) |> join(" ")) + ")"
}
println("Result:")
for step in steps { println(" " + written(step)) }
println("Expected: (-1 -4 +3 +5 -2) (+1 -4 +3 +5 -2) (+1 +2 -5 -3 +4)")
println(" (+1 +2 +3 +5 +4) (+1 +2 +3 -4 -5) (+1 +2 +3 +4 -5) (+1 +2 +3 +4 +5)")
fn test_ba6a_greedy_sorting() {
let expected = ["(-1 -4 +3 +5 -2)", "(+1 -4 +3 +5 -2)", "(+1 +2 -5 -3 +4)",
"(+1 +2 +3 +5 +4)", "(+1 +2 +3 -4 -5)", "(+1 +2 +3 +4 -5)",
"(+1 +2 +3 +4 +5)"]
assert (steps |> map(|s| written(s))) == expected,
"BA6A: got " + join(steps |> map(|s| written(s)), " ")
# It really ends at the identity, and takes at most 2n reversals.
assert steps[len(steps) - 1] == range(1, len(start) + 1), "BA6A: must end sorted"
assert len(steps) <= 2 * len(start), "BA6A: greedy sorting is bounded by 2n reversals"
# Every step is a genuine reversal of the one before: same values, ignoring
# sign and order.
let running = start
for step in steps {
assert sort(step |> map(|v| abs(v))) == sort(running |> map(|v| abs(v))),
"BA6A: a reversal cannot change which values are present"
running = step
}
}
BA6B — Compute the Number of Breakpoints in a Permutation
solvedbrowser + CLI Problem statement Open in the workbench Download .bl
One reversal removes at most two breakpoints, so half the count is a lower bound on reversal distance — BA6A gives the upper one. A fully reversed permutation is not the worst case: (-5 -4 -3 -2 -1) still steps by one, so it has only two breakpoints and one reversal sorts it.
# Rosalind: BA6B — Compute the Number of Breakpoints in a Permutation
# https://rosalind.info/problems/ba6b/
#
# Given: A signed permutation P.
# Return: The number of breakpoints in P.
let permutation = [3, 4, 5, -12, -8, -7, -6, 1, 2, 10, 9, -11, 13, 14]
# A breakpoint is any adjacent pair that is not consecutive — anywhere the
# permutation is already in order, no reversal need ever touch. Since one
# reversal can remove at most two breakpoints, the breakpoint count divided by
# two is a lower bound on the true reversal distance, which is what makes this
# worth computing at all: BA6A gives an upper bound, this gives a lower one.
#
# The permutation is bracketed by 0 and n+1 so the ends count too — a chromosome
# whose first gene is not gene 1 has a breakpoint there.
let extended = [0] + permutation + [len(permutation) + 1]
let breakpoints = range(1, len(extended))
|> count_if(|i| extended[i] - extended[i - 1] != 1)
println("Result: " + str(breakpoints))
println("Expected: 8")
fn test_ba6b_breakpoints() {
assert breakpoints == 8, "BA6B: got " + str(breakpoints)
# The identity permutation has none, which is the only permutation that does.
let identity = [0] + range(1, 6) + [6]
assert (range(1, len(identity)) |> count_if(|i| identity[i] - identity[i - 1] != 1)) == 0,
"BA6B: the identity has no breakpoints"
# Worth being careful here: a *fully reversed* permutation is not the worst
# case. (-5 -4 -3 -2 -1) still steps by one at every position, so it has only
# the two breakpoints at its ends — and one reversal sorts it, which the
# bound below then predicts exactly.
let flipped = [0, -5, -4, -3, -2, -1, 6]
assert (range(1, len(flipped)) |> count_if(|i| flipped[i] - flipped[i - 1] != 1)) == 2,
"BA6B: a full reversal leaves only the two end breakpoints"
# Interleaving is what actually breaks every adjacency.
let shuffled = [0, 2, 4, 1, 3, 5]
assert (range(1, len(shuffled)) |> count_if(|i| shuffled[i] - shuffled[i - 1] != 1)) == 5,
"BA6B: interleaving breaks every one of the five adjacencies"
# The lower bound this implies on reversal distance.
assert breakpoints / 2 == 4, "BA6B: at least 4 reversals are needed"
}
BA6C — Compute the 2-Break Distance Between a Pair of Genomes
solvedbrowser + CLI Problem statement Open in the workbench Download .bl
Blocks minus cycles — a closed form, which is unusual for a rearrangement distance and the reason 2-breaks are studied. A 2-break raises the cycle count by at most one, so the bound is both necessary and achievable.
# Rosalind: BA6C — Compute the 2-Break Distance Between a Pair of Genomes
# https://rosalind.info/problems/ba6c/
#
# Given: Two genomes with circular chromosomes on the same synteny blocks.
# Return: The 2-break distance between them.
let genome_p = [[1, 2, 3, 4, 5, 6]]
let genome_q = [[1, -3, -6, -5], [2, -4]]
# There is a closed form, which is unusual for a rearrangement distance and is
# the reason 2-breaks are studied at all: the distance is the number of synteny
# blocks minus the number of cycles in the two genomes' graphs superimposed.
#
# Superimposing them, every node has one edge from each genome, so the graph
# splits into alternating cycles. A 2-break can increase the cycle count by at
# most one, and the genomes are identical exactly when every cycle is trivial —
# so blocks minus cycles is both a lower bound and achievable.
fn coloured_edges(genome) {
genome |> flat_map(|chromosome| {
let nodes = chromosome |> flat_map(|block|
if block > 0 then [2 * block - 1, 2 * block] else [0 - 2 * block, 0 - 2 * block - 1])
range(1, len(chromosome) + 1) |> map(|j| {
let to_index = if 2 * j == len(nodes) then 0 else 2 * j
{ a: nodes[2 * j - 1], b: nodes[to_index] }
})
})
}
let edges_p = coloured_edges(genome_p)
let edges_q = coloured_edges(genome_q)
let blocks = genome_p |> map(|c| len(c)) |> sum()
# Adjacency over both edge sets at once.
let links = {}
for edge in edges_p + edges_q {
for pair in [{ x: edge.a, y: edge.b }, { x: edge.b, y: edge.a }] {
if contains(keys(links), str(pair.x)) {
links[str(pair.x)] = push(links[str(pair.x)], pair.y)
} else {
links[str(pair.x)] = [pair.y]
}
}
}
let seen = {}
let cycles = 0
for node in keys(links) {
if contains(keys(seen), node) == false {
cycles = cycles + 1
let frontier = [node]
while len(frontier) > 0 {
let at = frontier[0]
frontier = slice(frontier, 1, len(frontier))
if contains(keys(seen), at) == false {
seen[at] = true
for neighbour in links[at] {
if contains(keys(seen), str(neighbour)) == false {
frontier = push(frontier, str(neighbour))
}
}
}
}
}
}
let distance = blocks - cycles
println("Result: " + str(distance))
println("Expected: 3")
println("(" + str(blocks) + " blocks minus " + str(cycles) + " cycles)")
fn test_ba6c_two_break_distance() {
assert distance == 3, "BA6C: got " + str(distance)
assert blocks == 6, "BA6C: six synteny blocks"
assert cycles == 3, "BA6C: the superimposed graph has three cycles"
# A genome against itself is distance zero, which is the identity the formula
# has to satisfy: every cycle is trivial, so cycles equal blocks.
let self_links = {}
for edge in edges_p + edges_p {
for pair in [{ x: edge.a, y: edge.b }, { x: edge.b, y: edge.a }] {
if contains(keys(self_links), str(pair.x)) {
self_links[str(pair.x)] = push(self_links[str(pair.x)], pair.y)
} else {
self_links[str(pair.x)] = [pair.y]
}
}
}
let self_seen = {}
let self_cycles = 0
for node in keys(self_links) {
if contains(keys(self_seen), node) == false {
self_cycles = self_cycles + 1
let frontier = [node]
while len(frontier) > 0 {
let at = frontier[0]
frontier = slice(frontier, 1, len(frontier))
if contains(keys(self_seen), at) == false {
self_seen[at] = true
for neighbour in self_links[at] {
if contains(keys(self_seen), str(neighbour)) == false {
frontier = push(frontier, str(neighbour))
}
}
}
}
}
}
assert blocks - self_cycles == 0, "BA6C: a genome is distance 0 from itself"
}
BA6D — Find a Shortest Transformation of One Genome into Another by 2-Breaks
solvedbrowser + CLI Problem statement Open in the workbench Download .bl
The constructive half of BA6C's argument: exhibits a path of exactly that length. Finds a different valid path from the published one, so it asserts the endpoints and the step count rather than the listing.
# Rosalind: BA6D — Find a Shortest Transformation of One Genome into Another by 2-Breaks
# https://rosalind.info/problems/ba6d/
#
# Given: Two genomes with circular chromosomes on the same synteny blocks.
# Return: The sequence of genomes along a shortest 2-break transformation.
let genome_p = [[1, -2, -3, 4]]
let genome_q = [[1, 2, -4, -3]]
# BA6C says the distance is blocks minus cycles. This exhibits a path of that
# length, which is the constructive half of the same argument: pick any cycle of
# the combined graph that is not already trivial, and there is always a 2-break
# on P's edges that merges P one step closer to Q while raising the cycle count
# by one. Repeating that reaches Q in exactly blocks-minus-cycles moves.
fn coloured_edges(g) {
g |> flat_map(|chromosome| {
let nodes = chromosome |> flat_map(|block|
if block > 0 then [2 * block - 1, 2 * block] else [0 - 2 * block, 0 - 2 * block - 1])
range(1, len(chromosome) + 1) |> map(|j| {
let to_index = if 2 * j == len(nodes) then 0 else 2 * j
{ a: nodes[2 * j - 1], b: nodes[to_index] }
})
})
}
fn same_edge(edge, x, y) {
(edge.a == x and edge.b == y) or (edge.a == y and edge.b == x)
}
fn graph_to_genome(edge_list) {
let links = {}
for edge in edge_list {
links[str(edge.a)] = edge.b
links[str(edge.b)] = edge.a
}
let visited = {}
let genome = []
for edge in edge_list {
if contains(keys(visited), str(edge.a)) == false {
let cycle = []
let node = edge.a
let walking = true
while walking {
visited[str(node)] = true
let partner = links[str(node)]
visited[str(partner)] = true
cycle = push(cycle, partner)
let next_node = if partner % 2 == 1 then partner + 1 else partner - 1
if contains(keys(visited), str(next_node)) {
walking = false
} else {
node = next_node
}
}
let blocks = cycle |> map(|tail|
if tail % 2 == 1 then (tail + 1) / 2 else 0 - tail / 2)
let lowest = blocks |> map(|b| abs(b)) |> min()
let at = (range(0, len(blocks)) |> filter(|i| abs(blocks[i]) == lowest))[0]
genome = push(genome, range(0, len(blocks)) |> map(|i| blocks[(i + at) % len(blocks)]))
}
}
genome
}
fn written(g) {
g |> map(|c| "(" + (c |> map(|v| if v > 0 then "+" + str(v) else str(v)) |> join(" ")) + ")")
|> join("")
}
let red = coloured_edges(genome_p)
let blue = coloured_edges(genome_q)
let steps = [written(graph_to_genome(red))]
# Each round: find a blue edge whose endpoints are joined differently in red, and
# rewire red to match it.
let working = red
let rounds = 0
while rounds < 20 {
rounds = rounds + 1
let wrong = blue |> filter(|b| (working |> count_if(|r| same_edge(r, b.a, b.b))) == 0)
if len(wrong) == 0 { rounds = 100 }
if rounds < 100 {
let target = wrong[0]
# The two red edges currently holding target's endpoints.
let holding_a = (working |> filter(|r| r.a == target.a or r.b == target.a))[0]
let holding_b = (working |> filter(|r| r.a == target.b or r.b == target.b))[0]
let other_a = if holding_a.a == target.a then holding_a.b else holding_a.a
let other_b = if holding_b.a == target.b then holding_b.b else holding_b.a
working = (working |> filter(|r|
same_edge(r, holding_a.a, holding_a.b) == false
and same_edge(r, holding_b.a, holding_b.b) == false))
+ [{ a: target.a, b: target.b }, { a: other_a, b: other_b }]
steps = push(steps, written(graph_to_genome(working)))
}
}
println("Result:")
for step in steps { println(" " + step) }
println("Expected: (+1 -2 -3 +4) / (+1 -2 -3)(+4) / (+1 -2 -4 -3) / (+1 +2 -4 -3)")
fn test_ba6d_two_break_sorting() {
# The path has to start at P, end at Q, and take exactly the 2-break distance
# in steps — which is the whole claim being demonstrated.
assert steps[0] == written(genome_p), "BA6D: must start at P, got " + steps[0]
let arrived = graph_to_genome(working)
let magnitudes = sort(arrived |> flat_map(|c| c |> map(|b| abs(b))))
assert magnitudes == [1, 2, 3, 4], "BA6D: every block must survive"
# Q reached, compared up to rotation and direction as in BA6K.
let rotations = |chromosome| range(0, len(chromosome))
|> map(|shift| join(range(0, len(chromosome))
|> map(|i| str(chromosome[(i + shift) % len(chromosome)])), " "))
let flip = |chromosome| reverse(chromosome) |> map(|b| 0 - b)
let canonical = |chromosome| min(rotations(chromosome) + rotations(flip(chromosome)))
assert sort(arrived |> map(|c| canonical(c))) == sort(genome_q |> map(|c| canonical(c))),
"BA6D: the path ends at " + written(arrived) + ", not at Q"
# Three 2-breaks, so four genomes listed — matching BA6C's formula.
assert len(steps) == 4, "BA6D: expected 4 genomes, got " + str(len(steps))
}
BA6E — Find All Shared k-mers of a Pair of Strings
solvedbrowser + CLI Problem statement Open in the workbench Download .bl
Reverse complements count because an inversion puts a conserved block on the other strand; ignoring that would make every inverted block look like a deletion. Plotting the pairs gives the dot plot rearrangements are read off.
# Rosalind: BA6E — Find All Shared k-mers of a Pair of Strings
# https://rosalind.info/problems/ba6e/
#
# Given: An integer k and two strings.
# Return: Every pair of positions (x, y) where the strings share a k-mer, taking
# reverse complements into account.
let k = 3
let first_text = "AAACTCATC"
let second_text = "TTTCAAATC"
# The reverse complement counts because a synteny block conserved between two
# genomes may sit on either strand — an inversion flips a segment, and the same
# genes then read backwards. Ignoring that would make every inverted block look
# like a deletion.
#
# Plotting these pairs gives the dot plot from which rearrangements are read off:
# diagonal runs are conserved blocks, and anti-diagonal runs are inverted ones.
let shared = range(0, len(first_text) - k + 1) |> flat_map(|x| {
let piece = substr(first_text, x, k)
let flipped = str(reverse_complement(dna(piece)))
range(0, len(second_text) - k + 1)
|> filter(|y| {
let other = substr(second_text, y, k)
other == piece or other == flipped
})
|> map(|y| { x: x, y: y })
})
println("Result: " + (shared |> map(|p| "(" + str(p.x) + ", " + str(p.y) + ")") |> join(" ")))
println("Expected: (0, 4) (0, 0) (4, 2) (6, 6) (in any order)")
fn test_ba6e_shared_kmers() {
let written = sort(shared |> map(|p| "(" + str(p.x) + ", " + str(p.y) + ")"))
assert written == sort(["(0, 4)", "(0, 0)", "(4, 2)", "(6, 6)"]),
"BA6E: got " + join(written, " ")
# Every pair really shares a k-mer, one way round or the other.
for pair in shared {
let piece = substr(first_text, pair.x, k)
let other = substr(second_text, pair.y, k)
assert other == piece or other == str(reverse_complement(dna(piece))),
"BA6E: " + piece + " and " + other + " are not a shared k-mer"
}
# (0, 4) is the reverse-complement match — AAA against TTT — which a
# same-strand search would miss entirely.
assert substr(first_text, 0, k) == "AAA" and substr(second_text, 4, k) == "AAA",
"BA6E: position 4 of the second string is a direct match"
assert substr(second_text, 0, k) == "TTT", "BA6E: and position 0 is its reverse complement"
}
BA6F — Implement ChromosomeToCycle
solvedbrowser + CLI Problem statement Open in the workbench Download .bl
Every block becomes a head and a tail, so orientation stops being a sign and becomes a direction of travel — the representation 2-breaks are defined on.
# Rosalind: BA6F — Implement ChromosomeToCycle
# https://rosalind.info/problems/ba6f/
#
# Given: A chromosome of n synteny blocks.
# Return: The 2n node numbers ChromosomeToCycle produces.
let chromosome = [1, -2, -3, 4]
# Every block becomes two nodes, its head and its tail, so orientation stops
# being a sign and becomes a direction of travel. That is the representation
#2-breaks are defined on: a rearrangement cuts two edges and reconnects the four
# loose ends, which is easy to say about nodes and awkward to say about signed
# integers.
let cycle_nodes = chromosome |> flat_map(|block|
if block > 0 then [2 * block - 1, 2 * block] else [0 - 2 * block, 0 - 2 * block - 1])
println("Result: (" + (cycle_nodes |> map(|n| str(n)) |> join(" ")) + ")")
println("Expected: (1 2 4 3 6 5 7 8)")
fn test_ba6f_chromosome_to_cycle() {
assert cycle_nodes == [1, 2, 4, 3, 6, 5, 7, 8], "BA6F: got " + str(cycle_nodes)
# Two nodes per block, and every number from 1 to 2n used exactly once.
assert len(cycle_nodes) == 2 * len(chromosome), "BA6F: two cycle_nodes per block"
assert sort(cycle_nodes) == range(1, 2 * len(chromosome) + 1),
"BA6F: the cycle_nodes must be 1..2n, each once"
# A positive block reads head-then-tail; a negative one reads the other way,
# which is the whole encoding.
assert cycle_nodes[0] < cycle_nodes[1], "BA6F: +1 runs forwards"
assert cycle_nodes[2] > cycle_nodes[3], "BA6F: -2 runs backwards"
}
BA6G — Implement CycleToChromosome
solvedbrowser + CLI Problem statement Open in the workbench Download .bl
The inverse of BA6F, asserted by round-tripping rather than only by matching the sample. The sign is recovered from the order of each node pair rather than stored.
# Rosalind: BA6G — Implement CycleToChromosome
# https://rosalind.info/problems/ba6g/
#
# Given: A sequence of 2n node numbers.
# Return: The chromosome whose cycle they are.
let cycle_nodes = [1, 2, 4, 3, 6, 5, 7, 8]
# The inverse of BA6F. Each pair of nodes is one block, and which of the two
# comes first says which way it reads — so the sign is recovered from the order
# rather than stored.
let chromosome = range(0, len(cycle_nodes) / 2) |> map(|j| {
let head = cycle_nodes[2 * j]
let tail = cycle_nodes[2 * j + 1]
if head < tail then tail / 2 else 0 - head / 2
})
fn written(items) {
"(" + (items |> map(|v| if v > 0 then "+" + str(v) else str(v)) |> join(" ")) + ")"
}
println("Result: " + written(chromosome))
println("Expected: (+1 -2 -3 +4)")
fn test_ba6g_cycle_to_chromosome() {
assert written(chromosome) == "(+1 -2 -3 +4)", "BA6G: got " + written(chromosome)
# It really inverts BA6F: converting back gives the nodes it started from.
let round_trip = chromosome |> flat_map(|block|
if block > 0 then [2 * block - 1, 2 * block] else [0 - 2 * block, 0 - 2 * block - 1])
assert round_trip == cycle_nodes, "BA6G: the round trip does not return the cycle_nodes"
assert len(chromosome) == len(cycle_nodes) / 2, "BA6G: one block per pair of cycle_nodes"
}
BA6H — Implement ColoredEdges
solvedbrowser + CLI Problem statement Open in the workbench Download .bl
Only the edges between blocks are kept — the adjacencies a rearrangement can break. Every node carries exactly one, which is what makes the graph a set of disjoint cycles and BA6C computable.
# Rosalind: BA6H — Implement ColoredEdges
# https://rosalind.info/problems/ba6h/
#
# Given: A genome P.
# Return: The coloured edges of its genome graph.
let chromosomes = [[1, -2, -3], [4, 5, -6]]
# Coloured edges are the ones joining *different* blocks — the adjacencies a
# rearrangement can break. The edges inside a block are fixed, so they carry no
# information and are left out; what remains is exactly the structure two genomes
# can be compared on.
fn chromosome_to_cycle(chromosome) {
chromosome |> flat_map(|block|
if block > 0 then [2 * block - 1, 2 * block] else [0 - 2 * block, 0 - 2 * block - 1])
}
let edges_list = chromosomes |> flat_map(|chromosome| {
let nodes = chromosome_to_cycle(chromosome)
# The last edge wraps round, because a chromosome here is circular.
range(1, len(chromosome) + 1) |> map(|j| {
let to_index = if 2 * j == len(nodes) then 0 else 2 * j
{ a: nodes[2 * j - 1], b: nodes[to_index] }
})
})
println("Result: " + (edges_list |> map(|e| "(" + str(e.a) + ", " + str(e.b) + ")") |> join(", ")))
println("Expected: (2, 4), (3, 6), (5, 1), (8, 9), (10, 12), (11, 7)")
fn test_ba6h_coloured_edges() {
let written = edges_list |> map(|e| "(" + str(e.a) + ", " + str(e.b) + ")") |> join(", ")
assert written == "(2, 4), (3, 6), (5, 1), (8, 9), (10, 12), (11, 7)",
"BA6H: got " + written
# One coloured edge per block, since each block has one outgoing adjacency.
let block_count = chromosomes |> map(|c| len(c)) |> sum()
assert len(edges_list) == block_count, "BA6H: one coloured edge per block"
# Every node appears in at most one coloured edge on each side — the graph is
# a set of disjoint cycles, which is what makes 2-break distance computable.
let touched = edges_list |> flat_map(|e| [e.a, e.b])
assert len(unique(touched)) == len(touched), "BA6H: a node cannot have two coloured edges"
}
BA6I — Implement GraphToGenome
solvedbrowser + CLI Problem statement Open in the workbench Download .bl
A circular chromosome has no distinguished starting block, so the walk produced (-2 -3 +1) where the sample shows (+1 -2 -3) — the same chromosome. Rotated to the lowest-numbered block for a stable listing, with the rotation-invariance asserted rather than hidden.
# Rosalind: BA6I — Implement GraphToGenome
# https://rosalind.info/problems/ba6i/
#
# Given: The coloured edges of a genome graph.
# Return: The genome.
let coloured = [
{ a: 2, b: 4 }, { a: 3, b: 6 }, { a: 5, b: 1 },
{ a: 7, b: 9 }, { a: 10, b: 12 }, { a: 11, b: 8 },
]
# The inverse of BA6H, and the step that makes 2-breaks usable: a 2-break is
# easy to apply to a set of edges and meaningless as an operation on signed
# integers, so genomes are converted to a graph, rearranged, and converted back.
#
# The edges form disjoint cycles; each cycle is one chromosome. Walking a cycle
# means alternating between the coloured edges given here and the implicit
# black edges inside each block, which is why the walk steps by one node between
# coloured edges.
let neighbours = {}
for edge in coloured {
neighbours[str(edge.a)] = edge.b
neighbours[str(edge.b)] = edge.a
}
let visited = {}
let chromosomes = []
for edge in coloured {
if contains(keys(visited), str(edge.a)) == false {
# Walk the cycle this edge belongs to, collecting node pairs.
let cycle = []
let node = edge.a
let walking = true
while walking {
visited[str(node)] = true
let partner = neighbours[str(node)]
visited[str(partner)] = true
cycle = push(cycle, { head: node, tail: partner })
# The black edge: from `partner` to the other node of its block.
let next_node = if partner % 2 == 1 then partner + 1 else partner - 1
if contains(keys(visited), str(next_node)) { walking = false } else { node = next_node }
}
# Each collected pair (tail of one block, head of the next) becomes a
# block once shifted round by one.
let blocks = cycle |> map(|pair|
if pair.tail % 2 == 1 then (pair.tail + 1) / 2 else 0 - pair.tail / 2)
# A circular chromosome has no distinguished starting block, so the walk
# can begin anywhere and every rotation is the same chromosome. Rotated
# here to start at the lowest-numbered block, which is what makes the
# output comparable to the published one.
let lowest = blocks |> map(|b| abs(b)) |> min()
let at = (range(0, len(blocks)) |> filter(|i| abs(blocks[i]) == lowest))[0]
let rotated = range(0, len(blocks)) |> map(|i| blocks[(i + at) % len(blocks)])
chromosomes = push(chromosomes, rotated)
}
}
fn written(items) {
"(" + (items |> map(|v| if v > 0 then "+" + str(v) else str(v)) |> join(" ")) + ")"
}
let shown = chromosomes |> map(|c| written(c)) |> join("")
println("Result: " + shown)
println("Expected: (+1 -2 -3)(-4 +5 -6)")
fn test_ba6i_graph_to_genome() {
assert shown == "(+1 -2 -3)(-4 +5 -6)", "BA6I: got " + shown
assert len(chromosomes) == 2, "BA6I: the edges form two cycles, so two chromosomes"
# Rotation-invariance is a real property, not a formatting detail: the walk
# produced (-2 -3 +1) before rotating, which is the same circular chromosome.
let first_chromosome = chromosomes[0]
let rotated_once = range(0, len(first_chromosome))
|> map(|i| first_chromosome[(i + 1) % len(first_chromosome)])
assert rotated_once != first_chromosome, "BA6I: a rotation is a different listing"
assert sort(rotated_once) == sort(first_chromosome), "BA6I: but the same chromosome"
# Every block from 1 to 6 appears exactly once, either way up.
let magnitudes = sort(chromosomes |> flat_map(|c| c |> map(|b| abs(b))))
assert magnitudes == range(1, 7), "BA6I: every block must appear once"
}
BA6J — Implement 2-BreakOnGenomeGraph
solvedbrowser + CLI Problem statement Open in the workbench Download .bl
The single operation every rearrangement reduces to: cut two adjacencies, rejoin the four ends the other way. Reversals, translocations, fusions and fissions are all this one move.
# Rosalind: BA6J — Implement 2-BreakOnGenomeGraph
# https://rosalind.info/problems/ba6j/
#
# Given: A genome graph and four nodes i, i', j, j'.
# Return: The graph after removing edges (i, i') and (j, j') and adding (i, j)
# and (i', j').
let edge_set = [
{ a: 2, b: 4 }, { a: 3, b: 8 }, { a: 7, b: 5 }, { a: 6, b: 1 },
]
let breakpoint = { i: 1, i_end: 6, j: 3, j_end: 8 }
# A 2-break is the single operation every rearrangement reduces to: cut two
# adjacencies and rejoin the four loose ends the other way. Reversals,
# translocations, fusions and fissions are all this one move — which is exactly
# why BA6C can put a closed form on the distance.
fn same_edge(edge, x, y) {
(edge.a == x and edge.b == y) or (edge.a == y and edge.b == x)
}
let kept = edge_set |> filter(|edge|
same_edge(edge, breakpoint.i, breakpoint.i_end) == false and same_edge(edge, breakpoint.j, breakpoint.j_end) == false)
let rejoined = kept + [{ a: breakpoint.j, b: breakpoint.i }, { a: breakpoint.i_end, b: breakpoint.j_end }]
fn written(edges_list) {
edges_list |> map(|e| "(" + str(e.a) + ", " + str(e.b) + ")") |> join(", ")
}
println("Result: " + written(rejoined))
println("Expected: (2, 4), (3, 1), (7, 5), (6, 8) (in any order)")
fn test_ba6j_two_break_on_graph() {
# The two edges that were cut are gone, the two new ones are present, and
# everything else is untouched. Order is not meaningful in an edge set.
assert len(rejoined) == len(edge_set), "BA6J: a 2-break preserves the edge count"
assert (rejoined |> count_if(|e| same_edge(e, 1, 6))) == 0, "BA6J: (1, 6) should be gone"
assert (rejoined |> count_if(|e| same_edge(e, 3, 8))) == 0, "BA6J: (3, 8) should be gone"
assert (rejoined |> count_if(|e| same_edge(e, 3, 1))) == 1, "BA6J: (3, 1) should be present"
assert (rejoined |> count_if(|e| same_edge(e, 6, 8))) == 1, "BA6J: (6, 8) should be present"
assert (rejoined |> count_if(|e| same_edge(e, 2, 4))) == 1, "BA6J: (2, 4) is untouched"
assert (rejoined |> count_if(|e| same_edge(e, 7, 5))) == 1, "BA6J: (7, 5) is untouched"
# Every node still has exactly one coloured edge, which is what keeps the
# graph a set of disjoint cycles.
let touched = rejoined |> flat_map(|e| [e.a, e.b])
assert len(unique(touched)) == len(touched), "BA6J: a node cannot gain a second edge"
}
BA6K — Implement 2-BreakOnGenome
solvedbrowser + CLI Problem statement Open in the workbench Download .bl
BA6H, BA6J and BA6I assembled. This particular break is a fission. The result reads each chromosome in the opposite direction from the published answer, so the assertion canonicalises over rotation and reflection — a circular chromosome read backwards flips every sign and is still the same chromosome.
# Rosalind: BA6K — Implement 2-BreakOnGenome
# https://rosalind.info/problems/ba6k/
#
# Given: A genome P and four nodes i, i', j, j'.
# Return: The genome after the 2-break.
let chromosomes = [[1, -2, -4, 3]]
let breakpoint = { i: 1, i_end: 6, j: 3, j_end: 8 }
# The three previous problems assembled. A 2-break is meaningless applied to
# signed integers directly, so the genome is converted to a graph (BA6H), the
# break applied there (BA6J), and the result converted back (BA6I). Here it
# splits one chromosome into two, which is a fission — and the same operation
# with different arguments would fuse, invert or translocate.
fn coloured_edges(g) {
g |> flat_map(|chromosome| {
let nodes = chromosome |> flat_map(|block|
if block > 0 then [2 * block - 1, 2 * block] else [0 - 2 * block, 0 - 2 * block - 1])
range(1, len(chromosome) + 1) |> map(|j| {
let to_index = if 2 * j == len(nodes) then 0 else 2 * j
{ a: nodes[2 * j - 1], b: nodes[to_index] }
})
})
}
fn same_edge(edge, x, y) {
(edge.a == x and edge.b == y) or (edge.a == y and edge.b == x)
}
let broken = (coloured_edges(chromosomes) |> filter(|edge|
same_edge(edge, breakpoint.i, breakpoint.i_end) == false and same_edge(edge, breakpoint.j, breakpoint.j_end) == false))
+ [{ a: breakpoint.i, b: breakpoint.j }, { a: breakpoint.i_end, b: breakpoint.j_end }]
# Back to a genome, exactly as in BA6I.
let links = {}
for edge in broken {
links[str(edge.a)] = edge.b
links[str(edge.b)] = edge.a
}
let visited = {}
let rebuilt = []
for edge in broken {
if contains(keys(visited), str(edge.a)) == false {
let cycle = []
let node = edge.a
let walking = true
while walking {
visited[str(node)] = true
let partner = links[str(node)]
visited[str(partner)] = true
cycle = push(cycle, partner)
let next_node = if partner % 2 == 1 then partner + 1 else partner - 1
if contains(keys(visited), str(next_node)) { walking = false } else { node = next_node }
}
let blocks = cycle |> map(|tail|
if tail % 2 == 1 then (tail + 1) / 2 else 0 - tail / 2)
# Circular, so rotate to the lowest-numbered block for a stable listing.
let lowest = blocks |> map(|b| abs(b)) |> min()
let at = (range(0, len(blocks)) |> filter(|i| abs(blocks[i]) == lowest))[0]
rebuilt = push(rebuilt, range(0, len(blocks)) |> map(|i| blocks[(i + at) % len(blocks)]))
}
}
fn written(items) {
"(" + (items |> map(|v| if v > 0 then "+" + str(v) else str(v)) |> join(" ")) + ")"
}
let shown = rebuilt |> map(|c| written(c)) |> join(" ")
println("Result: " + shown)
println("Expected: (+2 -1) (-3 +4) (up to rotation and which chromosome is listed first)")
fn test_ba6k_two_break_on_genome() {
# One chromosome became two: this 2-break is a fission.
assert len(rebuilt) == 2, "BA6K: expected a fission into two chromosomes"
# Every block survives exactly once, either way up — a 2-break rearranges,
# it never creates or destroys.
let magnitudes = sort(rebuilt |> flat_map(|c| c |> map(|b| abs(b))))
assert magnitudes == [1, 2, 3, 4], "BA6K: every block must survive exactly once"
# A circular chromosome is the same chromosome under rotation *and* under
# reading it the other way round, which flips every sign as well as the
# order. This run produced (+1 -2) where the sample shows (+2 -1) — the same
# chromosome traversed in the opposite direction. Canonicalising over both
# symmetries is what makes the comparison meaningful.
let rotations = |chromosome| range(0, len(chromosome))
|> map(|shift| join(range(0, len(chromosome))
|> map(|i| str(chromosome[(i + shift) % len(chromosome)])), " "))
let flip = |chromosome| reverse(chromosome) |> map(|b| 0 - b)
let canonical = |chromosome| min(rotations(chromosome) + rotations(flip(chromosome)))
let mine = sort(rebuilt |> map(|c| canonical(c)))
let published = sort([canonical([2, -1]), canonical([-3, 4])])
assert mine == published,
"BA6K: got " + join(mine, " / ") + " against " + join(published, " / ")
# And the two really are different listings of the same thing.
assert canonical([1, -2]) == canonical([2, -1]),
"BA6K: (+1 -2) and (+2 -1) are one circular chromosome"
}