Make shortest cables in your rack using simulated annealing

You saw this:

(src)

Now the problem: you want to minimize the total length of all cables, by reordering all units/devices in rack cabinets.

There are 16 devices in this examples. All permutations 16!=20922789888000, this is too much for bruteforce.

The solution using Python simanneal package:

from __future__ import print_function
import math
import random
from simanneal import Annealer

DEVICES=16
# each tuple - "source" device and "destination" device
wires=[(0, 13), (0, 3), (0, 5), (0, 6), (1, 11), (1, 3), (3, 14), (3, 1), (3, 7), (3, 9), (5, 4), (6, 1), (6, 14), (6, 8), (7, 12), (8, 0), (9, 6), (10, 4), (10, 7), (12, 13), (12, 14), (13, 8), (13, 8), (14, 15), (14, 3), (15, 9), (15, 0), (15, 10)]

def fitness(state):
    # measure total length of all wires, for the given order of devices
    total_len=0
    for wire in wires:
        src_device=wire[0]
        dst_device=wire[1]
        total_len=total_len+abs(state.index(src_device)-state.index(dst_device))
    return total_len

class TestProblem(Annealer):

    def __init__(self, state):
        super(TestProblem, self).__init__(state)  # important!

    def move(self):
        """Swaps two (random) elements"""
        a = random.randint(0, len(self.state) - 1)
        b = random.randint(0, len(self.state) - 1)
        self.state[a], self.state[b] = self.state[b], self.state[a]

    def energy(self):
        return fitness(self.state)

if __name__ == '__main__':

    init_state = range(DEVICES)

    test = TestProblem(init_state)
    test.steps = 5000000
    # since our state is just a list, slice is the fastest way to copy
    test.copy_strategy = "slice"
    state, e = test.anneal()

    print()
    print ("order =", state)
    print ("total length of all wires =", fitness(state))

    wire_drawn=[0]*len(wires)
    for pos in range(DEVICES):
        d=state[pos]
        s="device %2d | " % d
        for w in range(len(wires)):
            src=wires[w][0]
            dst=wires[w][1]
            if d==src or d==dst:
                s=s+" * "
                wire_drawn[w]=wire_drawn[w]^1 # toggle
            else:
                if wire_drawn[w]==1:
                    s=s+" | "
                else:
                    s=s+"   "
        print (s)


The total length fluctuates around 70 from run to run. This is not the minimal value, as SMT solver could give. But this solution may be good enough to be used in practice.

order = [4, 5, 10, 7, 12, 13, 8, 0, 15, 6, 14, 3, 9, 1, 11, 2]
total length of all wires = 69
device  4 |                                *                    *                               
device  5 |        *                       *                    |                               
device 10 |        |                                            *  *                          * 
device  7 |        |                 *                 *           *                          | 
device 12 |        |                 |                 *              *  *                    | 
device 13 |  *     |                 |                                *  |  *  *              | 
device  8 |  |     |                 |              *     *              |  *  *              | 
device  0 |  *  *  *  *              |              |     *              |                 *  | 
device 15 |     |     |              |              |                    |        *     *  *  * 
device  6 |     |     *              |        *  *  *        *           |        |     |       
device 14 |     |              *     |        |  *           |           *        *  *  |       
device  3 |     *           *  *  *  *  *     |              |                       *  |       
device  9 |                 |     |     *     |              *                          *       
device  1 |              *  *     *           *                                                 
device 11 |              *                                                                      
device  2 |                                                                                     

If, for some reason, or for fun, you'll want to get a solution with the total length maximized, negate fitness() function's result: "return -fitness(self.state)":

order = [3, 6, 13, 15, 5, 11, 10, 12, 2, 8, 4, 7, 9, 1, 14, 0]
total length of all wires = 268
device  3 |     *           *  *  *  *  *                                            *          
device  6 |     |     *     |  |  |  |  |     *  *  *        *                       |          
device 13 |  *  |     |     |  |  |  |  |     |  |  |        |        *     *  *     |          
device 15 |  |  |     |     |  |  |  |  |     |  |  |        |        |     |  |  *  |  *  *  * 
device  5 |  |  |  *  |     |  |  |  |  |  *  |  |  |        |        |     |  |  |  |  |  |  | 
device 11 |  |  |  |  |  *  |  |  |  |  |  |  |  |  |        |        |     |  |  |  |  |  |  | 
device 10 |  |  |  |  |  |  |  |  |  |  |  |  |  |  |        |  *  *  |     |  |  |  |  |  |  * 
device 12 |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  *     |  |  |  *  *  |  |  |  |  |  |    
device  2 |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |     |  |  |     |  |  |  |  |  |  |    
device  8 |  |  |  |  |  |  |  |  |  |  |  |  |  |  *  |  *  |  |  |     |  *  *  |  |  |  |    
device  4 |  |  |  |  |  |  |  |  |  |  |  *  |  |     |  |  |  *  |     |        |  |  |  |    
device  7 |  |  |  |  |  |  |  |  |  *  |     |  |     *  |  |     *     |        |  |  |  |    
device  9 |  |  |  |  |  |  |  |  |     *     |  |        |  *           |        |  |  *  |    
device  1 |  |  |  |  |  *  *  |  *           *  |        |              |        |  |     |    
device 14 |  |  |  |  |        *                 *        |              *        *  *     |    
device  0 |  *  *  *  *                                   *                                *    

UPD. At reddit: 1, 2.


List of my other blog posts. Subscribe to my news feed,
If you enjoy my work, you can support it on patreon.
Some time ago (before 24-Mar-2025) there was Disqus JS script for comments. I dropped it --- it was so motley, distracting, animated, with too much ads. I never liked it. Also, comments din't appeared correctly (Disqus was buggy). Also, my blog is too chamberlike --- not many people write comments here. So I decided to switch to the model I once had at least in 2020 --- send me your comments by email (don't forget to include URL to this blog post) and I will copy&paste it here manually.
Let's party like it's ~1993-1996, in this ultimate, radical and uncompromisingly primitive pre-web1.0-style blog and website. This website is best viewed under lynx/links/elinks/w3m.
Or use my zulip for feedback.