Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2@file 

3@brief Function to solve the problem of the 

4`Route Inspection Problem <https://en.wikipedia.org/wiki/Route_inspection_problem>`_. 

5""" 

6import math 

7import random 

8import fractions 

9 

10 

11class SolutionException(Exception): 

12 """ 

13 wrong solution 

14 """ 

15 pass 

16 

17 

18def haversine_distance(lat1, lng1, lat2, lng2): 

19 """ 

20 Computes `Haversine formula <http://en.wikipedia.org/wiki/Haversine_formula>`_. 

21 

22 .. index:: Haversine 

23 

24 @param lat1 lattitude 

25 @param lng1 longitude 

26 @param lat2 lattitude 

27 @param lng2 longitude 

28 @return distance 

29 """ 

30 radius = 6371 

31 dlat = math.radians(lat2 - lat1) 

32 dlon = math.radians(lng2 - lng1) 

33 a = math.sin(dlat / 2) * math.sin(dlat / 2) + math.cos(math.radians(lat1)) \ 

34 * math.cos(math.radians(lat2)) * math.sin(dlon / 2) * math.sin(dlon / 2) 

35 c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) 

36 d = radius * c 

37 return d 

38 

39 

40def distance_solution(edges_index, edges, distances, solution, exc=True): 

41 """ 

42 Checks if a solution is really a solution and returns the distance of it, 

43 None if it is not a solution. The function does not case about the order. 

44 

