OR-Tools  9.6
find_graph_symmetries.h
Go to the documentation of this file.
1 // Copyright 2010-2022 Google LLC
2 // Licensed under the Apache License, Version 2.0 (the "License");
3 // you may not use this file except in compliance with the License.
4 // You may obtain a copy of the License at
5 //
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13 
14 // This class solves the graph automorphism problem
15 // (https://en.wikipedia.org/wiki/Graph_automorphism), a variant of the famous
16 // graph isomorphism problem (https://en.wikipedia.org/wiki/Graph_isomorphism).
17 //
18 // The algorithm is largely based on the following article, published in 2008:
19 // "Faster Symmetry Discovery using Sparsity of Symmetries" by Darga, Sakallah
20 // and Markov. http://web.eecs.umich.edu/~imarkov/pubs/conf/dac08-sym.pdf.
21 //
22 // See the comments on the class below for more details.
23 
24 #ifndef OR_TOOLS_ALGORITHMS_FIND_GRAPH_SYMMETRIES_H_
25 #define OR_TOOLS_ALGORITHMS_FIND_GRAPH_SYMMETRIES_H_
26 
27 #include <memory>
28 #include <string>
29 #include <vector>
30 
31 #include "absl/numeric/int128.h"
32 #include "absl/status/status.h"
33 #include "absl/time/time.h"
36 #include "ortools/graph/graph.h"
38 #include "ortools/util/stats.h"
40 
41 namespace operations_research {
42 
43 class SparsePermutation;
44 
46  public:
47  typedef ::util::StaticGraph<> Graph;
48 
49  // If the Graph passed to the GraphSymmetryFinder is undirected, i.e.
50  // for every arc a->b, b->a is also present, then you should set
51  // "is_undirected" to true.
52  // This will, in effect, DCHECK() that the graph is indeed undirected,
53  // and bypass the need for reverse adjacency lists.
54  //
55  // If you don't know this in advance, you may use GraphIsSymmetric() from
56  // ortools/graph/util.h.
57  //
58  // "graph" must not have multi-arcs.
59  // TODO(user): support multi-arcs.
60  GraphSymmetryFinder(const Graph& graph, bool is_undirected);
61 
62  // Whether the given permutation is an automorphism of the graph given at
63  // construction. This costs O(sum(degree(x))) (the sum is over all nodes x
64  // that are displaced by the permutation).
65  bool IsGraphAutomorphism(const DynamicPermutation& permutation) const;
66 
67  // Find a set of generators of the automorphism subgroup of the graph that
68  // respects the given node equivalence classes. The generators are themselves
69  // permutations of the nodes: see http://en.wikipedia.org/wiki/Automorphism.
70  // These permutations may only map a node onto a node of its equivalence
71  // class: two nodes i and j are in the same equivalence class iff
72  // node_equivalence_classes_io[i] == node_equivalence_classes_io[j];
73  //
74  // This set of generators is not necessarily the smallest possible (neither in
75  // the number of generators, nor in the size of these generators), but it is
76  // minimal in that no generator can be removed while keeping the generated
77  // group intact.
78  // TODO(user): verify the minimality in unit tests.
79  //
80  // Note that if "generators" is empty, then the graph has no symmetry: the
81  // only automorphism is the identity.
82  //
83  // The equivalence classes are actually an input/output: they are refined
84  // according to all asymmetries found. In the end, n1 and n2 will be
85  // considered equivalent (i.e. node_equivalence_classes_io[n1] ==
86  // node_equivalence_classes_io[n2]) if and only if there exists a
87  // permutation of nodes that:
88  // - keeps the graph invariant
89  // - maps n1 onto n2
90  // - maps each node to a node of its original equivalence class.
91  //
92  // This method also outputs the size of the automorphism group, expressed as
93  // a factorized product of integers (note that the size itself may be as
94  // large as N!).
95  //
96  // DEADLINE AND PARTIAL COMPLETION:
97  // If the deadline passed as argument (via TimeLimit) is reached, this method
98  // will return quickly (within a few milliseconds of the limit). The outputs
99  // may be partially filled:
100  // - Each element of "generators", if non-empty, will be a valid permutation.
101  // - "node_equivalence_classes_io" will contain the equivalence classes
102  // corresponding to the orbits under all the generators in "generators".
103  // - "factorized_automorphism_group_size" will also be incomplete, and
104  // partially valid: its last element may be undervalued. But all prior
105  // elements are valid factors of the automorphism group size.
106  absl::Status FindSymmetries(
107  std::vector<int>* node_equivalence_classes_io,
108  std::vector<std::unique_ptr<SparsePermutation> >* generators,
109  std::vector<int>* factorized_automorphism_group_size,
110  TimeLimit* time_limit = nullptr);
111 
112  // Fully refine the partition of nodes, using the graph as symmetry breaker.
113  // This means applying the following steps on each part P of the partition:
114  // - Compute the aggregated in-degree of all nodes of the graph, only looking
115  // at arcs originating from nodes in P.
116  // - For each in-degree d=1...max_in_degree, refine the partition by the set
117  // of nodes with in-degree d.
118  // And recursively applying it on all new or modified parts.
119  //
120  // In our use cases, we may call this in a scenario where the partition was
121  // already partially refined on all parts #0...#K, then you should set
122  // "first_unrefined_part_index" to K+1.
123  void RecursivelyRefinePartitionByAdjacency(int first_unrefined_part_index,
124  DynamicPartition* partition);
125 
126  // **** Methods below are public FOR TESTING ONLY. ****
127 
128  // Special wrapper of the above method: assuming that partition is already
129  // fully refined, further refine it by {node}, and propagate by adjacency.
130  // Also, optionally collect all the new singletons of the partition in
131  // "new_singletons", sorted by their part number in the partition.
132  void DistinguishNodeInPartition(int node, DynamicPartition* partition,
133  std::vector<int>* new_singletons_or_null);
134 
135  private:
136  const Graph& graph_;
137 
138  inline int NumNodes() const { return graph_.num_nodes(); }
139 
140  // If the graph isn't symmetric, then we store the reverse adjacency lists
141  // here: for each i in 0..NumNodes()-1, the list of nodes that have an
142  // outgoing arc to i is stored (sorted by node) in:
143  // flattened_reverse_adj_lists_[reverse_adj_list_index_[i] ...
144  // reverse_adj_list_index_[i + 1]]
145  // and can be iterated on easily with:
146  // for (const int tail : TailsOfIncomingArcsTo(node)) ...
147  //
148  // If the graph was specified as symmetric upon construction, both these
149  // vectors are empty, and TailsOfIncomingArcsTo() crashes.
150  std::vector<int> flattened_reverse_adj_lists_;
151  std::vector<int> reverse_adj_list_index_;
153  int node) const;
154 
155  // Deadline management. Populated upon FindSymmetries(). If the passed
156  // time limit is nullptr, time_limit_ will point to dummy_time_limit_ which
157  // is an object with infinite limits by default.
158  TimeLimit dummy_time_limit_;
159  TimeLimit* time_limit_;
160 
161  // Internal search code used in FindSymmetries(), split out for readability:
162  // find one permutation (if it exists) that maps root_node to root_image_node
163  // and such that the image of "base_partition" by that permutation is equal to
164  // the "image_partition". If no such permutation exists, returns nullptr.
165  //
166  // "generators_found_so_far" and "permutations_displacing_node" are used for
167  // pruning in the search. The former is just the "generators" vector of
168  // FindGraphSymmetries(), with the permutations found so far; and the latter
169  // is an inverted index from each node to all permutations (that we found)
170  // that displace it.
171  std::unique_ptr<SparsePermutation> FindOneSuitablePermutation(
172  int root_node, int root_image_node, DynamicPartition* base_partition,
173  DynamicPartition* image_partition,
174  const std::vector<std::unique_ptr<SparsePermutation> >&
175  generators_found_so_far,
176  const std::vector<std::vector<int> >& permutations_displacing_node);
177 
178  // Data structure used by FindOneSuitablePermutation(). See the .cc
179  struct SearchState {
180  int base_node;
181 
182  // We're tentatively mapping "base_node" to some image node. At first, we
183  // just pick a single candidate: we fill "first_image_node". If this
184  // candidate doesn't work out, we'll select all other candidates in the same
185  // image part, prune them by the symmetries we found already, and put them
186  // in "remaining_pruned_image_nodes" (and set "first_image_node" to -1).
187  int first_image_node;
188  std::vector<int> remaining_pruned_image_nodes;
189 
190  int num_parts_before_trying_to_map_base_node;
191 
192  // Only parts that are at or beyond this index, or their parent parts, may
193  // be mismatching between the base and the image partitions.
194  int min_potential_mismatching_part_index;
195 
196  SearchState(int bn, int in, int np, int mi)
197  : base_node(bn),
198  first_image_node(in),
199  num_parts_before_trying_to_map_base_node(np),
200  min_potential_mismatching_part_index(mi) {}
201 
202  std::string DebugString() const;
203  };
204  std::vector<SearchState> search_states_;
205 
206  // Subroutine of FindOneSuitablePermutation(), split out for modularity:
207  // With the partial candidate mapping given by "base_partition",
208  // "image_partition" and "current_permutation_candidate", determine whether
209  // we have a full match (eg. the permutation is a valid candidate).
210  // If so, simply return true. If not, return false but also fill
211  // "next_base_node" and "next_image_node" with what should be the next mapping
212  // decision.
213  //
214  // This also uses and updates "min_potential_mismatching_part_index_io"
215  // to incrementally search for mismatching parts along the partitions.
216  //
217  // Note(user): there may be false positives, i.e. this method may return true
218  // even if the partitions aren't actually a full match, because it uses
219  // fingerprints to compare part. This should almost never happen.
220  bool ConfirmFullMatchOrFindNextMappingDecision(
221  const DynamicPartition& base_partition,
222  const DynamicPartition& image_partition,
223  const DynamicPermutation& current_permutation_candidate,
224  int* min_potential_mismatching_part_index_io, int* next_base_node,
225  int* next_image_node) const;
226 
227  // Subroutine of FindOneSuitablePermutation(), split out for modularity:
228  // Keep only one node of "nodes" per orbit, where the orbits are described
229  // by a subset of "all_permutations": the ones with indices in
230  // "permutation_indices" and that are compatible with "partition".
231  // For each orbit, keep the first node that appears in "nodes".
232  void PruneOrbitsUnderPermutationsCompatibleWithPartition(
233  const DynamicPartition& partition,
234  const std::vector<std::unique_ptr<SparsePermutation> >& all_permutations,
235  const std::vector<int>& permutation_indices, std::vector<int>* nodes);
236 
237  // Temporary objects used by some of the class methods, and owned by the
238  // class to avoid (costly) re-allocation. Their resting states are described
239  // in the side comments; with N = NumNodes().
240  DynamicPermutation tmp_dynamic_permutation_; // Identity(N)
241  mutable std::vector<bool> tmp_node_mask_; // [0..N-1] = false
242  std::vector<int> tmp_degree_; // [0..N-1] = 0.
243  std::vector<int> tmp_stack_; // Empty.
244  std::vector<std::vector<int> > tmp_nodes_with_degree_; // [0..N-1] = [].
245  MergingPartition tmp_partition_; // Reset(N).
246  std::vector<const SparsePermutation*> tmp_compatible_permutations_; // Empty.
247 
248  // Internal statistics, used for performance tuning and debugging.
249  struct Stats : public StatsGroup {
250  Stats()
251  : StatsGroup("GraphSymmetryFinder"),
252  initialization_time("a Initialization", this),
253  initialization_refine_time("b ┗╸Refine", this),
254  invariant_dive_time("c Invariant Dive", this),
255  main_search_time("d Main Search", this),
256  invariant_unroll_time("e ┣╸Dive unroll", this),
257  permutation_output_time("f ┣╸Permutation output", this),
258  search_time("g ┗╸FindOneSuitablePermutation()", this),
259  search_time_fail("h ┣╸Fail", this),
260  search_time_success("i ┣╸Success", this),
261  initial_search_refine_time("j ┣╸Initial refine", this),
262  search_refine_time("k ┣╸Further refines", this),
263  quick_compatibility_time("l ┣╸Compatibility checks", this),
264  quick_compatibility_fail_time("m ┃ ┣╸Fail", this),
265  quick_compatibility_success_time("n ┃ ┗╸Success", this),
266  dynamic_permutation_refinement_time(
267  "o ┣╸Dynamic permutation refinement", this),
268  map_election_std_time(
269  "p ┣╸Mapping election / full match detection", this),
270  map_election_std_mapping_time("q ┃ ┣╸Mapping elected", this),
271  map_election_std_full_match_time("r ┃ ┗╸Full Match", this),
272  automorphism_test_time("s ┣╸[Upon full match] Automorphism check",
273  this),
274  automorphism_test_fail_time("t ┃ ┣╸Fail", this),
275  automorphism_test_success_time("u ┃ ┗╸Success", this),
276  search_finalize_time("v ┣╸[Upon auto success] Finalization", this),
277  dynamic_permutation_undo_time(
278  "w ┣╸[Upon auto fail, full] Dynamic permutation undo", this),
279  map_reelection_time(
280  "x ┣╸[Upon auto fail, partial] Mapping re-election", this),
281  non_singleton_search_time("y ┃ ┗╸Non-singleton search", this),
282  backtracking_time("z ┗╸Backtracking", this),
283  pruning_time("{ ┗╸Pruning", this),
284  search_depth("~ Search Stats: search_depth", this) {}
285 
286  TimeDistribution initialization_time;
287  TimeDistribution initialization_refine_time;
288  TimeDistribution invariant_dive_time;
289  TimeDistribution main_search_time;
290  TimeDistribution invariant_unroll_time;
291  TimeDistribution permutation_output_time;
292  TimeDistribution search_time;
293  TimeDistribution search_time_fail;
294  TimeDistribution search_time_success;
295  TimeDistribution initial_search_refine_time;
296  TimeDistribution search_refine_time;
297  TimeDistribution quick_compatibility_time;
298  TimeDistribution quick_compatibility_fail_time;
299  TimeDistribution quick_compatibility_success_time;
300  TimeDistribution dynamic_permutation_refinement_time;
301  TimeDistribution map_election_std_time;
302  TimeDistribution map_election_std_mapping_time;
303  TimeDistribution map_election_std_full_match_time;
304  TimeDistribution automorphism_test_time;
305  TimeDistribution automorphism_test_fail_time;
306  TimeDistribution automorphism_test_success_time;
307  TimeDistribution search_finalize_time;
308  TimeDistribution dynamic_permutation_undo_time;
309  TimeDistribution map_reelection_time;
310  TimeDistribution non_singleton_search_time;
311  TimeDistribution backtracking_time;
312  TimeDistribution pruning_time;
313 
314  IntegerDistribution search_depth;
315  };
316  mutable Stats stats_;
317 };
318 
319 // HELPER FUNCTIONS: PUBLIC FOR UNIT TESTING ONLY.
320 
321 // Returns, for each node A, the number of pairs of nodes (B, C) such that
322 // arcs A->B, A->C and B->C exist. Skips nodes with degree > max_degree
323 // (this allows to remain linear in the number of nodes, but gives partial
324 // results).
325 // The complexity is O(num_nodes * max_degree²).
326 //
327 // DIFFERENTIATION: In unit test CollisionImpliesIsomorphismInPractice,
328 // this metric differentiated 33 of the 34 non-isomorphic collisions found
329 // across 200K graphs: only one remained.
330 //
331 // Example graph differentiated by this metric, but not by LocalBfsFprint():
332 // ,-1-3-. ,-1-3-.
333 // 0 | | 5 and 0 X 5
334 // `-2-3-' `-2-4-'
335 std::vector<int> CountTriangles(const ::util::StaticGraph<int, int>& graph,
336  int max_degree);
337 
338 // Runs a Breadth-First-Search locally: it stops when we settled the given
339 // number of nodes, though it will finish the current radius.
340 // `visited` will contain either the full connected components, or all the nodes
341 // with distance ≤ R+1 from the source, where R is the radius where we stopped.
342 // `num_within_radius` contains the increasing number of nodes within distance
343 // 0, 1, .., R+1 of the source.
344 void LocalBfs(const ::util::StaticGraph<int, int>& graph, int source,
345  int stop_after_num_nodes, std::vector<int>* visited,
346  std::vector<int>* num_within_radius,
347  // For performance, the user provides us with an already-
348  // allocated bitmask of size graph.num_nodes() with all values set
349  // to "false", which we'll restore in the same state upon return.
350  std::vector<bool>* tmp_mask);
351 
352 } // namespace operations_research
353 
354 #endif // OR_TOOLS_ALGORITHMS_FIND_GRAPH_SYMMETRIES_H_
void RecursivelyRefinePartitionByAdjacency(int first_unrefined_part_index, DynamicPartition *partition)
bool IsGraphAutomorphism(const DynamicPermutation &permutation) const
void DistinguishNodeInPartition(int node, DynamicPartition *partition, std::vector< int > *new_singletons_or_null)
absl::Status FindSymmetries(std::vector< int > *node_equivalence_classes_io, std::vector< std::unique_ptr< SparsePermutation > > *generators, std::vector< int > *factorized_automorphism_group_size, TimeLimit *time_limit=nullptr)
GraphSymmetryFinder(const Graph &graph, bool is_undirected)
StatsGroup(absl::string_view name)
Definition: stats.h:140
A simple class to enforce both an elapsed time limit and a deterministic time limit in the same threa...
Definition: time_limit.h:106
ModelSharedTimeLimit * time_limit
Collection of objects used to extend the Constraint Solver library.
std::vector< int > CountTriangles(const ::util::StaticGraph< int, int > &graph, int max_degree)
void LocalBfs(const ::util::StaticGraph< int, int > &graph, int source, int stop_after_num_nodes, std::vector< int > *visited, std::vector< int > *num_within_radius, std::vector< bool > *tmp_mask)
int nodes