Cómo el orden refinamiento y la celda objetivo afectan al tamaño del árbol de particiones

Figura
Figura. Grafo del documento [D10]

El objetivo principal del algoritmo de McKay con la implementación que llevo a cabo con getPartitionTree() es obtener el etiquetado canónico del grafo que se devuelve en el campo canonicalLabel del resultado. Pero también esta implementación me sirve para analizar y trazar la ejecución del árbol de particiones, automorfismos y cómo las opciones de ejecución pueden modificar el resultado. Para ver esto último tomaremos el grafo de la Figura, que puede importar en la aplicación con el archivo graph-sample-10.txt

Figura
Figura. Opciones para obtener árbol particiones

En la Figura vemos las opciones para ejecutar el algoritmo getPartitionTree(), de las que ya hablamos en un tema anterior.

Las opciones orden refinamiento ("orderRefine") y celda objetivo ("targetCell") son las que veremos en este tema, empezando por esta última.

Obtenemos el siguiente resultado:

Time: 2 ms

Obtener árbol particiones (McKay): {
"useRefine":true,
    "orderRefine":"neighbors-asc-lengths-desc",
    "targetCell":"first",
    "typeCanon":"edges",
    "useMcr":true,
    "useMcrPlus":false,
    "useMcrBreak":false,
    "typeIndicator":"none",
"timeTotal":2,"timeIni":0,"timeRef":0,
"iter":14,"refine":9,"indexTree":8,
"leaves":5,
"prunningMcr":2,"prunningMcrBreak":0,
"prunningIndicators":0,"prunningIndicators2":0,
"firstCanonicalPartition":[[0],[1],[2],[3],[7],[6],[5],[4]],
"firstCanon":false,
"canonicalPartition":[[1],[0],[3],[2],[7],[5],[6],[4]],
"firstCanonicalLabel":[[0,6],[0,7],[1,5],[1,7],[2,4],[2,6],[3,4],[3,5],[4,6],[4,7],[5,6],[5,7]],
"canonicalLabel":[[0,6],[0,7],[1,5],[1,7],[2,4],[2,6],[3,4],[3,5],[4,5],[4,7],[5,6],[6,7]],
"canonicalIndicator":null,
"nodes":
[[[-1,[],[],"PA"]],
[[0,[0],[]],[0,[1],[],"R3"],[0,[2],[],"PA"]],
[[0,[0,1],[],"f"],[0,[0,2],[],"=(1 2)(4 5)(6 7)"],[1,[1,0],[],">*"],[1,[1,3],[],"=(0 3)(4 6)(5 7)"],[2,[2,0],[],"=(1 2)(4 5)(6 7)"]]],
"nodeBest":[2,2],
"mcr":[0,1,4],
"fixMcr":
[[[0,3],[0,1,3,4,6]],
[[1,2],[0,1,2,4,5]],
[[0,3],[0,1,3,4,6]]],
"lastEqual":true,"rebuiltMcr":1,"nonRebuiltMcr":4,
"tree":[{"index":0,"value":"(0 1 2 3)(4 5 6 7)","parent":-1},
{"index":1,"value":"(0)(1 2)(3)(6 7)(4 5)","parent":[{"index":0,"value":"0"}]},
{"index":2,"value":"(0)(1)(2)(3)(7)(6)(5)(4)","parent":[{"index":1,"value":"1"}]},
{"index":3,"value":"(0)(2)(1)(3)(6)(7)(4)(5)","parent":[{"index":1,"value":"2"}]},
{"index":4,"value":"(1)(0 3)(2)(5 7)(4 6)","parent":[{"index":0,"value":"1"}]},
{"index":5,"value":"(1)(0)(3)(2)(7)(5)(6)(4)","parent":[{"index":4,"value":"0"}]},
{"index":6,"value":"(1)(3)(0)(2)(5)(7)(4)(6)","parent":[{"index":4,"value":"3"}]},
{"index":7,"value":"(2)(0 3)(1)(4 6)(5 7)","parent":[{"index":0,"value":"2"}]},
{"index":8,"value":"(2)(0)(3)(1)(6)(4)(7)(5)","parent":[{"index":7,"value":"0"}]}],
"automorphisms":
[[[0],[1,2],[3],[4,5],[6,7]],
[[0,3],[1],[2],[4,6],[5,7]],
[[0],[1,2],[3],[4,5],[6,7]]],
"automorphismsPlus":0}
Figura
Figura. Árbol particiones del grafo de la Figura

En la Figura vemos su árbol de particiones con 9 nodos y a continuación el test de isomorfismos con todas las 8! = 40320 permutaciones del grafo resultando isomórficas.

Time: 3565 ms

Test isomorfismos (McKay): {"useRefine":true,"orderRefine":"neighbors-asc-lengths-desc","typeCanon":"edges","targetCell":"first","useMcr":true,"useMcrPlus":false,"useMcrBreak":false,"typeIndicator":"none",
"isomorphic":40320,"nonIsomorphic":0,
"firstTime":3,"minTime":0,"averageTime":0,"maxTime":3,
"iter1":14,"iter2":14,
"indexTree1":8,"indexTree2":7,
"minIndexTree":7,"maxIndexTree":8,
"refine1":9,"refine2":8,
"numAut1":3,"numAut2":2,"numAutPlus1":0,"numAutPlus2":0,
"prunningMcr1":2,"prunningMcr2":3,"prunningMcrBreak1":0,"prunningMcrBreak2":0,
"prunningIndicators1":0,"prunningIndicators2":0,
"prunningIndicators1b":0,"prunningIndicators2b":0,
"canonical":[[0,6],[0,7],[1,5],[1,7],[2,4],[2,6],[3,4],[3,5],[4,5],[4,7],[5,6],[6,7]]}

Si cambiamos la opción celda objetivo "targetCell":"first" a "targetCell":"last" obtendremos un árbol con 4 nodos cuando antes era de 9 nodos:

Figura
Figura. Árbol particiones del grafo de la Figura

Este es el resultado con "targetCell":"last"

Time: 0 ms

Obtener árbol particiones (McKay): {
"useRefine":true,
    "orderRefine":"neighbors-asc-lengths-desc",
    "targetCell":"last",
    "typeCanon":"edges",
    "useMcr":true,
    "useMcrPlus":false,
    "useMcrBreak":false,
    "typeIndicator":"none",
"timeTotal":0,"timeIni":0,"timeRef":0,
"iter":5,"refine":4,"indexTree":3,
"leaves":3,
"prunningMcr":1,"prunningMcrBreak":0,
"prunningIndicators":0,"prunningIndicators2":0,
"firstCanonicalPartition":[[3],[2],[1],[0],[4],[6],[7],[5]],
"firstCanon":true,
"canonicalPartition":[[3],[2],[1],[0],[4],[6],[7],[5]],
"firstCanonicalLabel":[[0,5],[0,6],[1,6],[1,7],[2,4],[2,5],[3,4],[3,7],[4,5],[4,6],[5,7],[6,7]],
"canonicalLabel":[[0,5],[0,6],[1,6],[1,7],[2,4],[2,5],[3,4],[3,7],[4,5],[4,6],[5,7],[6,7]],
"canonicalIndicator":null,
"nodes":
[[[-1,[],[],"PA"]],
[[0,[4],[],"f*"],[0,[5],[],"=(1 2)(4 5)(6 7)"],[0,[6],[],"=(0 3)(4 6)(5 7)"]]],
"nodeBest":[1,0],
"mcr":[0,1,4],
"fixMcr":
[[[0,3],[0,1,3,4,6]],
[[1,2],[0,1,2,4,5]]],
"lastEqual":true,"rebuiltMcr":0,"nonRebuiltMcr":2,
"tree":[{"index":0,"value":"(0 1 2 3)(4 5 6 7)","parent":-1},
{"index":1,"value":"(3)(2)(1)(0)(4)(6)(7)(5)","parent":[{"index":0,"value":"4"}]},
{"index":2,"value":"(3)(1)(2)(0)(5)(7)(6)(4)","parent":[{"index":0,"value":"5"}]},
{"index":3,"value":"(0)(2)(1)(3)(6)(4)(5)(7)","parent":[{"index":0,"value":"6"}]}],
"automorphisms":
[[[0],[1,2],[3],[4,5],[6,7]],
[[0,3],[1],[2],[4,6],[5,7]]],
"automorphismsPlus":0}

