Swarm-SLAM  1.0.0
C-SLAM Framework
algebraic_connectivity_maximization.py
Go to the documentation of this file.
1 from typing import NamedTuple
2 
3 import numpy as np
4 
5 from cslam.mac.mac import MAC
6 from cslam.mac.utils import Edge, weight_graph_lap_from_edge_list
7 
8 
9 class EdgeInterRobot(NamedTuple):
10  """ Inter-robot loop closure edge
11  """
12  robot0_id: int
13  robot0_keyframe_id: int
14  robot1_id: int
15  robot1_keyframe_id: int
16  weight: float
17 
18  def __eq__(self, other):
19  """ Overload the equal operator in order to ignore the weights
20 
21  Args:
22  other (EdgeInterRobot): Other edge to compare
23  """
24  return ((self.robot0_idrobot0_id == other.robot0_id) and
25  (self.robot0_keyframe_idrobot0_keyframe_id == other.robot0_keyframe_id) and
26  (self.robot1_idrobot1_id == other.robot1_id) and
27  (self.robot1_keyframe_idrobot1_keyframe_id == other.robot1_keyframe_id)) or (
28  (self.robot0_idrobot0_id == other.robot1_id) and
29  (self.robot0_keyframe_idrobot0_keyframe_id == other.robot1_keyframe_id) and
30  (self.robot1_idrobot1_id == other.robot0_id) and
31  (self.robot1_keyframe_idrobot1_keyframe_id == other.robot0_keyframe_id))
32 
33 
35 
36  def __init__(
37  self,
38  robot_id=0,
39  max_nb_robots=1,
40  max_iters=20,
41  fixed_weight=1.0,
42  extra_params={
43  "frontend.enable_sparsification": True,
44  "evaluation.enable_sparsification_comparison": False,
45  }):
46  """Initialization
47 
48  Args:
49  robot_id (int, optional): ID of the robot
50  max_nb_robots (int, optional): number of robots. Defaults to 1.
51  max_iters (int, optional): maximum number of iterations. Defaults to 20.
52  fixed_weight (float, optional): weight of fixed measurements. Defaults to 1.0.
53  """
54  self.fixed_weightfixed_weight = fixed_weight
55  self.paramsparams = extra_params
56 
57  self.fixed_edgesfixed_edges = []
58  self.candidate_edgescandidate_edges = {}
59  self.already_considered_matchesalready_considered_matches = set()
60 
61  self.max_itersmax_iters = max_iters
62 
63  self.max_nb_robotsmax_nb_robots = max_nb_robots
64  self.robot_idrobot_id = robot_id
65  self.total_nb_posestotal_nb_poses = 0
66 
67  self.initial_fixed_edge_existsinitial_fixed_edge_exists = {}
68  self.nb_posesnb_poses = {}
69  for i in range(self.max_nb_robotsmax_nb_robots):
70  self.nb_posesnb_poses[i] = 0
71  self.initial_fixed_edge_existsinitial_fixed_edge_exists[i] = False
72 
73  self.log_greedy_edgeslog_greedy_edges = []
74  self.log_mac_edgeslog_mac_edges = []
75 
76  def edge_key(self, edge):
77  """Get a unique key for an edge
78 
79  Args:
80  edge (EdgeInterRobot): inter-robot edge
81 
82  Returns:
83  tuple(int, int, int, int): unique key
84  """
85  if edge.robot0_id < edge.robot1_id:
86  return (edge.robot0_id, edge.robot0_keyframe_id, edge.robot1_id,
87  edge.robot1_keyframe_id)
88  else:
89  return (edge.robot1_id, edge.robot1_keyframe_id, edge.robot0_id,
90  edge.robot0_keyframe_id)
91 
92  def replace_weight(self, edge, weight):
93  """Replace the weight of an edge
94 
95  Args:
96  edge (EdgeInterRobot): inter-robot edge
97  weight (float): new weight
98 
99  Returns:
100  EdgeInterRobot: new edge with new weight
101  """
102  # Check if inter-robot edge
103  if type(edge) is EdgeInterRobot:
104  return EdgeInterRobot(edge.robot0_id, edge.robot0_keyframe_id,
105  edge.robot1_id, edge.robot1_keyframe_id,
106  weight)
107  elif type(edge) is Edge:
108  return Edge(edge.i, edge.j, weight)
109 
110  def update_nb_poses(self, edge):
111  """The number of poses should be the maximal edge id known
112 
113  Args:
114  edge (EdgeInterRobot): loop closure edge
115  """
116  self.nb_posesnb_poses[edge.robot0_id] = max(self.nb_posesnb_poses[edge.robot0_id],
117  edge.robot0_keyframe_id + 1)
118  self.nb_posesnb_poses[edge.robot1_id] = max(self.nb_posesnb_poses[edge.robot1_id],
119  edge.robot1_keyframe_id + 1)
120 
121  def update_initial_fixed_edge_exists(self, fixed_edge):
122  """Maintains a bool for each neighbors to know if we have at least a known link.
123  In cases where no initial fixed edge exists, we perform the greedy selection
124 
125  Args:
126  fixed_edge (EdgeInterRobot): fixed edge in the graph
127  """
128  if fixed_edge.robot0_id != fixed_edge.robot1_id:
129  self.initial_fixed_edge_existsinitial_fixed_edge_exists[fixed_edge.robot0_id] = True
130  self.initial_fixed_edge_existsinitial_fixed_edge_exists[fixed_edge.robot1_id] = True
131 
132  def set_graph(self, fixed_edges, candidate_edges):
133  """Fill graph struct
134 
135  Args:
136  fixed_edges (list(EdgeInterRobot)): edges that are already computed
137  candidate_edges (list(EdgeInterRobot)): candidate edges to compute
138  """
139  self.fixed_edgesfixed_edges = fixed_edges
140 
141  # Extract nb of poses for ids graphs
142  for e in self.fixed_edgesfixed_edges:
143  self.update_nb_posesupdate_nb_poses(e)
144  self.update_initial_fixed_edge_existsupdate_initial_fixed_edge_exists(e)
145 
146  for e in candidate_edges:
147  self.update_nb_posesupdate_nb_poses(e)
148 
149  for e in candidate_edges:
150  self.candidate_edgescandidate_edges[self.edge_keyedge_key(e)] = e
151 
152  def add_fixed_edge(self, edge):
153  """Add an already computed edge to the graph
154 
155  Args:
156  edge (EdgeInterRobot): inter-robot edge
157  """
158  self.fixed_edgesfixed_edges.append(edge)
159  # Update nb of poses and initial edge check
160  self.update_nb_posesupdate_nb_poses(edge)
161  self.update_initial_fixed_edge_existsupdate_initial_fixed_edge_exists(edge)
162 
163  def add_candidate_edge(self, edge):
164  """Add a candidate edge to the graph
165 
166  Args:
167  edge (EdgeInterRobot): inter-robot edge
168  """
169  # Check if the edge is not a failed edge or fixed edge
170  if self.edge_keyedge_key(edge) in self.already_considered_matchesalready_considered_matches:
171  return
172 
173  # Otherwise add it to the candidate edges
174  self.candidate_edgescandidate_edges[self.edge_keyedge_key(edge)] = edge
175  # Update nb of poses
176  self.update_nb_posesupdate_nb_poses(edge)
177 
178  def remove_candidate_edges(self, edges, failed=False):
179  """Remove candidate edge from the graph
180 
181  Args:
182  edges (list(EdgeInterRobot)): inter-robot edges
183  """
184  keys = list(self.candidate_edgescandidate_edges.keys())
185  for k in keys:
186  if self.candidate_edgescandidate_edges[k] in edges:
187  del self.candidate_edgescandidate_edges[k]
188 
189  for edge in edges:
190  self.already_considered_matchesalready_considered_matches.add(self.edge_keyedge_key(edge))
191 
192  def candidate_edges_to_fixed(self, edges):
193  """Move candidate edges to fixed.
194  Use when candidates are successfully converted into measurements
195 
196  Args:
197  edges (list(EdgeInterRobot)): inter-robot edges
198  """
199  for i in range(len(edges)):
200  edges[i] = self.replace_weightreplace_weight(edges[i], weight=self.fixed_weightfixed_weight)
201  self.update_initial_fixed_edge_existsupdate_initial_fixed_edge_exists(edges[i])
202  self.fixed_edgesfixed_edges.extend(edges)
203  self.remove_candidate_edgesremove_candidate_edges(edges)
204 
205  def greedy_initialization(self, nb_candidates_to_choose, edges):
206  """Greedy weight initialization
207 
208  Args:
209  nb_candidates_to_choose (int): number of edges to choose
210  edges (list(Edge)): candidate_edges
211  """
212  weights = [e.weight for e in edges]
213  nb_edges = len(weights)
214  w_init = np.zeros(nb_edges)
215  indices = np.argpartition(
216  weights, -nb_candidates_to_choose)[-nb_candidates_to_choose:]
217  w_init[indices] = 1.0
218  return w_init
219 
220  def pseudo_greedy_initialization(self, nb_candidates_to_choose, nb_random,
221  edges):
222  """Greedy weight initialization
223  Greedy initialization with for the first nb_candidates_to_choose-nb_random
224  then random choice
225 
226  Args:
227  nb_candidates_to_choose (int): number of edges to choose
228  nb_random (int): number of edges to choose randomly
229  edges (list(Edge)): candidate_edges
230  """
231  nb_greedy = nb_candidates_to_choose - nb_random
232  w_init = self.greedy_initializationgreedy_initialization(nb_greedy, edges)
233  nb_edges = len(edges)
234  i = 0
235  trial = 0
236  max_trials = 2 * nb_random
237  while i < nb_random and trial < max_trials:
238  j = int(np.random.rand() * nb_edges)
239  if w_init[j] < 0.5:
240  w_init[j] = 1.0
241  i = i + 1
242  trial += 1
243  if trial >= max_trials:
244  w_init = self.greedy_initializationgreedy_initialization(nb_candidates_to_choose, edges)
245  return w_init
246 
247  def random_initialization(self, nb_candidates_to_choose, edges):
248  """Random weight initialization
249 
250  Args:
251  nb_candidates_to_choose (int): number of edges to choose
252  """
253  for e in range(len(edges)):
254  edges[e] = self.replace_weightreplace_weight(edges[e], np.random.rand())
255  return self.greedy_initializationgreedy_initialization(nb_candidates_to_choose, edges)
256 
257  def connection_biased_greedy_selection(self, nb_candidates_to_choose,
258  edges, is_robot_included):
259  """Greedy weight initialization with connection bias
260  Prioritize edges that connect unconnected robots.
261  """
262  nb_candidate_chosen = 0
263  edges_copy = edges.copy()
264  edges_ids_to_select = []
265  rids = [r for r in is_robot_included.keys() if is_robot_included[r]]
266  for rid in rids:
267  if not self.initial_fixed_edge_existsinitial_fixed_edge_exists[rid]:
268  max_weight = -1
269  max_edge = None
270  for i in range(len(edges_copy)):
271  if edges_copy[i].robot0_id == rid or edges_copy[
272  i].robot1_id == rid:
273  if edges_copy[i].weight > max_weight:
274  max_weight = edges_copy[i].weight
275  max_edge = i
276  if max_edge is not None:
277  edges_ids_to_select.append(max_edge)
278  edges_copy[max_edge] = self.replace_weightreplace_weight(
279  edges_copy[max_edge], weight=0.0)
280  nb_candidate_chosen += 1
281 
282  w_init = np.zeros(len(edges))
283  if nb_candidates_to_choose - nb_candidate_chosen > 0:
284  w_init = self.greedy_initializationgreedy_initialization(
285  nb_candidates_to_choose - nb_candidate_chosen,
286  self.rekey_edgesrekey_edges(edges_copy, is_robot_included))
287  for i in edges_ids_to_select:
288  w_init[i] = 1.0
289  return w_init
290 
291  def compute_offsets(self, is_robot_included):
292  """Compute rekey offsets
293 
294  Args:
295  is_robot_included dict(int, bool): Indicates if the robot
296  is connected and in communication range
297  """
298  # Offsets required to put rekey nodes such
299  # that they are all in a single graph
300  self.offsetsoffsets = {}
301  for i in range(self.max_nb_robotsmax_nb_robots):
302  self.offsetsoffsets[i] = 0
303  # Compute offsets
304  previous_offset = 0
305  previous_nb_poses = 0
306  for id in range(self.max_nb_robotsmax_nb_robots):
307  if is_robot_included[id]:
308  self.offsetsoffsets[id] = previous_offset + previous_nb_poses
309  previous_offset = self.offsetsoffsets[id]
310  previous_nb_poses = self.nb_posesnb_poses[id]
311 
312  def rekey_edges(self, edges, is_robot_included):
313  """Modify keys (nodes ID) from robot_id+keyframe_id to node_id
314  Result example: 3 robots with 10 nodes eachs
315  robot 0 nodes id = 1 to 9
316  robot 1 nodes id = 10 to 19
317  robot 2 nodes id = 20 to 29
318 
319  Args:
320  edges (dict(EdgeInterRobot)): inter-robot edges
321  is_robot_included dict(int, bool): Indicates if the robot
322  is connected and in communication range
323 
324  Returns:
325  list(Edge): edges with keys for MAC problem
326  """
327  # Rekey edges
328  rekeyed_edges = []
329  for e in edges:
330  if is_robot_included[e.robot0_id] and is_robot_included[
331  e.robot1_id]:
332  i = self.offsetsoffsets[e.robot0_id] + e.robot0_keyframe_id
333  j = self.offsetsoffsets[e.robot1_id] + e.robot1_keyframe_id
334  rekeyed_edges.append(Edge(i, j, e.weight))
335  return rekeyed_edges
336 
337  def get_included_edges(self, edges, is_robot_included):
338  """TODO
339  """
340  # Rekey edges
341  included_edges = []
342  for e in edges:
343  if is_robot_included[e.robot0_id] and is_robot_included[
344  e.robot1_id]:
345  included_edges.append(e)
346  return included_edges
347 
348  def fill_odometry(self):
349  """Add odometry edges
350  We can infer the odometry edges directly from the number of poses,
351  without communication.
352 
353  Returns:
354  list(Edge): odometry edges
355  """
356  odom_edges = []
357  for i in range(len(self.nb_posesnb_poses)):
358  for k in range(self.nb_posesnb_poses[i] - 1):
359  odom_edges.append(
360  Edge(self.offsetsoffsets[i] + k, self.offsetsoffsets[i] + k + 1,
361  self.fixed_weightfixed_weight))
362  return odom_edges
363 
364  def recover_inter_robot_edges(self, edges, is_robot_included):
365  """Recover IDs from before rekey_edges()
366 
367  Args:
368  edges (list(Edge)): rekeyed edges
369  is_robot_included (dict(int, bool)): indicates if a robot is included
370 
371  Returns:
372  list(EdgeInterRobot): edges
373  """
374  recovered_inter_robot_edges = []
375  for c in range(len(edges)):
376  robot0_id = 0
377  robot1_id = 0
378  for o in self.offsetsoffsets:
379  if o != 0:
380  if is_robot_included[o] and edges[c].i >= self.offsetsoffsets[o]:
381  robot0_id = o
382  if is_robot_included[o] and edges[c].j >= self.offsetsoffsets[o]:
383  robot1_id = o
384  robot0_keyframe_id = edges[c].i - self.offsetsoffsets[robot0_id]
385  robot1_keyframe_id = edges[c].j - self.offsetsoffsets[robot1_id]
386  recovered_inter_robot_edges.append(
387  EdgeInterRobot(robot0_id, robot0_keyframe_id, robot1_id,
388  robot1_keyframe_id, edges[c].weight))
389  return recovered_inter_robot_edges
390 
391  def check_graph_disconnections(self, is_other_robot_considered):
392  """Check if the current graph of potential matches is connected
393 
394  Args:
395  is_other_robot_considered: dict(int, bool): indicates which
396  other robots are are to be included
397 
398  Returns:
399  dict(int, bool): dict indicating if each robot is connected
400  """
401  is_robot_connected = {}
402  for i in range(self.max_nb_robotsmax_nb_robots):
403  if i == self.robot_idrobot_id:
404  is_robot_connected[i] = True
405  else:
406  is_robot_connected[i] = False
407  for edge in self.fixed_edgesfixed_edges:
408  if is_other_robot_considered[edge.robot0_id]:
409  is_robot_connected[edge.robot0_id] = True
410  if is_other_robot_considered[edge.robot1_id]:
411  is_robot_connected[edge.robot1_id] = True
412  for edge in self.candidate_edgescandidate_edges.values():
413  if is_other_robot_considered[edge.robot0_id]:
414  is_robot_connected[edge.robot0_id] = True
415  if is_other_robot_considered[edge.robot1_id]:
416  is_robot_connected[edge.robot1_id] = True
417  return is_robot_connected
418 
419  def check_initial_fixed_measurements_exists(self, is_robot_included):
420  """Check if we have an initial fixed measurement with each robot included
421  If not, greedy selection should be used
422 
423  Args:
424  is_robot_included dict(bool): indicates if each robot is included
425 
426  Returns:
427  bool: check result
428  """
429  initial_fixed_measurements_exists = True
430  for rid in is_robot_included:
431  if is_robot_included[rid] and (
432  not self.initial_fixed_edge_existsinitial_fixed_edge_exists[rid]):
433  initial_fixed_measurements_exists = False
434  return initial_fixed_measurements_exists
435 
436  def run_mac_solver(self, fixed_edges, candidate_edges, w_init,
437  nb_candidates_to_choose):
438  """Run the maximalization of algebraic connectivity
439 
440  Args:
441  fixed_edges list(EdgeInterRobot): fixed edges
442  candidate_edges list(EdgeInterRobot): candidate edges
443  w_init (np.array): One-hot selection vector
444  nb_candidates_to_choose (int): budger
445  """
446  mac = MAC(fixed_edges, candidate_edges, self.total_nb_posestotal_nb_poses)
447 
448  result = w_init.copy()
449  trial = 0
450  while trial < nb_candidates_to_choose:
451  try:
452  result, _, _ = mac.fw_subset(w_init,
453  nb_candidates_to_choose,
454  max_iters=self.max_itersmax_iters)
455  break
456  except:
457  # This should happend very rarely.
458  # find_fieldler_pair triggers a singular matrix exception
459  # when the mac select measurements leading to graph disconnection.
460  # Once at least one measurement is fixed connecting each robot it won't happen.
461  # We vary with increasing randomness the initial guess until we reach a viable solution.
462  trial = trial + 1
463  w_init = self.pseudo_greedy_initializationpseudo_greedy_initialization(
464  nb_candidates_to_choose, trial, candidate_edges)
465  continue
466  return result
467 
469  nb_candidates_to_choose,
470  is_other_robot_considered,
471  greedy_initialization=True):
472  """Solve algebraic connectivity maximization
473 
474  Args:
475  nb_candidates_to_choose (int): number of candidates to choose,
476  related to a computation/communication budget
477  is_other_robot_considered: dict(int, bool): indicates which
478  other robots are in communication range
479  greedy_initialization: perform greedy initialization based on similarity
480 
481  Returns:
482  list(EdgeInterRobot): selected edges
483  """
484  # Check connectivity
485  is_robot_included = self.check_graph_disconnectionscheck_graph_disconnections(
486  is_other_robot_considered)
487 
488  # Rekey multi-robot edges to single graph
489  self.compute_offsetscompute_offsets(is_robot_included)
490  rekeyed_fixed_edges = self.rekey_edgesrekey_edges(self.fixed_edgesfixed_edges,
491  is_robot_included)
492  rekeyed_fixed_edges.extend(self.fill_odometryfill_odometry())
493  rekeyed_candidate_edges = self.rekey_edgesrekey_edges(
494  self.candidate_edgescandidate_edges.values(), is_robot_included)
495 
496  if nb_candidates_to_choose > len(rekeyed_candidate_edges):
497  nb_candidates_to_choose = len(rekeyed_candidate_edges)
498 
499  if len(rekeyed_candidate_edges) > 0:
500  # Compute number of poses
501  self.total_nb_posestotal_nb_poses = 0
502  for n in range(len(self.nb_posesnb_poses)):
503  self.total_nb_posestotal_nb_poses = self.total_nb_posestotal_nb_poses + self.nb_posesnb_poses[n]
504 
505  # Initial guess
506  if greedy_initialization:
507  w_init = self.greedy_initializationgreedy_initialization(nb_candidates_to_choose,
508  rekeyed_candidate_edges)
509  else:
510  w_init = self.random_initializationrandom_initialization(nb_candidates_to_choose,
511  rekeyed_candidate_edges)
512 
513  if self.paramsparams[
514  "frontend.enable_sparsification"] and self.check_initial_fixed_measurements_existscheck_initial_fixed_measurements_exists(
515  is_robot_included):
516  result = self.run_mac_solverrun_mac_solver(rekeyed_fixed_edges,
517  rekeyed_candidate_edges, w_init,
518  nb_candidates_to_choose)
519  else:
520  result = self.connection_biased_greedy_selectionconnection_biased_greedy_selection(
521  nb_candidates_to_choose,
522  self.get_included_edgesget_included_edges(self.candidate_edgescandidate_edges.values(),
523  is_robot_included),
524  is_robot_included)
525 
526  if self.paramsparams["evaluation.enable_sparsification_comparison"]:
527  self.sparsification_comparison_logssparsification_comparison_logs(rekeyed_candidate_edges,
528  is_robot_included, w_init,
529  result)
530 
531  selected_edges = [
532  rekeyed_candidate_edges[i]
533  for i in np.nonzero(result.astype(int))[0]
534  ]
535 
536  # Return selected multi-robot edges
537  inter_robot_edges = self.recover_inter_robot_edgesrecover_inter_robot_edges(
538  selected_edges, is_robot_included)
539  self.remove_candidate_edgesremove_candidate_edges(inter_robot_edges)
540 
541  return inter_robot_edges
542  else:
543  return []
544 
545  def sparsification_comparison_logs(self, rekeyed_candidate_edges,
546  is_robot_included, greedy_result,
547  mac_result):
548  """ TODO: document
549  """
550  self.log_greedy_edgeslog_greedy_edges = self.recover_inter_robot_edgesrecover_inter_robot_edges([
551  rekeyed_candidate_edges[i]
552  for i in np.nonzero(greedy_result.astype(int))[0]
553  ], is_robot_included)
554  self.log_mac_edgeslog_mac_edges = self.recover_inter_robot_edgesrecover_inter_robot_edges([
555  rekeyed_candidate_edges[i]
556  for i in np.nonzero(mac_result.astype(int))[0]
557  ], is_robot_included)
558 
559  def add_match(self, match):
560  """Add match
561 
562  Args:
563  match (EdgeInterRobot): potential match
564  """
565  key = (match.robot0_id, match.robot0_keyframe_id, match.robot1_id,
566  match.robot1_keyframe_id)
567 
568  if key in self.candidate_edgescandidate_edges:
569  if match.weight > self.candidate_edgescandidate_edges[key].weight:
570  self.add_candidate_edgeadd_candidate_edge(match)
571  else:
572  self.add_candidate_edgeadd_candidate_edge(match)
def __init__(self, robot_id=0, max_nb_robots=1, max_iters=20, fixed_weight=1.0, extra_params={ "frontend.enable_sparsification":True, "evaluation.enable_sparsification_comparison":False, })
def run_mac_solver(self, fixed_edges, candidate_edges, w_init, nb_candidates_to_choose)
def connection_biased_greedy_selection(self, nb_candidates_to_choose, edges, is_robot_included)
def select_candidates(self, nb_candidates_to_choose, is_other_robot_considered, greedy_initialization=True)
def sparsification_comparison_logs(self, rekeyed_candidate_edges, is_robot_included, greedy_result, mac_result)