Apr. 3rd, 2019

diziet: (Default)
My project to make an alternative board for Pandemic Rising Tide needed a program to lay out a planar graph, choosing exact coordinates for the vertices.

(The vertices in question are the vertices of the graph which is the dual of the adjacency graph of the board "squares" - what Pandemic Rising Tide calls Regions. For gameplay reasons the layout wants to be a straight line drawing - that is, one where every boundary is a straight line.)

Existing software

I found that this problem was not well handled by existing Free Software. The leading contender, graphviz, generally produces non-planar layouts even for planar inputs; and it does not provide a way to specify the planar embedding. There are some implementations of "straight line drawing" algorithms from the literature, but these produce layouts which meet the letter of the requirement for the drawing to consist only of nonintersecting straight lines, but they are very ugly and totally unsuitable for use as a game board layout.

My web searches for solutions to this problem yielded only forum postings etc. where people were asking roughly this question and not getting a satisfactory answer.

I have some experience with computer optimisation algorithms and I thought this should be a tractable problem, so I set out to solve it - well, at least well enough for my purposes.

My approach

My plan was to use one of the algorithms from the literature to generate a straight line drawing, and then use cost-driven nonlinear optimisation to shuffle the vertices about into something pretty and useable.

Helpfully Boost provides an implementation of Chrobak & Payne's straight line drawing algorithm. Unfortunately Boost's other planar graph functions were not suitable because they do not remember which face is the outer face. (In planar graph theory and algorithms the region outside the graph drawing is treated as a face, called the outer face.) So I also had to write my own implementations of various preparatory algorithms - yet more yak shaving before I could get to the really hard part.

Having been on a Rust jag recently, I decided on Rust as my implementation language. I don't regret this choice, although it did add a couple of yaks.

Cost function and constraints

My cost function has a number of components:
  • I wanted to minimise the edge lengths.
  • But there was a minimum edge length (for both gameplay and aesthetic reasons)
  • Also I wanted to avoid the faces having sharp corners (ie, small angles between edges at the same vertex)
  • And of course I needed the edges to still come out of each vertex in the right order.
You will notice that two of these are not costs, but constraints. Different optimisation algorithms handle this differently.

Also "the edges to still come out of each vertex in the right order" is hard to express as a continuous quantity. (Almost all of these algorithms demand that constraints take the form of a function which is to be nonnegative, or some such.) My solution is, at each vertex, to add up the angles between successive edges (in the intended order, and always treating each direction difference as a positive angle). Ie, to add up the face corner angles. They should sum to tau: if so, we have gone round once and the order is right. If the edges are out of order, we'll end up going round more than once. If the sum was only tau too much, I defined the violation quantity to be tau minus the largest corner angle; this is right because probably it's just that two edges next to each other are out of order and the face angle has become "negative"; this also means that for a non-violating vertex, the violation quantity is negative but still represents how close to violation we are. (For larger corner angle sums, I added half of the additional angle sum as an additional violation quantity. That seemed good enough in the end.)

Simulated annealing - and visual debug of the optimisation

My first attempt used GSL's simulated annealing functions. I have had reasonable success with siman in the past. The constraints are folded into the cost function. (An alternative approach is to somehow deal with them in the random step function, eg by adjusting violating layouts to similar non-violating ones, but that seemed quite tricky here.)

Siman did not seem to be working at all.

I was hampered by not knowing what was going on so I wrote a visual debug utility which would let me observe the candidate layouts being tried, in real time. (I should have taken my first instinct and done it in Tcl/Tk, but initially Qt seemed like it would be easier. But in the end I had to fight several of Qt's built-in behaviours.)

The visual debug showed me the graph randomly jiggling about without any sign of progress. It was clear that if this was going to work at all it would be far too slow.

More suitable optimisation algorithm

I felt that a gradient descent algorithm, or something like one, would work well for this problem. It didn't seem to me that there would be troublesome local minima. More web searching led me to Steven G. Johnson's very useful NLopt library. As well as having implementations of algorithms I thought would work well, it offered the ability to change algorithm without having to deal with a whole new API.

I quickly found that NLopt's Sbplx algorithm (T. Rowan's Subplex algorithm, reimplemented) did fairly well. That algorithm does not support constraints but the grandly-named Augmented Lagrangian Method can handle that: it adds the constraint violations to the cost. It then reruns the optimisation, cranking up the constraint violation cost factor until none of the constraints are violated by more than the tolerance.

Unfortunately the Augmented Lagrangian Method can convert a problem with a cost function without local minima, into one which does have bad local minima. The Sbplx algorithm is a kind of descent algorithm so it finds a local minimum and hopes it's what you wanted. But unfortunately for me it wasn't: during the initial optimisation, part of the graph "capsized", violating the edge order constraint and leaving a planar layout impossible. The subsequent cranking up of the constraint violation cost didn't help, I think maybe because my violation cost was not very helpful at guiding the algorithm when things were seriously wrong.

But I fixed this by the simple expedient of adding the edge order constraint with a high cost to my own cost function. The result worked pretty well for my simple tests and for my actual use case. The graph layout optimiation takes a couple of minutes. The results are nice, I think.

I made a screen capture video of the optimisation running. (First the debug build which is slower so captures the early shape better; then again with the release build.)

Software

The planar graph layout tool I wrote is plag-mangler.

It's really not very productised, but I think it will be useful to people who have similar problems. Many of the worst features (eg the bad command line syntax) would be easy to fix. OTOH if you have a graph it does badly on, please do file an issue on salsa, as it will guide me to help make the program more general.

References

See my first post about this project for some proper references to the academic literature etc.

(Edit 2019-04-04 12:55 +0100: Fixed typos and grammar.)

Profile

diziet: (Default)
Ian Jackson

March 2025

S M T W T F S
      1
2345678
9101112131415
16171819202122
2324252627 2829
3031     

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags