Conceptionally:
- think of an undirected graph where nodes are "colors" or "classes"
- add conflict-edges to this graph whenever two "colors" or "classes" are incompatible
- the term conflict-graph is well-known in combinatorial-opt
In your case, *it might make sense* (reason yourself!) to introduce color 0 as a default and for each node, which has dupes, introduce n_dupes + 1 new colors and add all edges of the this clique (fully connected subgraph)
Example: A, B, B', C, C', C'' (A has no dupe, B has a single dupe, C has two dupes)
Colors = 0, 1, 2, 3, 4, 5
Edges: (1, 2) = clique for B + (3,4), (4, 5),(3,5) clique for C
I recommend visualizing it on paper.
(The 0 was introduced as a default/dummy and there is no need to add a color for non-duped nodes; another D or E without dupes can reuse color 0 as it's not part of any edge we don't need it)
Implementation-wise:
- A: use SetVisitType to color nodes
- B: use CloseVisitTypes (search github issue-tracker or maybe this mailing-list about it)
- C: use AddHardTypeIncompatibility to add (conflict-)edges
I have no Python-code, but i did code something like this (C++) in the past, but keep in mind, that:
- this code is in some sense rather generic, as i compress / fuse multiple independent conflict-graphs defined at runtime (on some potentially very large graph) -> you can explicitly "compress"
- this code is in some sense reflecting some special use-case and therefore var-names and such can look strange
- this code calls into functions not shown (FindMaximalStrongStructuralEquivalence)
1 // "Compress" multiple independent conflict-graphs
2
FindMaximalStrongStructuralEquivalence mse(g);
3 mse.calculate();
4
5 absl::flat_hash_map<unsigned int, unsigned int> node_to_partition_class;
6 unsigned int pc = 0;
7 for(const auto& partition_class : mse.partition) {
8 for(auto node : partition_class) {
9 node_to_partition_class.insert( {node, pc} );
10 }
11 pc++;
12 }
13 VLOG(1) << absl::StrFormat("-> #partitions / visit-types: %d", mse.partition.size());
14
15 boost::adjacency_list<boost::setS, boost::vecS, boost::undirectedS> g_contracted(mse.partition.size());
16 {
17 auto es = boost::edges(g);
18 for(auto eit = es.first; eit != es.second; ++eit) {
19 auto v_source = boost::source(*eit, g);
20 auto v_target = boost::target(*eit, g);
21 boost::add_edge(
node_to_partition_class.at(v_source),
node_to_partition_class.at(v_target), g_contracted);
22 }
23 }
24
25 // or-tools -> set visit-types of nodes (without depot)
26 for(unsigned int i=0; i<routing_model.routing_nodes.size(); ++i) {
27 uint64_t node_shifted_zobrist_hash = node_to_hash[i + production_model.production_nodes.size()]; // shifted
28 unsigned int node_shifted_compressed_repr =
ordered_unique_hashes_inverse.at(node_shifted_zobrist_hash);
29 VLOG(5) << absl::StrFormat("prep visit-type %d", i + 1);
30 model->SetVisitType(
31 i + 1,
32
node_to_partition_class.at(node_shifted_compressed_repr),
33 operations_research::RoutingModel::VisitTypePolicy::TYPE_SIMULTANEOUSLY_ADDED_AND_REMOVED);
34 VLOG(5) << absl::StrFormat("node %d visit-type %d", i + 1,
node_to_partition_class.at(node_shifted_compressed_repr));
35 }
36 model->SetVisitType(0, -1, operations_research::RoutingModel::VisitTypePolicy::TYPE_SIMULTANEOUSLY_ADDED_AND_REMOVED); // play it safe with -1
37 VLOG(5) << absl::StrFormat("node %d visit-type %d", 0, -1);
38
39 // or-tools -> add conflicts over visit-types
40 model->CloseVisitTypes();
41
42 {
43 VLOG(1) << absl::StrFormat("-> #conflict-arcs over visit-types: %d", boost::num_edges(g_contracted));
44 auto es = boost::edges(g_contracted);
45 for(auto eit = es.first; eit != es.second; ++eit++) {
46 auto v_source = boost::source(*eit, g_contracted);
47 auto v_target = boost::target(*eit, g_contracted);
48 model->AddHardTypeIncompatibility(v_source, v_target);
49 VLOG(5) << absl::StrFormat("visit-type conflict: %d <-> %d", v_source, v_target);
50 }
51 }
52 }
53
The most relevant things for you:
- line 30/36 | A
- line 40 | B
- line 48 | C
operations_research::RoutingModel::VisitTypePolicy::TYPE_SIMULTANEOUSLY_ADDED_AND_REMOVED looks it a bit arbitrary, but you can copy it (i think i copy-pasted it from an internal test-case in the or-tools sources). There is some docstring, but i never really "got it" from a conceptional view.
The (3?) API-functions you need seem to exist in the Python-wrapper too.
Greetings,
Sascha