Y el test isomorfismos también conforme.

Time: 2234 ms

Test isomorfismos (McKay): {"useRefine":true,"orderRefine":"neighbors-asc-lengths-desc","typeCanon":"edges","targetCell":"last","useMcr":true,"useMcrPlus":false,"useMcrBreak":false,"typeIndicator":"none",
"isomorphic":40320,"nonIsomorphic":0,
"firstTime":0,"minTime":0,"averageTime":0,"maxTime":1,
"iter1":5,"iter2":5,
"indexTree1":3,"indexTree2":3,
"minIndexTree":3,"maxIndexTree":3,
"refine1":4,"refine2":4,
"numAut1":2,"numAut2":2,"numAutPlus1":0,"numAutPlus2":0,
"prunningMcr1":1,"prunningMcr2":1,"prunningMcrBreak1":0,"prunningMcrBreak2":0,
"prunningIndicators1":0,"prunningIndicators2":0,
"prunningIndicators1b":0,"prunningIndicators2b":0,
"canonical":[[0,5],[0,6],[1,6],[1,7],[2,4],[2,5],[3,4],[3,7],[4,5],[4,6],[5,7],[6,7]]}

En la siguiente tabla resumimos las combinaciones de las opciones orden refinamiento "orderRefine" y celda objetivo "targetCell" para ese grafo:

Size tree
Order refineTarget cell
firstlastmax-
length-
first
max-
length-
last
min-
length-
first
min-
length-
last
max-
joins
neighbors-desc-
lengths-desc
4949494
neighbors-desc-
lengths-asc
4949494
neighbors-desc4949494
neighbors-asc-
lengths-desc
9494949
neighbors-asc-
lengths-asc
9494949
neighbors-asc9494949

Estas opciones también influyen sobre el etiquetado canónico. A continuación vemos la particiones canónicas obtenidas según las celdas objetivo para dos órdenes refinamiento.

orderRefine = neighbors-asc-lengths-desc
targetCell          canonicalPartition
-----------------------------------------------------
first               [[1],[0],[3],[2],[7],[5],[6],[4]]
last                [[3],[2],[1],[0],[4],[6],[7],[5]]
max-length-first    [[1],[0],[3],[2],[7],[5],[6],[4]]
max-length-last     [[3],[2],[1],[0],[4],[6],[7],[5]]
min-length-first    [[1],[0],[3],[2],[7],[5],[6],[4]]
min-length-last     [[3],[2],[1],[0],[4],[6],[7],[5]]
max-joins           [[1],[0],[3],[2],[7],[5],[6],[4]]

orderRefine = neighbors-desc-lengths-desc
targetCell          canonicalPartition
-----------------------------------------------------
first               [[4],[7],[6],[5],[1],[0],[3],[2]]
last                [[4],[6],[5],[7],[1],[0],[3],[2]]
max-length-first    [[4],[7],[6],[5],[1],[0],[3],[2]]
max-length-last     [[4],[6],[5],[7],[1],[0],[3],[2]]
min-length-first    [[4],[7],[6],[5],[1],[0],[3],[2]]
min-length-last     [[4],[6],[5],[7],[1],[0],[3],[2]]
max-joins           [[4],[7],[6],[5],[1],[0],[3],[2]]
Figura
Figura. Grafo MZ con 40 nodos

Con el grafo Miyazaki de 40 nodos de la Figura (graph-mz-2.txt) el árbol de menor tamaño se consigue con las opciones orden refinamiento neighbors-desc-lengths-desc" y celda objetivo max-joins, como vemos en la siguiente tabla:

Size tree MZ-2
Order refineTarget cell
firstlastmax-
length-
first
max-
length-
last
min-
length-
first
min-
length-
last
max-
joins
neighbors-desc-
lengths-desc
191991429920114876
neighbors-desc-
lengths-asc
1711459888171310101
neighbors-desc179124867617927589
neighbors-asc-
lengths-desc
1351538611729915294
neighbors-asc-
lengths-asc
17022498151172343101
neighbors-asc1291498611728914994

Ejecutamos con esas opciones y pasamos el test isomorfismos para 16343 permutaciones, el máximo que puede ejecutar en 60 segundos.

Time: 13 ms

