| 1 | % (c) 2009-2020 Lehrstuhl fuer Softwaretechnik und Programmiersprachen, | |
| 2 | % Heinrich Heine Universitaet Duesseldorf | |
| 3 | % This software is licenced under EPL 1.0 (http://www.eclipse.org/org/documents/epl-v10.html) | |
| 4 | /* graph_canon.pl - graph isomorphism module 1/11/2005 */ | |
| 5 | ||
| 6 | :- module(graph_canon, [ | |
| 7 | print_cstate_graph/2, print_cstate_graph/1, | |
| 8 | initialise_nauty/0, clear_nauty/0, | |
| 9 | add_state_graph_to_nauty/2 | |
| 10 | ]). | |
| 11 | ||
| 12 | ||
| 13 | :- use_module(error_manager). | |
| 14 | ||
| 15 | %:- compile(sp4_compatibility). | |
| 16 | :- use_module(tools). | |
| 17 | ||
| 18 | :- use_module(module_information,[module_info/2]). | |
| 19 | :- module_info(group,symmetry). | |
| 20 | :- module_info(description,'Compute canonical forms of state graphs and store them using nauty C interface.'). | |
| 21 | ||
| 22 | ||
| 23 | ||
| 24 | /* given the properties clause of the bmachine, extract the names of the | |
| 25 | known constants, in the order of their declaration */ | |
| 26 | % currently works only for machines that do not use inheritance | |
| 27 | ||
| 28 | % removed | |
| 29 | ||
| 30 | % ------------------------------------------------------------------------------ | |
| 31 | ||
| 32 | /* Assert all the elements of the sets used by this machine, for this session of ProB, | |
| 33 | and also those that are symmetric or not */ | |
| 34 | % assert_global_set_info :- better use b_global_sets now | |
| 35 | ||
| 36 | /******************************************************************************/ | |
| 37 | ||
| 38 | /******** STATE AS SINGLE GRAPH ***************/ | |
| 39 | :- use_module(specfile,[expand_const_and_vars/3]). | |
| 40 | ||
| 41 | :- dynamic generate_state_for_dot/0. | |
| 42 | state_graph_for_dot(State,R) :- | |
| 43 | assert(generate_state_for_dot), | |
| 44 | call_cleanup(state_graph(State,R), retract(generate_state_for_dot)). | |
| 45 | ||
| 46 | state_graph(root,R) :- !,R=[]. | |
| 47 | state_graph(concrete_constants(C),SG) :- !, state_graph(C,SG). | |
| 48 | state_graph(const_and_vars(ConstID,S),SG) :- !, | |
| 49 | expand_const_and_vars(ConstID,S,Full), | |
| 50 | reset_n, | |
| 51 | state_graph(Full,[],SG). | |
| 52 | state_graph(S,SG) :- | |
| 53 | reset_n, | |
| 54 | state_graph(S,[],SG). | |
| 55 | state_graph([],Final,Final). | |
| 56 | state_graph([bind(VarName,Value)|T],C,F) :- | |
| 57 | tools:string_escape(VarName,EscVarName), | |
| 58 | (top_level_translate_node(Value,EscVarName,PG) -> true | |
| 59 | %print(translate(Value,VarName,PG)),nl | |
| 60 | ; add_error(state_graph,'Call failed: ',top_level_translate_node(Value,VarName,PG))), | |
| 61 | append(PG,C,C2), | |
| 62 | state_graph(T,C2,F). | |
| 63 | ||
| 64 | top_level_translate_node(DataValue,VariableName,NewTransitions) :- | |
| 65 | % Input: DataValue | |
| 66 | % Output: NewTransitions required to represent the structure | |
| 67 | % In contrast to translate_inner_node, we do not generate new inner artificial nodes; | |
| 68 | % VariableName is used as an arc-label to encode the top-level information | |
| 69 | expand_data_value(DataValue,S1), | |
| 70 | % check whether S1 is a relation/atom/set | |
| 71 | ( relation(S1) -> gen_relation(S1,VariableName,NewTransitions) | |
| 72 | ; emptyset(S1) -> NewTransitions = [(sg_root,VariableName,sg_root)] | |
| 73 | %% [('$Empty_Set',VariableName,sg_root)] has problems as we cannot distinguish between v={} and v={{}} | |
| 74 | %% [] has problems when we have set_up_constants and variables which are all initialised with | |
| 75 | %% the empty set: then we get confused between set_up_constants & initialise_machine | |
| 76 | ; list_set(S1) -> gset(S1,sg_root,VariableName,[],NewTransitions) | |
| 77 | ; S1=(P1,P2) -> % a pair | |
| 78 | translate_inner_node(P1,Node1,NewTrans1), | |
| 79 | translate_inner_node(P2,Node2,NewTrans2), | |
| 80 | append([(Node1,VariableName,Node2)|NewTrans1],NewTrans2,NewTransitions) | |
| 81 | ; % an atom | |
| 82 | NewTransitions = [(DataValue,VariableName,sg_root)] | |
| 83 | ). | |
| 84 | ||
| 85 | :- use_module(custom_explicit_sets,[is_custom_explicit_set/2, try_expand_custom_set_with_catch/3]). | |
| 86 | expand_data_value(DataValue,S1) :- | |
| 87 | (is_custom_explicit_set(DataValue,expand_data_value) | |
| 88 | -> try_expand_custom_set_with_catch(DataValue,S1,expand_data_value) | |
| 89 | ; DataValue=S1 | |
| 90 | ). | |
| 91 | % TO DO: convert to Difference-List to get rid of all the appends below | |
| 92 | ||
| 93 | ||
| 94 | gset([],_,_,F,F). | |
| 95 | gset([S1|T],Parent,TransitionLabel,F,Fi) :- | |
| 96 | translate_inner_node(S1,C1,S1_NewTransitions), | |
| 97 | append([(C1,TransitionLabel,Parent)|S1_NewTransitions],F,F4), | |
| 98 | gset(T,Parent,TransitionLabel,F4,Fi). | |
| 99 | ||
| 100 | gen_relation(S1,VariableName,NewTransitions) :- | |
| 101 | (S1=[H|_],is_pair_to_pair(H) -> MatchTriples=false ; MatchTriples=true), | |
| 102 | grelation(S1,MatchTriples,sg_root,VariableName,[],NewTransitions). | |
| 103 | ||
| 104 | :- use_module(kernel_objects,[infer_value_type/2]). | |
| 105 | is_pair_to_pair(((A,B),(C,D))) :- | |
| 106 | infer_value_type(A,T1), infer_value_type(C,T1), | |
| 107 | infer_value_type(B,T2), infer_value_type(D,T2). | |
| 108 | ||
| 109 | :- use_module(library(lists),[append/2]). | |
| 110 | grelation([],_,_,_,F,F). | |
| 111 | grelation([(E0,E1,E2)|T],true,Parent,VariableName,F,Fi) :- | |
| 112 | % interpret E0 as label for string; note: (E0,E1,E2) matches ((A,B),(C,D)) | |
| 113 | generate_state_for_dot, | |
| 114 | simple_value(E0), | |
| 115 | !, | |
| 116 | translate_inner_node(E1,C1,E1_NewTransitions), | |
| 117 | translate_inner_node(E2,C2,E2_NewTransitions), | |
| 118 | translate_bvalue_for_dot(E0,E0String), | |
| 119 | Label =.. [VariableName,E0String], | |
| 120 | append([E2_NewTransitions,[(C1,Label,C2)|E1_NewTransitions],F],F6), | |
| 121 | grelation(T,true,Parent,VariableName,F6,Fi). | |
| 122 | grelation([(E1,E2)|T],MatchTriples,Parent,VariableName,F,Fi) :- | |
| 123 | translate_inner_node(E1,C1,E1_NewTransitions), | |
| 124 | translate_inner_node(E2,C2,E2_NewTransitions), | |
| 125 | append([E2_NewTransitions,[(C1,VariableName,C2)|E1_NewTransitions],F],F6), | |
| 126 | grelation(T,MatchTriples,Parent,VariableName,F6,Fi). | |
| 127 | ||
| 128 | ||
| 129 | ||
| 130 | :- use_module(preferences,[get_preference/2]). | |
| 131 | ||
| 132 | translate_inner_node(DataValue,NodeinGraph,NewTransitions) :- %% print(trans(DataValue)),nl, | |
| 133 | % Input: DataValue | |
| 134 | % Output: NodeinGraph representing DataValue + NewTransitions required to represent the structure | |
| 135 | (get_preference(dot_state_graph_decompose,false) -> % treat as an atom | |
| 136 | NewTransitions = [], NodeinGraph = DataValue | |
| 137 | ; expand_data_value(DataValue,S1), | |
| 138 | % check whether S1 is a atom/set | |
| 139 | ( emptyset(S1) -> NodeinGraph = [], %'$Empty_Set', | |
| 140 | NewTransitions=[] | |
| 141 | ; cn_value(S1,NodeinGraph) -> NewTransitions=[] | |
| 142 | ; simple_pair_value(S1),generate_state_for_dot -> NodeinGraph=S1, NewTransitions=[] | |
| 143 | ; list_set(S1) -> | |
| 144 | new_sg_node(S1,NodeinGraph), | |
| 145 | gset(S1,NodeinGraph,'$to_el',[],NewTransitions) % use the $to_el label to encode membership, using ':' would be more readable; but less efficient for symmetry (extra layers required) | |
| 146 | ; S1=(P1,P2) | |
| 147 | -> % a complex pair | |
| 148 | translate_inner_node(P1,Node1,NewTrans1), | |
| 149 | translate_inner_node(P2,Node2,NewTrans2), | |
| 150 | new_sg_node(S1,NodeinGraph), | |
| 151 | append([(NodeinGraph,'$from',Node1), | |
| 152 | (NodeinGraph,'$to_el',Node2)|NewTrans1],NewTrans2,NewTransitions) | |
| 153 | ; % an atom | |
| 154 | NewTransitions = [], | |
| 155 | NodeinGraph = S1 | |
| 156 | ) | |
| 157 | ). | |
| 158 | ||
| 159 | % simple values which can easily be rendered like a record | |
| 160 | simple_pair_value((A,B)) :- !,simple_pair_value(A), simple_pair_value(B). | |
| 161 | simple_pair_value(X) :- simple_value(X). | |
| 162 | ||
| 163 | relation([(_,_)|_]). | |
| 164 | relation([(X,_,_)|_]) :- generate_state_for_dot, simple_value(X). % TO DO: check if it is a function ?; maybe also support other nested ternary relations | |
| 165 | emptyset([]). | |
| 166 | list_set([]). | |
| 167 | list_set([_|_]). | |
| 168 | % what about AVL : supposed expanded before | |
| 169 | ||
| 170 | is_set(avl_set(_)). | |
| 171 | is_set(X) :- list_set(X). | |
| 172 | ||
| 173 | simple_value(X) :- atomic(X). | |
| 174 | simple_value(int(_)). | |
| 175 | simple_value(fd(_,_)). | |
| 176 | simple_value(string(_)). | |
| 177 | simple_value(pred_false). | |
| 178 | simple_value(pred_true). | |
| 179 | ||
| 180 | :- dynamic cn/1, cn_value/2. | |
| 181 | ||
| 182 | reset_n :- | |
| 183 | retractall(cn(_)),retractall(cn_value(_,_)),asserta(cn(0)). | |
| 184 | ||
| 185 | /* generate a new state graph node */ | |
| 186 | new_sg_node(Value,abs_cn(J,NestingCol)) :- retract(cn(I)), J is I+1, | |
| 187 | asserta(cn(J)), | |
| 188 | get_abstract_node_type_colour(Value,NestingCol), | |
| 189 | assert(cn_value(Value,abs_cn(J,NestingCol))). | |
| 190 | ||
| 191 | % for the moment: just compute the "depth/nesting" of the relation; we should do something more intelligent | |
| 192 | get_abstract_node_type_colour([],C) :- !,C=1. | |
| 193 | get_abstract_node_type_colour([H|_],C) :- !, get_abstract_node_type_colour(H,CH), | |
| 194 | C is CH+1. | |
| 195 | get_abstract_node_type_colour((A,_),C) :- !, get_abstract_node_type_colour(A,CH), | |
| 196 | C is CH+1. | |
| 197 | get_abstract_node_type_colour(_,0). | |
| 198 | ||
| 199 | ||
| 200 | :- use_module(graph_iso_nauty). | |
| 201 | ||
| 202 | :- use_module(state_space,[current_expression/2]). | |
| 203 | print_cstate_graph(File) :- | |
| 204 | current_expression(_,State), | |
| 205 | print_cstate_graph(State,File). | |
| 206 | print_cstate_graph(State,File) :- | |
| 207 | state_graph_for_dot(State,GCE), | |
| 208 | %print(state_graph(GCE)),nl, | |
| 209 | (graph2dot(GCE,File) -> true | |
| 210 | ; add_error(graph_canon,'Dot conversion failed: ',graph2dot(GCE,File))). | |
| 211 | %,print_nauty_graph(GCE). | |
| 212 | ||
| 213 | /* ---------------------------- */ | |
| 214 | ||
| 215 | initialise_nauty :- nauty_init. | |
| 216 | clear_nauty :- nauty_clear. | |
| 217 | ||
| 218 | add_state_graph_to_nauty(StateExpression,Exists) :- | |
| 219 | (state_graph(StateExpression,SG) | |
| 220 | -> add_nauty_graph(SG,Exists) | |
| 221 | ; add_error(graph_canon,'Could not compute state_graph: ',StateExpression),fail). | |
| 222 | ||
| 223 | /* ---------------------------- */ | |
| 224 | ||
| 225 | ||
| 226 | graph2dot(G,File) :- reset_dot, | |
| 227 | (get_preference(dot_horizontal_layout,true) -> RDir='LR'; RDir='TB'), | |
| 228 | PSize='', %(get_preference(dot_with_page_size,true) -> PSize='page="8.5, 11",ratio=fill,size="7.5,10",' ; PSize=''), | |
| 229 | tell(File), | |
| 230 | format('digraph state {\n graph [~wfontsize=12]\nrankdir=~w;~n',[PSize,RDir]), | |
| 231 | edges2dot(G), | |
| 232 | nodes2dot, | |
| 233 | findall(cluster(Ty,El),cluster(Ty,El),T), | |
| 234 | clusters2dot(T), | |
| 235 | print('}'), | |
| 236 | told. | |
| 237 | :- use_module(b_global_sets). | |
| 238 | cluster(Type,Elements) :- b_global_set(Type), | |
| 239 | % compute all elements of global sets which are used in current state graph | |
| 240 | findall(Translation, (enum_global_type(El,Type), node_col(El,Translation,_,_)),Elements). | |
| 241 | % maybe add boolean ?? | |
| 242 | edges2dot([]). | |
| 243 | edges2dot([(A,B,C)|T]) :- /* edge from A -> C with label B */ | |
| 244 | translate_node_value(A,AID), | |
| 245 | (B = abs_cn(BB,ColBB) | |
| 246 | -> translate_abs_cn_node(BB,ColBB,EdgeLabel) | |
| 247 | ; EdgeLabel = B), | |
| 248 | translate_node_value(C,CID), | |
| 249 | get_col_assoc(edge,EdgeLabel,EdgCol), | |
| 250 | format('\"~w\" -> \"~w\" [label = \"~w\", color = \"~w\"];\n', | |
| 251 | [AID,CID,EdgeLabel,EdgCol]), | |
| 252 | edges2dot(T). | |
| 253 | ||
| 254 | reset_dot :- retractall(col_association(_,_,_)), | |
| 255 | retractall(node_col(_,_,_,_)), | |
| 256 | retractall(cur_col(_,_)), | |
| 257 | assert(cur_col(node,0)), assert(cur_col(edge,99)). | |
| 258 | ||
| 259 | translate_node_value(abs_cn(AA,ColAA), Translation) :- !, | |
| 260 | translate_abs_cn_node(AA,ColAA,Translation). | |
| 261 | translate_node_value(sg_root,Translation) :- !, | |
| 262 | Translation = 'ROOT-NODE', add_node(sg_root,Translation). | |
| 263 | translate_node_value(Val,Translation) :- | |
| 264 | translate_bvalue_for_dot(Val,Translation), | |
| 265 | add_node(Val,Translation). | |
| 266 | ||
| 267 | translate_bvalue_for_dot(string(S),Translation) :- !, | |
| 268 | % normal quotes confuse dot | |
| 269 | %ajoin(['''''',S,''''''],Translation). | |
| 270 | tools:string_escape(S,ES), | |
| 271 | ajoin(['\\"',ES,'\\"'],Translation). | |
| 272 | translate_bvalue_for_dot(Val,ETranslation) :- | |
| 273 | translate:translate_bvalue(Val,Translation), | |
| 274 | tools:string_escape(Translation,ETranslation). | |
| 275 | ||
| 276 | nodes2dot :- node_col(_,Translation,Col,Shape), | |
| 277 | (Shape=record(Label) | |
| 278 | -> | |
| 279 | format('\"~w\" [shape=record, label=\"~w\", color = \"~w\", style = \"filled, solid\"]\n',[Translation,Label,Col]) | |
| 280 | ; | |
| 281 | format('\"~w\" [color = \"~w\", style = \"filled, solid\", shape = \"~w\"]\n',[Translation,Col,Shape]) | |
| 282 | ), | |
| 283 | fail. | |
| 284 | nodes2dot. | |
| 285 | ||
| 286 | :- use_module(kernel_reals,[is_real/2]). | |
| 287 | :- dynamic node_col/4. | |
| 288 | add_node(X,_) :- node_col(X,_,_,_),!. % already processed | |
| 289 | add_node(sg_root,Translation) :- | |
| 290 | assert(node_col(sg_root,Translation,lightblue,diamond)). | |
| 291 | add_node(fd(X,Type),Translation) :- !, | |
| 292 | get_col_assoc(node,Type,Col), | |
| 293 | get_preference(dot_normal_node_shape,Shape), | |
| 294 | assert(node_col(fd(X,Type),Translation,Col,Shape)). | |
| 295 | add_node(int(X),Translation) :- !, | |
| 296 | Col = 'wheat3', %get_col_assoc(node,integer,Col), | |
| 297 | get_preference(dot_normal_node_shape,Shape), | |
| 298 | assert(node_col(int(X),Translation,Col,Shape)). | |
| 299 | add_node(string(X),Translation) :- !, | |
| 300 | Col = 'khaki1', %'lavender' 'burlywood', %get_col_assoc(node,integer,Col), | |
| 301 | get_preference(dot_normal_node_shape,Shape), | |
| 302 | assert(node_col(string(X),Translation,Col,Shape)). | |
| 303 | add_node(pred_true,T) :- !, assert(node_col(pred_true,T,'OliveDrab4',ellipse)). | |
| 304 | add_node(pred_false,T) :- !, assert(node_col(pred_false,T,brown,ellipse)). | |
| 305 | add_node(RecordOrPair,Translation) :- get_fields(RecordOrPair,Fields),!, | |
| 306 | translate_fields(Fields,Atoms), | |
| 307 | ajoin(['|{ ' | Atoms], RecTranslation), | |
| 308 | assert(node_col(RecordOrPair,Translation,burlywood,record(RecTranslation))). % TO DO: maybe use dot record | |
| 309 | add_node(Val,Translation) :- is_set(Val),!, | |
| 310 | Col = 'LightSteelBlue1', %get_col_assoc(node,integer,Col), | |
| 311 | get_preference(dot_normal_node_shape,Shape), | |
| 312 | assert(node_col(Val,Translation,Col,Shape)). | |
| 313 | add_node(Term,Translation) :- is_real(Term,_),!, | |
| 314 | Col = 'wheat2', %get_col_assoc(node,integer,Col), | |
| 315 | get_preference(dot_normal_node_shape,Shape), | |
| 316 | assert(node_col(Term,Translation,Col,Shape)). | |
| 317 | add_node(_Val,_Translation). | |
| 318 | ||
| 319 | get_fields(rec(Fields),Fields). | |
| 320 | get_fields((A,B),Fields) :- get_pair_fields((A,B),Fields,[]). | |
| 321 | ||
| 322 | get_pair_fields((A,B)) --> !, get_pair_fields(A), get_pair_fields(B). | |
| 323 | get_pair_fields(A) --> [field(prj,A)]. | |
| 324 | ||
| 325 | translate_fields([],[' }|']). | |
| 326 | translate_fields([field(Name,Value)],Res) :- !, | |
| 327 | trans_field(Name,VS,Res,[' }|']), | |
| 328 | translate_bvalue_for_dot(Value,VS). | |
| 329 | translate_fields([field(Name,Value)|T],Res) :- | |
| 330 | trans_field(Name,VS,Res,['|' | Rest]), | |
| 331 | translate_bvalue_for_dot(Value,VS), | |
| 332 | translate_fields(T,Rest). | |
| 333 | ||
| 334 | trans_field(prj,VS, [' { ', VS, ' } ' | Rest], Rest) :- !. | |
| 335 | trans_field(Name,VS, [' { ',Name,' | ', VS, ' } ' | Rest], Rest). | |
| 336 | ||
| 337 | :- dynamic col_association/3. % store colours used for types, edge labels,... | |
| 338 | get_col_assoc(Domain,T,Col) :- col_association(Domain,T,Col),!. | |
| 339 | get_col_assoc(Domain,T,Col) :- get_next_col(Domain,Col), | |
| 340 | assert(col_association(Domain,T,Col)). | |
| 341 | ||
| 342 | get_next_col(Type,Col) :- retract(cur_col(Type,N)),!, N1 is N+1, | |
| 343 | (default_colours(N1,Col) -> NX=N1 | |
| 344 | ; NX = 1, default_colours(1,Col)), | |
| 345 | assert(cur_col(Type,NX)). | |
| 346 | get_next_col(_T,red). | |
| 347 | ||
| 348 | :- dynamic cur_col/2. | |
| 349 | cur_col(node,0). | |
| 350 | cur_col(edge,99). | |
| 351 | default_colours(1,'#efdf84'). | |
| 352 | default_colours(2,'#bdef6b'). | |
| 353 | default_colours(3,'#5863ee'). | |
| 354 | default_colours(4,'LightSteelBlue1'). | |
| 355 | default_colours(5,gray). | |
| 356 | ||
| 357 | default_colours(100,firebrick). | |
| 358 | default_colours(101,sienna). | |
| 359 | default_colours(102,'SlateBlue4'). | |
| 360 | default_colours(103,black). | |
| 361 | ||
| 362 | translate_abs_cn_node(AA,ColAA,Translation) :- | |
| 363 | tools:string_concatenate('_',AA,AID1), | |
| 364 | tools:string_concatenate(ColAA,AID1,AID2), | |
| 365 | tools:string_concatenate('abs',AID2,Translation). | |
| 366 | ||
| 367 | clusters2dot([]). | |
| 368 | clusters2dot([cluster(Name,Elements)|T]) :- | |
| 369 | tools:string_escape(Name,EscName), | |
| 370 | format('subgraph \"cluster_~w\" {node [style=filled,color=white]; label=\"~w\"; style=filled;color=lightgrey; ',[EscName,EscName]), | |
| 371 | nodes2dot(Elements), | |
| 372 | clusters2dot(T). | |
| 373 | ||
| 374 | nodes2dot([]) :- print('}'),nl. | |
| 375 | nodes2dot([H|T]) :- | |
| 376 | translate:translate_params_for_dot([H],HP), | |
| 377 | %format('~w; ',[HP]), | |
| 378 | tools:print_escaped(HP), print('; '), | |
| 379 | nodes2dot(T). | |
| 380 | ||
| 381 | ||
| 382 | /**********************************************/ | |
| 383 | ||
| 384 | /****** CANONICAL LABELLING USING LEXICOGRAPHIC AND AUTOMORHISM PRUNING: ******* | |
| 385 | ****** Makes use of Schreier-Sims data structure defined in Kreher's ******* | |
| 386 | ****** C.A.G.E.S book, and uses the techniques for pruning described in ******* | |
| 387 | ****** the chapter 7. *******/ | |
| 388 | ||
| 389 | ||
| 390 | ||
| 391 | /******************************************************************* | |
| 392 | */ | |
| 393 | % schreiersims.pl implementation of the schreier sims representation of an | |
| 394 | % automorphism group taken from Kreher's Combinatorial Algorithms book. Provides | |
| 395 | % method for membership test of a permutation in the group that should run in O(n^2) | |
| 396 | % time. Plans are not to spend time on implementing a method for listing all permutations | |
| 397 | % represented in the group -- since this is not required at the time of writing. | |
| 398 | ||
| 399 | % THIS IS NOT CURRENTLY USED, BUT WAS ONCE USED TO MORE CLOSELY FOLLOW NAUTY. | |
| 400 | % HOWEVER, USING SCHREIERSIMS STRUCTURES IMPLEMENTATION IS VERY LIST INTENSIVE | |
| 401 | % RESULTING IN A POOR PERFORMANCE -- HENCE NOT USED NOW. FUTURE WORK MAY WANT | |
| 402 | % TO LOOK AT ITS USE BELOW.. | |
| 403 | ||
| 404 | ||
| 405 | /*** END OF CANONICAL LABELLING USING LEXICOGRAPHIC AND AUTOMORPHISM PRUNING ***/ |