1 Commits

Author SHA1 Message Date
b00dc96f5b Improved layouting (minimize #crossings) 2025-09-09 19:07:31 +02:00
2 changed files with 62 additions and 38 deletions

View File

@ -14,6 +14,15 @@ import kotlin.math.max
* This algorithm arranges the graph in layers, attempting to minimize edge crossings and distribute nodes evenly.
* It treats the graph as a collection of disjoint acyclic graphs, disregarding back-edges during layout.
*
* You can improve the layout by increasing the iteration count via [setIterationCount], at the cost of longer
* computation time (default: 3).
*
* Additionally, you can ignore certain edges during layout by providing a function to [ignoreInLayout]; during each
* of the phases where edges can be ignored ([LayoutPhase.LAYERING] (deciding layers for each vertex),
* [LayoutPhase.DISJOINTS] (deciding which sub-graphs to consider disjoint), and [LayoutPhase.SLOT_ASSIGNMENT] (deciding
* the order of vertices in each layer)), the function will be called for each edge (once), and if it returns true,
* the edge will be ignored for that phase. By default, no edges are ignored.
*
* This layout algorithm does not support freezing vertices; calling [freezeAt] will throw
* [UnsupportedOperationException], and [unfreeze] is a no-op.
* Additionally, [setGraph] and [setVertexSize] do not invalidate the cache.
@ -99,6 +108,7 @@ class PseudoForestLayout<V, E>(
private val _positions = mutableMapOf<V, GPoint>()
private var _vertexSize: (V) -> VertexSize = vertexSize
private var _boundingBox: GSize? = null
private var _repeat: Int = 3
@OptIn(ExperimentalAtomicApi::class)
private var _lock: AtomicBoolean = AtomicBoolean(false)
@ -124,6 +134,20 @@ class PseudoForestLayout<V, E>(
override fun graph(): IGraph<V, E> = _graph
override fun setGraph(graph: IGraph<V, E>) { locked { _graph = graph } }
/**
* Gets the number of iterations the layout algorithm will perform to optimize the layout.
*
* @return the number of iterations
*/
fun getIterationCount(): Int = _repeat
/**
* Sets the number of iterations the layout algorithm will perform to optimize the layout.
* More iterations may yield a better layout, but will take longer to compute.
*
* @param repeat the number of iterations
*/
fun setIterationCount(repeat: Int) { locked { _repeat = repeat } }
/**
* Sets the vertex measuring function.
*
@ -156,7 +180,6 @@ class PseudoForestLayout<V, E>(
fun directParentOf(other: Vert<V>, graph: IGraph<V, *>): Boolean = x.fold({ it1 ->
other.x.fold({ it2 -> graph.xToY(it1, it2) != null }) { it2 ->
println(" - Checking direct parenthood between $it1 and dummy $it2 (${it2.from}): ${it2.from?.x == it1}")
it2.from?.x == it1
}
}) { it1 ->
@ -176,8 +199,6 @@ class PseudoForestLayout<V, E>(
if(i == chain.size - 1) (v.x.asT2).to = last
else (v.x.asT2).to = chain[i + 1]
}
println(" - Breaking edge $from -> $to with chain: $chain")
return chain
}
@ -201,17 +222,10 @@ class PseudoForestLayout<V, E>(
private fun computeCrossings(lTop: List<Vert<V>>, lBot: List<Vert<V>>, edges: List<Pair<Vert<V>, Vert<V>>>): Int {
var count = 0
println(" - Computing crossings between layers:")
println(" - Top: $lTop")
println(" - Bot: $lBot")
println(" - Edges: $edges")
val conns = edges.map { (top, bot) -> lTop.indexOf(top) to lBot.indexOf(bot) }.sortedWith { (t1, b1), (t2, b2) ->
if (t1 != t2) t1 - t2
else b1 - b2
}
println(" - Connections (by index): $conns")
conns.forEachIndexed { i, conn ->
for(j in i + 1 until conns.size) {
@ -222,10 +236,7 @@ class PseudoForestLayout<V, E>(
val topFirst = conn.first < other.first
val botFirst = conn.second < other.second
if(topFirst != botFirst) {
println(" - Crossing between ${lTop[conn.first]}->${lBot[conn.second]} and ${lTop[other.first]}->${lBot[other.second]}")
count++
}
if(topFirst != botFirst) count++
}
}
return count
@ -246,24 +257,20 @@ class PseudoForestLayout<V, E>(
val layers = mutableMapOf<V, Pair<Int, MutableSet<V>>>()
val queue = ArrayDeque<Pair<V, Set<V>>>(roots.size * 2)
queue.addAll(roots.map { it to emptySet() })
println(" - Computing layers from roots: $roots")
while (!queue.isEmpty()) {
val (vertex, onPath) = queue.removeFirst()
val (layer, dep) = layers.getOrPut(vertex) { 0 to mutableSetOf() }
println(" - Visiting $vertex (layer $layer), path=$onPath, deps=$dep")
val succLayer = layer + 1
_graph.successors(vertex).forEach { (succ, edge) ->
if(ignoreInLayout(edge, LayoutPhase.LAYERING)) {
println(" - Ignoring edge $edge for layout")
return@forEach
}
if (succ in onPath) return@forEach
dep += succ
layers[succ]?.let { (l, sDep) ->
println(" - Successor $succ already had layer $l (might be increased, along with its dependents)")
dep += sDep
val delta = succLayer - l
@ -277,15 +284,12 @@ class PseudoForestLayout<V, E>(
} ?: run {
layers[succ] = succLayer to mutableSetOf()
}
println(" - Adding successor to queue: $succ (layer: ${layers[succ]?.first})")
queue.addLast(succ to onPath + vertex)
}
// ensure dependents are always up to date
layers.values.filter { it.second.contains(vertex) }.forEach { (_, d) -> d.addAll(dep) }
}
println(" - Assigned layers:")
layers.forEach { (v, p) -> println(" - Vertex $v: layer ${p.first}, dependents: ${p.second}") }
// Cache node sizes
val vertexSizes = layers.mapValues { (v, _) -> _vertexSize(v).let { it.fullSize() to it.vCenterInBox() } }
@ -293,7 +297,6 @@ class PseudoForestLayout<V, E>(
// Compute layer y positions (and thus the bounding box height).
val layerCount = layers.maxOf { it.value.first } + 1
val layerHeights = MutableList(layerCount) { 0.0f }
println(" - Have $layerCount layers")
var minOffset = Float.POSITIVE_INFINITY
var maxOffset = Float.NEGATIVE_INFINITY
@ -316,13 +319,6 @@ class PseudoForestLayout<V, E>(
y
}
println(" - Layer measurements: (height, y) = ${(layerHeights zip layerY)}")
println(" - Layers with nodes:")
for(i in 0 until layerCount) {
val verts = layers.filter { it.value.first == i }.keys
println(" - Layer $i: $verts")
}
// Compute disjoint graphs
val disjoint = roots.fold(listOf<Set<V>>()) { acc, root ->
val reachable = reachableFrom(root, LayoutPhase.DISJOINTS).toMutableSet()
@ -340,7 +336,6 @@ class PseudoForestLayout<V, E>(
}
var currentXZero = 0.0f
disjoint.forEach { sub ->
println(" - Layouting disjoint subgraph: $sub")
// Put each vertex in a list by layer
val layered = List(layerCount) { layer ->
sub.mapNotNull {
@ -349,9 +344,6 @@ class PseudoForestLayout<V, E>(
}.toMutableList()
}
println(" - Initial layered vertices:")
layered.forEachIndexed { idx, list -> println(" - Layer $idx: $list") }
// Break up multi-layer edges with dummy nodes
layered.forEachIndexed { idx, list ->
list.forEach { v ->
@ -413,22 +405,41 @@ class PseudoForestLayout<V, E>(
}
val edges = mutableListOf<MutableList<Pair<Vert<V>, Vert<V>>>>()
val crossings = mutableListOf<Int>()
for(i in 1 until layered.size) {
edges.add(mutableListOf())
val current = edges[i - 1]
layered[i].forEach { vBot ->
val forVBot = layered[i - 1].filter {
println(" - Checking edge between $it and $vBot: ${it.directParentOf(vBot, _graph)} || ${vBot.directParentOf(it, _graph)}")
it.directParentOf(vBot, _graph) || vBot.directParentOf(it, _graph)
}.map { it to vBot }
current.addAll(forVBot)
}
}
val c = computeCrossings(layered[i - 1], layered[i], current)
crossings.add(c)
println(" - Connections between layer ${i - 1} and $i have $c crossings")
repeat(_repeat) {
val optP0 = layered[0].permutations().map { perm ->
val crossings = computeCrossings(perm, layered[1], edges[0])
println(" - Layer 0 permutation $perm (to ${layered[1]} has $crossings crossings")
perm to crossings
}.fold(Float.POSITIVE_INFINITY to listOf<Vert<V>>()) { (accCost, accSeq), (perm, cost) ->
if (accCost > cost) cost.toFloat() to perm
else accCost to accSeq
}
layered[0].clear()
layered[0].addAll(optP0.second.toMutableList())
for (i in 1 until layered.size) {
val optPi = layered[i].permutations().map { perm ->
val crossings = computeCrossings(layered[i - 1], perm, edges[i - 1])
println(" - Layer $i permutation $perm (from ${layered[i - 1]} has $crossings crossings")
perm to crossings
}.fold(Float.POSITIVE_INFINITY to listOf<Vert<V>>()) { (accCost, accSeq), (perm, cost) ->
if (accCost > cost) cost.toFloat() to perm
else accCost to accSeq
}
layered[i].clear()
layered[i].addAll(optPi.second.toMutableList())
}
}
// Assign x positions

View File

@ -0,0 +1,13 @@
package com.jaytux.altgraph.layout
fun <T> List<T>.permutations(): Sequence<List<T>> = sequence {
if(isEmpty()) yield(emptyList())
else {
for(i in indices) {
val elem = this@permutations[i]
for(perm in (this@permutations - elem).permutations()) {
yield(listOf(elem) + perm)
}
}
}
}