Membership and exact order of a checked computational group agree with its Mathlib subgroup, using the original generator presentation.
35.7. The Mathlib correspondence
HexPermGroupTheory lets a Mathlib user prove facts about a subgroup of
Equiv.Perm (Fin n) generated by explicit permutations. The perm_group
tactic proves the order of such a subgroup, that a given permutation does or
does not lie in it, and that it is the whole symmetric group. Here M11 is
the subgroup generated by the two permutations of the introduction:
def M11 : Subgroup (Equiv.Perm (Fin 11)) :=
Subgroup.closure
{c[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
c[2, 6, 10, 7] * c[3, 9, 4, 5]}
example : Nat.card M11 = 7920 := ⊢ Nat.card ↥M11 = 7920
All goals completed! 🐙
example : c[0, 1] ∉ M11 := ⊢ (↑[0, 1]).formPerm ⋯ ∉ M11
All goals completed! 🐙
example : Subgroup.closure
({c[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], c[0, 1]} :
Set (Equiv.Perm (Fin 11))) = ⊤ := ⊢ Subgroup.closure {(↑[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).formPerm ⋯, (↑[0, 1]).formPerm ⋯} = ⊤
All goals completed! 🐙
Since M11 is transitive on ordered pairs of points, any transposition in it
would be conjugate in it to c[0, 1], so the second example shows that M11
contains no transposition. The third shows that an 11-cycle and a
transposition of two adjacent points of the cycle generate the symmetric
group on 11 points.
Behind the tactic are theorems relating each computation to the subgroup of
Equiv.Perm (Fin n) generated by the same permutations, written
Hex.PermGroup.closure of the array of generators.
Hex.PermGroup.Group.order_card states that the order of a group is
Nat.card of that subgroup, and
Hex.PermGroup.Group.contains_spec relates membership:
Hex.PermGroup.Group.contains_spec {n : ℕ} (G : Hex.PermGroup.Group n) (p : Hex.Perm n) : G.contains p = true ↔ p.toEquiv ∈ Hex.PermGroup.closure G.generatorsHex.PermGroup.Group.contains_spec {n : ℕ} (G : Hex.PermGroup.Group n) (p : Hex.Perm n) : G.contains p = true ↔ p.toEquiv ∈ Hex.PermGroup.closure G.generators
These theorems hold for every input. The theorems cited in earlier sections
from HexPermGroupTheory, such as
Hex.PermGroup.Group.blocks_spec and
Hex.PermGroup.Group.sample_uniform, translate other computations in
the same way.
35.7.1. How perm_group works
Applying Hex.PermGroup.Group.order_card to a particular group
requires Lean to evaluate Hex.PermGroup.Group.ofGenerators during type
checking, and evaluation there is far slower than running compiled code. The
perm_group tactic instead builds a Hex.PermGroup.Kernel.Certificate
in compiled code, which is cheap to check. It is a stabilizer chain of its own:
the first level's generators are the distinct input permutations other than
the identity, and each later level's are a few products of Schreier generators
of the level above that already generate the stabilizer, usually two or three,
so the check has few Schreier generators to reduce.
The certificate keeps only the levels whose orbit has more than one point. Each
level stores its base point, its generators, its orbit, the representatives
and their inverses, and enough bookkeeping to recompute the representatives
from the generators. Every permutation is written as a single natural number,
with the image of each point in a fixed number of bits. For M11 there are
four levels left, with orbits of 11, 10, 9 and 8 points:
open Hex Hex.PermGroup in
def m11Certificate : Kernel.Certificate :=
let S : Array (Perm 11) :=
#[Perm.ofEquiv c[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
Perm.ofEquiv (c[2, 6, 10, 7] * c[3, 9, 4, 5])]
match Kernel.certify S with
| .ok c => c
| .error _ => []
#eval m11Certificate.map (·.size)
#eval Hex.PermGroup.Kernel.order m11Certificate
Lean then evaluates Hex.PermGroup.Kernel.check on the certificate
during type checking. It checks that the first level is generated by the
input permutations, that each level's orbit and representatives are correct,
and that every Schreier generator of every level reduces to the identity
through the levels after it. Here is the Schreier generator formed from the
first generator and the second orbit point of the first level, reducing to
the identity:
open Hex.PermGroup.Kernel in
#eval
let W := width 11
match m11Certificate with
| L :: rest =>
sift 11 W (ident 11 W) rest (schreier 11 W L 0 1)
| [] => false
When the check succeeds, Hex.PermGroup.Kernel.card_closure gives the
order:
Hex.PermGroup.Kernel.card_closure {n : ℕ} {S : Array (Hex.Perm n)} {c : Hex.PermGroup.Kernel.Certificate} (h : Hex.PermGroup.Kernel.check n (List.map Hex.PermGroup.Kernel.pack S.toList) c = true) : Nat.card ↥(Hex.PermGroup.closure S) = Hex.PermGroup.Kernel.order cHex.PermGroup.Kernel.card_closure {n : ℕ} {S : Array (Hex.Perm n)} {c : Hex.PermGroup.Kernel.Certificate} (h : Hex.PermGroup.Kernel.check n (List.map Hex.PermGroup.Kernel.pack S.toList) c = true) : Nat.card ↥(Hex.PermGroup.closure S) = Hex.PermGroup.Kernel.order c
An accepted certificate gives the cardinality of Mathlib's closure.
So that no single declaration takes too long to check, the tactic splits the
check into pieces: one for the input generators, one for each level, and one
for each range of Schreier generators. Each piece is a separate declaration
proved by evaluation, and the tactic combines them to obtain the hypothesis
of card_closure, or of the corresponding theorem for membership.
For M11 the whole perm_group proof takes about 0.1 seconds, most of it
spent by the kernel checking the certificate.