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.
Soundness —
canonAdj n adjG = canonAdj n adjH → adjG ≅ adjH(exists_relabel_of_canonAdj_eq): a canonical-form comparison never conflates non-isomorphic graphs. The run-time check above is exactly what buys it.Invariance —
canonAdj n (relabel σ adj) = canonAdj n adj(canonAdj_relabel): the canonical form depends only on the isomorphism class, so anything defined through it descends to the quotient. On raw arrays the same statement isLabellingInvariant, whichIsoGraph/Canon/Correct.leanobtains from the soundness and optimality of the search.
Arrays as permutations #
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 n² 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.
The adjacency matrix of adj, as an array of rows.
Equations
- IsoGraph.Canon.adjArray n adj = Array.ofFn fun (i : Fin n) => Array.ofFn fun (j : Fin n) => adj i j
Instances For
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.
Instances For
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
- IsoGraph.Canon.canonAdj n adj = fun (i j : Fin n) => adj ((IsoGraph.Canon.canonPerm n adj) i) ((IsoGraph.Canon.canonPerm n adj) j)
Instances For
Adjacency matrices #
The type the canonical form is actually delivered in. Two things are going on:
- it is a structure, not a bare
Fin n → Fin n → Bool, because the compiler η-expands every definition whose type is a function type — adef f (x) : Fin n → Fin n → Bool := <search>; fun i j ↦ …re-runs<search>on every single query. One field is enough to block that, and a one-field structure is unboxed at runtime, so the wrapper is free; - it is indexed by its size, so that "the canonical form of a graph on
V" can live inAdjMatrix (Fintype.card V)— a type that does not mention the listing ofVused to compute it, which is what makes the quotient lift inIsoGraph.Basictypecheck.
Query a matrix at plain naturals; false out of range.
Equations
- M.get a b = IsoGraph.Canon.oracleOfFin n M.adj a b
Instances For
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.
Instances For
The graph adj read through the permutation σ, as a matrix.
Equations
- IsoGraph.Canon.matrixOfPerm n adj σ = { adj := fun (i j : Fin n) => adj (σ i) (σ j) }
Instances For
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
- IsoGraph.Canon.canonMatrix n adj = IsoGraph.Canon.matrixOfPerm n adj (IsoGraph.Canon.canonPerm n adj)
Instances For
Relabelling #
Soundness: equal canonical forms come from isomorphic graphs #
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.
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
- IsoGraph.Canon.LabellingIsPerm = ∀ (m : ℕ) (f : ℕ → ℕ → Bool), (IsoGraph.Canon.labelling m f).size = m ∧ IsoGraph.Canon.IsPerm m fun (v : ℕ) => (IsoGraph.Canon.labelling m f)[v]!
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 #
σ as a renaming of plain naturals, fixing everything outside the vertex set.
Equations
- IsoGraph.Canon.natOfPerm m σ v = if h : v < m then ↑(σ ⟨v, h⟩) else v
Instances For
Relabelling a graph on Fin n is renaming its oracle.
From the array level to Equiv.Perm #
The canonical form, evaluated: it is the oracle read at the labelling.
Invariance of the canonical form, from the two array-level statements. Nothing else about the algorithm enters.
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.
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.
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.
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.