# -*- coding: utf-8 -*-
from itertools import product
from math import ceil, sqrt
#from typing import override # Python 3.12 or more
from random import sample, seed; seed(0)
from pysimple import Element, Parameter, IntegerVariable, BinaryVariable, Problem, NuoptStatus, Sum
from pysimple.typing import Table
from .column_generator import ColumnGenerator
[ドキュメント]class VehicleRouting(ColumnGenerator):
"""Vehicle Routing Problem"""
def __init__(self, capacity: int, volume: dict[str, int], dis: dict[tuple[str, str], float]) -> None:
self.capacity = capacity
self.volume = volume # volume[i]
self.dis = dis # dis[ijX]
[ドキュメント] def create_init_pattern(self) -> None:
"""倉庫 X と店舗 i を往復するルート"""
pattern, rd = {}, {} # rd: ルート r の巡回距離
for r, i in enumerate(self.volume, 1):
pattern[r,i] = 1
rd[r] = self.dis['X',i]*2
super().create_init_pattern(pattern=pattern, c=rd)
[ドキュメント] def create_new_pattern(self, lmbval: Table, *, silent: bool=True) -> tuple[Table, Table]: # CVRP
"""Sum(pattern[p,i]*lmb[i], i) < rd[p] となる p を見つける"""
iX = Element(value=['X', *self.volume]) # 倉庫(i=X)および店舗
jX = Element(set=iX.set)
ijX = iX!=jX
i = iX!='X' # 倉庫以外の店舗
j = Element(set=i.set)
ij = i!=j
v = Parameter(index=i, value=self.volume) # 店舗 i への配送量
d = Parameter(index=ijX, value=self.dis)
lmb = Parameter(index=j, value=lmbval)
N = len(i.set)
z = BinaryVariable(index=ijX) # iX から jX に移動するか
u = IntegerVariable(index=i, lb=1, ub=N)
prb = Problem(type=max)
prb += Sum(lmb[ij(1)]*z[ij]) - Sum(d[ijX]*z[ijX]), 'obj'
prb += Sum(z['X',i], i) == 1, '倉庫から出発'
prb += Sum(z[i,'X'], i) == 1, '倉庫へ帰還'
prb += Sum(z[ij], ij(1)) <= 1, '店舗 i から出発は 1 台まで'
prb += Sum(z[ij], ij(0)) <= 1, '店舗 j へ到着は 1 台まで'
prb += Sum(z[ijX], ijX(0))[i] == Sum(z[ijX], ijX(1))[i], '店舗 i からの到着==出発'
prb += u[ij(0)] - u[ij(1)] + N*z[ij] <= N-1, 'MTZ'
iXj = iX!=j
prb += Sum(v[iXj(1)]*z[iXj]) <= self.capacity, '最大積載量'
prb.solve(silent=silent)
assert prb.status == NuoptStatus.OPTIMAL
return z[z[ijX].val>0].val, prb.objective.val
#@override
[ドキュメント] def update_pattern(self, zval: Table) -> None:
"""update self.pattern, self.c"""
zdct: dict[str, str] = dict(zval.keys())
route = [cur:='X'] + [cur:=zdct[cur] for i in range(len(zdct))]
pn = self.patternnum
print(f'new pattern{pn}', '->'.join(route))
self.pattern.update({(pn, i): 1 for i in route[1:-1]})
self.c[pn] = sum(self.dis[i,j] for i, j in zip(route[:-1], route[1:]))
[ドキュメント] def visualize(self, zval: Table) -> None:
"""結果表示"""
for r, in zval:
route = [i for r_, i in self.pattern if r_==r]
print(f'route{r}:', '->'.join(['X', *route, 'X']))
[ドキュメント]def create_init_data(*, N: int) -> tuple[int, dict[str, int], dict[tuple[str, str], float]]:
"""N: 店舗数"""
mapsize = 2*ceil(sqrt(N)) # 密度 25% 程度
I = [f'p{t}' for t in range(1, N+1)] # 店舗の種類
volume = dict(zip(I, sample(range(1, N+1), N))) # 配送量 >0
capacity = sum(volume.values())//2 # sum(volme) 以上だと一回で巡回できてしまう
# 倉庫 X はマップの中央,他店舗はランダム
point: list[tuple[float, ...]] = sample(list(product(range(mapsize+1), repeat=2)), N)
I.insert(0, 'X')
point.insert(0, (mapsize//2+0.5, mapsize//2+0.5))
dis = {(i1,i2): sqrt((x1-x2)**2 + (y1-y2)**2)
for (i1,(x1,y1)), (i2,(x2,y2)) in product(zip(I, point), repeat=2) if i1 != i2}
return capacity, volume, dis
if __name__ == '__main__':
CAPACITY, VOLUME, DIS = create_init_data(N=15)
vr = VehicleRouting(CAPACITY, VOLUME, DIS)
ZVAL = vr.solve()
vr.visualize(ZVAL)