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