Obtener árbol particiones (McKay): {
"useRefine":true,"orderRefine":"neighbors-desc-lengths-desc","targetCell":"max-joins","typeCanon":"edges","useMcr":true,"useMcrPlus":false,"useMcrBreak":false,"typeIndicator":"none",
"timeTotal":13,"timeIni":1,"timeRef":9,
"iter":282,"refine":76,"indexTree":75,
"leaves":24,
"prunningMcr":155,"prunningMcrBreak":0,
"prunningIndicators":0,"prunningIndicators2":0,
"firstCanonicalPartition":[[0],[38],[39],[37],[36],[30],[31],[33],[32],[35],[34],[24],[25],[29],[27],[26],[28],[14],[23],[21],[22],[20],[18],[19],[10],[11],[13],[12],[17],[16],[1],[6],[15],[4],[3],[5],[9],[2],[7],[8]],
"firstCanon":false,
"canonicalPartition":[[16],[8],[9],[4],[27],[25],[35],[37],[36],[30],[31],[33],[32],[38],[39],[34],[0],[1],[3],[2],[20],[22],[24],[10],[12],[26],[14],[7],[6],[28],[29],[19],[18],[17],[21],[23],[5],[13],[11],[15]],
"firstCanonicalLabel":[[0,37],[0,38],[0,39],[1,5],[1,7],[1,10],[2,6],[2,8],[2,10],[3,5],[3,8],[3,9],[4,6],[4,7],[4,9],[5,8],[6,7],[9,12],[10,11],[11,13],[11,16],[12,14],[12,15],[13,19],[13,20],[14,20],[14,21],[15,18],[15,19],[16,18],[16,21],[17,22],[17,23],[17,33],[18,26],[19,25],[20,27],[21,24],[22,24],[22,26],[23,25],[23,27],[24,28],[25,29],[26,29],[27,28],[28,32],[29,32],[30,31],[30,34],[30,36],[31,34],[31,35],[32,35],[33,36],[33,39],[34,39],[35,38],[36,37],[37,38]],
"canonicalLabel":[[0,37],[0,38],[0,39],[1,3],[1,16],[1,18],[2,3],[2,17],[2,19],[3,26],[4,5],[4,20],[4,21],[5,6],[5,25],[6,7],[6,8],[7,9],[7,12],[8,10],[8,11],[9,12],[9,13],[10,11],[10,14],[11,13],[12,14],[13,15],[14,15],[15,22],[16,19],[16,27],[17,18],[17,28],[18,28],[19,27],[20,23],[20,29],[21,24],[21,30],[22,29],[22,30],[23,32],[23,33],[24,31],[24,33],[25,34],[25,35],[26,31],[26,32],[27,36],[28,36],[29,35],[30,34],[31,38],[32,37],[33,39],[34,38],[35,37],[36,39]],
"canonicalIndicator":null,
"nodes":
[[[-1,[],[]]],
[[0,[0],[]],[0,[1],[]],[0,[2],[]],[0,[4],[]],[0,[5],[]],[0,[6],[]],[0,[8],[]],[0,[10],[]],[0,[14],[]],[0,[15],[]],[0,[16],[]],[0,[18],[]],[0,[20],[]],[0,[24],[]],[0,[25],[]],[0,[26],[]]],
[[0,[0,10],[]],[0,[0,11],[]],[0,[0,12],[]],[1,[1,10],[]],[2,[2,10],[]],[3,[4,10],[]],[4,[5,10],[]],[5,[6,10],[]],[6,[8,10],[]],[7,[10,22],[]],[7,[10,23],[]],[8,[14,10],[]],[9,[15,10],[]],[10,[16,30],[]],[11,[18,30],[]],[12,[20,12],[]],[12,[20,13],[]],[13,[24,20],[]],[14,[25,20],[]],[15,[26,0],[]]],
[[0,[0,10,30],[]],[0,[0,10,31],[]],[0,[0,10,32],[]],[1,[0,11,30],[]],[2,[0,12,30],[]],[3,[1,10,30],[]],[4,[2,10,30],[]],[5,[4,10,30],[]],[6,[5,10,30],[]],[7,[6,10,30],[]],[8,[8,10,30],[]],[9,[10,22,30],[]],[10,[10,23,30],[]],[11,[14,10,30],[]],[12,[15,10,30],[]],[13,[16,30,0],[]],[14,[18,30,0],[]],[15,[20,12,0],[]],[16,[20,13,0],[]],[17,[24,20,0],[]],[18,[25,20,0],[]],[19,[26,0,30],[]]],
[[7,[4,10,30,0],[]],[8,[5,10,30,0],[]],[9,[6,10,30,8],[]],[10,[8,10,30,1],[]],[10,[8,10,30,2],[]],[11,[10,22,30,0],[]],[12,[10,23,30,0],[]],[13,[14,10,30,0],[]],[14,[15,10,30,0],[]],[15,[16,30,0,20],[]],[16,[18,30,0,21],[]],[16,[18,30,0,22],[]],[17,[20,12,0,30],[]],[18,[20,13,0,30],[]],[19,[24,20,0,30],[]],[20,[25,20,0,30],[]],[21,[26,0,30,10],[]]]],
"nodeBest":[4,9],
"mcr":[0,4,5,6,8,10,14,15,16,18],
"fixMcr":
[[[0,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,34,35],[0,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,32,34,35,36,38]],
[[0,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,34,35,36,37],[0,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,34,35,36,37,38]],
[[0,1,2,3,4,5,6,7,8,9,14,15,24,25,30,31,32,33,34,35,36,37,38,39],[0,1,2,3,4,5,6,7,8,9,10,12,14,15,16,18,20,22,24,25,26,28,30,31,32,33,34,35,36,37,38,39]],
[[0,1,2,3,4,5,6,7,8,9,14,15,16,17,24,25,26,27,30,31,32,33,34,35,36,37,38,39],[0,1,2,3,4,5,6,7,8,9,10,11,14,15,16,17,18,20,21,24,25,26,27,28,30,31,32,33,34,35,36,37,38,39]],
[[4,5,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],[0,2,4,5,6,8,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]],
[[4,5,6,7,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],[0,1,4,5,6,7,8,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]],
[[],[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]]],
"lastEqual":true,"rebuiltMcr":130,"nonRebuiltMcr":47,
"tree":null,
"automorphisms":7,
"automorphismsPlus":0}
Time: 62152 ms

Test isomorfismos (McKay): {"useRefine":true,"orderRefine":"neighbors-desc-lengths-desc","typeCanon":"edges","targetCell":"max-joins","useMcr":true,"useMcrPlus":false,"useMcrBreak":false,"typeIndicator":"none",
"isomorphic":16343,"nonIsomorphic":0,
"firstTime":17,"minTime":3,"averageTime":4,"maxTime":17,
"iter1":282,"iter2":282,
"indexTree1":75,"indexTree2":76,
"minIndexTree":74,"maxIndexTree":89,
"refine1":76,"refine2":77,
"numAut1":7,"numAut2":7,"numAutPlus1":0,"numAutPlus2":0,
"prunningMcr1":155,"prunningMcr2":154,"prunningMcrBreak1":0,"prunningMcrBreak2":0,
"prunningIndicators1":0,"prunningIndicators2":0,
"prunningIndicators1b":0,"prunningIndicators2b":0,
"canonical":[[0,37],[0,38],[0,39],[1,3],[1,16],[1,18],[2,3],[2,17],[2,19],[3,26],[4,5],[4,20],[4,21],[5,6],[5,25],[6,7],[6,8],[7,9],[7,12],[8,10],[8,11],[9,12],[9,13],[10,11],[10,14],[11,13],[12,14],[13,15],[14,15],[15,22],[16,19],[16,27],[17,18],[17,28],[18,28],[19,27],[20,23],[20,29],[21,24],[21,30],[22,29],[22,30],[23,32],[23,33],[24,31],[24,33],[25,34],[25,35],[26,31],[26,32],[27,36],[28,36],[29,35],[30,34],[31,38],[32,37],[33,39],[34,38],[35,37],[36,39]],
"warnings":["The graph has more than 9 nodes and only the first 9!=362880 permutations will be tested or less if the maximum time of 60 seconds is exceeded"]}
Figura
Figura. Orden refinamiento
Figura
Figura. Seleccionar celda objetivo

Ya explicamos las opciones orden refinamiento y celda objetivo en un tema anterior. Veamos un pequeño resumen que define cada opción.

Una partición se divide en celdas con uno o más vértices. Cada vértice del grafo tiene un determinado número de vecinos, que también denominamos como grado del vértice, vecinos que se pueden obtener con el algoritmo getNeighbors() y grados con getVertexDegrees(). Entonces cada celda tendrá una longitud que es el número de vértices que contiene y un número de vecinos que es la suma de los grados de sus vértices. Con estos dos parámetros se ordenan las particiones tras cada refinamiento:

  • Orden refinamiento ("orderRefine"):
    • neighbors-desc-lengths-desc por número vecinos descendente primero y si hay dos iguales se ordena por longitudes también descendente.
    • neighbors-desc-lengths-asc por número vecinos descendente primero y si hay dos iguales se ordena por longitudes ascendente.
    • neighbors-desc por número vecinos descendente ignorando las longitudes.
    • neighbors-asc-lengths-desc por número vecinos ascendente primero y si hay dos iguales se ordena por longitudes descendente.
    • neighbors-asc-lengths-asc por número vecinos ascendente primero y si hay dos iguales se ordena por longitudes ascendente.
    • neighbors-asc por número vecinos ascendente ignorando las longitudes.

