Skip to content

Effective distance

Loops bring their anchors together, so two sites far apart on the lattice can be close in space. We can approximate this by calculating the effective distance, which is the shortest linear path between between two sites that walks the lattice and may also cross LEFs.

The rules

  • A step moves one site along the lattice and costs step_length (s).
  • A hop crosses a LEF, from one leg to the other, and costs hop_length (h).
  • Both are integers you choose; neither has a default. s must be positive, h can be 0. Distances are therefore exact integers.
  • Only leg positions matter: whether a leg is moving or captured, and where the CTCFs are, doesn't.

Rosettes. Two LEFs are linked when one's right leg sits at site p and the other's left leg at p + 1. A chain of linked LEFs is a rosette. Crossing it costs one hop per LEF crossed, and the junctions between linked LEFs are free. Walking across a junction is still an ordinary step.

For linked LEFs 1 (legs l₁, r₁) and 2 (l₂, r₂), with h ≥ s:

Between Distance Route
r₁ and l₂ s the junction step
r₁ and r₂ s + h step, then hop across LEF 2
l₁ and r₂ 2h across the chain, the junction free
l₁ and l₂ h + s hop across LEF 1, then step

Keep hops at least as long as steps

With hop_length < step_length, a path can double back across a LEF it just crossed and beat a direct step: r₁ to l₂ becomes min(s, 4h). Nothing currently prevents this; a fix is sketched in the design notes.

How it's computed

The lattice is reduced to a graph of leg positions: consecutive legs are joined by the walk between them, and each rosette gets two one-way "express lanes", one per direction, that carry a path across its LEFs at a hop each. A site joins the graph through its nearest leg on each side. Distances come from Dijkstra's algorithm, run once per distinct entry leg when many sites are asked for together. At a million sites with 5,000 LEFs, all pairs of 1,000 sites take about a second.

Using it

let graph = LoopGraph::new(sim.world(), 1, 1)?; // step_length, hop_length
let d = graph.distance(100, 2500)?;
let pairs = graph.pairwise(&sites)?; // condensed upper triangle
w = sim.world()
w.effective_distance(100, 2500, step_length=1, hop_length=1)  # int
d = w.effective_distances(sites, step_length=1, hop_length=1)  # all pairs, as scipy's pdist
times, d = replayer.effective_distances(sites, dt=10.0, step_length=1, hop_length=1)  # over time

Results are array.array("q"), which numpy wraps without copying.

const lengths = { stepLength: 1, hopLength: 1 };
effectiveDistance(sim.world(), 100, 2500, lengths); // number
effectiveDistances(sim.world(), sites, lengths); // all pairs, as scipy's pdist
effectiveDistancesOver(lattice, snapshots, sites, lengths); // one row of pairs per snapshot

Results are Float64Arrays, exact because the lengths are checked so no distance can pass 2^53.