-
Notifications
You must be signed in to change notification settings - Fork 0
/
EdgeCanonicalMultiOutput.java
417 lines (391 loc) · 17.4 KB
/
EdgeCanonicalMultiOutput.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
package com.vincentderk.acircuitminer.miner.canonical;
import com.vincentderk.acircuitminer.miner.Graph;
import com.vincentderk.acircuitminer.miner.StateSingleOutput;
import com.vincentderk.acircuitminer.miner.StateMultiOutput;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import java.util.ArrayDeque;
import java.util.Arrays;
//TODO: Update DOC
/**
* Class that can be used to retrieve the canonical labeling of a rooted DAG.
* <p>
* It does this by converting the graph to a different representation:
* (0,1)(0,2)A(1,3)(1,4)B... Where (0,1) stands for a connection between node 0
* and node 1, 0 being the node higher in the hierarchy. A and B are the labels
* of the node. The idea is that the label follows after enumerating all
* connections coming into the node (e.g 0 and 1 respectively for A and B).
* <p>
* To get a canonical representation, there are certain restrictions such as the
* least numbers first. This is shown in the example where first the connections
* of 0 are enumerated, then the label (operation) of 0 followed by the
* connections of node 1 and its label (operation). To remove the dependency of
* the representation on the node identifiers, every possible combination of
* assignments (id to node) is looked at and again in a certain order the best
* ('smallest') is picked. This means that, two isomorphic graphs will have the
* same canonical labeling returned.
* <p>
* The possible assignments are performed in a certain way to get a fast
* assignment that adheres to the rule that isomorphic graphs have the canonical
* labeling and non-isomorphic graphs have a different. There are two versions
* of the algorithm which are similar in the assignment but differ in the way
* the assignments are checked.
* <p>
* The first algorithm is a breadth-first approach and is thought (not checked)
* to be the fastest for small patterns. It performs an extra assignment for
* each case and checks whether it can already throw away cases (due to 'larger'
* canonical labeling so far).
* <p>
* The second algorithm is a depth-first approach that first completes one case
* (all nodes have an assignment). Then the other cases (other full
* assignments)are checked in a depth-first manner where in-between the
* assignment of another node we check if the code is 'smaller'/'equal' to the
* current 'smallest' canonical labeling. A branch in the search tree where we
* find a 'larger' canonical labeling can be stopped as soon as it is found to
* be 'larger'. This likely requires less memory than the first algorithm but
* might be slower for small patterns where less memory (and thus IO) was
* required anyway. ({@link #minCanonicalPermutationDFS(dagfsm.Graph)}.
* <p>
* <b>Notable methods:</b>
* <ul>
* <li> {@link #printCode(long[])} Convert the given canonical labeling to a
* more readable (*,+,0,1,2,..) version.</li>
* <li> {@link #minCanonicalPermutation(dagfsm.Graph)} </li> Breadth-first
* approach (likely fastest for small graphs)
* <li> {@link #minCanonicalPermutation(dagfsm.Graph, dagfsm.State)} </li>
* Breadth-first approach (likely fastest for small graphs)
* <li> {@link #minCanonicalPermutationDFS(dagfsm.Graph)} </li> Depth-first
* approach (likely fastest for larger graphs)
* <li> {@link #minCanonicalPermutationDFS(dagfsm.Graph, dagfsm.State)} </li>
* Depth-first approach (likely fastest for larger graphs)
* </ul>
*
* @author Vincent Derkinderen
* @version 2.0
*/
public class EdgeCanonicalMultiOutput {
/**
* Get the minimal canonical code. This creates a {@link StateSingleOutput}
* of the given Graph and calls
* {@link #minCanonicalPermutation(Graph, State)}. During conversion, all
* nodes with the special marker {@link Graph#MARKER} are excluded.
*
* @param g The graph to get the code of.
* @return The canonical code result of the given graph g while excluding
* the nodes with the special marker as label.
*
* @see #minCanonicalPermutation(Graph, State)
*/
public static CodeOccResult minCanonicalPermutation(Graph g) {
int[] outputNodes = {g.getFirstNonOutputNode()};
int[] vertices;
int[] expandable = new int[0];
int[] unexpandable;
IntArrayList verticesList = new IntArrayList();
IntArrayList unexpandableList = new IntArrayList();
for (int i = 0; i < g.label.length; i++) {
if (g.label[i] == Graph.INPUT) {
unexpandableList.add(i);
} else if (g.label[i] != Graph.MARKER) {
verticesList.add(i);
}
}
vertices = verticesList.toIntArray();
verticesList = null;
unexpandable = unexpandableList.toIntArray();
unexpandableList = null;
StateMultiOutput state = new StateMultiOutput(vertices, expandable, unexpandable, outputNodes);
return minCanonicalPermutation(g, state);
}
/**
* Get the minimal canonical code. It is assumed that the root node is
* already correct.
* <p>
* Beware, the incoming edges to each node may only contain distinct values.
* No node may be directly connected to the same node twice.
*
* @param g The graph
* @param state The state of which the code is returned.
* @return The minimal canonical code of the given state. Each (e1,e2) is a
* long. Every set of edges starting from a certain node is followed by the
* label of that node.
*/
public static CodeOccResult minCanonicalPermutation(Graph g, StateMultiOutput state) {
int nbVertex = state.vertices.length;
CodeOccResult bestResult = null;
CanonicalStateMultiOutput bestCanonicalState = null;
int[] outputNodeSequence = state.outputNodes.clone(); //Relies on outputNodes being sorted
do {
CanonicalStateMultiOutput root = new CanonicalStateMultiOutput(state, outputNodeSequence);
ArrayDeque<CanonicalStateMultiOutput> queue = new ArrayDeque<>();
ArrayDeque<CanonicalStateMultiOutput> list = new ArrayDeque<>();
queue.add(root);
//Encoding of the edges
long[] code = new long[0];
for (int i = 0; i < nbVertex; i++) {
while (!queue.isEmpty()) {
CanonicalStateMultiOutput c_state = queue.poll();
list.addAll(c_state.expand(g));
}
queue = prune(list);
list = new ArrayDeque<>();
//Update current code
long[] extra = queue.peek().code;
int oldSize = code.length;
code = Arrays.copyOf(code, code.length + extra.length);
System.arraycopy(extra, 0, code, oldSize, extra.length);
}
CanonicalStateMultiOutput canState = queue.getFirst();
if (bestCanonicalState == null || bestCanonicalState.compareTo(canState) > 0) {
bestResult = new CodeOccResult(code, canState.getVerticesInOrder(), canState.inputCount);
bestCanonicalState = canState;
}
} while ((outputNodeSequence = CanonicalStateMultiOutput.getNextPermutation(outputNodeSequence)) != null);
return bestResult;
}
/**
* Convert the given code to a more readable version (+,*,0,1,2,...)
*
* @param code The canonical labeling, as is stored in
* {@link CodeOccResult}.
* @return The readable version of the given code. Each element in code
* represents either a (x,y) or A where A is an operation.
* <ul>
* <li>If it is an operation, it is converted to *,+ or i (input). To denote
* and operation with an additional external output, o is appended. e.g. *o
* and +o.</li>
* <li>If it an edge, it is converted to the (x,y) representation.</li>
* </ul>
* This is done for each element and appended in that order.
*
* @throws IllegalArgumentException if there is an operation that it does
* not cover. Only
* {@link Graph#SUM}, {@link Graph#SUM_OUTPUT}, {@link Graph#PRODUCT}, {@link Graph#PRODUCT_OUTPUT}
* and {@link Graph#INPUT} are allowed.
*/
public static String printCode(long[] code) {
StringBuilder builder = new StringBuilder();
long MASK = ((long) 1 << 32) - 1; //first 32 bits
for (long l : code) {
if (l < Long.MAX_VALUE - Graph.HIGHEST_OP) {
builder.append("(");
builder.append(l >> 32);
builder.append(",");
builder.append(l & MASK);
builder.append(")");
} else {
short label = (short) (Long.MAX_VALUE - l);
switch (label) {
case Graph.PRODUCT:
builder.append("*");
break;
case Graph.PRODUCT_OUTPUT:
builder.append("*o");
break;
case Graph.SUM:
builder.append("+");
break;
case Graph.SUM_OUTPUT:
builder.append("+o");
break;
case Graph.INPUT:
builder.append("i");
break;
default:
throw new IllegalArgumentException("Error in edge code for l: " + l);
}
}
}
return builder.toString();
}
//TODO: Update CanonicalState compareTo + add output label
/**
* Iterates over the given list and returns the
* {@link CanonicalState CanonicalStates} with the 'smallest' code.
*
* @param list The list of CanonicalStates to check. This list is modified.
* @return A new list of all the CanonicalStates that have the 'smallest'
* code as defined by {@link CanonicalState#compareTo(CanonicalState)}.
*/
private static ArrayDeque<CanonicalStateMultiOutput> prune(ArrayDeque<CanonicalStateMultiOutput> list) {
ArrayDeque<CanonicalStateMultiOutput> result = new ArrayDeque<>();
CanonicalStateMultiOutput min = list.poll();
while (!list.isEmpty()) {
CanonicalStateMultiOutput state = list.poll();
int compare = min.compareTo(state);
if (compare == 0) {
result.add(state);
} else if (compare > 0) { //new minimum
min = state;
result.clear();
}/* else {
System.out.println("pruned " + printCode(state.code) + " by " + printCode(min.code) + " with prunedRev: " + state.revAllocation);
}*/
}
result.add(min);
return result;
}
// /**
// * Get the minimal canonical code using a Depth first search. This creates a
// * {@link State} of the given Graph and calls
// * {@link #minCanonicalPermutationDFS(Graph, State)}. During
// * conversion, all nodes with the special marker {@link Graph#MARKER} are
// * excluded.
// *
// * @param g The graph to get the code of.
// * @return The canonical code result of the given graph g while excluding
// * the nodes with the special marker as label.
// *
// * @see #minCanonicalPermutationDFS(Graph, State)
// */
// public static CodeOccResult minCanonicalPermutationDFS(Graph g) {
// int root = g.getFirstNonOutputNode();
// int[] vertices;
// int[] expandable = new int[0];
// int[] unexpandable;
// int interNode = -1;
//
// IntArrayList verticesList = new IntArrayList();
// IntArrayList unexpandableList = new IntArrayList();
//
// for (int i = 0; i < g.label.length; i++) {
// if (g.label[i] == Graph.INPUT) {
// unexpandableList.add(i);
// } else if (g.label[i] != Graph.MARKER) {
// verticesList.add(i);
// }
// }
//
// vertices = verticesList.toIntArray();
// verticesList = null;
// unexpandable = unexpandableList.toIntArray();
// unexpandableList = null;
//
// State state = new State(root, vertices, expandable, unexpandable, interNode);
// return minCanonicalPermutationDFS(g, state);
// }
//
// /**
// * Get the minimal canonical code using an Depth First approach. It is
// * assumed that the root node is already correct.
// *
// * @param g The graph
// * @param state The state of which the code is returned.
// * @return The minimal canonical code of the given state. Each (e1,e2) is a
// * long. Every set of edges starting from a certain node is followed by the
// * label of that node.
// */
// public static CodeOccResult minCanonicalPermutationDFS(Graph g, State state) {
// /**
// * Since we perform a depth-first approach we only need one
// * CanonicalState (c_state) to keep track of the ids assigned to the
// * nodes and which node to assign next.
// *
// * We use DecisionPointInfo objects (returned by c_state.expandDFS) that
// * store the information on the decision made (branch). We store these
// * objects on a stack and when we backtrack to choose a next branch, we
// * use that information object to rollback the c_state. After that,
// * still using the same decision object, we can go into the next branch
// * (expandDFS(g, dpI)).
// */
// ArrayDeque<DecisionPointInfo> stack = new ArrayDeque<>();
//
// CanonicalState c_state = new CanonicalState(state);
// DecisionPointInfo root = c_state.expandDFS(g);
// stack.add(root);
//
// long[] best_code = c_state.code;
// Int2IntOpenHashMap best_revAllocation = null;
//
// //init - full_forward
// while (c_state.extensions.length > 0) {
// stack.push(c_state.expandDFS(g));
// //add code
// int oldLength = best_code.length;
// best_code = Arrays.copyOf(best_code, best_code.length + c_state.code.length);
// System.arraycopy(c_state.code, 0, best_code, oldLength, c_state.code.length);
// }
// best_revAllocation = c_state.revAllocation.clone();
// int inputCount = c_state.inputCount; //Stays the same
// int currentCodeCompareIndex = best_code.length;
//
// // Go back and forth in search tree
// while (!stack.isEmpty()) {
// //backwards
// DecisionPointInfo lastDpI = stack.peek();
// c_state.rollBack(lastDpI);
//
// if (c_state.expandDFS(g, lastDpI) == null) {
// stack.pop();
// currentCodeCompareIndex -= (g.inc[lastDpI.extension].length + 1);
// continue; //back to while to continue going backwards
// }
//
// //forwards
// boolean stop = false;
// boolean replacing = false;
// while (c_state.extensions.length > 0 && !stop) {
// stack.push(c_state.expandDFS(g));
//
// if (replacing) {
// //fill in best
// System.arraycopy(c_state.code, 0, best_code, currentCodeCompareIndex, c_state.code.length);
// } else {
// int c = compareSubcode(c_state.code, 0, best_code, currentCodeCompareIndex);
//
// if (c > 0) {
// stop = true;
// lastDpI = stack.pop();
// c_state.rollBack(lastDpI);
// currentCodeCompareIndex -= (g.inc[lastDpI.extension].length + 1);
// } else if (c < 0) {
// replacing = true;
// System.arraycopy(c_state.code, 0, best_code, currentCodeCompareIndex, c_state.code.length);
// }
// }
// currentCodeCompareIndex += c_state.code.length;
// }
//
// if (replacing) {
// best_revAllocation = c_state.revAllocation.clone();
// }
// }
// CodeOccResult result = new CodeOccResult(best_code, c_state.getVerticesInOrder(best_revAllocation), inputCount);
//
// return result;
// }
//
// /**
// * Check how arr1 compares to arr2 starting from startIndex1 and startIndex2
// * respectively.
// *
// * @param arr1 The first array
// * @param startIndex1 The index to start comparing from, of the first array.
// * @param arr2 The second array
// * @param startIndex2 The index to start comparing with, of the second
// * array.
// * @return The elements of arr1 and arr2 are compared starting from
// * startIndex1 and startIndex2 respectively.
// * <ul>
// * <li>value less than 0 is returned if arr1 has an element smaller than
// * that of arr2.</li>
// * <li>0 is returned if arr1 and arr2 are equal in the compared subset.</li>
// * <li>value greater than 0 is returned if arr1 has an alement larger than
// * that of arr2.</li>
// * </ul>
// *
// * When one array is longer than the other but they are equal up to that
// * point, only up to that point is checked.
// */
// private static int compareSubcode(long[] arr1, int startIndex1, long[] arr2, int startIndex2) {
// for (int i = startIndex1, j = startIndex2; i < arr1.length && j < arr2.length; i++, j++) {
// int r = Long.compare(arr1[i], arr2[j]);
//
// if (r != 0) {
// return r;
// }
// }
//
// //Equal so far.
// return 0;
// }
}