OR-Tools  9.3
find_graph_symmetries.h
Go to the documentation of this file.
1// Copyright 2010-2021 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 <vector>
29
30#include "absl/numeric/int128.h"
31#include "absl/status/status.h"
32#include "absl/time/time.h"
35#include "ortools/graph/graph.h"
37#include "ortools/util/stats.h"
39
40namespace operations_research {
41
42class SparsePermutation;
43
45 public:
46 typedef ::util::StaticGraph<> Graph;
47
48 // If the Graph passed to the GraphSymmetryFinder is undirected, i.e.
49 // for every arc a->b, b->a is also present, then you should set
50 // "is_undirected" to true.
51 // This will, in effect, DCHECK() that the graph is indeed undirected,
52 // and bypass the need for reverse adjacency lists.
53 //
54 // If you don't know this in advance, you may use GraphIsSymmetric() from
55 // ortools/graph/util.h.
56 //
57 // "graph" must not have multi-arcs.
58 // TODO(user): support multi-arcs.
59 GraphSymmetryFinder(const Graph& graph, bool is_undirected);
60
61 // Whether the given permutation is an automorphism of the graph given at
62 // construction. This costs O(sum(degree(x))) (the sum is over all nodes x
63 // that are displaced by the permutation).
64 bool IsGraphAutomorphism(const DynamicPermutation& permutation) const;
65
66 // Find a set of generators of the automorphism subgroup of the graph that
67 // respects the given node equivalence classes. The generators are themselves
68 // permutations of the nodes: see http://en.wikipedia.org/wiki/Automorphism.
69 // These permutations may only map a node onto a node of its equivalence
70 // class: two nodes i and j are in the same equivalence class iff
71 // node_equivalence_classes_io[i] == node_equivalence_classes_io[j];
72 //
73 // This set of generators is not necessarily the smallest possible (neither in
74 // the number of generators, nor in the size of these generators), but it is
75 // minimal in that no generator can be removed while keeping the generated
76 // group intact.
77 // TODO(user): verify the minimality in unit tests.
78 //
79 // Note that if "generators" is empty, then the graph has no symmetry: the
80 // only automorphism is the identity.
81 //
82 // The equivalence classes are actually an input/output: they are refined
83 // according to all asymmetries found. In the end, n1 and n2 will be
84 // considered equivalent (i.e. node_equivalence_classes_io[n1] ==
85 // node_equivalence_classes_io[n2]) if and only if there exists a
86 // permutation of nodes that:
87 // - keeps the graph invariant
88 // - maps n1 onto n2
89 // - maps each node to a node of its original equivalence class.
90 //
91 // This method also outputs the size of the automorphism group, expressed as
92 // a factorized product of integers (note that the size itself may be as
93 // large as N!).
94 //
95 // DEADLINE AND PARTIAL COMPLETION:
96 // If the deadline passed as argument (via TimeLimit) is reached, this method
97 // will return quickly (within a few milliseconds of the limit). The outputs
98 // may be partially filled:
99 // - Each element of "generators", if non-empty, will be a valid permutation.
100 // - "node_equivalence_classes_io" will contain the equivalence classes
101 // corresponding to the orbits under all the generators in "generators".
102 // - "factorized_automorphism_group_size" will also be incomplete, and
103 // partially valid: its last element may be undervalued. But all prior
104 // elements are valid factors of the automorphism group size.
105 absl::Status FindSymmetries(
106 std::vector<int>* node_equivalence_classes_io,
107 std::vector<std::unique_ptr<SparsePermutation> >* generators,
108 std::vector<int>* factorized_automorphism_group_size,
109 TimeLimit* time_limit = nullptr);
110
111 // Fully refine the partition of nodes, using the graph as symmetry breaker.
112 // This means applying the following steps on each part P of the partition:
113 // - Compute the aggregated in-degree of all nodes of the graph, only looking
114 // at arcs originating from nodes in P.
115 // - For each in-degree d=1...max_in_degree, refine the partition by the set
116 // of nodes with in-degree d.
117 // And recursively applying it on all new or modified parts.
118 //
119 // In our use cases, we may call this in a scenario where the partition was
120 // already partially refined on all parts #0...#K, then you should set
121 // "first_unrefined_part_index" to K+1.
122 void RecursivelyRefinePartitionByAdjacency(int first_unrefined_part_index,
123 DynamicPartition* partition);
124
125 // **** Methods below are public FOR TESTING ONLY. ****
126
127 // Special wrapper of the above method: assuming that partition is already
128 // fully refined, further refine it by {node}, and propagate by adjacency.
129 // Also, optionally collect all the new singletons of the partition in
130 // "new_singletons", sorted by their part number in the partition.
131 void DistinguishNodeInPartition(int node, DynamicPartition* partition,
132 std::vector<int>* new_singletons_or_null);
133
134 private:
135 const Graph& graph_;
136
137 inline int NumNodes() const { return graph_.num_nodes(); }
138
139 // If the graph isn't symmetric, then we store the reverse adjacency lists
140 // here: for each i in 0..NumNodes()-1, the list of nodes that have an
141 // outgoing arc to i is stored (sorted by node) in:
142 // flattened_reverse_adj_lists_[reverse_adj_list_index_[i] ...
143 // reverse_adj_list_index_[i + 1]]
144 // and can be iterated on easily with:
145 // for (const int tail : TailsOfIncomingArcsTo(node)) ...
146 //
147 // If the graph was specified as symmetric upon construction, both these
148 // vectors are empty, and TailsOfIncomingArcsTo() crashes.
149 std::vector<int> flattened_reverse_adj_lists_;
150 std::vector<int> reverse_adj_list_index_;
152 int node) const;
153
154 // Deadline management. Populated upon FindSymmetries(). If the passed
155 // time limit is nullptr, time_limit_ will point to dummy_time_limit_ which
156 // is an object with infinite limits by default.
157 TimeLimit dummy_time_limit_;
158 TimeLimit* time_limit_;
159
160 // Internal search code used in FindSymmetries(), split out for readability:
161 // find one permutation (if it exists) that maps root_node to root_image_node
162 // and such that the image of "base_partition" by that permutation is equal to
163 // the "image_partition". If no such permutation exists, returns nullptr.
164 //
165 // "generators_found_so_far" and "permutations_displacing_node" are used for
166 // pruning in the search. The former is just the "generators" vector of
167 // FindGraphSymmetries(), with the permutations found so far; and the latter
168 // is an inverted index from each node to all permutations (that we found)
169 // that displace it.
170 std::unique_ptr<SparsePermutation> FindOneSuitablePermutation(
171 int root_node, int root_image_node, DynamicPartition* base_partition,
172 DynamicPartition* image_partition,
173 const std::vector<std::unique_ptr<SparsePermutation> >&
174 generators_found_so_far,
175 const std::vector<std::vector<int> >& permutations_displacing_node);
176
177 // Data structure used by FindOneSuitablePermutation(). See the .cc
178 struct SearchState {
179 int base_node;
180
181 // We're tentatively mapping "base_node" to some image node. At first, we
182 // just pick a single candidate: we fill "first_image_node". If this
183 // candidate doesn't work out, we'll select all other candidates in the same
184 // image part, prune them by the symmetries we found already, and put them
185 // in "remaining_pruned_image_nodes" (and set "first_image_node" to -1).
186 int first_image_node;
187 std::vector<int> remaining_pruned_image_nodes;
188
189 int num_parts_before_trying_to_map_base_node;
190
191 // Only parts that are at or beyond this index, or their parent parts, may
192 // be mismatching between the base and the image partitions.
193 int min_potential_mismatching_part_index;
194
195 SearchState(int bn, int in, int np, int mi)
196 : base_node(bn),
197 first_image_node(in),
198 num_parts_before_trying_to_map_base_node(np),
199 min_potential_mismatching_part_index(mi) {}
200
201 std::string DebugString() const;
202 };
203 std::vector<SearchState> search_states_;
204
205 // Subroutine of FindOneSuitablePermutation(), split out for modularity:
206 // With the partial candidate mapping given by "base_partition",
207 // "image_partition" and "current_permutation_candidate", determine whether
208 // we have a full match (eg. the permutation is a valid candidate).
209 // If so, simply return true. If not, return false but also fill
210 // "next_base_node" and "next_image_node" with what should be the next mapping
211 // decision.
212 //
213 // This also uses and updates "min_potential_mismatching_part_index_io"
214 // to incrementally search for mismatching parts along the partitions.
215 //
216 // Note(user): there may be false positives, i.e. this method may return true
217 // even if the partitions aren't actually a full match, because it uses
218 // fingerprints to compare part. This should almost never happen.
219 bool ConfirmFullMatchOrFindNextMappingDecision(
220 const DynamicPartition& base_partition,
221 const DynamicPartition& image_partition,
222 const DynamicPermutation& current_permutation_candidate,
223 int* min_potential_mismatching_part_index_io, int* next_base_node,
224 int* next_image_node) const;
225
226 // Subroutine of FindOneSuitablePermutation(), split out for modularity:
227 // Keep only one node of "nodes" per orbit, where the orbits are described
228 // by a subset of "all_permutations": the ones with indices in
229 // "permutation_indices" and that are compatible with "partition".
230 // For each orbit, keep the first node that appears in "nodes".
231 void PruneOrbitsUnderPermutationsCompatibleWithPartition(
232 const DynamicPartition& partition,
233 const std::vector<std::unique_ptr<SparsePermutation> >& all_permutations,
234 const std::vector<int>& permutation_indices, std::vector<int>* nodes);
235
236 // Temporary objects used by some of the class methods, and owned by the
237 // class to avoid (costly) re-allocation. Their resting states are described
238 // in the side comments; with N = NumNodes().
239 DynamicPermutation tmp_dynamic_permutation_; // Identity(N)
240 mutable std::vector<bool> tmp_node_mask_; // [0..N-1] = false
241 std::vector<int> tmp_degree_; // [0..N-1] = 0.
242 std::vector<int> tmp_stack_; // Empty.
243 std::vector<std::vector<int> > tmp_nodes_with_degree_; // [0..N-1] = [].
244 MergingPartition tmp_partition_; // Reset(N).
245 std::vector<const SparsePermutation*> tmp_compatible_permutations_; // Empty.
246
247 // Internal statistics, used for performance tuning and debugging.
248 struct Stats : public StatsGroup {
249 Stats()
250 : StatsGroup("GraphSymmetryFinder"),
251 initialization_time("a Initialization", this),
252 initialization_refine_time("b ┗╸Refine", this),
253 invariant_dive_time("c Invariant Dive", this),
254 main_search_time("d Main Search", this),
255 invariant_unroll_time("e ┣╸Dive unroll", this),
256 permutation_output_time("f ┣╸Permutation output", this),
257 search_time("g ┗╸FindOneSuitablePermutation()", this),
258 search_time_fail("h ┣╸Fail", this),
259 search_time_success("i ┣╸Success", this),
260 initial_search_refine_time("j ┣╸Initial refine", this),
261 search_refine_time("k ┣╸Further refines", this),
262 quick_compatibility_time("l ┣╸Compatibility checks", this),
263 quick_compatibility_fail_time("m ┃ ┣╸Fail", this),
264 quick_compatibility_success_time("n ┃ ┗╸Success", this),
265 dynamic_permutation_refinement_time(
266 "o ┣╸Dynamic permutation refinement", this),
267 map_election_std_time(
268 "p ┣╸Mapping election / full match detection", this),
269 map_election_std_mapping_time("q ┃ ┣╸Mapping elected", this),
270 map_election_std_full_match_time("r ┃ ┗╸Full Match", this),
271 automorphism_test_time("s ┣╸[Upon full match] Automorphism check",
272 this),
273 automorphism_test_fail_time("t ┃ ┣╸Fail", this),
274 automorphism_test_success_time("u ┃ ┗╸Success", this),
275 search_finalize_time("v ┣╸[Upon auto success] Finalization", this),
276 dynamic_permutation_undo_time(
277 "w ┣╸[Upon auto fail, full] Dynamic permutation undo", this),
278 map_reelection_time(
279 "x ┣╸[Upon auto fail, partial] Mapping re-election", this),
280 non_singleton_search_time("y ┃ ┗╸Non-singleton search", this),
281 backtracking_time("z ┗╸Backtracking", this),
282 pruning_time("{ ┗╸Pruning", this),
283 search_depth("~ Search Stats: search_depth", this) {}
284
285 TimeDistribution initialization_time;
286 TimeDistribution initialization_refine_time;
287 TimeDistribution invariant_dive_time;
288 TimeDistribution main_search_time;
289 TimeDistribution invariant_unroll_time;
290 TimeDistribution permutation_output_time;
291 TimeDistribution search_time;
292 TimeDistribution search_time_fail;
293 TimeDistribution search_time_success;
294 TimeDistribution initial_search_refine_time;
295 TimeDistribution search_refine_time;
296 TimeDistribution quick_compatibility_time;
297 TimeDistribution quick_compatibility_fail_time;
298 TimeDistribution quick_compatibility_success_time;
299 TimeDistribution dynamic_permutation_refinement_time;
300 TimeDistribution map_election_std_time;
301 TimeDistribution map_election_std_mapping_time;
302 TimeDistribution map_election_std_full_match_time;
303 TimeDistribution automorphism_test_time;
304 TimeDistribution automorphism_test_fail_time;
305 TimeDistribution automorphism_test_success_time;
306 TimeDistribution search_finalize_time;
307 TimeDistribution dynamic_permutation_undo_time;
308 TimeDistribution map_reelection_time;
309 TimeDistribution non_singleton_search_time;
310 TimeDistribution backtracking_time;
311 TimeDistribution pruning_time;
312
313 IntegerDistribution search_depth;
314 };
315 mutable Stats stats_;
316};
317
318} // namespace operations_research
319
320#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(const std::string &name)
Definition: stats.h:138
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.
int nodes