En el algoritmo que obtiene el árbol de particiones hemos de seleccionar una celda objetivo ("targetCell") de una partición no discreta con objeto de empezar a fijar vértices y refinar particiones, selección que puede obedecer a diferentes criterios. La celda que se selecciona siempre es una que contenga más de un vértice. Vea que en este momento ya tenemos una partición no discreta que previamente fue refinada y, por tanto, procede con el orden refinamiento impuesto.

  • Celda objetivo ("targetCell"):
    • first selecciona la primera.
    • last selecciona la última.
    • max-length-first selecciona la primera de mayor tamaño.
    • max-length-last selecciona la última de mayor tamaño.
    • min-length-first selecciona la primera de menor tamaño.
    • min-length-last selecciona la última de menor tamaño.
    • max-joins selecciona la que tenga el máximo de uniones.

La última max-joins se basa en dar a cada celda un valor en función del número de vecinos comunes con el primer vértice del resto de celdas. Es una opción costosa de obtener y que estoy probando, pues no he podido verificar una mejora en los ejemplos a excepción de algunos como el de la Figura que vimos antes.

Uso de MCR plus y corte MCR

Figura
Figura. Grafo del documento [D3]

En el tema anterior vimos el mismo grafo de la Figura que puede importar en la aplicación con graph-sample-4-plus-break.txt

Figura
Figura. Use mcr

En ese tema marcábamos sólo la opción usar mcr ("useMcr") y ahora además utilizaremos usar mcr plus ("useMcrPlus") y usar corte mcr ("useMcrBreak"). Vamos a ver como estas dos opciones reducen el tamaño del árbol y el número de iteraciones respectivamente.

Vemos primero el resultado obtenido:

Time: 1 ms

Obtener árbol particiones (McKay): {
"useRefine":true,"orderRefine":"neighbors-desc-lengths-desc","targetCell":"max-length-first","typeCanon":"edges","useMcr":true,"useMcrPlus":true,"useMcrBreak":true,"typeIndicator":"none",
"timeTotal":1,"timeIni":0,"timeRef":0,
"iter":10,"refine":8,"indexTree":7,
"leaves":6,
"prunningMcr":1,"prunningMcrBreak":1,
"prunningIndicators":0,"prunningIndicators2":0,
"firstCanonicalPartition":[[0],[7],[3],[6],[2],[9],[5],[1],[8],[4]],
"firstCanon":false,
"canonicalPartition":[[1],[5],[6],[7],[4],[9],[8],[3],[2],[0]],
"firstCanonicalLabel":[[0,7],[0,8],[0,9],[1,6],[1,8],[1,9],[2,4],[2,7],[2,8],[3,5],[3,6],[3,9],[4,5],[4,7],[5,6]],
"canonicalLabel":[[0,7],[0,8],[0,9],[1,2],[1,3],[1,5],[2,4],[2,5],[3,4],[3,6],[4,9],[5,8],[6,7],[6,9],[7,8]],
"canonicalIndicator":null,
"nodes":
[[[-1,[],[],"R5","PA","PA4"]],
[[0,[0],[],"f"],[0,[1],[],">*"],[0,[2],[]],[0,[3],[],"=(0 8)(1 3)(4 7)(5 6)"],[0,[4],[],"<"]],
[[2,[2,5],[],"<"],[2,[2,6],[],"<"]]],
"nodeBest":[1,1],
"mcr":[0,1,2],
"fixMcr":
[[[2,9],[0,1,2,4,5,9]],
[[],[0,1,2,3,7]]],
"lastEqual":false,"rebuiltMcr":1,"nonRebuiltMcr":1,
"tree":[{"index":0,"value":"(0 1 2 3 4 5 6 7 8 9)","parent":-1},
{"index":1,"value":"(0)(7)(3)(6)(2)(9)(5)(1)(8)(4)","parent":[{"index":0,"value":"0"}]},
{"index":2,"value":"(1)(5)(6)(7)(4)(9)(8)(3)(2)(0)","parent":[{"index":0,"value":"1"}]},
{"index":3,"value":"(2)(5 6)(0 8)(4 7)(1 3)(9)","parent":[{"index":0,"value":"2"}]},
{"index":4,"value":"(2)(5)(6)(0)(8)(7)(4)(3)(1)(9)","parent":[{"index":3,"value":"5"}]},
{"index":5,"value":"(2)(6)(5)(8)(0)(4)(7)(1)(3)(9)","parent":[{"index":3,"value":"6"}]},
{"index":6,"value":"(3)(6)(5)(4)(7)(9)(0)(1)(2)(8)","parent":[{"index":0,"value":"3"}]},
{"index":7,"value":"(4)(8)(5)(1)(9)(2)(3)(6)(7)(0)","parent":[{"index":0,"value":"4"}]}],
"automorphisms":
[[[0,8],[1,3],[2],[4,7],[5,6],[9]]],
"automorphismsPlus":
[[[0,4],[1,6],[2,9],[3,5],[7,8]]]}

Y el árbol obtenido, donde agregamos la rama "5" en rojo que aparecía antes en el árbol del tema anterior y que ahora se poda por la acción de "useMcrPlus":

Figura
Figura. Árbol particiones del grafo Figura

Antes utilizando solo "useMcr" obteníamos el árbol con 9 nodos (indexTree+1), 7 hojas (leaves), 14 iteraciones (iter) y 2 automorfismos:

...
"iter":14,"refine":9,"indexTree":8,
"leaves":7,
"prunningMcr":4,"prunningMcrBreak":0,
...
"automorphisms":
[[[0,8],[1,3],[2],[4,7],[5,6],[9]],
[[0,7],[1,5],[2,9],[3,6],[4,8]]],
"automorphismsPlus":0}

Ahora además con "useMcrPlus" y "useMcrBreak" tenemos un árbol con 8 nodos, 6 hojas, 10 iteraciones, un automorfismo (pues el otro era el de la última hoja de la rama "5" que se poda) y otro nuevo que se incluye en automorphismsPlus:

...
"iter":10,"refine":8,"indexTree":7,
"leaves":6,
"prunningMcr":1,"prunningMcrBreak":1,
...
"automorphisms":
[[[0,8],[1,3],[2],[4,7],[5,6],[9]]],
"automorphismsPlus":
[[[0,4],[1,6],[2,9],[3,5],[7,8]]]}

Para entender el efecto de "useMcrPlus" vemos a continuación el campo "nodes" puesto en forma vertical, agregando las particiones hojas:

Nº Nivel  Nodos                                       Hojas
---------------------------------------------------------------------------------
0  0      [-1,[],[],"R5","PA","PA4"]  (antes "R5","PA","PA","PA","PA"])
1  1          [[0,[0],[],"f"]                         (0)(7)(3)(6)(2)(9)(5)(1)(8)(4)
2  1          [0,[1],[],">*"]                         (1)(5)(6)(7)(4)(9)(8)(3)(2)(0)
3  1          [0,[2],[]]
4  2              [2,[2,5],[],"<"]                    (2)(5)(6)(0)(8)(7)(4)(3)(1)(9)
5  2              [2,[2,6],[],"<"]                    (2)(6)(5)(8)(0)(4)(7)(1)(3)(9)
6  1          [0,[3],[],"=(0 8)(1 3)(4 7)(5 6)"]      (3)(6)(5)(4)(7)(9)(0)(1)(2)(8)
                          (0 4)(1 6)(2 9)(3 5)(7 8)
7  1          [0,[4],[],"<"]                          (4)(8)(5)(1)(9)(2)(3)(6)(7)(0)
8  1          [0,[5],[],"=(0 7)(1 5)(2 9)(3 6)(4 8)"] (5)(1)(3)(0)(8)(2)(4)(6)(9)(7)

El algoritmo siempre usa la primera hoja como canónica (0)(7)(3)(6)(2)(9)(5)(1)(8)(4), anotada con "f", pero luego encontró que la segunda hoja (1)(5)(6)(7)(4)(9)(8)(3)(2)(0) era mayor, con lo que la marca como canónica (anotada con "<*") y que se guarda en canonicalPartition, pero sigue almacenando la primera en firstCanonicalPartition. Cuando se produzca un "=" la compara con la canónica, pero si también usamos "useMcrPlus" también la comparará con la primera canónica. Así en este ejemplo encuentra el automorfismo (0 8)(1 3)(4 7)(5 6) con la canónica y (0 4)(1 6)(2 9)(3 5)(7 8) con la primera canónica. Observamos que "5" está en el MCR (minimum cell representative) del primer automorfismo pero no del segundo. Por eso se recorta la última rama que fijaba el vértice "5" gracias a usar "usarMcrPlus".

Por otro lado con usar corte mcr ("useMcrBreak") no se reduce el tamaño del árbol, pero si el número de iteraciones. En el esquema anterior sin usar esta opción teníamos que el nodo raíz era (0 1 2 3 4 5 6 7 8 9) con cuatro podas "PA","PA","PA","PA" de los vértices 6, 7, 8, y 9, recordando que la anotación "PA" indicaba una poda por automorfismo en ese nodo. Ahora tenemos "PA","PA4", donde el primer "PA" es la poda del vértice "5" que dijimos antes por efecto de "useMcrPlus" y "PA4" son las 4 podas de los vértices 6, 7, 8, y 9 usando "useMcrBreak", que ahora se hacen todas juntas cuando entra en el primero "6", así que reduce en 4 iteraciones, de 14 antes a 10 ahora.

Comprobamos que esas opciones "useMcrPlus" y "useMcrBreak" no afectan al test de isomorfismos, al menos hasta el máximo de 362880 tests:

Time: 54069 ms

Test isomorfismos (McKay): {"useRefine":true,"orderRefine":"neighbors-desc-lengths-desc","typeCanon":"edges","targetCell":"max-length-first","useMcr":true,"useMcrPlus":true,"useMcrBreak":true,"typeIndicator":"none",
"isomorphic":362880,"nonIsomorphic":0,
"firstTime":4,"minTime":0,"averageTime":0,"maxTime":10,
"iter1":10,"iter2":10,
"indexTree1":7,"indexTree2":7,
"minIndexTree":6,"maxIndexTree":10,
"refine1":8,"refine2":8,
"numAut1":1,"numAut2":2,"numAutPlus1":1,"numAutPlus2":0,
"prunningMcr1":1,"prunningMcr2":1,"prunningMcrBreak1":1,"prunningMcrBreak2":1,
"prunningIndicators1":0,"prunningIndicators2":0,
"prunningIndicators1b":0,"prunningIndicators2b":0,
"canonical":[[0,7],[0,8],[0,9],[1,2],[1,3],[1,5],[2,4],[2,5],[3,4],[3,6],[4,9],[5,8],[6,7],[6,9],[7,8]],
"warnings":["The graph has more than 9 nodes and only the first 9!=362880 permutations will be tested or less if the maximum time of 60 seconds is exceeded"]}

La opción "useMcrPlus" sólo se producirán en árboles con secuencias de ">*" seguida posteriormente de "=", por lo que no es muy frecuente encontrar grafos de ejemplos y, en todo caso, la mejora no es muy significativa al menos en los casos que he podido analizar.

Permutaciones a cíclica permutationsToCyclic()

Figura
Figura. Convertir dos permutaciones a una permutación cíclica

Cuando comparamos los etiquetados canónicos y resultan iguales "=" entonces habremos encontrado un automorfismo. Para obtenerlo tomamos las dos particiones discretas, es decir, la canónica que habíamos almacenado previamente y la actual con la que hemos comprobado que resultan iguales, y las convertimos en una permutación cíclica, concepto que ya habíamos explicado en el tema de la serie de combinatoria, en permutaciones cíclicas, siendo entonces el automorfismo encontrado.

[[1],[3],[2]]
(1)(3)(2)
[1,3,2]
1,3,2
1 3 2

El algoritmo permutationsToCyclic() que observamos en la Figura se ejecuta en el panel del asistente de generación, al que se accede con el botón con el icono . Este algoritmo es independiente de cualquier grafo, pues solo intervienen las dos permutaciones que se van a convertir en una cíclica. Las permutaciones pueden pasarse en diversos formatos, como el ejemplo adjunto a este párrafo.

En la Figura vemos como obtenemos el automorfismo [[0,6,8,2],[1,3,7,5],[4]] a partir de las dos permutaciones (1)(3)(5)(7)(0)(2)(6)(8)(4) y (3)(7)(1)(5)(6)(0)(8)(2)(4), que son la primera partición discreta con el etiquetado canónico y la cuarta que puede verse en una tabla de un tema anterior con las 8 particiones discretas que reproducimos a continuación con todos los automorfismos que encontraba el grafo:

(1)(3)(5)(7)(0)(2)(6)(8)(4)    ()     Canónica
(1)(5)(3)(7)(2)(0)(8)(6)(4)    (0 2)(3 5)(6 8)
(3)(1)(7)(5)(0)(6)(2)(8)(4)    (1 3)(2 6)(5 7)
(3)(7)(1)(5)(6)(0)(8)(2)(4)    (0 6 8 2)(1 3 7 5)
(5)(1)(7)(3)(2)(8)(0)(6)(4)    (0 2 8 6)(1 5 7 3)
(5)(7)(1)(3)(8)(2)(6)(0)(4)    (0 8)(1 5)(3 7)
(7)(3)(5)(1)(6)(8)(0)(2)(4)    (0 6)(1 7)(2 8)
(7)(5)(3)(1)(8)(6)(2)(0)(4)    (0 8)(1 7)(2 6)(3 5)
Figura
Figura. Calculadora de permutaciones cíclicas

Para convertir dos permutaciones en una permutación cíclica hemos de obtener las notaciones cíclicas de cada una y obtener la división de las dos permutaciones cíclicas. Podemos usar la calculadora de permutaciones cíclicas en la aplicación Combine Tools como vemos en la Figura.

La partición discreta (1)(3)(5)(7)(0)(2)(6)(8)(4) se corresponde con la notación 1 línea en 1 3 5 7 0 2 6 8 4. Con la operación notación línea a ciclos convertimos esa permutación en 1 línea en notación cíclica (0 1 3 7 8 4)(2 5), como vemos en la Figura. Así convirtiendo a notación cíclica todas las particiones discretas y diviéndolas en esa aplicación por esa primera canónica obtenemos los mismos automorfismos que se descubren con el árbol de particiones, como vemos a continuación:

(1)(3)(5)(7)(0)(2)(6)(8)(4) ≡ 1 3 5 7 0 2 6 8 4 ≡ (0 1 3 7 8 4)(2 5) Canónica
(0 1 3 7 8 4)(2 5) ÷ (0 1 3 7 8 4)(2 5) = ()

(1)(5)(3)(7)(2)(0)(8)(6)(4) ≡ 1 5 3 7 2 0 8 6 4 ≡ (0 1 5)(2 3 7 6 8 4)
(0 1 5)(2 3 7 6 8 4) ÷ (0 1 3 7 8 4)(2 5) = (0 2)(3 5)(6 8)

(3)(1)(7)(5)(0)(6)(2)(8)(4) ≡ 3 1 7 5 0 6 2 8 4 ≡ (0 3 5 6 2 7 8 4)
(0 3 5 6 2 7 8 4) ÷ (0 1 3 7 8 4)(2 5) = (1 3)(2 6)(5 7)

(3)(7)(1)(5)(6)(0)(8)(2)(4) ≡ 3 7 1 5 6 0 8 2 4 ≡ (0 3 5)(1 7 2)(4 6 8)
(0 3 5)(1 7 2)(4 6 8) ÷ (0 1 3 7 8 4)(2 5) = (0 6 8 2)(1 3 7 5)

(5)(1)(7)(3)(2)(8)(0)(6)(4) ≡ 5 1 7 3 2 8 0 6 4 ≡ (0 5 8 4 2 7 6)
(0 5 8 4 2 7 6) ÷ (0 1 3 7 8 4)(2 5) = (0 2 8 6)(1 5 7 3)

(5)(7)(1)(3)(8)(2)(6)(0)(4) ≡ 5 7 1 3 8 2 6 0 4 ≡ (0 5 2 1 7)(4 8)
(0 5 2 1 7)(4 8) ÷ (0 1 3 7 8 4)(2 5) = (0 8)(1 5)(3 7)

(7)(3)(5)(1)(6)(8)(0)(2)(4) ≡ 7 3 5 1 6 8 0 2 4 ≡ (0 7 2 5 8 4 6)(1 3)
(0 7 2 5 8 4 6)(1 3) ÷ (0 1 3 7 8 4)(2 5) = (0 6)(1 7)(2 8)

(7)(5)(3)(1)(8)(6)(2)(0)(4) ≡ 7 5 3 1 8 6 2 0 4 ≡ (0 7)(1 5 6 2 3)(4 8)
(0 7)(1 5 6 2 3)(4 8) ÷ (0 1 3 7 8 4)(2 5) = (0 8)(1 7)(2 6)(3 5)

Observe que la primera partición que es la canónica se le asigna el automorfismo identidad, pues al dividir siempre por la primera canónica resultará la identidad (0 1 3 7 8 4)(2 5) ÷ (0 1 3 7 8 4)(2 5) = () como es obvio.

Si tenemos la partición canónica π y luego encontramos otra τ, con permutaciones cíclicas σπ y στ respectivamente, particiones cuyos etiquetados canónicos son iguales, entonces generan el automorfismo γ, de tal forma que γ = στ ÷ σπ = στ × (σπ)-1. Para el último ejemplo anterior vemos el cálculo con la inversa de la primera partición ((0 1 3 7 8 4)(2 5))-1 = (0 4 8 7 3 1)(2 5), que se obtiene manteniendo el primer índice de cada celda e invirtiendo el resto, así el cálculo con el producto resulta igual:

(0 7)(1 5 6 2 3)(4 8) ÷ (0 1 3 7 8 4)(2 5) = 
(0 7)(1 5 6 2 3)(4 8) × ((0 1 3 7 8 4)(2 5))-1 = 
(0 7)(1 5 6 2 3)(4 8) × (0 4 8 7 3 1)(2 5) = (0 8)(1 7)(2 6)(3 5)

En la aplicación de la calculadora de ciclos disponemos de los algoritmos para esas operaciones, una línea a ciclos para convertir la notación 1 línea en notación cíclica y división ciclos, que no es otra cosa que hacer el producto por el inverso tal como hemos dicho. Para usarlo con la obtención del árbol de particiones lo simplificamos usando el algoritmo permutationsToCyclic() que al final resulta en los mismos cálculos:

function permutationsToCyclic({permutation1=[], permutation2=[]}) {
    let dev = {error: "", cycles: []};
    try {
        let [p1, p2] = [permutation1.flat(), permutation2.flat()];
        let n = p1.length;
        if (p2.length!==n) throw new Error("Permutations lengths are wrong");
        let keys = p1.toSorted((v,w) => w-v);
        while (keys.length>0) {
            let a = keys.pop();
            let pivot = null;
            let cycle = [a];
            while (a!==pivot) {
                if (pivot===null) pivot = a;
                let i = p1.indexOf(a);
                if (i===-1) throw new Error(`i===-1`);
                let b = p2[i];
                if (b!==pivot) {
                    cycle.push(b);
                    let k = keys.indexOf(b);
                    if (k>-1) keys.splice(k, 1);
                }
                a = b;
            }
            dev.cycles.push(cycle);
        }
    } catch(e){dev.error = `#Error permutationsToCyclic(): ${e.message}`}
    return dev;
}

Vamos a seguir este algoritmo obteniendo el automorfismo (0 6 8 2)(1 3 7 5) a partir de las dos hojas discretas (1)(3)(5)(7)(0)(2)(6)(8)(4) y (3)(7)(1)(5)(6)(0)(8)(2)(4):

i =       0  1  2  3  4  5  6  7  8
p1 =     (1)(3)(5)(7)(0)(2)(6)(8)(4)
p2 =     (3)(7)(1)(5)(6)(0)(8)(2)(4)

cycles[]
keys[8,7,6,5,4,3,2,1,0]

a=0, keys=[8,7,6,5,4,3,2,1]
pivot=null, cycle=[0]
    pivot===null ⇒ pivot=a=0
    p1[4]=0 ⇒ i=4
    b=p2[4]=6
    b≠pivot ⇒ cycle=[0,6], keys=[8,7,5,4,3,2,1]
    a=b=6

    p1[6]=6 ⇒ i=6
    b=p2[6]=8
    b≠pivot ⇒ cycle=[0,6,8], keys=[7,5,4,3,2,1]
    a=b=8

    p1[7]=8 ⇒ i=7
    b=p2[7]=2
    b≠pivot ⇒ cycle=[0,6,8,2], keys=[7,5,4,3,1]
    a=b=2

    p1[5]=2 ⇒ i=5
    b=p2[5]=0
    a=b=0
    a===pivot ⇒ cycles=[[0,6,8,2]]

a=1, keys=[7,5,4,3]
pivot=null, cycle=[1]
    pivot===null ⇒ pivot=a=1
    p1[0]=1 ⇒ i=0
    b=p2[0]=3
    b≠pivot ⇒ cycle=[1,3], keys=[7,5,4]
    a=b=3

    p1[1]=3 ⇒ i=1
    b=p2[1]=7
    b≠pivot ⇒ cycle=[1,3,7], keys=[5,4]
    a=b=7

    p1[3]=7 ⇒ i=3
    bp2[3]=5
    b≠pivot ⇒ cycle=[1,3,7,5], keys=[4]
    a=b=5

    p1[2]=5 ⇒ i=2
    b=p2[2]=1
    a=b=1
    a===pivot ⇒ cycles=[[0,6,8,2],[1,3,7,5]]

a=4, keys=[]
pivot=null, cycle=[4]
    pivot===null ⇒ pivot=a=4
    p1[8]=4 ⇒ i=8
    b=p2[8]=4
    a=b=4
    a===pivot ⇒ cycles=[[0,6,8,2],[1,3,7,5],[4]]

keys=[] ⇒ cycles=[[0,6,8,2],[1,3,7,5],[4]]

Código JavaScript del algoritmo básico para el etiquetado canónico con refinamiento y poda automorfismos

result = {
    canonicalPartition:null,
    canonicalLabel:null,
    nodes:[],
    mcr:null,
    fixMcr:null,
    lastEqual:false
}

A continuación vemos el pseudocódigo del algoritmo básico para obtener el etiquetado canónico con refinamiento y poda automorfismos. El resultado result contiene la partición canónica y el etiquetado canónico, con el resto de campos necesarios para la poda automorfismos. El árbol de particiones se devuelve en el campo "nodes".

Function getPartitionTreeRefineMcr(matrix, partition, result)
    If partition.length===0  // INITIAL STEPS
        partition = Initial refinement
        Add first root node to nodes
        Apply order refine
    End If
    if (partition.length===n) // DISCRETE PARTITION  (after initial refinement)
        canon = get canon with this permutation
        result.canonicalPartition = partition
        result.canonicalLabel = canon
    Else
        cell = Select target cell
        While cell.size>0
            // Fix vertex
            vertex = minimum of cell
            indexCell = position of vertex in cell
            delete vertex in cell
            filter = indexCell===0 or result.mcr===null
            If not filter
                // PRUNNING AUTORMORPHISMS
                If not lastEqual
                    result.mcr = Rebuilt MCR with result.fixMcr and node fixed path
                End If
                filter = result.mcr has vertex
            End If
            If filter
                partition = individualize vertex and refine partition
                Add new node with this fixed path to result.nodes
                If partition.length===n  // DISCRETE PARTITION
                    canon = get canon with this permutation
                    If result.canonicalLabel===null
                        result.canonicalPartition = partition
                        result.canonicalLabel = canon
                    Else
                        // Compare canon
                        If canon > result.canonicalLabel
                            result.canonicalPartition = partition
                            result.canonicalLabel = canon
                            result.lastEqual = false
                        Else if canon ===  result.canonicalLabel
                            Get automorphism with partition and result.canonicalPartition
                            and refresh result.mcr and result.fixMcr
                            result.lastEqual = true
                        Else
                            result.lastEqual = false
                        End If
                    End If
                Else
                    getPartitionTreeRefineMcr(matrix, partition, result)
                End If
            End If
        End While
    End If
    return result
End Function

El algoritmo en JavaScript básico para obtener el etiquetado canónico con refinamiento y poda automorfismos es el siguiente, versión previa a la definitiva getPartitionTree() que expondremos en otro tema. Es la misma base del que vimos en el tema anterior sólo con refinamiento, usando la misma función refinePartition() para refinar la partición.

Tenemos una función auxiliar getCanon() que ya vimos en temas anteriores para obtener el etiquetado canónico a partir de una partición discreta.

// Algorithm for get canonical label
function getCanon({matrix=[], edgesGraph=[], partition=[]}) {
    let canon = [];
    try {
        let indexes = partition.map(v => v[0]);
        let edges = edgesGraph.length>0 ? edgesGraph : matrix.edges;
        for (let edge of edges) {
            let [v, w] = edge;
            let [i, j] = [indexes.indexOf(v), indexes.indexOf(w)];
            canon.push(i<j ? [i, j] : [j, i]);
        }
        canon = canon.map(v => v[0]>v[1] ? [v[1],v[0]] : [v[0],v[1]]).toSorted((v,w) => (v[0]===w[0]) ? v[1]-w[1] : v[0]-w[0]);
    } catch(e){canon = e.message}
    return canon;
}

Y este es el algoritmo para obtener el árbol de particiones:

function getPartitionTreeRefineMcr({matrix=[], partition=[], level=-1, positionPrev=0, identity=null, 
    result={canonicalPartition:null, canonicalLabel:null, nodes:[], mcr:null, fixMcr:null, lastEqual:false, automorphisms:[]}}) {
    try {
        level++;
        let n = matrix.length;
        if (partition.length===0) {
            // Initial refinement
            for (let index=0; index<n; index++) {
                let num = matrix.neighbors[index].length;
                let j = partition.findIndex(v => v.num===num);
                if (j===-1) {
                    j = partition.length;
                    let arr = [];
                    arr.num = num;
                    partition.push(arr);
                }
                partition[j].push(index);
            }
            // Add first root node to nodes
            result.nodes.push([[-1, []]]);
            // For use with fixMcr
            identity = new Set(Array.from({length:n}, (v,i)=>i));
            // orderRefine (neighbors-desc-length-desc)
            partition.sort((v,w) => v.length===w.length ?  w.num-v.num : w.length-v.length);
        }
        if (partition.length===n) { // DISCRETE PARTITION  (after initial refinement)
            // Get canon with this permutation;
            if (temp = getCanon({matrix, partition}), typeof temp==="string") throw new Error(temp);
            result.canonicalLabel = temp;
            result.canonicalPartition = partition;
        } else {
            // Select target cell (max-length-first)
            let parts = partition.map((v,i) => [i, v]).filter(v => v[1].length>1);
            let [iMax, len] = [-1, 0];
            for (let j=0; j<parts.length; j++) {
                let [i,v] = parts[j];
                let vl = v.length;
                if (vl>len) [iMax, len] = [i, vl];
            }
            let index = iMax;
            if (index>-1) {
                let cell = partition[index];
                let sCell = new Set(cell);
                while (sCell.size>0) {
                    // Fix vertex
                    let vertex = Math.min(...sCell);
                    let indexCell = cell.indexOf(vertex);
                    sCell.delete(vertex);
                    let filter =  indexCell===0 || result.mcr===null;
                    if (!filter) {
                        if (!result.lastEqual) {
                            let f = result.nodes[level][positionPrev][1];
                            let m = identity;
                            for (let j=0; j<result.fixMcr.length; j++) {
                                let [fix, mcr] = result.fixMcr[j];
                                if (f.every(v => fix.includes(v))){
                                    m = m.intersection(mcr);
                                }
                            }
                            result.mcr = m;
                        }
                        filter = result.mcr.has(vertex);
                    }
                    if (filter) {
                        let part = partition.map(v => v.slice(0));
                        part[index] = cell.toSpliced(indexCell, 1);
                        part = part.toSpliced(index, 0, [vertex]);
                        // Refine partition
                        if (part.length<n) {
                            let cells = [[vertex]];
                            part = refinePartition({matrix, partition:part, cells});
                            if (typeof part==="string") throw new Error(part);
                        }
                        // Add node to tree nodes
                        let lv = result.nodes[level].length-1;
                        if (result.nodes.length<level+2) result.nodes.push([]);
                        let prev = result.nodes[level][lv][1];
                        let fix = [...prev, vertex];
                        result.nodes[level+1].push([lv, fix]);
                        let position = result.nodes[level+1].length-1;
                        if (part.length===n) {// DISCRETE PARTITION 
                            // Get canon with this permutation
                            if (temp = getCanon({matrix, partition:part}), typeof temp==="string") throw new Error(temp);
                            let canon = temp;
                            if (result.canonicalLabel===null) {
                                result.canonicalPartition = part;
                                result.canonicalLabel = canon;
                            } else {
                                // Compare canon
                                let compCanon = 0;
                                for (let i=0; i<canon.length; i++) {
                                    if (canon[i]<result.canonicalLabel[i]) {
                                        compCanon = 1;
                                        break;
                                    } else if (canon[i]>result.canonicalLabel[i]) {
                                        compCanon = -1;
                                        break;
                                    }
                                }
                                if (compCanon===1) {
                                    result.canonicalPartition = part;
                                    result.canonicalLabel = canon;
                                    result.lastEqual = false;
                                } else if (compCanon===0) {
                                    if (temp = getAutomorphism({result, part}), typeof temp==="string") throw new Error(temp);
                                    result.lastEqual = true;
                                } else {
                                    result.lastEqual = false;
                                }
                            }
                        } else {
                            let res = getPartitionTreeRefineMcr({matrix, partition:part, level, positionPrev:position, identity, result});
                            if (typeof res==="string") throw new Error(res);
                        }
                    }
                }
            }
        }
    } catch(e){result = "getPartitionTreeRefineMcr(): " + e.message}
    return result;
}

La función getAutomorphism() usa permutationsToCyclic() que vimos en un apartado anterior para obtener el automorfismo a partir de dos particiones, actualizando los campos "mcr" y "fixMcr":

function getAutomorphism({result, part}) {
    try {
        if (temp = permutationsToCyclic({permutation1:result.canonicalPartition, permutation2:part}), temp.error) throw new Error(temp.error);
        let cyclic = temp.cycles;
        result.automorphisms.push(cyclic);
        let mcr = [];
        let fix = [];
        for (let i=0; i<cyclic.length; i++) {
            let cell = cyclic[i];
            if (cell.length===1) fix.push(cell[0]);
            mcr.push(cell[0]);
        }
        if (result.fixMcr===null) {
            result.fixMcr = [[fix, new Set(mcr)]];
        } else {
            result.fixMcr.push([fix, new Set(mcr)]);
        }
        if (result.mcr===null){
            result.mcr = new Set(mcr);
        } else {
            result.mcr = result.mcr.intersection(new Set(mcr));
        }
    } catch(e){result = "getAutomorphism(): " + e.message}
    return result;
}

Para ejecutarlo puede descargar las funciones en este archivo de texto get-partition-tree-mcr.txt, copiarlo y pegarlo en la consola del navegador ejecutándose dos posibles ejemplos que ya hemos visto en un tema anterior. Vea que le pasamos la matriz de adyacencia y adjudicamos manualmente su lista de aristas y vecinos, aunque pueden obtenerser con los algoritmos getEdges() y getNeighbors()

function runGetPartitionTreeRefineMcr(sample=1) {
    let matrix;
    if (sample===1) { // Explained in https://www.wextensible.com/temas/grafos/isomorfismos-automorfismos.html#t_prunning-automorphisms
        matrix = [[0,1,0,1,0,0,0,0,0],[1,0,1,1,1,1,0,0,0],[0,1,0,0,0,1,0,0,0],[1,1,0,0,1,0,1,1,0],[0,1,0,1,0,1,0,1,0],[0,1,1,0,1,0,0,1,1],[0,0,0,1,0,0,0,1,0],[0,0,0,1,1,1,1,0,1],[0,0,0,0,0,1,0,1,0]];
        matrix.edges = [[0,1],[0,3],[1,2],[1,3],[1,4],[1,5],[2,5],[3,4],[3,6],[3,7],[4,5],[4,7],[5,7],[5,8],[6,7],[7,8]];
        matrix.neighbors = [[1,3],[0,2,3,4,5],[1,5],[0,1,4,6,7],[1,3,5,7],[1,2,4,7,8],[3,7],[3,4,5,6,8],[5,7]];
    } else if (sample===2) { // Explained in https://www.wextensible.com/temas/grafos/isomorfismos-automorfismos.html#t_rebuiltMcr 
        matrix = [[0,1,0,0,1,0,0,0,1,0],[1,0,1,1,0,0,0,0,0,0],[0,1,0,1,0,0,0,0,0,1],[0,1,1,0,0,0,0,0,1,0],[1,0,0,0,0,0,1,1,0,0],[0,0,0,0,0,0,1,1,0,1],[0,0,0,0,1,1,0,0,0,1],[0,0,0,0,1,1,0,0,1,0],[1,0,0,1,0,0,0,1,0,0],[0,0,1,0,0,1,1,0,0,0]];
        matrix.edges = [[0,1],[0,4],[0,8],[1,2],[1,3],[2,3],[2,9],[3,8],[4,6],[4,7],[5,6],[5,7],[5,9],[6,9],[7,8]];
        matrix.neighbors = [[1,4,8],[0,2,3],[1,3,9],[1,2,8],[0,6,7],[6,7,9],[4,5,9],[4,5,8],[0,3,7],[2,5,6]];
    }
    let result = getPartitionTreeRefineMcr({matrix});
    // Only for view of Sets of JavaScript with JSON.stringify
    result.mcr = [...result.mcr];
    result.fixMcr = result.fixMcr.map(v => [v[0], [...v[1]]]);
    return result;
}
/* The result should be for sample 1:

{"canonicalPartition":[[1],[3],[5],[7],[0],[2],[6],[8],[4]],
"canonicalLabel":[[0,1],[0,2],[0,4],[0,5],[0,8],[1,3],[1,4],[1,6],[1,8],[2,3],[2,5],[2,7],[2,8],[3,6],[3,7],[3,8]],
"nodes":[[[-1,[]]],[[0,[1]],[0,[3]]],[[0,[1,3]],[0,[1,5]],[1,[3,1]]]],
"mcr":[0,1,4],
"fixMcr":[[[1,4,7],[0,1,3,4,6,7]],[[0,4,8],[0,1,2,4,5,8]]],
"lastEqual":true,
"automorphisms":[
    [[0,2],[1],[3,5],[4],[6,8],[7]],
    [[0],[1,3],[2,6],[4],[5,7],[8]]
]}

And for sample 2:

{"canonicalPartition":[[1],[5],[6],[7],[4],[9],[8],[3],[2],[0]],
"canonicalLabel":[[0,7],[0,8],[0,9],[1,2],[1,3],[1,5],[2,4],[2,5],[3,4],[3,6],[4,9],[5,8],[6,7],[6,9],[7,8]],
"nodes":[[[-1,[]]],[[0,[0]],[0,[1]],[0,[2]],[0,[3]],[0,[4]],[0,[5]]],[[2,[2,5]],[2,[2,6]]]],
"mcr":[0,1,2,4],
"fixMcr":[[[2,9],[0,1,2,4,5,9]],[[],[0,1,2,3,4]]],
"lastEqual":true,
"automorphisms":[
    [[0,8],[1,3],[2],[4,7],[5,6],[9]],
    [[0,7],[1,5],[2,9],[3,6],[4,8]]
]}
*/
console.log(JSON.stringify(runGetPartitionTreeRefineMcr(2)));