45 @param edges_index list of indices of edges (if None --> range(len(edges)) 

46 @param edges list of tuple (vertex A, vertex B) 

47 @param distances list of distances of each edge 

48 @param solution proposed solutions (list of edge indices) 

49 @param exc raises an exception in case of error 

50 """ 

51 if edges_index is None: 

52 edges_index = list(range(len(edges))) 

53 indices = set(edges_index) 

54 solset = set(solution) 

55 if len(indices) != len(solset): 

56 if exc: 

57 raise SolutionException("Different number of distinct edges:\nexpected={0} got={1}\n" 

58 "Did you cover all the edges?".format(len(indices), len(solset))) 

59 return None 

60 

61 for s in solution: 

62 if s not in indices: 

63 raise SolutionException( 

64 "Index {0} is not in edges_index".format(s)) 

65 

66 doubles = {} 

67 for a, b in edges: 

68 if a > b: 

69 a, b = b, a 

70 if (a, b) in doubles: 

71 if exc: 

72 raise SolutionException( 

73 "Edge {0} is duplicated in edges.".format((a, b))) 

74 return None 

75 doubles[a, b] = 1 

76 

77 corres = {e: i for i, e in enumerate(edges_index)} 

78 degrees = {} 

79 for s in solution: 

80 a, b = edges[corres[s]] 

81 degrees[a] = degrees.get(a, 0) + 1 

82 degrees[b] = degrees.get(b, 0) + 1 

83 

84 odd, even = 0, 0 

85 for d in degrees.values(): 

86 if d % 2 == 0: 

87 even += 1 

88 else: 

89 odd += 1 

90 

91 if odd > 2: 

92 if exc: 

93 red = list(sorted([(k, v) 

94 for k, v in degrees.items() if v % 2 != 0])) 

95 if len(red) > 10: 

96 red = red[:10] + ["..."] 

97 raise SolutionException( 

98 "Are you sure? The path is inconsistent. Some help:\n" + str(red)) 

99 return None 

100 else: 

101 return sum(distances[corres[s]] for s in solution) 

102 

103 

104def compute_degrees(edges): 

105 """ 

106 Compute the degree of vertices. 

107 

108 @param edges list of tuple 

109 @return dictionary {key: degree} 

110 """ 

111 res = {} 

112 for a, b in edges: 

113 res[a] = res.get(a, 0) + 1 

114 res[b] = res.get(b, 0) + 1 

115 return res 

116 

117 

118def euler_path(edges_index, edges, solution): 

119 """ 

120 Computes an Eulerian path. 

121 

122 .. index:: Eulerian 

123 

124 @param edges_index list of indices of edges (if None --> range(len(edges)) 

125 @param edges list of tuple (vertex A, vertex B) 

126 @param solution proposed solutions (list of edge indices) 

127 @return path, list of edges indices 

128 

129 The function assumes every vertex in the graph defined by *edges* 

130 has an even degree. 

131 """ 

132 if edges_index is None: 

133 edges_index = range(len(edges)) 

134 pos = {k: i for i, k in enumerate(edges_index)} 

135 indices = [pos[s] for s in solution] 

136 edges = [edges[i] for i in indices] 

137 

138 degrees = compute_degrees(edges) 

139 odd = {k: v for k, v in degrees.items() if v % 2 == 1} 

140 if len(odd) not in (0, 2): 

141 odd = list(sorted((k, v) for k, v in odd.items())) 

142 if len(odd) > 10: 

143 odd = odd[:10] + ["..."] 

144 raise SolutionException( 

145 "Some vertices have an odd degree. This is not allowed.\n" + str(odd)) 

146 

147 if len(odd) == 2: 

148 # we add an extra edge which we remove later 

149 odd = list(odd.keys()) 

150 remove = (odd[0], odd[1]) 

151 edges.append(remove) 

152 else: 

153 remove = None 

154 

155 res = _euler_path(edges) 

156 pathi = [_[1][0] for _ in res] 

157 if remove is not None: 

158 index = pathi.index(len(edges) - 1) 

159 if index == 0: 

160 pathi = pathi[1:] 

161 elif index == len(edges) - 1: 

162 pathi = pathi[:-1] 

163 else: 

164 pathi = pathi[index + 1:] + pathi[:index] 

165 pathi = [solution[i] for i in pathi] 

166 return pathi 

167 

168 

169def _euler_path(edges): 

170 """ 

171 Computes an Eulerian path. 

172 

173 @param edges edges 

174 @return path, list of (vertex,edge) 

175 

176 The function assumes every vertex in the graph defined by *edges* 

177 has an even degree. 

178 """ 

179 alledges = {} 

180 edges_from = {} 

181 for i, k in enumerate(edges): 

182 if isinstance(k, list): 

183 k = tuple(k) 

184 v = (i,) + k 

185 alledges[k] = v 

186 a, b = k 

187 alledges[b, a] = alledges[a, b] 

188 if a not in edges_from: 

189 edges_from[a] = [] 

190 if b not in edges_from: 

191 edges_from[b] = [] 

192 edges_from[a].append(alledges[a, b]) 

193 edges_from[b].append(alledges[a, b]) 

194 

195 degre = {} 

196 for a, v in edges_from.items(): 

197 t = len(v) 

198 degre[t] = degre.get(t, 0) + 1 

199 

200 two = [a for a, v in edges_from.items() if len(v) == 2] 

201 odd = [a for a, v in edges_from.items() if len(v) % 2 == 1] 

202 if len(odd) not in (0, 2): 

203 add = "\n" + str(odd) if len(odd) < 10 else "" 

204 raise SolutionException( 

205 "Some vertices have an odd degree. This is not allowed." + add) 

206 

207 begin = two[0] 

208 

209 # checking 

210 for v, le in edges_from.items(): 

211 for e in le: 

212 to = e[1] if v != e[1] else e[2] 

213 if to not in edges_from: 

214 raise SolutionException( 

215 "Unable to find vertex {0} for edge ({0},{1}).".format(to, v)) 

216 if to == v: 

217 raise SolutionException("Circular edge {0}.".format(to)) 

218 

219 # loop 

220 path = _explore_path(edges_from, begin) 

221 for p in path: 

222 if len(p) == 0: 

223 raise NotImplementedError("This exception should not happen.") 

224 while len(edges_from) > 0: 

225 start = None 

226 for i, p in enumerate(path): 

227 if p[0] in edges_from: 

228 start = i, p 

229 break 

230 sub = _explore_path(edges_from, start[1][0]) 

231 i = start[0] 

232 path[i:i + 1] = path[i:i + 1] + sub 

233 return path 

234 

235 

236def _delete_edge(edges_from, n, to): 

237 """ 

238 Removes an edge from the graph. 

239 

240 @param edges_from structure which contains the edges (will be modified) 

241 @param n first vertex 

242 @param to second vertex 

243 @return the edge 

244 """ 

245 le = edges_from[to] 

246 f = None 

247 for i, e in enumerate(le): 

248 if (e[1] == to and e[2] == n) or (e[2] == to and e[1] == n): 

249 f = i 

250 break 

251 

252 assert f is not None 

253 del le[f] 

254 if len(le) == 0: 

255 del edges_from[to] 

256 

257 le = edges_from[n] 

258 f = None 

259 for i, e in enumerate(le): 

260 if (e[1] == to and e[2] == n) or (e[2] == to and e[1] == n): 

261 f = i 

262 break 

263 

264 assert f is not None 

265 keep = le[f] 

266 del le[f] 

267 if len(le) == 0: 

268 del edges_from[n] 

269 

270 return keep 

271 

272 

273def _explore_path(edges_from, begin): 

274 """ 

275 Explores an Eulerian path, remove used edges from *edges_from*. 

276 

277 @param edges_from structure which contains the edges (will be modified) 

278 @param begin first vertex to use 

279 @return path 

280 """ 

281 path = [(begin, None)] 

282 stay = True 

283 while stay and len(edges_from) > 0: 

284 

285 n = path[-1][0] 

286 if n not in edges_from: 

287 # fin 

288 break 

289 le = edges_from[n] 

290 

291 if len(le) == 1: 

292 h = 0 

293 e = le[h] 

294 to = e[1] if n != e[1] else e[2] 

295 else: 

296 to = None 

297 nb = 100 

298 while to is None or to == begin: 

299 h = random.randint(0, len(le) - 1) if len(le) > 1 else 0 

300 e = le[h] 

301 to = e[1] if n != e[1] else e[2] 

302 nb -= 1 

303 if nb < 0: 

304 raise NotImplementedError( 

305 "Algorithm issue {0}".format(len(path))) 

306 

307 if len(edges_from[to]) == 1: 

308 if begin != to: 

309 raise NotImplementedError("Wrong algorithm.") 

310 stay = False 

311 

312 keep = _delete_edge(edges_from, n, to) 

313 path.append((to, keep)) 

314 

315 return path[1:] 

316 

317 

318def distance_vertices(edges, vertices, distances): 

319 """ 

320 Computes the length of edges if distances is None. 

321 

322 @param edges list of tuple (vertex A, vertex B) 

323 @param vertices locations of the vertices 

324 @param distances distances (None or list of floats) 

325 @return distances (list of float) 

326 """ 

327 if distances is None: 

328 distances = [] 

329 while len(distances) < len(edges): 

330 distances.append(None) 

331 for i, edge in enumerate(edges): 

332 if distances[i] is not None: 

333 continue 

334 a, b = edge 

335 va = vertices[a] 

336 vb = vertices[b] 

337 d = haversine_distance(va[0], va[1], vb[0], vb[1]) 

338 distances[i] = d 

339 return distances 

340 

341 

342def bellman_distances(edges, distances, fLOG=None): 

343 """ 

344 Computes shortest distances between all vertices. 

345 It assumes edges are symmetric. 

346 

347 @param edges list of tuple (vertex A, vertex B) 

348 @param distances distances (list of floats) 

349 @param fLOG logging function 

350 @return dictionary of distances 

351 

352 This function could be implemented based on 

353 `shortest_path <https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csgraph.shortest_path.html>`_. 

354 """ 

355 dist = {(a, b): d for d, (a, b) in zip(distances, edges)} 

356 dist.update({(b, a): d for d, (a, b) in zip(distances, edges)}) 

357 dist0 = dist.copy() 

358 

359 iter = 0 # pylint: disable=W0622 

360 up = dist 

361 while len(up) > 0: 

362 iter += 1 

363 up = {} 

364 for (a, b), d1 in dist.items(): 

365 if (a, b) not in dist0 and (a, b) not in up: 

366 # no need to continue as no update during the last iteration 

367 continue 

368 for (aa, bb), d2 in dist.items(): 

369 # not the most efficient 

370 if b == aa and a != bb: 

371 d = d1 + d2 

372 if (a, bb) not in dist or dist[a, bb] > d: 

373 up[a, bb] = d 

374 up[bb, a] = d 

375 dist.update(up) 

376 if fLOG: 

377 sum_values = sum(dist.values()) 

378 mes = "iteration={0} modif={1} #dist={2} sum_values={3} avg={4}".format( 

379 iter, len(up), len(dist), sum_values, sum_values / len(dist)) 

380 fLOG("[bellman_distances] " + mes) 

381 

382 return dist 

383 

384 

385def dijkstra_path(edges, distances, va, vb): 

386 """ 

387 Returns the best path between two vertices. 

388 Uses `Dikjstra <https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm>`_ algorithm. 

389 

390 @param edges list of edges. 

391 @param distances list of distances 

392 @param va first vertex 

393 @param vb last vertex 

394 @return list of edges 

395 

396 This function could be implemented based on 

397 `shortest_path <https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csgraph.shortest_path.html>`_. 

398 """ 

399 dist = {va: 0} 

400 prev = {va: None} 

401 modif = 1 

402 while modif > 0: 

403 modif = 0 

404 for (a, b), d in zip(edges, distances): 

405 if a in dist: 

406 d2 = dist[a] + d 

407 if b not in dist or dist[b] > d2: 

408 dist[b] = d2 

409 prev[b] = a 

410 modif += 1 

411 if b in dist: 

412 d2 = dist[b] + d 

413 if a not in dist or dist[a] > d2: 

414 dist[a] = d2 

415 prev[a] = b 

416 modif += 1 

417 rev = {(a, b): i for i, (a, b) in enumerate(edges)} 

418 rev.update({(b, a): i for i, (a, b) in enumerate(edges)}) 

419 path = [] 

420 v = vb 

421 while v is not None: 

422 path.append(v) 

423 v = prev[v] 

424 path.reverse() 

425 return [rev[a, b] for a, b in zip(path[:-1], path[1:])] 

426 

427 

428def matching_vertices(distances, algo="blossom"): 

429 """ 

430 Finds the best match between vertices. 

431 

432 @param distances result of function @bellman_distances but 

433 only for odd vertices (odd = odd degree), 

434 dictionary { (vi,vj) : distance} 

435 @param algo algorithm (see below) 

436 @return sequences of best matches. 

437 

438 If ``algo=='hungarian'``, 

439 the function relies on `linear_sum_assignment 

440 <https://docs.scipy.org/doc/scipy-0.18.1/reference/generated/scipy.optimize.linear_sum_assignment.html>`_ 

441 from `scipy <https://docs.scipy.org/doc/scipy-0.18.1/>`_ which uses the 

442 `Hungarian Algorithm <https://en.wikipedia.org/wiki/Hungarian_algorithm>`_. 

443 Vertex index do not have to start from zero and be continuous. 

444 The function will handle that particular case. However, the method 

445 works for a bi-partite matching and is not suitable here unless 

446 the algorithm is modified. Not done (yet?). 

447 

448 If ``algo=='blossom'``, it uses the 

449 `Blossom algorithm <https://en.wikipedia.org/wiki/Blossom_algorithm>`_ 

450 which is known to be in :math:`O(n^3)` and finds the optimal matching. 

451 

452 If ``algo=='basic'``, the function sorts the distances 

453 by increasing order and builds new edges as they come in this list. 

454 It does not return an optimal solution but is much faster 

455 when the graph is big. 

456 """ 

457 if not isinstance(distances, dict): 

458 raise TypeError( 

459 "Unexpected type for distances, this should be a dictionary.") 

460 from numpy import empty 

461 from scipy.optimize import linear_sum_assignment 

462 unique = set(_[0] for _ in distances) | set(_[1] for _ in distances) 

463 mapping = {} 

464 rev = {} 

465 for s in unique: 

466 n = len(mapping) 

467 mapping[s] = n 

468 rev[n] = s 

469 mx = len(mapping) 

470 

471 # Hungarian algorithm 

472 if algo == "hungarian": 

473 cost = empty((mx, mx)) 

474 mx = abs(max(distances.values())) * len(mapping) * 10 

475 cost[:] = mx 

476 for (i, j), v in distances.items(): 

477 if i == j: 

478 raise ValueError( 

479 "Unreasonable case: {0} == {1}, v={2}".format(i, j, v)) 

480 cost[mapping[i], mapping[j]] = v 

481 

482 row_ind, col_ind = linear_sum_assignment( # pylint: disable=W0632 

483 cost) # pylint: disable=W0632 

484 pairs = [(rev[i], rev[j]) for i, j in zip(row_ind, col_ind)] 

485 for a, b in pairs: 

486 if a == b: 

487 raise ValueError("Issue with one pair a == b.\n{0}".format( 

488 "\n".join(str(_) for _ in pairs))) 

489 

490 # we remove duplicates 

491 done = set() 

492 final = [] 

493 for a, b in pairs: 

494 if (a, b) not in done: 

495 final.append((a, b)) 

496 done.add((b, a)) 

497 if len(final) * 2 != len(pairs): 

498 mes = "final={0}\n{3}={1}\ncost\n{2}".format( 

499 final, pairs, cost, algo) 

500 raise ValueError( 

501 "Did you use the tweak? The matching should be symmetric.\n" + mes) 

502 return final 

503 elif algo == "basic": 

504 # we sort pair by increasing order 

505 dists = [(v, i, j) for (i, j), v in distances.items()] 

506 dists.sort() 

507 

508 # the graph is fully connected so we don't need to check 

509 # if selecting an edge leads to an impossible solution 

510 vdone = set() 

511 pairs = [] 

512 for v, i, j in dists: 

513 if i not in vdone and j not in vdone: 

514 pairs.append((i, j)) 

515 vdone.add(i) 

516 vdone.add(j) 

517 return pairs 

518 

519 elif algo == "blossom": 

520 from .blossom import Vertex, StructureUpToDate, TreeStructureChanged, MaximumDualReached, INF 

521 

522 vertices = {} 

523 for v in mapping: 

524 vertices[v] = Vertex(v) 

525 for (i, j), v in distances.items(): 

526 f = fractions.Fraction(v) 

527 vertices[i].add_edge_to(vertices[j], f) 

528 

529 def update_tree_structures(roots): 

530 try: 

531 while True: 

532 try: 

533 for root in roots: 

534 root.alter_tree(roots) 

535 raise StructureUpToDate() 

536 except TreeStructureChanged: 

537 pass 

538 except StructureUpToDate: 

539 pass 

540 

541 def get_max_delta(roots): 

542 if len(roots) == 0: 

543 raise MaximumDualReached() 

544 delta = INF 

545 for root in roots: 

546 delta = min(delta, root.get_max_delta()) 

547 assert delta >= 0 

548 return delta 

549 

550 roots = set(vertices.values()) 

551 

552 try: 

553 while True: 

554 delta = get_max_delta(roots) 

555 for root in roots: 

556 root.adjust_charge(delta) 

557 update_tree_structures(roots) 

558 except MaximumDualReached: 

559 # done 

560 pass 

561 

562 M = set() 

563 for v in vertices.values(): 

564 M.update(e for e in v.edges if e.selected) 

565 

566 pairs = [e.extremities for e in M] 

567 return pairs 

568 else: 

569 raise NotImplementedError("Not recognized: {0}".format(algo)) 

570 

571 

572def best_euler_path(edges, vertices, distances, edges_index=None, algo="blossom", fLOG=None): 

573 """ 

574 Computes the final solution for the Eulerian path. 

575 

576 @param edges edges (list of tuple) 

577 @param vertices location of the vertices (not needed if distances are filled) 

578 @param distances distances for each edge (list of distance) 

579 @param edges_index list of indices of edges (if None --> range(len(edges)) 

580 @param algo algorithm to use to computes the matching 

581 (see @see fn matching_vertices) 

582 @param fLOG logging function 

583 @return list of edges as tuple, list of edges as indices, distance 

584 """ 

585 if fLOG: 

586 fLOG("[best_euler_path] distance_vertices #edges={0}".format( 

587 len(edges))) 

588 distances = distance_vertices(edges, vertices, distances) 

589 if fLOG: 

590 fLOG("[best_euler_path] bellman_distances") 

591 dist = bellman_distances(edges, distances, fLOG=fLOG) 

592 if fLOG: 

593 fLOG("[best_euler_path] degrees and distances between odd vertices") 

594 degrees = compute_degrees(edges) 

595 odd = {k: v for k, v in degrees.items() if v % 2 != 0} 

596 odd_dist = {k: v for k, v in dist.items() if k[0] in odd and k[1] in odd} 

597 if fLOG: 

598 fLOG("[best_euler_path] matching #odd={0}, #odd_dist={1}".format( 

599 len(odd), len(odd_dist))) 

600 pairs = matching_vertices(odd_dist, algo=algo) 

601 if fLOG: 

602 fLOG("[best_euler_path] build solution") 

603 solution = list(edges) 

604 for va, vb in pairs: 

605 dij = dijkstra_path(edges, distances, va, vb) 

606 solution.extend([edges[e] for e in dij]) 

607 if fLOG: 

608 fLOG("[best_euler_path] order edges to get the path #edges={0}".format( 

609 len(solution))) 

610 if edges_index is None: 

611 edges_index = list(range(len(edges))) 

612 mapping = {} 

613 rev = {} 

614 for edge, index in zip(edges, edges_index): 

615 mapping[edge] = index 

616 rev[index] = edge 

617 sol_index = [mapping[e] for e in solution] 

618 euler_index = euler_path(edges_index, edges, sol_index) 

619 euler = [rev[i] for i in euler_index] 

620 if fLOG: 

621 fLOG("[best_euler_path] done.") 

622 d = distance_solution(edges_index, edges, distances, sol_index) 

623 return euler, euler_index, d