Documentation

LeanPool.IsoGraph.Canon.Spec

From the canonical labelling algorithm to permutations, and its specification #

IsoGraph.Canon.Algorithm computes with raw Array Nats. This file wraps that up as an honest Equiv.Perm (Fin n) and states the two properties that characterise it.

The wrapper #

permOfArrays turns the algorithm's output and its inverse into an Equiv.Perm (Fin n) by checking at run time (in O(n)) that the two arrays really are mutually inverse, falling back to the identity if not. That keeps canonPerm total, and it makes exists_relabel_of_canonAdj_eq below hold for whatever the algorithm returns: canonAdj n adj is the graph adj read through some permutation, hence isomorphic to it.

The specification #

Write relabel σ adj for adj with its vertices renamed along σ. Two statements matter.

Arrays as permutations #

def IsoGraph.Canon.finFn (n : ) (a : Array ) (i : Fin n) :
Fin n

Read an array of naturals as a function Fin n → Fin n, sending out-of-range entries to themselves.

Equations
Instances For

    Build a permutation of Fin n out of an array and its claimed inverse.

    The two arrays are checked (in O(n)) to be mutually inverse, and the identity is returned if they are not. So this is total and needs no facts about the algorithm that produced them; the fallback is unreachable in practice.

    Equations
    • One or more equations did not get rendered due to their size.
    Instances For

      The inverse of an array-encoded permutation of {0, …, n-1}.

      Equations
      Instances For

        Tabulating an adjacency function #

        An adjacency function is called far more than times by the search below, and for most graphs each call does real work — a scan of an edge list, a comparison of two coordinates, a recursive call under a complement. Filling an n × n array of Bool once and reading it thereafter is therefore worth several times the cost of the fill; CGraph.canonOfArray does exactly that, and CGraph.cache in Cache.lean offers it to the rest of the library.

        The shape is forced. Lean maximises the arity of a top-level definition, so a def whose type ends in Fin n → Fin n → Bool and whose body builds a table is compiled with the table inside the two-argument function, and rebuilds it on every query. matLookup is therefore a top-level definition of its own, applied to the array alone: what is passed around is a closure holding the table.

        def IsoGraph.Canon.adjArray (n : ) (adj : Fin nFin nBool) :

        The adjacency matrix of adj, as an array of rows.

        Equations
        Instances For
          def IsoGraph.Canon.matLookup (n : ) (a : Array (Array Bool)) (i j : Fin n) :

          Read the entry of an adjacency matrix at (i, j). Top-level, and meant to be applied to the array alone, for the reason above.

          Equations
          Instances For
            @[simp]
            theorem IsoGraph.Canon.matLookup_adjArray (n : ) (adj : Fin nFin nBool) (i j : Fin n) :
            matLookup n (adjArray n adj) i j = adj i j
            theorem IsoGraph.Canon.matLookup_adjArray_eq (n : ) (adj : Fin nFin nBool) :
            matLookup n (adjArray n adj) = adj
            def IsoGraph.Canon.oracleOfFin (n : ) (adj : Fin nFin nBool) (v w : ) :

            Adjacency oracle on {0, …, n-1} coming from an adjacency function on Fin n.

            Equations
            Instances For
              def IsoGraph.Canon.canonPerm (n : ) (adj : Fin nFin nBool) :

              The canonical labelling of a graph on Fin n: canonical position i holds the vertex canonPerm n adj i.

              Equations
              • One or more equations did not get rendered due to their size.
              Instances For
                def IsoGraph.Canon.canonAdj (n : ) (adj : Fin nFin nBool) :
                Fin nFin nBool

                The canonical form of a graph on Fin n: the graph relabelled so that its adjacency matrix is the canonical one.

                This is the specification, not the way to compute. Lean η-expands every function-typed definition, so each query canonAdj n adj i j re-runs the whole search. To compute, use canonMatrix, whose result is a structure and therefore shares the search across queries.

                Equations
                Instances For
                  def IsoGraph.Canon.finEq {m n : } (h : m = n) :
                  Fin m Fin n

                  Fin m ≃ Fin n from m = n. Unlike Equiv.cast this has a definitional val.

                  Equations
                  Instances For
                    @[simp]
                    theorem IsoGraph.Canon.finEq_val {m n : } (h : m = n) (i : Fin m) :
                    ((finEq h) i) = i
                    @[simp]
                    theorem IsoGraph.Canon.finEq_symm_val {m n : } (h : m = n) (j : Fin n) :
                    ((finEq h).symm j) = j
                    theorem IsoGraph.Canon.oracleOfFin_apply {n : } (f : Fin nFin nBool) {a b : } (ha : a < n) (hb : b < n) :
                    oracleOfFin n f a b = f a, ha b, hb
                    theorem IsoGraph.Canon.oracleOfFin_comm {n : } {f : Fin nFin nBool} (hf : ∀ (i j : Fin n), f i j = f j i) (a b : ) :
                    oracleOfFin n f a b = oracleOfFin n f b a
                    theorem IsoGraph.Canon.oracleOfFin_irrefl {n : } {f : Fin nFin nBool} (hf : ∀ (i : Fin n), f i i = false) (a : ) :

                    Adjacency matrices #

                    The type the canonical form is actually delivered in. Two things are going on:

                    The adjacency matrix of a graph on Fin n.

                    • adj : Fin nFin nBool

                      The adjacency function.

                    Instances For
                      theorem IsoGraph.Canon.AdjMatrix.ext' {n : } {M N : AdjMatrix n} (h : M.adj = N.adj) :
                      M = N

                      Query a matrix at plain naturals; false out of range.

                      Equations
                      Instances For
                        theorem IsoGraph.Canon.AdjMatrix.get_eq {n : } (M : AdjMatrix n) {a b : } (ha : a < n) (hb : b < n) :
                        M.get a b = M.adj a, ha b, hb

                        Move a matrix onto the index set Fin m, reading false outside the common range.

                        This is the one place where an index set of the "wrong" size is tolerated, and it is what lets the canonical form of a graph be stated on Fin (Fintype.card V) while being computed from a listing whose length is only provably that.

                        Equations
                        Instances For
                          @[simp]
                          theorem IsoGraph.Canon.AdjMatrix.reindex_adj {n m : } (M : AdjMatrix n) (i j : Fin m) :
                          (M.reindex m).adj i j = M.get i j
                          theorem IsoGraph.Canon.AdjMatrix.reindex_congr {n k m : } {M : AdjMatrix n} {N : AdjMatrix k} (h : ∀ (a b : ), M.get a b = N.get a b) :
                          M.reindex m = N.reindex m
                          theorem IsoGraph.Canon.AdjMatrix.get_comm {n : } {M : AdjMatrix n} (h : ∀ (i j : Fin n), M.adj i j = M.adj j i) (a b : ) :
                          M.get a b = M.get b a
                          theorem IsoGraph.Canon.AdjMatrix.get_irrefl {n : } {M : AdjMatrix n} (h : ∀ (i : Fin n), M.adj i i = false) (a : ) :
                          M.get a a = false
                          theorem IsoGraph.Canon.AdjMatrix.heq_of_adj {m n : } (h : m = n) {M : AdjMatrix m} {N : AdjMatrix n} (hMN : ∀ (x y : Fin m), M.adj x y = N.adj ((finEq h) x) ((finEq h) y)) :
                          M N

                          Matrices of the same size, agreeing pointwise up to the identification of the index sets, are heterogeneously equal.

                          def IsoGraph.Canon.matrixOfPerm (n : ) (adj : Fin nFin nBool) (σ : Equiv.Perm (Fin n)) :

                          The graph adj read through the permutation σ, as a matrix.

                          Equations
                          Instances For
                            def IsoGraph.Canon.canonMatrix (n : ) (adj : Fin nFin nBool) :

                            The canonical form of a graph on Fin n, computed. The search runs once, when this is forced — σ is an argument of matrixOfPerm, so it is evaluated before the closure is built — and each query of the resulting adj is then O(1).

                            Equations
                            Instances For
                              @[simp]
                              theorem IsoGraph.Canon.canonMatrix_adj (n : ) (adj : Fin nFin nBool) :
                              (canonMatrix n adj).adj = canonAdj n adj
                              theorem IsoGraph.Canon.canonMatrix_get (n : ) (adj : Fin nFin nBool) (a b : ) :
                              (canonMatrix n adj).get a b = oracleOfFin n (canonAdj n adj) a b

                              Relabelling #

                              def IsoGraph.Canon.relabel {n : } (σ : Equiv.Perm (Fin n)) (adj : Fin nFin nBool) :
                              Fin nFin nBool

                              adj with its vertices renamed along σ: the vertex i of relabel σ adj plays the role of the vertex σ i of adj.

                              Equations
                              Instances For
                                @[simp]
                                theorem IsoGraph.Canon.relabel_apply {n : } (σ : Equiv.Perm (Fin n)) (adj : Fin nFin nBool) (i j : Fin n) :
                                relabel σ adj i j = adj (σ i) (σ j)
                                @[simp]
                                theorem IsoGraph.Canon.relabel_refl {n : } (adj : Fin nFin nBool) :
                                relabel (Equiv.refl (Fin n)) adj = adj
                                theorem IsoGraph.Canon.relabel_relabel {n : } (σ τ : Equiv.Perm (Fin n)) (adj : Fin nFin nBool) :
                                relabel σ (relabel τ adj) = relabel (Equiv.trans σ τ) adj
                                @[simp]
                                theorem IsoGraph.Canon.canonAdj_apply {n : } (adj : Fin nFin nBool) (i j : Fin n) :
                                canonAdj n adj i j = adj ((canonPerm n adj) i) ((canonPerm n adj) j)

                                canonAdj is, pointwise, the original adjacency read through canonPerm.

                                theorem IsoGraph.Canon.canonAdj_eq_relabel {n : } (adj : Fin nFin nBool) :
                                canonAdj n adj = relabel (canonPerm n adj) adj

                                The canonical form is a relabelling of the original graph.

                                theorem IsoGraph.Canon.canonAdj_comm {n : } {adj : Fin nFin nBool} (h : ∀ (i j : Fin n), adj i j = adj j i) (i j : Fin n) :
                                canonAdj n adj i j = canonAdj n adj j i
                                theorem IsoGraph.Canon.canonAdj_irrefl {n : } {adj : Fin nFin nBool} (h : ∀ (i : Fin n), ¬adj i i = true) (i : Fin n) :
                                ¬canonAdj n adj i i = true

                                Soundness: equal canonical forms come from isomorphic graphs #

                                theorem IsoGraph.Canon.exists_relabel_of_canonAdj_eq {n : } {adjG adjH : Fin nFin nBool} (h : canonAdj n adjG = canonAdj n adjH) :
                                ∃ (σ : Equiv.Perm (Fin n)), relabel σ adjG = adjH

                                Soundness. If two graphs on Fin n have the same canonical form then they are isomorphic — indeed, an explicit isomorphism is produced.

                                Nothing about the search is needed here. canonAdj n adj is by construction adj read through the permutation canonPerm n adj, and permOfArrays guarantees that this really is a permutation whatever the algorithm returned; so equal canonical forms exhibit the two graphs as relabellings of one common graph.

                                Invariance #

                                Renaming the vertices of a graph does not change its canonical form: canonAdj_relabel below. Consequently the canonical form depends only on the isomorphism class, and IsoGraph may be Quotient.lifted through it.

                                The statement is phrased here for Equiv.Perm (Fin n), and comes from two statements about the raw array algorithm, LabellingIsPerm and LabellingInvariant, which mention nothing but Array Nat and canonicalLabellingOfOracle. The Fin/Equiv.Perm wrapper in between — the permOfArrays run-time check, the invArray inverse, the translation between Equiv.Perm (Fin n) and a renaming of {0, …, n-1} — is what this section is about; see IsoGraph/Canon/Equivariance.lean for the groundwork on the other side.

                                Of the two, LabellingIsPerm is cheap: canonicalLabellingOfOracle verifies it at run time in O(n). LabellingInvariant is where all the work is; it comes from canonical_cert_relabel of IsoGraph/Canon/Correct.lean.

                                @[reducible, inline]
                                abbrev IsoGraph.Canon.labelling (m : ) (f : Bool) :

                                The labelling the search returns for the oracle f on m vertices: canonical position i holds the vertex labelling m f.

                                Equations
                                Instances For

                                  The labelling is a permutation of the vertices. canonicalLabellingOfOracle checks this in O(n) and returns the identity if the check fails, so it holds regardless of what the search does. See labellingIsPerm.

                                  Equations
                                  Instances For

                                    The labelling the search settles on is equivariant.

                                    Renaming the vertices along s and canonicalising gives the same adjacency matrix as canonicalising and not renaming. Note this is weaker than "the labelling itself transforms along s", which is false: the winner is only determined up to an automorphism, and which of several equally-good leaves the search happens to reach does depend on vertex names. What must not depend on them is the matrix read off at the winner, which is what this says.

                                    Equations
                                    • One or more equations did not get rendered due to their size.
                                    Instances For

                                      The search's answer satisfies the specification BestKey (canonSt_bestKey), which is manifestly an isomorphism invariant, so the certificate it returns does not depend on the vertex names (canonical_cert_relabel); certOf_get reads the adjacency matrix back out of that certificate.

                                      invArray and permOfArrays on a genuine permutation #

                                      theorem IsoGraph.Canon.invArray_apply {m : } {a : Array } (h : IsPerm m fun (v : ) => a[v]!) (i : ) (hi : i < m) :
                                      (invArray m a)[a[i]!]! = i

                                      On a permutation array, invArray really is the inverse.

                                      theorem IsoGraph.Canon.permOfArrays_val {m : } {a : Array } (h : IsPerm m fun (v : ) => a[v]!) (hinv : i < m, (invArray m a)[a[i]!]! = i) (i : Fin m) :
                                      ((permOfArrays m a (invArray m a)) i) = a[i]!

                                      So the run-time check inside permOfArrays succeeds, and the permutation it returns is the array read literally.

                                      The renaming of {0, …, n-1} induced by a permutation of Fin n #

                                      def IsoGraph.Canon.natOfPerm (m : ) (σ : Equiv.Perm (Fin m)) (v : ) :

                                      σ as a renaming of plain naturals, fixing everything outside the vertex set.

                                      Equations
                                      Instances For
                                        theorem IsoGraph.Canon.natOfPerm_lt {m : } (σ : Equiv.Perm (Fin m)) {v : } (hv : v < m) :
                                        natOfPerm m σ v = (σ v, hv)
                                        theorem IsoGraph.Canon.oracleOfFin_relabel (m : ) (σ : Equiv.Perm (Fin m)) (adj : Fin mFin mBool) :
                                        oracleOfFin m (relabel σ adj) = fun (v w : ) => oracleOfFin m adj (natOfPerm m σ v) (natOfPerm m σ w)

                                        Relabelling a graph on Fin n is renaming its oracle.

                                        From the array level to Equiv.Perm #

                                        theorem IsoGraph.Canon.canonPerm_val {n : } (hA : LabellingIsPerm) (adj : Fin nFin nBool) (i : Fin n) :
                                        ((canonPerm n adj) i) = (labelling n (oracleOfFin n adj))[i]!

                                        When the labelling is a permutation, canonPerm is the labelling array read literally.

                                        theorem IsoGraph.Canon.canonAdj_eq_oracle {n : } (hA : LabellingIsPerm) (adj : Fin nFin nBool) (i j : Fin n) :
                                        canonAdj n adj i j = oracleOfFin n adj (labelling n (oracleOfFin n adj))[i]! (labelling n (oracleOfFin n adj))[j]!

                                        The canonical form, evaluated: it is the oracle read at the labelling.

                                        theorem IsoGraph.Canon.canonAdj_relabel_of {n : } (hA : LabellingIsPerm) (hB : LabellingInvariant) (σ : Equiv.Perm (Fin n)) (adj : Fin nFin nBool) :
                                        canonAdj n (relabel σ adj) = canonAdj n adj

                                        Invariance of the canonical form, from the two array-level statements. Nothing else about the algorithm enters.

                                        theorem IsoGraph.Canon.canonAdj_relabel {n : } (σ : Equiv.Perm (Fin n)) (adj : Fin nFin nBool) :
                                        canonAdj n (relabel σ adj) = canonAdj n adj

                                        Invariance of the canonical form. Renaming the vertices of a graph does not change its canonical form, so the canonical form depends only on the isomorphism class of the graph and anything read off it is a graph invariant.

                                        The labelling is not equivariant: the winner is determined only up to an automorphism, and which of several equally-good leaves the search reaches does depend on the vertex names. What does not depend on them is the adjacency matrix read off at the winner.

                                        theorem IsoGraph.Canon.canonAdj_eq_of_equiv {n : } {A B : Fin nFin nBool} (σ : Equiv.Perm (Fin n)) ( : ∀ (a b : Fin n), B (σ a) (σ b) = A a b) :

                                        Two adjacency functions related by a permutation have the same canonical form.

                                        theorem IsoGraph.Canon.oracleOfFin_canonAdj_congr {m k : } (h : m = k) {A : Fin mFin mBool} {B : Fin kFin kBool} (σ : Fin m Fin k) ( : ∀ (a b : Fin m), B (σ a) (σ b) = A a b) :

                                        The -indexed form of canonAdj_eq_of_equiv: two adjacency functions, on index sets of the same size, related by a bijection, have the same canonical adjacency oracle.

                                        theorem IsoGraph.Canon.canonMatrix_get_congr {m k : } (h : m = k) {A : Fin mFin mBool} {B : Fin kFin kBool} (σ : Fin m Fin k) ( : ∀ (a b : Fin m), B (σ a) (σ b) = A a b) (a b : ) :
                                        (canonMatrix m A).get a b = (canonMatrix k B).get a b

                                        Canonical forms of isomorphic graphs agree entrywise, at the level of plain naturals.

                                        theorem IsoGraph.Canon.canonMatrix_reindex_congr {m k : } (h : m = k) {A : Fin mFin mBool} {B : Fin kFin kBool} (σ : Fin m Fin k) ( : ∀ (a b : Fin m), B (σ a) (σ b) = A a b) (N : ) :

                                        Canonical forms of isomorphic graphs, moved onto a common index set, are equal. N is arbitrary, so it may be taken to be Fintype.card V, independently of any listing of the vertices.

                                        theorem IsoGraph.Canon.canonAdj_eq_iff {n : } {adjG adjH : Fin nFin nBool} :
                                        canonAdj n adjG = canonAdj n adjH ∃ (σ : Equiv.Perm (Fin n)), relabel σ adjG = adjH

                                        Two graphs on Fin n have the same canonical form exactly when they are isomorphic.

                                        theorem IsoGraph.Canon.canonAdj_congr {m n : } (h : m = n) {adjG : Fin mFin mBool} {adjH : Fin nFin nBool} (σ : Fin m Fin n) ( : ∀ (a b : Fin m), adjH (σ a) (σ b) = adjG a b) (x y : Fin m) :
                                        canonAdj m adjG x y = canonAdj n adjH (h x) (h y)

                                        The transported form of canonAdj_relabel: graphs on Fin m and Fin n that are isomorphic (so in particular m = n) have the same canonical form.