Documentation

LeanPool.IsoGraph.Canon.Equivariance

Equivariance of the pieces of the canonical labelling #

This file works at the level of IsoGraph.Canon.Algorithm: raw Array Nats, and permutations of {0, …, n-1} represented as NatNat. The translation to Equiv.Perm (Fin n) happens in IsoGraph/Canon/Spec.lean.

Fix a renaming σ of the vertices and write q ≈ p for "the ordered partitions q and p have the same cell boundaries, and the i-th cell of q is the σ-image of the i-th cell of p as a set" (PartEquiv). Only as a set: refinement's counting sort is stable, so the order within a cell is inherited from the parent cell and therefore does depend on the vertex names. What is invariant is the sequence of cells.

Each piece of the search respects :

Permutations of an initial segment #

structure IsoGraph.Canon.IsPerm (n : ) (σ : ) :

σ permutes {0, …, n-1}: it maps the segment into itself and is injective there. Since the segment is finite this is the same as being a bijection of it, but the two clauses are what the proofs actually use.

  • maps (v : ) : v < nσ v < n

    σ maps the segment into itself.

  • inj (v : ) : v < nw < n, σ v = σ wv = w

    σ is injective on the segment.

Instances For
    theorem IsoGraph.Canon.IsPerm.comp {n : } {σ τ : } ( : IsPerm n σ) ( : IsPerm n τ) :
    IsPerm n fun (v : ) => σ (τ v)
    theorem IsoGraph.Canon.range_isPerm (n : ) :
    (Array.range n).size = n IsPerm n fun (v : ) => (Array.range n)[v]!

    The identity labelling is a permutation — the fallback branch of canonicalLabellingOfOracle.

    theorem IsoGraph.Canon.isPermArray_spec {n : } {a : Array } (h : isPermArray n a = true) :
    a.size = n IsPerm n fun (v : ) => a[v]!

    isPermArray is sound: what it accepts really is a permutation of {0, …, n-1}. (It is also complete, but nothing below needs that — an unsound accept would be the problem.)

    The labelling really is a labelling. Not because the search is known to produce one — that is still open — but because canonicalLabellingOfOracle checks, and falls back to the identity if the check fails.

    Graph.ofOracle #

    The algorithm never reads an oracle directly: it reads the adj matrix and the nbr lists that Graph.ofOracle builds from it. So every statement about renaming vertices has to pass through these three lemmas.

    @[simp]
    theorem IsoGraph.Canon.ofOracle_n (n : ) (f : Bool) :
    @[simp]
    theorem IsoGraph.Canon.ofOracle_adj (n : ) (f : Bool) (v w : ) (hv : v < n) (hw : w < n) :
    (Graph.ofOracle n f).adj[v]![w]! = f v w

    The dense matrix of Graph.ofOracle is the oracle, on the intended range.

    theorem IsoGraph.Canon.ofOracle_nbr (n : ) (f : Bool) (v : ) (hv : v < n) :

    A neighbour list of Graph.ofOracle is the row of the oracle, as a filtered range.

    theorem IsoGraph.Canon.ofOracle_mem_nbr (n : ) (f : Bool) (v w : ) (hv : v < n) (hw : w < n) :

    The neighbour lists of Graph.ofOracle are the rows of the oracle.

    theorem IsoGraph.Canon.ofOracle_nbr_lt (n : ) (f : Bool) (v w : ) (hv : v < n) (hw : w (Graph.ofOracle n f).nbr[v]!) :
    w < n

    Neighbour lists stay inside the vertex set.

    @[simp]
    theorem IsoGraph.Canon.ofOracle_nbr_size (n : ) (f : Bool) :

    The number of neighbour lists is the number of vertices.

    theorem IsoGraph.Canon.ofOracle_nbr_lt' (n : ) (f : Bool) (u x : ) (hx : x (Graph.ofOracle n f).nbr[u]!) :
    x < n

    Neighbour lists stay inside the vertex set, with no hypothesis on the vertex: out of range nbr[u]! is the empty array, which has no members either.

    Readers of a partition that see only the cell boundaries #

    Step 2 of the decomposition: shapeHash and targetCell walk the partition cell by cell using cen alone, never touching lab. So they agree on any two partitions with the same boundaries — in particular on partitions related by a renaming of the vertices, whatever that renaming does inside the cells.

    theorem IsoGraph.Canon.cenHashFrom_congr {n : } {c d : Array } (h : i < n, c[i]! = d[i]!) (fuel i : ) (x : UInt64) :
    cenHashFrom c n fuel i x = cenHashFrom d n fuel i x

    The cell walk only ever reads cen at indices < n, so two cens that agree there send it along the same path. Both congruence lemmas below are this observation at fuel = n, i = 0.

    theorem IsoGraph.Canon.cenTargetFrom_congr {n : } {c d : Array } (h : i < n, c[i]! = d[i]!) (fuel i : ) :
    cenTargetFrom c n fuel i = cenTargetFrom d n fuel i
    theorem IsoGraph.Canon.shapeHash_congr (n : ) (p q : Part) (h : i < n, p.cen[i]! = q.cen[i]!) :
    theorem IsoGraph.Canon.targetCell_congr (n : ) (p q : Part) (h : i < n, p.cen[i]! = q.cen[i]!) :
    theorem IsoGraph.Canon.cenTargetFrom_lt {n : } {c : Array } (fuel i j : ) :
    cenTargetFrom c n fuel i = some jj < n

    The walk only reports a cell start it has already checked is < n.

    theorem IsoGraph.Canon.targetCell_lt (n : ) (p : Part) (i : ) (h : p.targetCell n = some i) :
    i < n

    The target cell, when there is one, is a cell start inside the vertex set that is not a singleton. (Used to know that individualising is legitimate.)

    Certificates #

    Step 4 of the decomposition, certificate half. A certificate is the adjacency matrix read in the order given by lab; so renaming the vertices along σ and reading in the order lab gives the same bits as leaving the graph alone and reading in the order σ ∘ lab. This is the point at which "the search found corresponding leaves" turns into "the two runs return the same array".

    theorem IsoGraph.Canon.certRow_congr {n : } {b b' : Bool} (h : j < n, b j = b' j) (fuel j : ) (acc : UInt64) (k : ) (out : Array UInt64) :
    j + fuel = ncertRow n b fuel j acc k out = certRow n b' fuel j acc k out

    One packed row only reads columns j < n. The fuel invariant j + fuel = n is what makes that available: it says the loop is at position j with fuel columns left, so fuel ≥ 1 forces j < n.

    theorem IsoGraph.Canon.certRowsFrom_congr {n : } {b b' : Bool} (w : ) (h : i < n, j < n, b i j = b' i j) (fuel i : ) (out : Array UInt64) :
    i + fuel = ncertRowsFrom n b w fuel i out = certRowsFrom n b' w fuel i out

    The same for the rows: row i is packed only for i < n.

    theorem IsoGraph.Canon.certBits_congr (n : ) (b b' : Bool) (h : i < n, j < n, b i j = b' i j) :

    certBits reads the matrix only inside {0, …, n-1}².

    theorem IsoGraph.Canon.certOf_eq (G : Graph) (lab : Array ) :
    certOf G lab = certBits G.n fun (i j : ) => G.adj[lab[i]!]![lab[j]!]!
    theorem IsoGraph.Canon.certOf_congr (G H : Graph) (lab lab' : Array ) (hn : G.n = H.n) (hlab : i < G.n, j < G.n, G.adj[lab[i]!]![lab[j]!]! = H.adj[lab'[i]!]![lab'[j]!]!) :
    certOf G lab = certOf H lab'

    certOf reads the adjacency matrix only at the pairs named by lab.

    theorem IsoGraph.Canon.certOf_relabel (n : ) (f : Bool) (σ : ) ( : IsPerm n σ) (lab : Array ) (hsz : lab.size = n) (hlab : i < n, lab[i]! < n) :
    certOf (Graph.ofOracle n fun (v w : ) => f (σ v) (σ w)) lab = certOf (Graph.ofOracle n f) (Array.map σ lab)

    Certificates are equivariant. Reading the renamed graph along lab is reading the original along σ ∘ lab.

    The vocabulary the rest of the decomposition is phrased in. Fix a renaming σ and think of two runs of the algorithm: one on f, one on the graph fun v w => f (σ v) (σ w) whose vertex v "is" vertex σ v of the first. The two runs do not produce partitions that agree positionwise under σ — they both start from Part.unit n, whose lab is Array.range n in either run, and refinement's counting sort is stable, so the order within a cell is inherited from the parent and depends on vertex names. What they do produce is partitions whose cells sit at the same positions and correspond as sets, which is what PartEquiv says.

    structure IsoGraph.Canon.Part.WF (n : ) (p : Part) :

    p is a well-formed ordered partition of {0, …, n-1}: lab and pos are mutually inverse bijections of the segment, and cst/cen mark off a decomposition of it into intervals.

    The last two clauses are what make cst/cen describe cells rather than arbitrary bounds: the interval [cst[i], cen[i]) is the same for every i inside it, so cst[i] is a well-defined name for the cell containing position i.

    • labSize : p.lab.size = n

      lab covers the segment.

    • posSize : p.pos.size = n

      pos covers the segment.

    • cstSize : p.cst.size = n

      cst covers the segment.

    • cenSize : p.cen.size = n

      cen covers the segment.

    • labLt (i : ) : i < np.lab[i]! < n

      Positions hold vertices of the segment.

    • posLt (v : ) : v < np.pos[v]! < n

      Vertices sit at positions in the segment.

    • posLab (i : ) : i < np.pos[p.lab[i]!]! = i

      pos is a left inverse of lab.

    • labPos (v : ) : v < np.lab[p.pos[v]!]! = v

      lab is a left inverse of pos.

    • cstLe (i : ) : i < np.cst[i]! i

      A cell starts at or before each of its positions.

    • ltCen (i : ) : i < ni < p.cen[i]!

      A cell ends after each of its positions.

    • cenLe (i : ) : i < np.cen[i]! n

      Cells stay inside the segment.

    • cellCst (i : ) : i < n∀ (j : ), p.cst[i]! jj < p.cen[i]!p.cst[j]! = p.cst[i]!

      Every position of a cell reports the same start.

    • cellCen (i : ) : i < n∀ (j : ), p.cst[i]! jj < p.cen[i]!p.cen[j]! = p.cen[i]!

      Every position of a cell reports the same end.

    Instances For
      theorem IsoGraph.Canon.Part.WF.cst_eq_iff {n : } {p : Part} (hp : WF n p) {i : } (hi : i < n) {k : } (hk : k < n) :
      p.cst[k]! = p.cst[i]! p.cst[i]! k k < p.cen[i]!

      Cells are determined by their starts. Two positions report the same cell start exactly when they lie in the same interval — so cst really is a set-theoretic partition of positions, not just a monotone pair of arrays. This is the workhorse behind individualize_cell.

      p is discrete: every cell is a singleton, so each position is its own cell start.

      Equations
      Instances For
        theorem IsoGraph.Canon.cst_succ {n : } {p : Part} (hp : Part.WF n p) {i : } (hcen : p.cen[i]! = i + 1) (h : i + 1 < n) :
        p.cst[i + 1]! = i + 1

        The position after a singleton cell starts the next one.

        theorem IsoGraph.Canon.cenTargetFrom_none {n : } {p : Part} (hp : Part.WF n p) (fuel i : ) :
        n i + fuel(i < np.cst[i]! = i)cenTargetFrom p.cen n fuel i = none∀ (k : ), i kk < np.cst[k]! = k

        The cell walk reaches n only by stepping through singletons.

        A partition the walk finds no target in is discrete. This is what discharges the Discrete hypotheses of step 4 at the leaves of the search: the algorithm stops individualising exactly when targetCell returns none, and that is the same condition.

        structure IsoGraph.Canon.PartEquiv (n : ) (σ : ) (p q : Part) :

        p and q are the same ordered partition up to the renaming σ: the cells occupy the same ranges of positions, and vertex v of q's graph lies in the cell where σ v lies in p.

        The third clause is how "the cells correspond as sets" is said pointwise: a cell is named by its start position, so it asserts that σ maps the cell of v in q onto the cell at the same place in p.

        Instances For
          theorem IsoGraph.Canon.PartEquiv.shapeHash {n : } {σ : } {p q : Part} (h : PartEquiv n σ p q) :

          Related partitions have the same cell-size hash.

          theorem IsoGraph.Canon.PartEquiv.targetCell {n : } {σ : } {p q : Part} (h : PartEquiv n σ p q) :

          Related partitions individualise at the same position.

          The search starts from a well-formed partition: one cell, [0, n), in vertex order.

          theorem IsoGraph.Canon.partEquiv_unit (n : ) (σ : ) :

          The unit partition is related to itself under every renaming: one cell carries no order information.

          theorem IsoGraph.Canon.lab_eq_of_discrete {n : } {σ : } {p q : Part} ( : IsPerm n σ) (hp : Part.WF n p) (hq : Part.WF n q) (hpd : Part.Discrete n p) (hqd : Part.Discrete n q) (h : PartEquiv n σ p q) (i : ) (hi : i < n) :
          p.lab[i]! = σ q.lab[i]!

          Once the partitions are discrete, σ relates the labellings positionwise. This is where "cells correspond as sets" turns into an equation between arrays: a singleton cell has only one member, so there is nothing left for the stable sort to have permuted.

          theorem IsoGraph.Canon.certOf_of_partEquiv {n : } {σ : } {p q : Part} ( : IsPerm n σ) (hp : Part.WF n p) (hq : Part.WF n q) (hpd : Part.Discrete n p) (hqd : Part.Discrete n q) (h : PartEquiv n σ p q) (f : Bool) :
          certOf (Graph.ofOracle n fun (v w : ) => f (σ v) (σ w)) q.lab = certOf (Graph.ofOracle n f) p.lab

          Step 4 of the decomposition. Two runs that reach related discrete partitions read off the same certificate — the renamed graph along q.lab is the original along p.lab. This is the point at which "the search found corresponding leaves" becomes "the two runs return the same array".

          Step 3 of the decomposition: individualisation #

          individualize p v splits v off to the front of its cell. The two runs pick different vertices to displace — p.lab[c] need not be σ (q.lab[c]), since the order inside a cell is name-dependent — so the arrays are genuinely different. What survives is exactly what PartEquiv records: the cell boundaries move the same way, and a vertex other than the individualised one lands in the second fragment of the old cell precisely when it was in that cell to begin with.

          theorem IsoGraph.Canon.setCstFrom_size (c ec fuel j : ) (cst : Array ) :
          (setCstFrom c ec fuel j cst).size = cst.size
          theorem IsoGraph.Canon.setCstFrom_getElemD {c ec : } {cst : Array } (hec : ec cst.size) (fuel j : ) :
          ec j + fuel∀ (k : ), (setCstFrom c ec fuel j cst)[k]! = if j k k < ec then c + 1 else cst[k]!

          The position returned by individualize is the start of the cell that was split.

          structure IsoGraph.Canon.IndivData (n : ) (p : Part) (v i c ec u : ) :

          The facts about p that every statement below is phrased in: v sits at position i, whose cell is [c, ec), and u is the vertex displaced from the front of that cell.

          • vLt : v < n

            v is a vertex of the segment.

          • posv : p.pos[v]! = i

            i is where v sits.

          • csti : p.cst[i]! = c

            c is the start of i's cell.

          • ceni : p.cen[i]! = ec

            ec is its end.

          • labc : p.lab[c]! = u

            u is the vertex at the front of it.

          Instances For
            theorem IsoGraph.Canon.IndivData.iLt {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) :
            i < n
            theorem IsoGraph.Canon.IndivData.cLe {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) :
            c i
            theorem IsoGraph.Canon.IndivData.iLtEc {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) :
            i < ec
            theorem IsoGraph.Canon.IndivData.ecLe {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) :
            ec n
            theorem IsoGraph.Canon.IndivData.cLt {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) :
            c < n
            theorem IsoGraph.Canon.IndivData.cstc {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) :
            p.cst[c]! = c

            The cell start is its own cell start.

            theorem IsoGraph.Canon.IndivData.uLt {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) :
            u < n
            theorem IsoGraph.Canon.IndivData.posu {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) :
            p.pos[u]! = c

            The displaced vertex sits at the front of the cell.

            theorem IsoGraph.Canon.IndivData.mem_iff {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) {k : } (hk : k < n) :
            p.cst[k]! = c c k k < ec

            Membership in the split cell is visible from cst alone.

            theorem IsoGraph.Canon.IndivData.cst_eq {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) {k : } (h1 : c k) (h2 : k < ec) :
            p.cst[k]! = c
            theorem IsoGraph.Canon.IndivData.cen_eq {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) {k : } (h1 : c k) (h2 : k < ec) :
            p.cen[k]! = ec
            theorem IsoGraph.Canon.individualize_cst_getElemD {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) (k : ) :
            (individualize p v).1.cst[k]! = if c + 1 k k < ec then c + 1 else p.cst[k]!

            After individualisation the old cell [c, ec) has been cut into {c} and [c+1, ec).

            theorem IsoGraph.Canon.individualize_cen_getElemD {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) (k : ) :
            (individualize p v).1.cen[k]! = if k = c then c + 1 else p.cen[k]!
            theorem IsoGraph.Canon.individualize_pos_getElemD {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) (w : ) :
            (individualize p v).1.pos[w]! = if w = u then i else if w = v then c else p.pos[w]!
            theorem IsoGraph.Canon.individualize_lab_getElemD {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) (k : ) :
            (individualize p v).1.lab[k]! = if k = i then u else if k = c then v else p.lab[k]!
            theorem IsoGraph.Canon.individualize_pos_self {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) :

            The individualised vertex now occupies the singleton cell at c.

            theorem IsoGraph.Canon.individualize_cell {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) (w : ) (hw : w < n) :

            The cell of each vertex after individualisation. v gets the singleton cell c; everything else that was in v's cell moves to c + 1; everything else is untouched. Note that this says nothing about where inside its cell a vertex sits — which is exactly why it is stable under a renaming that reorders cells internally.

            theorem IsoGraph.Canon.individualize_wf {n : } {p : Part} {v i c ec u : } (hp : Part.WF n p) (hd : IndivData n p v i c ec u) :

            Individualisation preserves well-formedness. lab/pos stay inverse because the update is a transposition, and cst/cen still describe intervals because [c, ec) was cut in two.

            theorem IsoGraph.Canon.individualize_partEquiv {n : } {σ : } {p q : Part} ( : IsPerm n σ) (hp : Part.WF n p) (hq : Part.WF n q) (h : PartEquiv n σ p q) {v : } (hv : v < n) :
            PartEquiv n σ (individualize p (σ v)).1 (individualize q v).1 (individualize p (σ v)).2 = (individualize q v).2

            Step 3 of the decomposition. Individualising corresponding vertices of related partitions gives related partitions, and at the same position — so the two runs stay in step through the branch, and the recursive call sees the same splitter.

            What refineStep counts #

            refineStep is the one piece of the algorithm whose equivariance is not positionwise. Every other loop walks positions, and corresponding positions hold corresponding data; but the counting phase walks a cell in lab order, and corresponding cells are related only as sets. So the quantity it computes has to be described set-theoretically before it can be shown invariant, and that description is cellCount: the number of vertices of a given cell satisfying a predicate.

            The arithmetic comes first (cellCount_equiv: the quantity is invariant), then the first of refineStep's loops (countFrom_cellCount: the loop computes the quantity).

            theorem IsoGraph.Canon.IsPerm.surj {n : } {σ : } ( : IsPerm n σ) {w : } (hw : w < n) :
            v < n, σ v = w

            A permutation of a finite initial segment is onto it. Not part of IsPerm because nothing before this section needed it — injectivity was always enough.

            def IsoGraph.Canon.cellCount (n : ) (p : Part) (s : ) (P : Bool) :

            How many vertices of the cell starting at position s satisfy P. Every number refineStep computes is of this form: the neighbour count of v is P w := adj w v, a bucket size in the counting sort is P w := cnt w == t, and a cell size is P w := true.

            Equations
            Instances For
              theorem IsoGraph.Canon.cellCount_equiv {n : } {σ : } {p q : Part} ( : IsPerm n σ) (h : PartEquiv n σ p q) (s : ) (P : Bool) :
              cellCount n p s P = cellCount n q s fun (w : ) => P (σ w)

              The arithmetic behind step 1. Corresponding cells have the same size, and more generally agree on any count, because σ restricts to a bijection between them. Note this is a genuine cardinality argument — there is no order-preserving correspondence to appeal to.

              theorem IsoGraph.Canon.cellSize_equiv {n : } {σ : } {p q : Part} ( : IsPerm n σ) (h : PartEquiv n σ p q) (s : ) :
              (cellCount n p s fun (x : ) => true) = cellCount n q s fun (x : ) => true

              Corresponding cells have the same size.

              theorem IsoGraph.Canon.cellNbrCount_equiv {n : } {σ : } {p q : Part} ( : IsPerm n σ) (h : PartEquiv n σ p q) (f : Bool) (s v : ) :
              (cellCount n p s fun (w : ) => f w (σ v)) = cellCount n q s fun (w : ) => (fun (a b : ) => f (σ a) (σ b)) w v

              The neighbour counts that drive refinement are equivariant: σ v sees as many neighbours in p's cell at s as v does in q's.

              The counting loop #

              countFrom is phase (1) of refineStep: it walks the splitter cell lab[s:e] and, for every vertex w, accumulates in cnt[w] the number of cell members adjacent to w. Written as a for loop this would be out of reach — see the note on cenHashFrom — so Algorithm.lean gives it as a structural recursion on fuel, and the three lemmas below read the result off.

              The bridge to cellCount is List.count: the inner loop bumps cnt[w] once per occurrence of w in a neighbour list, and Graph.ofOracle's neighbour lists are filtered ranges, hence duplicate-free, so each cell member contributes at most one.

              theorem IsoGraph.Canon.bumpFrom_size (nbrs : Array ) (fuel j : ) (cnt touched : Array ) :
              (bumpFrom nbrs fuel j cnt touched).1.size = cnt.size

              The inner loop only writes, never resizes.

              theorem IsoGraph.Canon.bumpFrom_getElemD (nbrs : Array ) (fuel j : ) (cnt touched : Array ) :
              nbrs.size j + fuelw < cnt.size, (bumpFrom nbrs fuel j cnt touched).1[w]! = cnt[w]! + List.count w (List.drop j nbrs.toList)

              The inner loop adds to cnt[w] the multiplicity of w in the part of the neighbour list it scans. Entries of nbrs outside cnt write nothing, but they are not w either, so the statement needs no hypothesis on them.

              theorem IsoGraph.Canon.countFrom_succ (G : Graph) (lab : Array ) (e fuel k : ) (cnt touched : Array ) :
              countFrom G lab e (fuel + 1) k cnt touched = if k e then (cnt, touched) else countFrom G lab e fuel (k + 1) (bumpFrom G.nbr[lab[k]!]! G.nbr[lab[k]!]!.size 0 cnt touched).1 (bumpFrom G.nbr[lab[k]!]! G.nbr[lab[k]!]!.size 0 cnt touched).2

              Unfolding lemma for the outer loop. The definition destructures the inner loop's result rather than projecting it (that is what keeps cnt unshared, see Algorithm.lean), and definitional eta for structures makes the two forms interchangeable.

              theorem IsoGraph.Canon.countFrom_size (G : Graph) (lab : Array ) (e fuel k : ) (cnt touched : Array ) :
              (countFrom G lab e fuel k cnt touched).1.size = cnt.size

              The counting phase only writes, never resizes.

              theorem IsoGraph.Canon.countFrom_getElemD (G : Graph) (lab : Array ) (e fuel k : ) (cnt touched : Array ) :
              e k + fuelw < cnt.size, (countFrom G lab e fuel k cnt touched).1[w]! = cnt[w]! + iFinset.Ico k e, List.count w G.nbr[lab[i]!]!.toList

              The counting phase adds to cnt[w] one for every occurrence of w in a neighbour list of a vertex sitting at a position in [k, e).

              theorem IsoGraph.Canon.ofOracle_nbr_count (n : ) (f : Bool) (u v : ) (hu : u < n) (hv : v < n) :

              Neighbour lists of Graph.ofOracle are duplicate-free, so a vertex occurs in one at most once. This is what turns the multiplicities counted above into a 0/1 adjacency test.

              theorem IsoGraph.Canon.countFrom_cellCount {n : } (f : Bool) {p : Part} (hp : Part.WF n p) {s : } (hs : s < n) (hcst : p.cst[s]! = s) {w : } (hw : w < n) :
              (countFrom (Graph.ofOracle n f) p.lab p.cen[s]! (p.cen[s]! - s) s (Array.replicate n 0) #[]).1[w]! = cellCount n p s fun (u : ) => f u w

              What the counting phase computes. Run from cleared scratch over the cell starting at position s, countFrom leaves cnt[w] holding the number of vertices of that cell adjacent to w — that is, exactly cellCount n p s (· is adjacent to w).

              With cellCount_equiv, which says that quantity is invariant, this says the loop computes an invariant.

              theorem IsoGraph.Canon.countFrom_equiv {n : } {σ : } {f : Bool} {p q : Part} ( : IsPerm n σ) (hp : Part.WF n p) (hq : Part.WF n q) (h : PartEquiv n σ p q) {s : } (hs : s < n) (hcst : q.cst[s]! = s) {w : } (hw : w < n) :
              (countFrom (Graph.ofOracle n f) p.lab p.cen[s]! (p.cen[s]! - s) s (Array.replicate n 0) #[]).1[σ w]! = (countFrom (Graph.ofOracle n fun (a b : ) => f (σ a) (σ b)) q.lab q.cen[s]! (q.cen[s]! - s) s (Array.replicate n 0) #[]).1[w]!

              The counting phase of refineStep is equivariant. Putting the two halves together: in two runs whose partitions correspond under σ, the count the f-run records at σ w is the one the f ∘ σ-run records at w. Note the cells need only correspond as sets — the two runs walk them in different orders, and the proof goes through cellCount precisely to avoid caring.

              structure IsoGraph.Canon.Touched (cnt touched : Array ) :

              The invariant the counting phase maintains on its scratch space: touched lists exactly the vertices whose count is nonzero, each once.

              refineStep needs both halves. Completeness is what makes the collected cells the right ones — a cell met by the splitter has a member with a nonzero count, so it is represented. Soundness and no-repetition are what make the restore loop at the end of the step put the scratch back exactly, in time proportional to what was dirtied rather than to n.

              • nodup : touched.toList.Nodup

                No vertex is recorded twice.

              • lt (w : ) : w touchedw < cnt.size

                Only vertices are recorded.

              • mem (w : ) : w < cnt.size → (w touched cnt[w]! 0)

                Recorded is the same as counted.

              Instances For
                theorem IsoGraph.Canon.touched_empty (cnt : Array ) (h : w < cnt.size, cnt[w]! = 0) :

                Cleared scratch satisfies the invariant.

                theorem IsoGraph.Canon.bumpFrom_touched (nbrs : Array ) (fuel j : ) (cnt touched : Array ) :
                (∀ xnbrs, x < cnt.size)Touched cnt touchedTouched (bumpFrom nbrs fuel j cnt touched).1 (bumpFrom nbrs fuel j cnt touched).2

                The inner loop maintains the scratch invariant: it pushes a vertex exactly when it is raising that vertex's count off zero. The hypothesis on nbrs is needed — a neighbour outside cnt would leave the count at the getElem! default of 0 and so be pushed on every visit.

                theorem IsoGraph.Canon.countFrom_touched (G : Graph) (lab : Array ) (e fuel k : ) (cnt touched : Array ) :
                (∀ (u x : ), x G.nbr[u]!x < cnt.size)Touched cnt touchedTouched (countFrom G lab e fuel k cnt touched).1 (countFrom G lab e fuel k cnt touched).2

                The outer loop maintains the scratch invariant.

                theorem IsoGraph.Canon.countFrom_touched_spec {n : } (f : Bool) (lab : Array ) (e s : ) :
                Touched (countFrom (Graph.ofOracle n f) lab e (e - s) s (Array.replicate n 0) #[]).1 (countFrom (Graph.ofOracle n f) lab e (e - s) s (Array.replicate n 0) #[]).2

                The counting phase as refineStep actually calls it, from cleared scratch.

                theorem IsoGraph.Canon.countFrom_mem_touched {n : } (f : Bool) {p : Part} (hp : Part.WF n p) {s : } (hs : s < n) (hcst : p.cst[s]! = s) {w : } (hw : w < n) :
                w (countFrom (Graph.ofOracle n f) p.lab p.cen[s]! (p.cen[s]! - s) s (Array.replicate n 0) #[]).2 (cellCount n p s fun (u : ) => f u w) 0

                The vertices the counting phase records form an invariant set. touched is exactly the set of vertices that the splitter cell reaches, and membership is stated in terms of cellCount, which cellCount_equiv shows corresponding runs agree on.

                The order of touched is not invariant — it is first-touch order, which depends on vertex names. That is why refineStep maps it to cell starts and sorts before using it.

                Collecting the cells met by the splitter #

                Phase (2) of refineStep turns the touched vertices into the list of cells that have to be split. Its output has to be canonical in a stronger sense than phase (1)'s: not just the same set of cells in both runs, but the same array, since the step then processes them in order. That is what the sort is for, and why sortNats goes through List.mergeSort rather than Array.qsortqsort has no specification in this toolchain, and an unspecified order is exactly what cannot be tolerated here. sortNats_ext is the punchline of the sorting half ("duplicate-free arrays with the same elements sort alike"), collect_equiv of the whole phase.

                @[simp]

                Sorting a List and sorting the Array of the same elements agree.

                theorem IsoGraph.Canon.sortNats_ext {a b : Array } (ha : a.toList.Nodup) (hb : b.toList.Nodup) (h : ∀ (c : ), c a c b) :

                Sorting normalises. Two duplicate-free arrays with the same elements sort to the same array; this is what makes the list of cells met by a splitter canonical.

                structure IsoGraph.Canon.Collected (hit : Array Bool) (cells : Array ) :

                The scratch invariant of the collection phase: cells lists exactly the cell starts marked in hit, each once. The analogue of Touched for phase (2).

                • nodup : cells.toList.Nodup

                  No cell is collected twice — this is what hit is for.

                • lt (c : ) : c cellsc < hit.size

                  Collected cells are in range.

                • mem (c : ) : c < hit.size → (c cells hit[c]! = true)

                  hit marks exactly the collected cells.

                Instances For
                  theorem IsoGraph.Canon.collected_empty (hit : Array Bool) (h : c < hit.size, hit[c]! = false) :
                  theorem IsoGraph.Canon.Collected.push {hit : Array Bool} {cells : Array } (h : Collected hit cells) {c : } (hc : c < hit.size) (hf : hit[c]! = false) :
                  Collected (hit.set! c true) (cells.push c)

                  Marking and pushing an unmarked cell preserves the invariant.

                  theorem IsoGraph.Canon.collectFrom_collected (pos cst touched : Array ) (fuel j : ) (hit : Array Bool) (cells : Array ) :
                  (∀ vtouched, cst[pos[v]!]! < hit.size)Collected hit cellsCollected (collectFrom pos cst touched fuel j hit cells).1 (collectFrom pos cst touched fuel j hit cells).2
                  theorem IsoGraph.Canon.collectFrom_mem (pos cst touched : Array ) (fuel j : ) (hit : Array Bool) (cells : Array ) :
                  touched.size j + fuel(∀ vtouched, cst[pos[v]!]! < hit.size)Collected hit cells∀ (c : ), c (collectFrom pos cst touched fuel j hit cells).2 c cells vList.drop j touched.toList, cst[pos[v]!]! = c

                  What the collection phase collects: exactly the cell starts of the vertices it is given, each once (the Nodup half is collectFrom_collected).

                  theorem IsoGraph.Canon.collect_mem {n : } {p : Part} (hp : Part.WF n p) {touched : Array } (htn : vtouched, v < n) {hit : Array Bool} (hsz : hit.size = n) (hf0 : c < n, hit[c]! = false) (c : ) :
                  c (collectFrom p.pos p.cst touched touched.size 0 hit #[]).2 vtouched, p.cst[p.pos[v]!]! = c

                  The collection phase as refineStep runs it: from an all-clear hit, it collects exactly the cells that the touched vertices lie in.

                  theorem IsoGraph.Canon.collect_nodup {n : } {p : Part} (hp : Part.WF n p) {touched : Array } (htn : vtouched, v < n) {hit : Array Bool} (hsz : hit.size = n) (hf0 : c < n, hit[c]! = false) :
                  (collectFrom p.pos p.cst touched touched.size 0 hit #[]).2.toList.Nodup
                  theorem IsoGraph.Canon.countFrom_touched_lt {n : } (f : Bool) (lab : Array ) (e s : ) {v : } (hv : v (countFrom (Graph.ofOracle n f) lab e (e - s) s (Array.replicate n 0) #[]).2) :
                  v < n

                  Every vertex the counting phase touches is a vertex.

                  theorem IsoGraph.Canon.collect_equiv {n : } {σ : } {f : Bool} {p q : Part} ( : IsPerm n σ) (hp : Part.WF n p) (hq : Part.WF n q) (h : PartEquiv n σ p q) {s : } (hs : s < n) (hcst : q.cst[s]! = s) {hit : Array Bool} (hsz : hit.size = n) (hf0 : c < n, hit[c]! = false) {tp tq : Array } (htp : tp = (countFrom (Graph.ofOracle n f) p.lab p.cen[s]! (p.cen[s]! - s) s (Array.replicate n 0) #[]).2) (htq : tq = (countFrom (Graph.ofOracle n fun (a b : ) => f (σ a) (σ b)) q.lab q.cen[s]! (q.cen[s]! - s) s (Array.replicate n 0) #[]).2) :
                  sortNats (collectFrom p.pos p.cst tp tp.size 0 hit #[]).2 = sortNats (collectFrom q.pos q.cst tq tq.size 0 hit #[]).2

                  Phase (2) of refineStep is equivariant. The two runs meet the same cells, in the same order: the cells are the same positions because PartEquiv matches cell boundaries, and the sort makes the order depend on nothing but the set.

                  The counting sort #

                  The heart of refineStep: each cell that the splitter met is sorted by neighbour count. The sort is a counting sort in five passes — count the buckets (bucketFrom), turn the counts into offsets (offsetFrom), scatter the vertices into a block (scatterFrom), write the block back (writeFrom), install the new fragment boundaries (boundsFrom) — and each pass gets its own getElem! characterisation here. The one substantial argument is scatterFrom_block: the scatter writes each vertex to a distinct slot, which needs the buckets' offset ranges to be disjoint (Sep) and is what makes the whole thing a permutation of the cell.

                  def IsoGraph.Canon.bucketSize (lab cnt : Array ) (k ec t : ) :

                  How many of the positions [k, ec) hold a vertex of neighbour count t.

                  Equations
                  Instances For
                    theorem IsoGraph.Canon.bucketSize_zero (lab cnt : Array ) {k ec t : } (h : ec k) :
                    bucketSize lab cnt k ec t = 0
                    theorem IsoGraph.Canon.bucketSize_succ (lab cnt : Array ) {k ec t : } (h : k < ec) :
                    bucketSize lab cnt k ec t = (if cnt[lab[k]!]! = t then 1 else 0) + bucketSize lab cnt (k + 1) ec t
                    theorem IsoGraph.Canon.bucketFrom_size (lab cnt : Array ) (ec fuel k : ) (bc ks : Array ) :
                    (bucketFrom lab cnt ec fuel k bc ks).1.size = bc.size
                    theorem IsoGraph.Canon.bucketFrom_getElemD (lab cnt : Array ) (ec fuel k : ) (bc ks : Array ) :
                    ec k + fuelt < bc.size, (bucketFrom lab cnt ec fuel k bc ks).1[t]! = bc[t]! + bucketSize lab cnt k ec t
                    theorem IsoGraph.Canon.bucketFrom_touched (lab cnt : Array ) (ec fuel k : ) (bc ks : Array ) :
                    (∀ (v : ), cnt[v]! < bc.size)Touched bc ksTouched (bucketFrom lab cnt ec fuel k bc ks).1 (bucketFrom lab cnt ec fuel k bc ks).2

                    The bucket table and the list of occurring counts satisfy the same invariant as the count array and its touched list: ks lists exactly the counts with a nonzero bucket, each once.

                    theorem IsoGraph.Canon.bucket_mem {lab cnt bc : Array } {c ec : } (hcb : ∀ (v : ), cnt[v]! < bc.size) (hbc0 : t < bc.size, bc[t]! = 0) (t : ) :
                    t (bucketFrom lab cnt ec (ec - c) c bc #[]).2 t < bc.size bucketSize lab cnt c ec t 0

                    What the bucketing phase collects: exactly the counts that occur in the cell.

                    theorem IsoGraph.Canon.bucketSize_cellCount {n : } {p : Part} (hp : Part.WF n p) {c : } (hc : c < n) (hcst : p.cst[c]! = c) (cnt : Array ) (t : ) :
                    bucketSize p.lab cnt c p.cen[c]! t = cellCount n p c fun (u : ) => cnt[u]! == t

                    The bucket sizes are cell counts, so the vocabulary of step 1 applies to them: the same position-to-vertex bijection as in countFrom_cellCount.

                    theorem IsoGraph.Canon.offsetFrom_size1 (ks : Array ) (fuel j : ) (sizes bc : Array ) (acc : ) :
                    (offsetFrom ks fuel j sizes bc acc).1.size = sizes.size
                    theorem IsoGraph.Canon.offsetFrom_size2 (ks : Array ) (fuel j : ) (sizes bc : Array ) (acc : ) :
                    (offsetFrom ks fuel j sizes bc acc).2.size = bc.size
                    theorem IsoGraph.Canon.offsetFrom_ne (ks : Array ) (fuel j : ) (sizes bc : Array ) (acc x : ) :
                    (∀ (i : ), j ii < ks.sizex ks[i]!)(offsetFrom ks fuel j sizes bc acc).2[x]! = bc[x]!

                    The pass writes only at the indices named in ks[j:].

                    theorem IsoGraph.Canon.offsetFrom_sizes (ks : Array ) (hnd : ks.toList.Nodup) (fuel j : ) (sizes bc : Array ) (acc : ) :
                    ks.size j + fuelsizes.size = ks.sizei < ks.size, (offsetFrom ks fuel j sizes bc acc).1[i]! = if i < j then sizes[i]! else bc[ks[i]!]!

                    The fragment sizes: sizes[j] is the size of the bucket of the j-th count.

                    theorem IsoGraph.Canon.offsetFrom_bc (ks : Array ) (hnd : ks.toList.Nodup) (fuel j : ) (sizes bc : Array ) (acc : ) :
                    ks.size j + fuel(∀ i < ks.size, ks[i]! < bc.size)∀ (i : ), j ii < ks.size(offsetFrom ks fuel j sizes bc acc).2[ks[i]!]! = acc + i'Finset.Ico j i, bc[ks[i']!]!

                    The fragment offsets: after the pass, the bucket of the i-th count starts at the sum of the sizes of the buckets before it.

                    theorem IsoGraph.Canon.bucketSize_split (lab cnt : Array ) {k i ec t : } (h1 : k i) (h2 : i ec) :
                    bucketSize lab cnt k ec t = bucketSize lab cnt k i t + bucketSize lab cnt i ec t
                    theorem IsoGraph.Canon.bucketSize_mono (lab cnt : Array ) {k i ec t : } (h1 : k i) (h2 : i ec) :
                    bucketSize lab cnt i ec t bucketSize lab cnt k ec t
                    theorem IsoGraph.Canon.bucketSize_pos (lab cnt : Array ) {k ec t : } (h : k < ec) (ht : cnt[lab[k]!]! = t) :
                    0 < bucketSize lab cnt k ec t
                    theorem IsoGraph.Canon.scatterFrom_size1 (lab cnt : Array ) (ec fuel k : ) (block bc : Array ) :
                    (scatterFrom lab cnt ec fuel k block bc).1.size = block.size
                    theorem IsoGraph.Canon.scatterFrom_size2 (lab cnt : Array ) (ec fuel k : ) (block bc : Array ) :
                    (scatterFrom lab cnt ec fuel k block bc).2.size = bc.size
                    def IsoGraph.Canon.scatterAt (lab cnt bc : Array ) (k i : ) :

                    The offset the scatter writes the vertex at position i to, as seen from position k: the bucket's current offset plus the number of items of the same count already scattered.

                    Equations
                    Instances For
                      theorem IsoGraph.Canon.scatterAt_step (lab cnt bc : Array ) {k i : } (hk : k < i) (hb : cnt[lab[k]!]! < bc.size) :
                      scatterAt lab cnt (bc.set! cnt[lab[k]!]! (bc[cnt[lab[k]!]!]! + 1)) (k + 1) i = scatterAt lab cnt bc k i

                      Taking one step leaves every later item's target offset where it was: the bucket that just grew also advanced by one.

                      theorem IsoGraph.Canon.scatterFrom_ne (lab cnt : Array ) (ec fuel k : ) (block bc : Array ) (o : ) :
                      (∀ (v : ), cnt[v]! < bc.size)(∀ (i : ), k ii < ecscatterAt lab cnt bc k i o)(scatterFrom lab cnt ec fuel k block bc).1[o]! = block[o]!

                      The scatter writes only at the offsets its remaining items name.

                      def IsoGraph.Canon.Sep (lab cnt : Array ) (k ec : ) (bc : Array ) :

                      The buckets do not overlap: distinct counts have disjoint offset ranges, each as wide as the number of items still to be scattered into it.

                      Equations
                      • One or more equations did not get rendered due to their size.
                      Instances For
                        theorem IsoGraph.Canon.Sep.step {lab cnt bc : Array } {k ec : } (h : Sep lab cnt k ec bc) (hk : k < ec) (hb : cnt[lab[k]!]! < bc.size) :
                        Sep lab cnt (k + 1) ec (bc.set! cnt[lab[k]!]! (bc[cnt[lab[k]!]!]! + 1))
                        theorem IsoGraph.Canon.scatterFrom_block (lab cnt : Array ) (ec fuel k : ) (block bc : Array ) :
                        ec k + fuel(∀ (v : ), cnt[v]! < bc.size)Sep lab cnt k ec bc(∀ (i : ), k ii < ecscatterAt lab cnt bc k i < block.size)∀ (i : ), k ii < ec(scatterFrom lab cnt ec fuel k block bc).1[scatterAt lab cnt bc k i]! = lab[i]!

                        Where the scatter puts each vertex. The vertex at position i of the cell lands at its bucket's offset plus the number of same-count vertices before it — a stable counting sort.

                        Writing the sorted block back #

                        theorem IsoGraph.Canon.writeFrom_size1 (block : Array ) (c fuel k : ) (lab pos : Array ) :
                        (writeFrom block c fuel k lab pos).1.size = lab.size
                        theorem IsoGraph.Canon.writeFrom_size2 (block : Array ) (c fuel k : ) (lab pos : Array ) :
                        (writeFrom block c fuel k lab pos).2.size = pos.size
                        theorem IsoGraph.Canon.writeFrom_lab_ne (block : Array ) (c fuel k : ) (lab pos : Array ) (x : ) :
                        x < c + k c + block.size x(writeFrom block c fuel k lab pos).1[x]! = lab[x]!

                        Positions outside the cell keep their label.

                        theorem IsoGraph.Canon.writeFrom_lab (block : Array ) (c fuel k : ) (lab pos : Array ) :
                        block.size k + fuelc + block.size lab.size∀ (m : ), k mm < block.size(writeFrom block c fuel k lab pos).1[c + m]! = block[m]!

                        The block is laid down at positions c, c+1, ….

                        theorem IsoGraph.Canon.writeFrom_pos_ne (block : Array ) (c fuel k : ) (lab pos : Array ) (v : ) :
                        (∀ (m : ), k mm < block.sizeblock[m]! v)(writeFrom block c fuel k lab pos).2[v]! = pos[v]!

                        Vertices the block does not mention keep their position.

                        theorem IsoGraph.Canon.writeFrom_pos (block : Array ) (c : ) (hnd : block.toList.Nodup) (fuel k : ) (lab pos : Array ) :
                        block.size k + fuel(∀ m < block.size, block[m]! < pos.size)∀ (m : ), k mm < block.size(writeFrom block c fuel k lab pos).2[block[m]!]! = c + m

                        Where each vertex of the block ends up. The block has no repeats, so the write that places a vertex is the only one that touches its position.

                        The fragment boundaries #

                        theorem IsoGraph.Canon.fillBoundsFrom_size1 (st en fuel i : ) (cst cen : Array ) :
                        (fillBoundsFrom st en fuel i cst cen).1.size = cst.size
                        theorem IsoGraph.Canon.fillBoundsFrom_size2 (st en fuel i : ) (cst cen : Array ) :
                        (fillBoundsFrom st en fuel i cst cen).2.size = cen.size
                        theorem IsoGraph.Canon.fillBoundsFrom_ne (st en fuel i : ) (cst cen : Array ) (x : ) :
                        x < i en x(fillBoundsFrom st en fuel i cst cen).1[x]! = cst[x]! (fillBoundsFrom st en fuel i cst cen).2[x]! = cen[x]!
                        theorem IsoGraph.Canon.fillBoundsFrom_getElemD (st en fuel i : ) (cst cen : Array ) :
                        en i + fuelen cst.sizeen cen.size∀ (x : ), i xx < en(fillBoundsFrom st en fuel i cst cen).1[x]! = st (fillBoundsFrom st en fuel i cst cen).2[x]! = en
                        def IsoGraph.Canon.sizesSum (sizes : Array ) (a b : ) :

                        The total size of the fragments [a, b).

                        Equations
                        Instances For
                          theorem IsoGraph.Canon.sizesSum_self (sizes : Array ) (a : ) :
                          sizesSum sizes a a = 0
                          theorem IsoGraph.Canon.sizesSum_succ (sizes : Array ) {a b : } (h : a < b) :
                          sizesSum sizes a b = sizes[a]! + sizesSum sizes (a + 1) b
                          theorem IsoGraph.Canon.sizesSum_le (sizes : Array ) {a b c : } (h1 : a b) (h2 : b c) :
                          sizesSum sizes a b sizesSum sizes a c
                          theorem IsoGraph.Canon.sizesSum_split (sizes : Array ) {a b c : } (h1 : a b) (h2 : b c) :
                          sizesSum sizes a c = sizesSum sizes a b + sizesSum sizes b c
                          theorem IsoGraph.Canon.boundsFrom_ne (ks sizes : Array ) (fuel j : ) (cst cen starts : Array ) (st : ) (tr : UInt64) (x : ) :
                          x < st st + sizesSum sizes j ks.size x(boundsFrom ks sizes fuel j cst cen starts st tr).1[x]! = cst[x]! (boundsFrom ks sizes fuel j cst cen starts st tr).2.1[x]! = cen[x]!

                          Positions outside the split cell keep their boundaries.

                          theorem IsoGraph.Canon.boundsFrom_getElemD (ks sizes : Array ) (fuel j : ) (cst cen starts : Array ) (st : ) (tr : UInt64) :
                          ks.size j + fuelst + sizesSum sizes j ks.size cst.sizest + sizesSum sizes j ks.size cen.size∀ (j' : ), j j'j' < ks.size∀ (x : ), st + sizesSum sizes j j' xx < st + sizesSum sizes j (j' + 1)(boundsFrom ks sizes fuel j cst cen starts st tr).1[x]! = st + sizesSum sizes j j' (boundsFrom ks sizes fuel j cst cen starts st tr).2.1[x]! = st + sizesSum sizes j (j' + 1)

                          The boundaries the split installs. Every position of fragment j' reports that fragment's range.

                          Odds and ends: array extensionality, the leftover bucket counters, clearing scratch #

                          theorem IsoGraph.Canon.scatterFrom_bc_ne (lab cnt : Array ) (ec fuel k : ) (block bc : Array ) (t : ) :
                          (∀ (i : ), k ii < eccnt[lab[i]!]! t)(scatterFrom lab cnt ec fuel k block bc).2[t]! = bc[t]!

                          Counters the scatter never advances keep their value.

                          theorem IsoGraph.Canon.boundsFrom_congr (ks sizes : Array ) (fuel j : ) (cst cen cst' cen' starts : Array ) (st : ) (tr : UInt64) :
                          (boundsFrom ks sizes fuel j cst cen starts st tr).2.2 = (boundsFrom ks sizes fuel j cst' cen' starts st tr).2.2

                          The fragment starts and the trace do not depend on the boundary arrays being written.

                          theorem IsoGraph.Canon.clearCntFrom_size (touched : Array ) (fuel j : ) (cnt : Array ) :
                          (clearCntFrom touched fuel j cnt).size = cnt.size
                          theorem IsoGraph.Canon.clearCntFrom_ne (touched : Array ) (fuel j : ) (cnt : Array ) (v : ) :
                          (∀ (j' : ), j j'j' < touched.sizetouched[j']! v)(clearCntFrom touched fuel j cnt)[v]! = cnt[v]!
                          theorem IsoGraph.Canon.clearCntFrom_mem (touched : Array ) (fuel j : ) (cnt : Array ) :
                          touched.size j + fuel∀ (j' : ), j j'j' < touched.sizetouched[j']! < cnt.size(clearCntFrom touched fuel j cnt)[touched[j']!]! = 0
                          theorem IsoGraph.Canon.clearCntFrom_zero {cnt touched : Array } (h : Touched cnt touched) (v : ) (hv : v < cnt.size) :
                          (clearCntFrom touched touched.size 0 cnt)[v]! = 0

                          Clearing the counts. Every vertex the counting loop touched is reset, so the scratch is back to all zeros.

                          theorem IsoGraph.Canon.clearHitFrom_ne (cells : Array ) (fuel j : ) (hit : Array Bool) (v : ) :
                          (∀ (j' : ), j j'j' < cells.sizecells[j']! v)(clearHitFrom cells fuel j hit)[v]! = hit[v]!
                          theorem IsoGraph.Canon.clearHitFrom_mem (cells : Array ) (fuel j : ) (hit : Array Bool) :
                          cells.size j + fuel∀ (j' : ), j j'j' < cells.sizecells[j']! < hit.size(clearHitFrom cells fuel j hit)[cells[j']!]! = false
                          theorem IsoGraph.Canon.clearHitFrom_zero {hit : Array Bool} {cells : Array } (h : Collected hit cells) (v : ) (hv : v < hit.size) :
                          (clearHitFrom cells cells.size 0 hit)[v]! = false

                          Clearing the cell marks. Every collected cell is unmarked, so the scratch is back to all false.

                          theorem IsoGraph.Canon.clearBcFrom_size (ks : Array ) (fuel j : ) (bc : Array ) :
                          (clearBcFrom ks fuel j bc).size = bc.size
                          theorem IsoGraph.Canon.clearBcFrom_ne (ks : Array ) (fuel j : ) (bc : Array ) (v : ) :
                          (∀ (j' : ), j j'j' < ks.sizeks[j']! v)(clearBcFrom ks fuel j bc)[v]! = bc[v]!
                          theorem IsoGraph.Canon.clearBcFrom_mem (ks : Array ) (fuel j : ) (bc : Array ) :
                          ks.size j + fuel∀ (j' : ), j j'j' < ks.sizeks[j']! < bc.size(clearBcFrom ks fuel j bc)[ks[j']!]! = 0
                          theorem IsoGraph.Canon.bucketSize_card (lab cnt : Array ) (c ec t : ) :
                          bucketSize lab cnt c ec t = {iFinset.Ico c ec | cnt[lab[i]!]! = t}.card
                          theorem IsoGraph.Canon.sum_bucketSize (lab cnt ks : Array ) {c ec : } (hnd : ks.toList.Nodup) (hmem : ∀ (i : ), c ii < ecj < ks.size, ks[j]! = cnt[lab[i]!]!) :
                          jFinset.range ks.size, bucketSize lab cnt c ec ks[j]! = ec - c

                          The fragments of a split cell exhaust it: the bucket sizes sum to the size of the cell.

                          def IsoGraph.Canon.fragStart (n : ) (p : Part) (cnt : Array ) (c t : ) :

                          Where the fragment of neighbour count t starts: after every vertex of the cell with a smaller count. The point of this description is that it never mentions the bucket list, so two runs of refineStep on isomorphic inputs manifestly agree on it.

                          Equations
                          Instances For
                            theorem IsoGraph.Canon.fragStart_eq {n : } {p : Part} {c : } {cnt ks sizes : Array } (hnd : ks.toList.Nodup) (hsorted : List.Pairwise (fun (x1 x2 : ) => x1 x2) ks.toList) (hmem : ∀ (t : ), t ks (cellCount n p c fun (u : ) => cnt[u]! == t) 0) (hsizes : j < ks.size, sizes[j]! = cellCount n p c fun (u : ) => cnt[u]! == ks[j]!) (j : ) (hj : j < ks.size) :
                            c + sizesSum sizes 0 j = fragStart n p cnt c ks[j]!

                            The prefix sums the algorithm computes are the fragment starts. The bucket list is sorted and holds exactly the counts that occur, so summing along it is summing over all smaller counts.

                            The partition carried by the cell loop's state.

                            Equations
                            Instances For
                              structure IsoGraph.Canon.SplitOk (n : ) (p : Part) (cnt : Array ) (c : ) (p' : Part) :

                              What splitting one cell does. Outside the cell [c, cen[c]) nothing moves; inside, each vertex lands in the fragment of its neighbour count, and the fragments sit in increasing order of count. Everything is phrased through fragStart/cellCount, which mention only the cell as a set — that is what makes the description equivariant.

                              Instances For
                                theorem IsoGraph.Canon.cellCount_congr {n : } {p : Part} {c : } {P Q : Bool} (h : w < n, p.cst[p.pos[w]!]! = cP w = Q w) :
                                cellCount n p c P = cellCount n p c Q
                                theorem IsoGraph.Canon.cellCount_true {n : } {p : Part} (hp : Part.WF n p) {c : } (hc : c < n) (hcst : p.cst[c]! = c) :
                                (cellCount n p c fun (x : ) => true) = p.cen[c]! - c

                                A cell has as many vertices as it has positions.

                                theorem IsoGraph.Canon.splitOk_of_uniform {n : } {p : Part} (hp : Part.WF n p) {c : } (hc : c < n) (hcst : p.cst[c]! = c) (cnt : Array ) (huni : ∀ (v w : ), v < nw < np.cst[p.pos[v]!]! = cp.cst[p.pos[w]!]! = ccnt[v]! = cnt[w]!) :
                                SplitOk n p cnt c p

                                A cell whose vertices all have the same count does not split. Both easy branches of splitCell are this: the cell is a singleton, or the counting pass found a single bucket.

                                theorem IsoGraph.Canon.boundsFrom_size1 (ks sizes : Array ) (fuel j : ) (cst cen starts : Array ) (st : ) (tr : UInt64) :
                                (boundsFrom ks sizes fuel j cst cen starts st tr).1.size = cst.size
                                theorem IsoGraph.Canon.boundsFrom_size2 (ks sizes : Array ) (fuel j : ) (cst cen starts : Array ) (st : ) (tr : UInt64) :
                                (boundsFrom ks sizes fuel j cst cen starts st tr).2.1.size = cen.size
                                theorem IsoGraph.Canon.sizesSum_one (sizes : Array ) (j : ) :
                                sizesSum sizes j (j + 1) = sizes[j]!
                                theorem IsoGraph.Canon.exists_of_bucketSize {lab cnt : Array } {c ec t : } (h : bucketSize lab cnt c ec t 0) :
                                ∃ (i : ), c i i < ec cnt[lab[i]!]! = t

                                A nonempty bucket has a member.

                                The counting sort is a permutation of the cell #

                                Everything the scatter needs is packaged in Offsets: the counts occurring in the cell, in some order and without duplicates (ks), their bucket sizes (sizes) and the offset each bucket was given (bc1, the prefix sums). From that alone the scatter is a bijection from the cell onto [0, ec - c).

                                structure IsoGraph.Canon.Offsets (lab cnt ks sizes bc1 : Array ) (c ec : ) :

                                What the count-and-offset passes leave behind.

                                • nodup : ks.toList.Nodup

                                  The counts are listed once each.

                                • sizes_eq (j : ) : j < ks.sizesizes[j]! = bucketSize lab cnt c ec ks[j]!

                                  sizes[j] is the size of bucket ks[j].

                                • bc_eq (j : ) : j < ks.sizebc1[ks[j]!]! = sizesSum sizes 0 j

                                  Bucket ks[j] was given the offset just past all earlier buckets.

                                • mem (i : ) : c ii < ecj < ks.size, ks[j]! = cnt[lab[i]!]!

                                  Every count occurring in the cell is listed.

                                Instances For
                                  theorem IsoGraph.Canon.Offsets.total {lab cnt ks sizes bc1 : Array } {c ec : } (h : Offsets lab cnt ks sizes bc1 c ec) :
                                  sizesSum sizes 0 ks.size = ec - c

                                  The buckets exhaust the cell.

                                  theorem IsoGraph.Canon.Offsets.index {lab cnt ks sizes bc1 : Array } {c ec : } (h : Offsets lab cnt ks sizes bc1 c ec) {t : } (ht : bucketSize lab cnt c ec t 0) :
                                  j < ks.size, ks[j]! = t
                                  theorem IsoGraph.Canon.Offsets.sep {lab cnt ks sizes bc1 : Array } {c ec : } (h : Offsets lab cnt ks sizes bc1 c ec) :
                                  Sep lab cnt c ec bc1

                                  Distinct buckets get disjoint ranges of slots.

                                  theorem IsoGraph.Canon.Offsets.scatterAt_mem {lab cnt ks sizes bc1 : Array } {c ec : } (h : Offsets lab cnt ks sizes bc1 c ec) {i : } (h1 : c i) (h2 : i < ec) {j : } (hj : j < ks.size) (hjt : ks[j]! = cnt[lab[i]!]!) :
                                  sizesSum sizes 0 j scatterAt lab cnt bc1 c i scatterAt lab cnt bc1 c i < sizesSum sizes 0 (j + 1)

                                  The slot a cell position is scattered to lies in its bucket's range.

                                  theorem IsoGraph.Canon.Offsets.scatterAt_lt {lab cnt ks sizes bc1 : Array } {c ec : } (h : Offsets lab cnt ks sizes bc1 c ec) {i : } (h1 : c i) (h2 : i < ec) :
                                  scatterAt lab cnt bc1 c i < ec - c
                                  theorem IsoGraph.Canon.Offsets.scatterAt_ne {lab cnt ks sizes bc1 : Array } {c ec : } (h : Offsets lab cnt ks sizes bc1 c ec) {i i' : } (h1 : c i) (hii : i < i') (h2 : i' < ec) :
                                  scatterAt lab cnt bc1 c i scatterAt lab cnt bc1 c i'

                                  Distinct cell positions are scattered to distinct slots.

                                  theorem IsoGraph.Canon.Offsets.scatterAt_inj {lab cnt ks sizes bc1 : Array } {c ec : } (h : Offsets lab cnt ks sizes bc1 c ec) {i i' : } (h1 : c i) (h2 : i < ec) (h1' : c i') (h2' : i' < ec) (he : scatterAt lab cnt bc1 c i = scatterAt lab cnt bc1 c i') :
                                  i = i'
                                  theorem IsoGraph.Canon.Offsets.scatterAt_surj {lab cnt ks sizes bc1 : Array } {c ec : } (h : Offsets lab cnt ks sizes bc1 c ec) {m : } (hm : m < ec - c) :
                                  ∃ (i : ), c i i < ec scatterAt lab cnt bc1 c i = m

                                  The scatter is onto: every slot of the block is written.

                                  Reading off one step of splitCell #

                                  splitCell has three branches; these equations name the state each one produces, so that the rest of the file never has to unfold splitCell again: unfolding it in place would leave the trace hash of the branch in the goal, and deciding whether two such hashes agree is something the kernel should never be asked to do.

                                  theorem IsoGraph.Canon.part_mk (lab pos cst cen : Array ) (inW : Array Bool) (tr : UInt64) (bc : Array ) :
                                  { lab := lab, pos := pos, cst := cst, cen := cen, inW := inW, tr := tr, bc := bc }.part = { lab := lab, pos := pos, cst := cst, cen := cen }
                                  theorem IsoGraph.Canon.part_update (st : SplitState) (inW : Array Bool) (tr : UInt64) (bc : Array ) :
                                  { lab := st.lab, pos := st.pos, cst := st.cst, cen := st.cen, inW := inW, tr := tr, bc := bc }.part = st.part
                                  theorem IsoGraph.Canon.splitCell_eq_singleton {cnt : Array } {c : } {st : SplitState} (h : (st.cen[c]! - c == 1) = true) :
                                  splitCell cnt c st = { lab := st.lab, pos := st.pos, cst := st.cst, cen := st.cen, inW := st.inW, tr := mixN (mixN st.tr c) cnt[st.lab[c]!]!, bc := st.bc }

                                  A singleton cell: only the trace hash moves.

                                  theorem IsoGraph.Canon.splitCell_eq_one {cnt : Array } {c : } {st : SplitState} {bc0 ks0 : Array } (hb : bucketFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c st.bc #[] = (bc0, ks0)) (h1 : ¬(st.cen[c]! - c == 1) = true) (h2 : (ks0.size == 1) = true) :
                                  splitCell cnt c st = { lab := st.lab, pos := st.pos, cst := st.cst, cen := st.cen, inW := st.inW, tr := mixN (mixN st.tr c) ks0[0]!, bc := bc0.set! ks0[0]! 0 }

                                  A cell with a single bucket: again only the trace hash moves (and the bucket counter is put back).

                                  theorem IsoGraph.Canon.splitCell_eq_general {cnt : Array } {c : } {st : SplitState} {bc0 ks0 : Array } (hb : bucketFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c st.bc #[] = (bc0, ks0)) (h1 : ¬(st.cen[c]! - c == 1) = true) (h2 : ¬(ks0.size == 1) = true) {sizes bc1 : Array } (ho : offsetFrom (sortNats ks0) (sortNats ks0).size 0 (Array.replicate (sortNats ks0).size 0) bc0 0 = (sizes, bc1)) {block bc2 : Array } (hsc : scatterFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c (Array.replicate (st.cen[c]! - c) 0) bc1 = (block, bc2)) {lab pos : Array } (hw : writeFrom block c block.size 0 st.lab st.pos = (lab, pos)) {cst cen starts : Array } {tr : UInt64} (hbd : boundsFrom (sortNats ks0) sizes (sortNats ks0).size 0 st.cst st.cen #[] c (mixN st.tr c) = (cst, cen, starts, tr)) :
                                  splitCell cnt c st = { lab := lab, pos := pos, cst := cst, cen := cen, inW := if st.inW[c]! = true then markAllFrom starts starts.size 0 st.inW else markExceptFrom starts (maxIdxFrom sizes sizes.size 0 0) starts.size 0 st.inW, tr := tr, bc := clearBcFrom (sortNats ks0) (sortNats ks0).size 0 bc2 }

                                  The general branch, with each pass of the counting sort named.

                                  theorem IsoGraph.Canon.splitCell_part_singleton {cnt : Array } {c : } {st : SplitState} (h : (st.cen[c]! - c == 1) = true) :
                                  (splitCell cnt c st).part = st.part

                                  The partition after a singleton cell "splits": unchanged.

                                  theorem IsoGraph.Canon.splitCell_part_one {cnt : Array } {c : } {st : SplitState} {bc0 ks0 : Array } (hb : bucketFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c st.bc #[] = (bc0, ks0)) (h1 : ¬(st.cen[c]! - c == 1) = true) (h2 : (ks0.size == 1) = true) :
                                  (splitCell cnt c st).part = st.part

                                  The partition after a one-bucket cell "splits": unchanged.

                                  theorem IsoGraph.Canon.splitCell_part_general {cnt : Array } {c : } {st : SplitState} {bc0 ks0 : Array } (hb : bucketFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c st.bc #[] = (bc0, ks0)) (h1 : ¬(st.cen[c]! - c == 1) = true) (h2 : ¬(ks0.size == 1) = true) {sizes bc1 : Array } (ho : offsetFrom (sortNats ks0) (sortNats ks0).size 0 (Array.replicate (sortNats ks0).size 0) bc0 0 = (sizes, bc1)) {block bc2 : Array } (hsc : scatterFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c (Array.replicate (st.cen[c]! - c) 0) bc1 = (block, bc2)) {lab pos : Array } (hw : writeFrom block c block.size 0 st.lab st.pos = (lab, pos)) {cst cen starts : Array } {tr : UInt64} (hbd : boundsFrom (sortNats ks0) sizes (sortNats ks0).size 0 st.cst st.cen #[] c (mixN st.tr c) = (cst, cen, starts, tr)) :
                                  (splitCell cnt c st).part = { lab := lab, pos := pos, cst := cst, cen := cen }

                                  The partition after a genuine split.

                                  theorem IsoGraph.Canon.sizesSum_exists (sizes : Array ) (K x : ) :
                                  x < sizesSum sizes 0 Kj < K, sizesSum sizes 0 j x x < sizesSum sizes 0 (j + 1)

                                  Every offset below the total is inside exactly one fragment.

                                  theorem IsoGraph.Canon.splitOk_general {n : } {cnt : Array } {c : } {st : SplitState} (hp : Part.WF n st.part) (hc : c < n) (hcst : st.cst[c]! = c) (hbc : t < st.bc.size, st.bc[t]! = 0) (hcb : ∀ (v : ), cnt[v]! < st.bc.size) {bc0 ks0 : Array } (hb : bucketFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c st.bc #[] = (bc0, ks0)) (h1 : ¬(st.cen[c]! - c == 1) = true) (h2 : ¬(ks0.size == 1) = true) :
                                  SplitOk n st.part cnt c (splitCell cnt c st).part

                                  The general branch of splitCell: the counting sort really does sort the cell into fragments by count, and installs the fragment boundaries.

                                  theorem IsoGraph.Canon.splitCell_spec {n : } {cnt : Array } {c : } {st : SplitState} (hp : Part.WF n st.part) (hc : c < n) (hcst : st.cst[c]! = c) (hbc : t < st.bc.size, st.bc[t]! = 0) (hcb : ∀ (v : ), cnt[v]! < st.bc.size) :
                                  SplitOk n st.part cnt c (splitCell cnt c st).part
                                  theorem IsoGraph.Canon.fragStart_disjoint {n : } {p : Part} {cnt : Array } {c t t' : } (h : t < t') :
                                  (fragStart n p cnt c t + cellCount n p c fun (u : ) => cnt[u]! == t) fragStart n p cnt c t'

                                  The fragments of a split cell occupy disjoint ranges of positions: the fragment of t ends before the fragment of any larger count starts.

                                  theorem IsoGraph.Canon.splitOk_partEquiv {n : } {σ : } {p q p' q' : Part} {cnt cnt' : Array } {c : } ( : IsPerm n σ) (hp : Part.WF n p) (hq : Part.WF n q) (he : PartEquiv n σ p q) (hc : c < n) (hcst : q.cst[c]! = c) (hcnt : v < n, cnt[σ v]! = cnt'[v]!) (h1 : SplitOk n p cnt c p') (h2 : SplitOk n q cnt' c q') :
                                  PartEquiv n σ p' q'

                                  The split is equivariant. Two corresponding cells are split into corresponding fragments: the fragment of a vertex is determined by its count, and corresponding vertices have corresponding counts, so the two runs lay their cells out identically.

                                  Reading off the scratch fields of one splitCell step #

                                  The same branch equations again, now for the fields the partition proofs ignore: the trace, the worklist, and the bucket scratch.

                                  theorem IsoGraph.Canon.splitCell_tr_singleton {cnt : Array } {c : } {st : SplitState} (h : (st.cen[c]! - c == 1) = true) :
                                  (splitCell cnt c st).tr = mixN (mixN st.tr c) cnt[st.lab[c]!]!
                                  theorem IsoGraph.Canon.splitCell_inW_singleton {cnt : Array } {c : } {st : SplitState} (h : (st.cen[c]! - c == 1) = true) :
                                  (splitCell cnt c st).inW = st.inW
                                  theorem IsoGraph.Canon.splitCell_bc_singleton {cnt : Array } {c : } {st : SplitState} (h : (st.cen[c]! - c == 1) = true) :
                                  (splitCell cnt c st).bc = st.bc
                                  theorem IsoGraph.Canon.splitCell_tr_one {cnt : Array } {c : } {st : SplitState} {bc0 ks0 : Array } (hb : bucketFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c st.bc #[] = (bc0, ks0)) (h1 : ¬(st.cen[c]! - c == 1) = true) (h2 : (ks0.size == 1) = true) :
                                  (splitCell cnt c st).tr = mixN (mixN st.tr c) ks0[0]!
                                  theorem IsoGraph.Canon.splitCell_inW_one {cnt : Array } {c : } {st : SplitState} {bc0 ks0 : Array } (hb : bucketFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c st.bc #[] = (bc0, ks0)) (h1 : ¬(st.cen[c]! - c == 1) = true) (h2 : (ks0.size == 1) = true) :
                                  (splitCell cnt c st).inW = st.inW
                                  theorem IsoGraph.Canon.splitCell_bc_one {cnt : Array } {c : } {st : SplitState} {bc0 ks0 : Array } (hb : bucketFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c st.bc #[] = (bc0, ks0)) (h1 : ¬(st.cen[c]! - c == 1) = true) (h2 : (ks0.size == 1) = true) :
                                  (splitCell cnt c st).bc = bc0.set! ks0[0]! 0
                                  theorem IsoGraph.Canon.splitCell_tr_general {cnt : Array } {c : } {st : SplitState} {bc0 ks0 : Array } (hb : bucketFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c st.bc #[] = (bc0, ks0)) (h1 : ¬(st.cen[c]! - c == 1) = true) (h2 : ¬(ks0.size == 1) = true) {sizes bc1 : Array } (ho : offsetFrom (sortNats ks0) (sortNats ks0).size 0 (Array.replicate (sortNats ks0).size 0) bc0 0 = (sizes, bc1)) {block bc2 : Array } (hsc : scatterFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c (Array.replicate (st.cen[c]! - c) 0) bc1 = (block, bc2)) {lab pos : Array } (hw : writeFrom block c block.size 0 st.lab st.pos = (lab, pos)) {cst cen starts : Array } {tr : UInt64} (hbd : boundsFrom (sortNats ks0) sizes (sortNats ks0).size 0 st.cst st.cen #[] c (mixN st.tr c) = (cst, cen, starts, tr)) :
                                  (splitCell cnt c st).tr = tr
                                  theorem IsoGraph.Canon.splitCell_inW_general {cnt : Array } {c : } {st : SplitState} {bc0 ks0 : Array } (hb : bucketFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c st.bc #[] = (bc0, ks0)) (h1 : ¬(st.cen[c]! - c == 1) = true) (h2 : ¬(ks0.size == 1) = true) {sizes bc1 : Array } (ho : offsetFrom (sortNats ks0) (sortNats ks0).size 0 (Array.replicate (sortNats ks0).size 0) bc0 0 = (sizes, bc1)) {block bc2 : Array } (hsc : scatterFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c (Array.replicate (st.cen[c]! - c) 0) bc1 = (block, bc2)) {lab pos : Array } (hw : writeFrom block c block.size 0 st.lab st.pos = (lab, pos)) {cst cen starts : Array } {tr : UInt64} (hbd : boundsFrom (sortNats ks0) sizes (sortNats ks0).size 0 st.cst st.cen #[] c (mixN st.tr c) = (cst, cen, starts, tr)) :
                                  (splitCell cnt c st).inW = if st.inW[c]! = true then markAllFrom starts starts.size 0 st.inW else markExceptFrom starts (maxIdxFrom sizes sizes.size 0 0) starts.size 0 st.inW
                                  theorem IsoGraph.Canon.splitCell_bc_general {cnt : Array } {c : } {st : SplitState} {bc0 ks0 : Array } (hb : bucketFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c st.bc #[] = (bc0, ks0)) (h1 : ¬(st.cen[c]! - c == 1) = true) (h2 : ¬(ks0.size == 1) = true) {sizes bc1 : Array } (ho : offsetFrom (sortNats ks0) (sortNats ks0).size 0 (Array.replicate (sortNats ks0).size 0) bc0 0 = (sizes, bc1)) {block bc2 : Array } (hsc : scatterFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c (Array.replicate (st.cen[c]! - c) 0) bc1 = (block, bc2)) {lab pos : Array } (hw : writeFrom block c block.size 0 st.lab st.pos = (lab, pos)) {cst cen starts : Array } {tr : UInt64} (hbd : boundsFrom (sortNats ks0) sizes (sortNats ks0).size 0 st.cst st.cen #[] c (mixN st.tr c) = (cst, cen, starts, tr)) :
                                  (splitCell cnt c st).bc = clearBcFrom (sortNats ks0) (sortNats ks0).size 0 bc2
                                  theorem IsoGraph.Canon.splitCell_bc {cnt : Array } {c : } {st : SplitState} (hbc : t < st.bc.size, st.bc[t]! = 0) (hcb : ∀ (v : ), cnt[v]! < st.bc.size) :
                                  (splitCell cnt c st).bc.size = st.bc.size t < st.bc.size, (splitCell cnt c st).bc[t]! = 0

                                  The bucket scratch array is restored. Whatever branch it takes, splitCell hands back a bc of the same size and again all zero, so the next split can run on it.

                                  theorem IsoGraph.Canon.bucketFrom_facts {cnt : Array } {c : } {st : SplitState} {bc0 ks0 : Array } (hbc : t < st.bc.size, st.bc[t]! = 0) (hcb : ∀ (v : ), cnt[v]! < st.bc.size) (hb : bucketFrom st.lab cnt st.cen[c]! (st.cen[c]! - c) c st.bc #[] = (bc0, ks0)) :
                                  ks0.toList.Nodup (∀ (t : ), t ks0 bucketSize st.lab cnt c st.cen[c]! t 0) ∀ (sizes bc1 : Array ), offsetFrom (sortNats ks0) (sortNats ks0).size 0 (Array.replicate (sortNats ks0).size 0) bc0 0 = (sizes, bc1)sizes.size = (sortNats ks0).size j < (sortNats ks0).size, sizes[j]! = bucketSize st.lab cnt c st.cen[c]! (sortNats ks0)[j]!

                                  What the bucketing and offset passes leave behind, in a form that mentions neither the size of the scratch array nor the order the counts were met in: the counts are listed once each, they are exactly the counts occurring in the cell, and the fragment sizes are their bucket sizes.

                                  theorem IsoGraph.Canon.splitCell_scratch_equiv {n : } {σ : } {cnt cnt' : Array } {c : } {stp stq : SplitState} ( : IsPerm n σ) (hp : Part.WF n stp.part) (hq : Part.WF n stq.part) (he : PartEquiv n σ stp.part stq.part) (hc : c < n) (hcst : stq.cst[c]! = c) (hcnt : v < n, cnt[σ v]! = cnt'[v]!) (hbcp : t < stp.bc.size, stp.bc[t]! = 0) (hcbp : ∀ (v : ), cnt[v]! < stp.bc.size) (hbcq : t < stq.bc.size, stq.bc[t]! = 0) (hcbq : ∀ (v : ), cnt'[v]! < stq.bc.size) (htr : stp.tr = stq.tr) (hinW : stp.inW = stq.inW) :
                                  (splitCell cnt c stp).tr = (splitCell cnt' c stq).tr (splitCell cnt c stp).inW = (splitCell cnt' c stq).inW

                                  The scratch fields move in lockstep too. Corresponding cells hash to the same trace and queue the same fragments: the branch taken, the fragment sizes and the fragment starts are all determined by data the two runs share.

                                  One refinement step #

                                  Everything above is about a single cell. splitCellsFrom walks a list of cells, so the facts have to be packaged into an invariant that survives the walk.

                                  structure IsoGraph.Canon.SplitInv (n : ) (cnt : Array ) (st : SplitState) :

                                  What a split step needs of its state: a well-formed partition, a cleared bucket array, and counts that index into it.

                                  • wf : Part.WF n st.part

                                    The partition is well-formed.

                                  • bcSize : st.bc.size = n + 1

                                    The bucket scratch has one slot per possible count.

                                  • bcZero (t : ) : t < st.bc.sizest.bc[t]! = 0

                                    The bucket scratch is cleared.

                                  • cntLt (v : ) : cnt[v]! < st.bc.size

                                    Every count indexes the bucket scratch.

                                  Instances For
                                    structure IsoGraph.Canon.SplitRel (n : ) (σ : ) (stp stq : SplitState) :

                                    What two split states run in parallel share: corresponding partitions, and identical trace and worklist.

                                    • part : PartEquiv n σ stp.part stq.part

                                      The partitions correspond under σ.

                                    • tr : stp.tr = stq.tr

                                      The traces agree.

                                    • inW : stp.inW = stq.inW

                                      The worklists agree.

                                    Instances For
                                      theorem IsoGraph.Canon.splitCell_inv {n : } {cnt : Array } {c : } {st : SplitState} (hinv : SplitInv n cnt st) (hc : c < n) (hcst : st.cst[c]! = c) :
                                      SplitInv n cnt (splitCell cnt c st)

                                      A split step preserves the invariant.

                                      theorem IsoGraph.Canon.splitCell_start {n : } {cnt : Array } {c : } {st : SplitState} (hinv : SplitInv n cnt st) (hc : c < n) (hcst : st.cst[c]! = c) {c' : } (hc' : c' < n) (hne : c' c) (hcst' : st.cst[c']! = c') :
                                      (splitCell cnt c st).cst[c']! = c'

                                      Splitting one cell leaves the other cell starts alone.

                                      theorem IsoGraph.Canon.splitCell_rel {n : } {σ : } {cnt cnt' : Array } {c : } {stp stq : SplitState} ( : IsPerm n σ) (hip : SplitInv n cnt stp) (hiq : SplitInv n cnt' stq) (hr : SplitRel n σ stp stq) (hc : c < n) (hcst : stq.cst[c]! = c) (hcnt : v < n, cnt[σ v]! = cnt'[v]!) :
                                      SplitRel n σ (splitCell cnt c stp) (splitCell cnt' c stq)

                                      A split step preserves the relation: this is step 1 for a single cell.

                                      theorem IsoGraph.Canon.splitCellsFrom_inv {n : } {cnt cells : Array } (hnd : cells.toList.Nodup) (fuel j : ) (st : SplitState) :
                                      SplitInv n cnt st(∀ (j' : ), j j'j' < cells.sizecells[j']! < n st.cst[cells[j']!]! = cells[j']!)SplitInv n cnt (splitCellsFrom cnt cells fuel j st)

                                      Splitting a list of distinct cells preserves the invariant.

                                      theorem IsoGraph.Canon.splitCellsFrom_rel {n : } {σ : } {cnt cnt' cells : Array } ( : IsPerm n σ) (hnd : cells.toList.Nodup) (hcnt : v < n, cnt[σ v]! = cnt'[v]!) (fuel j : ) (stp stq : SplitState) :
                                      SplitInv n cnt stpSplitInv n cnt' stqSplitRel n σ stp stq(∀ (j' : ), j j'j' < cells.sizecells[j']! < n stq.cst[cells[j']!]! = cells[j']!)SplitRel n σ (splitCellsFrom cnt cells fuel j stp) (splitCellsFrom cnt' cells fuel j stq)

                                      Step 1 for a refinement pass. Two runs that split the same list of cells with corresponding counts stay in correspondence.

                                      Reading off one refinement step #

                                      theorem IsoGraph.Canon.refineStep_eq_empty {G : Graph} {p : Part} {inW : Array Bool} {s : } {tr : UInt64} {sc : Scratch} {cnt touched : Array } (hc : countFrom G p.lab p.cen[s]! (p.cen[s]! - s) s sc.cnt #[] = (cnt, touched)) (h : touched.isEmpty = true) :
                                      refineStep G p inW s tr sc = (p, inW, mixN tr s, sc)
                                      theorem IsoGraph.Canon.refineStep_eq {G : Graph} {p : Part} {inW : Array Bool} {s : } {tr : UInt64} {sc : Scratch} {cnt touched : Array } (hc : countFrom G p.lab p.cen[s]! (p.cen[s]! - s) s sc.cnt #[] = (cnt, touched)) (h : ¬touched.isEmpty = true) {hit : Array Bool} {collected : Array } (hcl : collectFrom p.pos p.cst touched touched.size 0 sc.hit #[] = (hit, collected)) {st : SplitState} (hst : splitCellsFrom cnt (sortNats collected) (sortNats collected).size 0 { lab := p.lab, pos := p.pos, cst := p.cst, cen := p.cen, inW := inW, tr := mixN tr s, bc := sc.bc } = st) :
                                      refineStep G p inW s tr sc = (st.part, st.inW, st.tr, { cnt := clearCntFrom touched touched.size 0 cnt, hit := clearHitFrom (sortNats collected) (sortNats collected).size 0 hit, bc := st.bc })

                                      The scratch space is restored, and the partition stays well-formed #

                                      theorem IsoGraph.Canon.collectFrom_size (pos cst touched : Array ) (fuel j : ) (hit : Array Bool) (cells : Array ) :
                                      (collectFrom pos cst touched fuel j hit cells).1.size = hit.size
                                      theorem IsoGraph.Canon.clearHitFrom_size (cells : Array ) (fuel j : ) (hit : Array Bool) :
                                      (clearHitFrom cells fuel j hit).size = hit.size
                                      theorem IsoGraph.Canon.Collected.sortNats {hit : Array Bool} {cells : Array } (h : Collected hit cells) :

                                      Sorting the collected cells changes neither the invariant nor the marks.

                                      theorem IsoGraph.Canon.cellCount_le (n : ) (p : Part) (s : ) (P : Bool) :
                                      cellCount n p s P n

                                      A cell has at most n members, so counts fit in the bucket array.

                                      theorem IsoGraph.Canon.countFrom_lt {n : } (f : Bool) {p : Part} (hp : Part.WF n p) {s : } (hs : s < n) (hcst : p.cst[s]! = s) (v : ) :
                                      (countFrom (Graph.ofOracle n f) p.lab p.cen[s]! (p.cen[s]! - s) s (Array.replicate n 0) #[]).1[v]! < n + 1
                                      theorem IsoGraph.Canon.countFrom_touched_lt' {n : } (f : Bool) (p : Part) (s : ) {cnt touched : Array } (hc : countFrom (Graph.ofOracle n f) p.lab p.cen[s]! (p.cen[s]! - s) s (Array.replicate n 0) #[] = (cnt, touched)) (v : ) :
                                      v touchedv < n

                                      Every vertex the counting phase touches is a vertex — packaged for a named result.

                                      theorem IsoGraph.Canon.collect_start {n : } {p : Part} (hp : Part.WF n p) {touched : Array } (htn : vtouched, v < n) {hit : Array Bool} {collected : Array } (hcl : collectFrom p.pos p.cst touched touched.size 0 (Array.replicate n false) #[] = (hit, collected)) :
                                      collected.toList.Nodup ccollected, c < n p.cst[c]! = c

                                      The cells the collection phase gathers are distinct cell starts.

                                      theorem IsoGraph.Canon.splitInv_init {n : } {f : Bool} {p : Part} (hp : Part.WF n p) {s : } (hs : s < n) (hcst : p.cst[s]! = s) {cnt touched : Array } (hc : countFrom (Graph.ofOracle n f) p.lab p.cen[s]! (p.cen[s]! - s) s (Array.replicate n 0) #[] = (cnt, touched)) (inW : Array Bool) (tr' : UInt64) :
                                      SplitInv n cnt { lab := p.lab, pos := p.pos, cst := p.cst, cen := p.cen, inW := inW, tr := tr', bc := (Scratch.empty n).bc }

                                      The state the cell loop starts from satisfies its invariant.

                                      theorem IsoGraph.Canon.refineStep_wf {n : } {f : Bool} {p : Part} (hp : Part.WF n p) {s : } (hs : s < n) (hcst : p.cst[s]! = s) (inW : Array Bool) (tr : UInt64) :
                                      Part.WF n (refineStep (Graph.ofOracle n f) p inW s tr (Scratch.empty n)).1 (refineStep (Graph.ofOracle n f) p inW s tr (Scratch.empty n)).2.2.2 = Scratch.empty n

                                      What one refinement step leaves behind. The partition is still well-formed, the cells it split are cells of the old partition, and the scratch space is back to its cleared state — the last point is what lets the worklist loop keep reusing it.

                                      One refinement step is equivariant #

                                      theorem IsoGraph.Canon.refineStep_equiv {n : } {σ : } {f : Bool} {p q : Part} ( : IsPerm n σ) (hp : Part.WF n p) (hq : Part.WF n q) (he : PartEquiv n σ p q) {s : } (hs : s < n) (hcst : q.cst[s]! = s) (inW : Array Bool) (tr : UInt64) :
                                      PartEquiv n σ (refineStep (Graph.ofOracle n f) p inW s tr (Scratch.empty n)).1 (refineStep (Graph.ofOracle n fun (a b : ) => f (σ a) (σ b)) q inW s tr (Scratch.empty n)).1 (refineStep (Graph.ofOracle n f) p inW s tr (Scratch.empty n)).2.1 = (refineStep (Graph.ofOracle n fun (a b : ) => f (σ a) (σ b)) q inW s tr (Scratch.empty n)).2.1 (refineStep (Graph.ofOracle n f) p inW s tr (Scratch.empty n)).2.2.1 = (refineStep (Graph.ofOracle n fun (a b : ) => f (σ a) (σ b)) q inW s tr (Scratch.empty n)).2.2.1

                                      Step 1. One refinement pass commutes with relabelling: run on corresponding partitions with corresponding adjacency oracles it produces corresponding partitions, the same worklist and the same trace.

                                      Reading off the worklist loop #

                                      theorem IsoGraph.Canon.refineLoop_none {G : Graph} {fuel : } {p : Part} {inW : Array Bool} {tr : UInt64} {sc : Scratch} (hfs : firstSet inW = none) :
                                      refineLoop G (fuel + 1) p inW tr sc = (p, tr)
                                      theorem IsoGraph.Canon.refineLoop_step {G : Graph} {fuel : } {p : Part} {inW : Array Bool} {tr : UInt64} {sc : Scratch} {s : } (hfs : firstSet inW = some s) (hg : (decide (s < G.n) && p.cst[s]! == s) = true) {p' : Part} {inW' : Array Bool} {tr' : UInt64} {sc' : Scratch} (hstep : refineStep G p (inW.set! s false) s tr sc = (p', inW', tr', sc')) :
                                      refineLoop G (fuel + 1) p inW tr sc = refineLoop G fuel p' inW' tr' sc'
                                      theorem IsoGraph.Canon.refineLoop_skip {G : Graph} {fuel : } {p : Part} {inW : Array Bool} {tr : UInt64} {sc : Scratch} {s : } (hfs : firstSet inW = some s) (hg : (decide (s < G.n) && p.cst[s]! == s) = false) :
                                      refineLoop G (fuel + 1) p inW tr sc = refineLoop G fuel p (inW.set! s false) tr sc

                                      The worklist loop is equivariant #

                                      theorem IsoGraph.Canon.refineLoop_equiv {n : } {σ : } {f : Bool} ( : IsPerm n σ) (fuel : ) (p q : Part) (inW : Array Bool) (tr : UInt64) :
                                      Part.WF n pPart.WF n qPartEquiv n σ p qPartEquiv n σ (refineLoop (Graph.ofOracle n f) fuel p inW tr (Scratch.empty n)).1 (refineLoop (Graph.ofOracle n fun (a b : ) => f (σ a) (σ b)) fuel q inW tr (Scratch.empty n)).1 (refineLoop (Graph.ofOracle n f) fuel p inW tr (Scratch.empty n)).2 = (refineLoop (Graph.ofOracle n fun (a b : ) => f (σ a) (σ b)) fuel q inW tr (Scratch.empty n)).2

                                      Step 1 for a whole refinement. Two runs of the worklist loop on corresponding partitions stay in correspondence and produce the same trace. The two runs pop the same positions in the same order, because the worklist is literally the same array in both.

                                      theorem IsoGraph.Canon.refineLoop_wf {n : } {f : Bool} (fuel : ) (p : Part) (inW : Array Bool) (tr : UInt64) :
                                      Part.WF n pPart.WF n (refineLoop (Graph.ofOracle n f) fuel p inW tr (Scratch.empty n)).1

                                      The worklist loop keeps the partition well-formed.

                                      Refinement from a partition, and from the unit partition #

                                      theorem IsoGraph.Canon.refine_wf {n : } {f : Bool} {p : Part} (hp : Part.WF n p) (inW : Array Bool) (tr : UInt64) :
                                      Part.WF n (refine (Graph.ofOracle n f) p inW tr).1
                                      theorem IsoGraph.Canon.refine_equiv {n : } {σ : } {f : Bool} {p q : Part} ( : IsPerm n σ) (hp : Part.WF n p) (hq : Part.WF n q) (he : PartEquiv n σ p q) (inW : Array Bool) (tr : UInt64) :
                                      PartEquiv n σ (refine (Graph.ofOracle n f) p inW tr).1 (refine (Graph.ofOracle n fun (a b : ) => f (σ a) (σ b)) q inW tr).1 (refine (Graph.ofOracle n f) p inW tr).2 = (refine (Graph.ofOracle n fun (a b : ) => f (σ a) (σ b)) q inW tr).2
                                      theorem IsoGraph.Canon.initialRefine_equiv {n : } {σ : } {f : Bool} ( : IsPerm n σ) :
                                      PartEquiv n σ (initialRefine (Graph.ofOracle n f)).1 (initialRefine (Graph.ofOracle n fun (a b : ) => f (σ a) (σ b))).1 (initialRefine (Graph.ofOracle n f)).2 = (initialRefine (Graph.ofOracle n fun (a b : ) => f (σ a) (σ b))).2

                                      Step 1. The initial equitable refinement is equivariant.

                                      Certificate readback #

                                      The search compares packed certificates; certOf_get below turns an equality of certificates back into an equality of adjacency entries, which is what Spec.LabellingInvariant asks for.

                                      def IsoGraph.Canon.certGet (n : ) (c : Array UInt64) (i j : ) :

                                      Bit j of row i of a certificate packed by certBits.

                                      Equations
                                      Instances For
                                        def IsoGraph.Canon.AccOk (b : Bool) (j : ) (acc : UInt64) :

                                        The accumulator invariant of certRow: acc holds the bits of columns 64 * (j / 64) … j - 1, right-aligned, with column j - 1 at bit 0.

                                        Equations
                                        Instances For
                                          theorem IsoGraph.Canon.certRow_spec (n : ) (b : Bool) (base fuel j : ) (acc : UInt64) (out res : Array UInt64) :
                                          j + fuel = nbase + rowWords n out.sizeAccOk b j acccertRow n b fuel j acc (base + j / 64) out = resres.size = out.size (∀ (x : ), x < base + j / 64 base + rowWords n xres[x]! = out[x]!) j' < n, 64 * (j / 64) j'res[base + j' / 64]!.toBitVec.getLsbD (63 - j' % 64) = b j'
                                          theorem IsoGraph.Canon.certRowsFrom_spec (n : ) (bit : Bool) (fuel i : ) (out res : Array UInt64) :
                                          i + fuel = nn * rowWords n out.sizecertRowsFrom n bit (rowWords n) fuel i out = resres.size = out.size (∀ x < i * rowWords n, res[x]! = out[x]!) ∀ (i' j' : ), i i'i' < nj' < ncertGet n res i' j' = bit i' j'
                                          theorem IsoGraph.Canon.certBits_get (n : ) (bit : Bool) {i j : } (hi : i < n) (hj : j < n) :
                                          certGet n (certBits n bit) i j = bit i j
                                          theorem IsoGraph.Canon.certOf_get {G : Graph} {lab : Array } {i j : } (hi : i < G.n) (hj : j < G.n) :
                                          certGet G.n (certOf G lab) i j = G.adj[lab[i]!]![lab[j]!]!

                                          Reading the packed certificate back: bit (i, j) of certOf G lab is the adjacency of the i-th and j-th vertices in the order lab.

                                          theorem IsoGraph.Canon.leafUpdate_best (G : Graph) (path : Array ) (invPath : Array UInt64) (lab : Array ) (st : St) :
                                          (leafUpdate G path invPath lab st).best = st.best (leafUpdate G path invPath lab st).best = some { path := path, invPath := invPath, cert := certOf G lab, lab := lab }

                                          leafUpdate either keeps the incumbent or replaces it with the new leaf.

                                          theorem IsoGraph.Canon.leafUpdate_first (G : Graph) (path : Array ) (invPath : Array UInt64) (lab : Array ) (st : St) :
                                          (leafUpdate G path invPath lab st).first = st.first (leafUpdate G path invPath lab st).first = some { path := path, invPath := invPath, cert := certOf G lab, lab := lab }

                                          Same for the first leaf, which is only ever set once.

                                          The search only ever stores honest leaves #

                                          Two facts about every leaf the search records: its labelling is a permutation of the vertices, and its certificate is the certificate of that labelling. Together they say that the winner returned by canonical passes the isPermArray check of canonicalLabellingOfOracle and that Result.cert and Result.lab belong together, which is what turns an equality of certificates into Spec.LabellingInvariant.

                                          structure IsoGraph.Canon.LeafOk (G : Graph) (l : Leaf) :

                                          A stored leaf whose labelling is a vertex permutation and whose certificate matches it.

                                          • size : l.lab.size = G.n

                                            The labelling has one entry per vertex.

                                          • lt (i : ) : i < G.nl.lab[i]! < G.n

                                            Its entries are vertices.

                                          • inj (i : ) : i < G.nj < G.n, l.lab[i]! = l.lab[j]!i = j

                                            Distinct positions hold distinct vertices.

                                          • cert : l.cert = certOf G l.lab

                                            The stored certificate is the one the labelling determines.

                                          Instances For
                                            def IsoGraph.Canon.StOk (G : Graph) (st : St) :

                                            Every leaf a state remembers is honest.

                                            Equations
                                            Instances For
                                              theorem IsoGraph.Canon.leafOk_of_wf {n : } {G : Graph} (hn : G.n = n) {p : Part} (hp : Part.WF n p) (path : Array ) (invPath : Array UInt64) :
                                              LeafOk G { path := path, invPath := invPath, cert := certOf G p.lab, lab := p.lab }
                                              theorem IsoGraph.Canon.leafUpdate_ok {G : Graph} {path : Array } {invPath : Array UInt64} {lab : Array } {st : St} (hl : LeafOk G { path := path, invPath := invPath, cert := certOf G lab, lab := lab }) (hst : StOk G st) :
                                              StOk G (leafUpdate G path invPath lab st)
                                              theorem IsoGraph.Canon.StOk.addAuto {G : Graph} {st : St} (h : StOk G st) (g : Array ) :
                                              StOk G (st.addAuto g)
                                              theorem IsoGraph.Canon.individualize_wf' {n : } {p : Part} (hp : Part.WF n p) {v : } (hv : v < n) :
                                              theorem IsoGraph.Canon.pruneNode_ok {G : Graph} {invPath : Array UInt64} {st st' : St} (h : pruneNode invPath st = some st') (hst : StOk G st) :
                                              StOk G st'
                                              theorem IsoGraph.Canon.mem_extract_lt {n : } {p : Part} (hp : Part.WF n p) {a b v : } (h : v (p.lab.extract a b).toList) :
                                              v < n

                                              dfsNode and dfsChildren are written as one match/if cascade each, with lets naming the intermediate states. Rewriting inside those lets is painful, so each branch gets an equation here, stated with the intermediates named by orbRefresh and unwind. Proofs elsewhere only ever use these, never the definitions.

                                              def IsoGraph.Canon.orbRefresh (G : Graph) (path processed : Array ) (orb : Orbits) (st : St) :

                                              The orbit cache of dfsChildren, refreshed if new generators have turned up.

                                              Equations
                                              • One or more equations did not get rendered due to their size.
                                              Instances For
                                                def IsoGraph.Canon.unwind (path : Array ) (st : St) :

                                                Absorb a backjump request aimed at this depth.

                                                Equations
                                                • One or more equations did not get rendered due to their size.
                                                Instances For
                                                  theorem IsoGraph.Canon.dfsNode_zero (G : Graph) (path : Array ) (invPath : Array UInt64) (p : Part) (st : St) :
                                                  dfsNode G 0 path invPath p st = st
                                                  theorem IsoGraph.Canon.dfsNode_abort {G : Graph} {fuel : } {path : Array } {invPath : Array UInt64} {p : Part} {st : St} (h : st.abortTo.isSome = true) :
                                                  dfsNode G (fuel + 1) path invPath p st = st
                                                  theorem IsoGraph.Canon.dfsNode_pruned {G : Graph} {fuel : } {path : Array } {invPath : Array UInt64} {p : Part} {st : St} (h : st.abortTo.isSome = false) (hp : pruneNode invPath st = none) :
                                                  dfsNode G (fuel + 1) path invPath p st = st
                                                  theorem IsoGraph.Canon.dfsNode_leaf {G : Graph} {fuel : } {path : Array } {invPath : Array UInt64} {p : Part} {st st' : St} (h : st.abortTo.isSome = false) (hp : pruneNode invPath st = some st') (hc : p.targetCell G.n = none) :
                                                  dfsNode G (fuel + 1) path invPath p st = leafUpdate G path invPath p.lab { best := st'.best, first := st'.first, autos := st'.autos, nodes := st'.nodes + 1, abortTo := st'.abortTo }
                                                  theorem IsoGraph.Canon.dfsNode_branch {G : Graph} {fuel : } {path : Array } {invPath : Array UInt64} {p : Part} {st st' : St} {c : } (h : st.abortTo.isSome = false) (hp : pruneNode invPath st = some st') (hc : p.targetCell G.n = some c) :
                                                  dfsNode G (fuel + 1) path invPath p st = dfsChildren G fuel path invPath p (p.lab.extract c p.cen[c]!).toList #[] { nGens := st'.autos.size, gens := usableAutos st'.autos path, mark := Array.replicate G.n false } { best := st'.best, first := st'.first, autos := st'.autos, nodes := st'.nodes + 1, abortTo := st'.abortTo }
                                                  theorem IsoGraph.Canon.dfsChildren_nil (G : Graph) (fuel : ) (path : Array ) (invPath : Array UInt64) (p : Part) (processed : Array ) (orb : Orbits) (st : St) :
                                                  dfsChildren G fuel path invPath p [] processed orb st = st
                                                  theorem IsoGraph.Canon.dfsChildren_abort {G : Graph} {fuel : } {path : Array } {invPath : Array UInt64} {p : Part} {v : } {vs : List } {processed : Array } {orb : Orbits} {st : St} (h : st.abortTo.isSome = true) :
                                                  dfsChildren G fuel path invPath p (v :: vs) processed orb st = st
                                                  theorem IsoGraph.Canon.dfsChildren_marked {G : Graph} {fuel : } {path : Array } {invPath : Array UInt64} {p : Part} {v : } {vs : List } {processed : Array } {orb : Orbits} {st : St} (h : st.abortTo.isSome = false) (hm : (orbRefresh G path processed orb st).mark[v]! = true) :
                                                  dfsChildren G fuel path invPath p (v :: vs) processed orb st = dfsChildren G fuel path invPath p vs processed (orbRefresh G path processed orb st) st
                                                  theorem IsoGraph.Canon.dfsChildren_step {G : Graph} {fuel : } {path : Array } {invPath : Array UInt64} {p : Part} {v : } {vs : List } {processed : Array } {orb : Orbits} {st : St} {p' p'' : Part} {s : } {tr : UInt64} (h : st.abortTo.isSome = false) (hm : (orbRefresh G path processed orb st).mark[v]! = false) (hi : individualize p v = (p', s)) (hr : refine G p' ((Array.replicate G.n false).set! s true) hashSeed = (p'', tr)) :
                                                  dfsChildren G fuel path invPath p (v :: vs) processed orb st = have st1 := unwind path (dfsNode G fuel (path.push v) (invPath.push (mix tr (p''.shapeHash G.n))) p'' st); if st1.abortTo.isSome = true then st1 else dfsChildren G fuel path invPath p vs (processed.push v) (have __src := orbRefresh G path processed orb st; { nGens := __src.nGens, gens := __src.gens, mark := closureLoop (orbRefresh G path processed orb st).gens (G.n + 1) ((orbRefresh G path processed orb st).mark.set! v true) #[v] }) st1
                                                  theorem IsoGraph.Canon.dfsChildren_step_stop {G : Graph} {fuel : } {path : Array } {invPath : Array UInt64} {p : Part} {v : } {vs : List } {processed : Array } {orb : Orbits} {st : St} {p' p'' : Part} {s : } {tr : UInt64} (h : st.abortTo.isSome = false) (hm : (orbRefresh G path processed orb st).mark[v]! = false) (hi : individualize p v = (p', s)) (hr : refine G p' ((Array.replicate G.n false).set! s true) hashSeed = (p'', tr)) (hs : (unwind path (dfsNode G fuel (path.push v) (invPath.push (mix tr (p''.shapeHash G.n))) p'' st)).abortTo.isSome = true) :
                                                  dfsChildren G fuel path invPath p (v :: vs) processed orb st = unwind path (dfsNode G fuel (path.push v) (invPath.push (mix tr (p''.shapeHash G.n))) p'' st)
                                                  theorem IsoGraph.Canon.dfsChildren_step_go {G : Graph} {fuel : } {path : Array } {invPath : Array UInt64} {p : Part} {v : } {vs : List } {processed : Array } {orb : Orbits} {st : St} {p' p'' : Part} {s : } {tr : UInt64} (h : st.abortTo.isSome = false) (hm : (orbRefresh G path processed orb st).mark[v]! = false) (hi : individualize p v = (p', s)) (hr : refine G p' ((Array.replicate G.n false).set! s true) hashSeed = (p'', tr)) (hs : (unwind path (dfsNode G fuel (path.push v) (invPath.push (mix tr (p''.shapeHash G.n))) p'' st)).abortTo.isSome = false) :
                                                  dfsChildren G fuel path invPath p (v :: vs) processed orb st = dfsChildren G fuel path invPath p vs (processed.push v) (have __src := orbRefresh G path processed orb st; { nGens := __src.nGens, gens := __src.gens, mark := closureLoop (orbRefresh G path processed orb st).gens (G.n + 1) ((orbRefresh G path processed orb st).mark.set! v true) #[v] }) (unwind path (dfsNode G fuel (path.push v) (invPath.push (mix tr (p''.shapeHash G.n))) p'' st))
                                                  theorem IsoGraph.Canon.unwind_best (path : Array ) (st : St) :
                                                  (unwind path st).best = st.best

                                                  unwind only clears a backjump request; it never touches the recorded leaves.

                                                  theorem IsoGraph.Canon.unwind_first (path : Array ) (st : St) :
                                                  (unwind path st).first = st.first
                                                  theorem IsoGraph.Canon.StOk.unwind {G : Graph} {st : St} (h : StOk G st) (path : Array ) :
                                                  StOk G (Canon.unwind path st)
                                                  theorem IsoGraph.Canon.dfsNode_ok (n : ) (f : Bool) (fuel : ) (path : Array ) (invPath : Array UInt64) (p : Part) (st : St) :
                                                  Part.WF n pStOk (Graph.ofOracle n f) stStOk (Graph.ofOracle n f) (dfsNode (Graph.ofOracle n f) fuel path invPath p st)

                                                  Every leaf the search stores is honest. By functional induction over the two mutually recursive halves of the search: the only place a leaf is created is leafUpdate, and there it is built from the lab of the current partition, which the refinement keeps well-formed.

                                                  isPermArray is complete #

                                                  isPermArray_spec says an accept is honest. Here is the converse: an honest array is accepted. That is what turns canonicalLabellingOfOracle's if into a no-op, so that the labelling really is the search's output and not the identity fallback.

                                                  theorem IsoGraph.Canon.invLab_foldl_unchanged (n : ) (a : Array ) (l : List ) (b : Array ) (x : ) (h : il, a[i]! x) :
                                                  (List.foldl (fun (b : Array ) (i : ) => if a[i]! < n then b.set! a[i]! i else b) b l)[x]! = b[x]!

                                                  Positions not written by any step of the invLab fold keep their old value.

                                                  theorem IsoGraph.Canon.invLab_foldl_size (n : ) (a : Array ) (l : List ) (b : Array ) :
                                                  (List.foldl (fun (b : Array ) (i : ) => if a[i]! < n then b.set! a[i]! i else b) b l).size = b.size
                                                  theorem IsoGraph.Canon.invLab_foldl_get (n : ) (a : Array ) (l : List ) (hnd : l.Nodup) (hinj : il, jl, a[i]! = a[j]!i = j) (b : Array ) (hb : b.size = n) (i : ) (hi : i l) (hai : a[i]! < n) :
                                                  (List.foldl (fun (b : Array ) (i : ) => if a[i]! < n then b.set! a[i]! i else b) b l)[a[i]!]! = i
                                                  theorem IsoGraph.Canon.invLab_get {n : } {a : Array } (hinj : i < n, j < n, a[i]! = a[j]!i = j) {i : } (hi : i < n) (hai : a[i]! < n) :
                                                  (invLab n a)[a[i]!]! = i
                                                  theorem IsoGraph.Canon.isPermArray_of {n : } {a : Array } (hsize : a.size = n) (hlt : i < n, a[i]! < n) (hinj : i < n, j < n, a[i]! = a[j]!i = j) :

                                                  isPermArray is complete. Anything of the right size whose entries are in range and pairwise distinct is accepted.

                                                  The search's output is an honest leaf #

                                                  The Leaf view of a Result, so that LeafOk can be reused for it.

                                                  Equations
                                                  Instances For

                                                    The canonical labelling is a permutation and its certificate is the graph read at it.

                                                    With the check now known to pass, the labelling is the search's output.