grbl1.1+Arduino CNCシールドV3.5+bCNCを使用中。
BluetoothモジュールおよびbCNCのPendant機能でスマホからもワイヤレス操作可能。
その他、電子工作・プログラミング、機械学習などもやっています。
MacとUbuntuを使用。

CNCマシン全般について:
国内レーザー加工機と中国製レーザー加工機の比較
中国製レーザーダイオードについて
CNCミリングマシンとCNCルーターマシンいろいろ
その他:
利用例や付加機能など:
CNCルーター関係:



*CNCマシンの制作記録は2016/04/10〜の投稿に書いてあります。


2019年7月10日水曜日

TSP LP: US48 cities / 巡回セールスマン問題 線形計画法

今回はTSPLIBにある「att48.tsp」というアメリカ48都市のデータを用いて前回の方法(LP:Lazy constraints)を試してみました。前回同様Pulpを使ってLPで解くことにします。TSPLIBには厳密解の巡回路データ「att48.opt.tour」も用意されているので結果を照らし合わせることができます。前回、線形計画法によるTSPの解法がある程度理解できたのでアルゴリズムを整理し直してみました。


事前準備:
まず、ダウンロードした「att48.tsp」をPandasで読み込みます。
data = pd.read_csv('att48.tsp', header=5)
df = data['NODE_COORD_SECTION'].str.split(' ', expand=True)
X = df[1].values[:-1].astype('int32')
Y = df[2].values[:-1].astype('int32')
XY = X + Y * 1j
num = len(X)
データは6行目から各都市の「番号 X座標 Y座標」となっており、主にX座標とY座標だけ抜き出します。最後の行には「EOF(End Of File)」があるので、その部分を含めないようにします。

厳密解データ(解答)も読み込んでおきます。厳密解は巡回順の整数値が並んでいるだけです。
T_opt = pd.read_table('att48.opt.tour', header=4)
T_opt = list(T_opt.values[:-2, 0].astype('int32') - 1) + [0]
draw_plot(T_opt)
表示用draw_plot()関数を用意しておき厳密解を表示させます。

実際このような巡回路となれば正解というわけです。48都市あるので各都市のインデックス番号を0〜47としておきます。TSPLIBでは1〜48の番号になっていますが、とりあえず今回は0〜47で計算することにします。


LPアルゴリズム:
48都市あるので、2都市間経路の組み合わせは48*47/2=1128となります。この場合都市0から都市1への移動となる(0, 1)という組み合わせに対して、その反転した組み合わせ(1, 0)は同じものとみなします(順列ではなく組み合わせのほうが数が減るので)。
prob = LpProblem(name='TSP_LP', sense=LpMinimize)

dist = {(i, j): abs(XY[i] - XY[j]) for i in range(num) for j in range(num) if i < j}
x = LpVariable.dicts('x', dist, 0, 1, LpBinary)

prob += lpSum([dist[(i, j)] * x[(i, j)] for i, j in dist])

for n in range(num):
    prob += lpSum([x[(i, j)] for i, j in dist if n in (i, j)]) == 2
    
result = prob.solve()
print(LpStatus[result])

ED = [(i, j) for (i, j) in x if x[(i, j)].varValue == 1]
draw_plot(False, ED)
前回同様Pulpを使って最小化する式を用意します。ここで注意すべきことは、i<jという大小関係になっていることと、変数xがバイナリLpBinaryになっているということです。変数x[(i, j)]はその組み合わせが経路として採用されれば1で不採用なら0となります。dist[(i, j)]*x[(i, j)]を積算しその値が最小値になるようにLPで解いていきます。
ただし、最低限の制約として、
for n in range(num):
    prob += lpSum([x[(i, j)] for i, j in dist if n in (i, j)]) == 2
があります。この制約の部分を前回よりも簡潔に書き直してみました。巡回路になるためには各都市において必ず二つの経路が接続されていなければならないので、都市1であれば、(i, 1)もしくは(1, j)が二つ必要ということになります。以下のような感じです。
(0, 1), (0, 2), (0, 3)...(0, 46), (0, 47) から二つ採用(都市0において)
(0, 1), (1, 2), (1, 3)...(1, 46), (1, 47) から二つ採用(都市1において)
(0, 2), (1, 2), (2, 3)...(2, 46), (2, 47) から二つ採用(都市2において)
(0, 3), (1, 3), (2, 3)...(3, 46), (3, 47) から二つ採用(都市3において)
...
(0, 46), (1, 46), (2, 46)...(45, 46), (46, 47) から二つ採用(都市46において)
(0, 47), (1, 47), (2, 47)...(45, 47), (46, 47) から二つ採用(都市47において)

一旦この制約でsolve()し、varValue=1となる経路(採用された経路)を表示させます。
そうするとこのような複数の部分巡回路が出来上がります。この段階では各辺がバラバラなので部分巡回路ごとにグループ分けする関数を用意して仕分けします。
def subtour_list(ED):
    E = []
    for (i, j) in ED:
        E_flat = sum(E, [])
        if (i, j) not in E_flat:
            S = [(i, j)]
            while True:
                SC = S.copy()
                for (ii, jj) in ED:
                    if (ii, jj) not in S and (ii, jj) not in E_flat:
                        if {ii, jj} & set(sum(S, ())):
                            S.append((ii, jj))
                if SC == S:
                    E.append(S)
                    break

    EL = [len(e) for e in E]
    AS = np.argsort(EL)
    ST = [E[a] for a in AS]
    return ST

# output subtours
ST = subtour_list(ED)
for st in ST: 
    print(len(st), st)
部分巡回路ごとにグループ分けすると、
4 [(6, 17), (6, 27), (17, 35), (27, 35)]
5 [(2, 21), (2, 33), (15, 21), (15, 40), (33, 40)]
7 [(5, 29), (5, 36), (18, 36), (29, 42), (16, 42), (18, 26), (16, 26)]
13 [(1, 28), (1, 41), (4, 28), (4, 47), (25, 41), (38, 47), (3, 25), (3, 34), (31, 38), (34, 44), (9, 44), (23, 31), (9, 23)]
19 [(0, 7), (0, 8), (7, 37), (8, 39), (14, 39), (30, 37), (30, 43), (43, 45), (11, 14), (32, 45), (10, 11), (10, 22), (13, 22), (13, 24), (19, 32), (19, 46), (20, 46), (12, 20), (12, 24)]
になります。一応、辺数の少ない順に並べ替えています。先頭の値は辺数。


Lasy constraints/部分巡回路除去:
次にこれら部分巡回路のひとつをターゲットとしLazy constraintsを追加し部分巡回路除去を行います。今回の場合は辺数の一番少ない[(6, 17), (6, 27), (17, 35), (27, 35)]に制約を与えることにしました(前回は複数の部分巡回路に制約を与えていましたが)。
部分巡回路は完結した閉路なので、最低でも二本の経路を外につなげるように仕向けます。それが以下の制約になります。
st_flat = set(sum(ST[0], ()))
prob += lpSum([x[(i, j)] for (i, j) in dist if len({i, j} & st_flat) == 1]) >= 2
prob.solve()
(i, j)の組み合わせのうち、iかjのどちらか一方がその部分巡回路に含まれて、他方が含まれていない組み合わせが二つ以上必要という条件にします。「if len({i, j} & st_flat) == 1」の部分が、(i, j)のどちらか一方が部分巡回路に含まれているかどうかという条件です。こうすることで、部分巡回路内にある最低でも二つの都市が他の部分巡回路の都市へつながろうとします。
この条件を与えたあと一旦solve()して結果を見ます。部分巡回路に多少変化が見られるので、何度か繰り返すと部分巡回路がまとまって一つの大きな巡回路になります。while文で最終的にひとつの巡回路になるまで繰り返すコードが以下です。
while len(ST) > 1:
    st_flat = set(sum(ST[0], ()))
    prob += lpSum([x[(i, j)] for (i, j) in dist if len({i, j} & st_flat) == 1]) >= 2
    prob.solve()
    ED = [(i, j) for (i, j) in x if x[(i, j)].varValue == 1]
    ST = subtour_list(ED)
    print('ST[0]:', ST[0])
今回は15ループ(3秒ほど)で48都市を最適化できました。
上記の状態だとまだバラバラの辺の集合なので、以下の処理で最終的な都市順路に並び替えます。
T = list(ED[0])
while len(T) < num:
    for (i, j) in ED:
        if i == T[-1] and j not in T:
            T.append(j)
        elif i not in T and j == T[-1]:
            T.append(i)

T += [T[0]]
print('Optimal Tour:', T)
draw_plot(T)
この結果、
Optimal Tour: [0, 7, 37, 30, 43, 17, 6, 27, 5, 36, 18, 26, 16, 42, 29, 35, 45, 32, 19, 46, 20, 31, 38, 47, 4, 41, 23, 9, 44, 34, 3, 25, 1, 28, 33, 40, 15, 21, 2, 22, 13, 24, 12, 10, 11, 14, 39, 8, 0]

という順路リストが得られます。逆順になっている場合もあるので必要に応じて反転させます。TSPLIBにあるデータは都市番号1から始まっているので、それぞれに+1すれば同じ値になります。


アニメーション:
また変化をアニメーション表示するには以下(前半の設定は省略)。
from matplotlib import animation
from IPython.display import HTML

fig = plt.figure()
ims = []

while True:
    # lazy constraints
    st_flat = set(sum(ST[0], ()))
    prob += lpSum([x[(i, j)] for (i, j) in dist if len({i, j} & st_flat) == 1]) >= 2
    prob.solve()
    
    img = draw_plot(False, ED, ST[0], ANIM=True)
    ims.append(img)
    
    ED = [(i, j) for (i, j) in x if x[(i, j)].varValue == 1]
    ST = subtour_list(ED)
    
    if len(ST) == 1:
        img = draw_plot(False, ED, ST[0], ANIM=True)
        ims.append(img)
        break

ani = animation.ArtistAnimation(fig, ims, interval=800)
#ani.save("tsp_48_1.gif", writer = "imagemagick") # save as animation gif
plt.close()
HTML(ani.to_jshtml())

以下が作成したアニメーション。赤い部分は制約を与える部分巡回路。





Once Loop Reflect

以下が全体のコード。
numpy
matplotlib
pulp
pandas
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

上記Gist内のコードには(i, j)の組み合わせが倍の2256通りあるパターンについても試しています。組み合わせが多いのでその分時間がかかりますが、同じアルゴリズムで解くことができます。

人気の投稿