Overview#
AutStr represents infinite mathematical structures as finite automata and decides first-order and monadic second-order questions about them. This chapter is the guided tour: it works through the two things the library represents — single structures and whole classes — the operations that combine them, the rank-width families, implicit evaluation, and finally how the machine works underneath. Every part has a matching executable notebook, linked as we go.
The README <https://github.com/fariedabuzaid/AutStr>_ is the short version of
this page; the API reference is generated from the docstrings and linked in the
sidebar.
Decision procedure and theorem prover#
An AutomaticPresentation bundles automata for a domain and its relations, and
decides first-order statements about the presented structure:
from autstr.buildin.presentations import BuechiArithmeticZ
Z = BuechiArithmeticZ() # (ℤ, +, <, |) as automata
Z.check('all x.(exists y.(A(x,y,x)))') # ∀x ∃y: x+y=x — True
Z.check('exists x.(all y.(Lt(x,y)))') # a least integer? — False
Because the first-order theory of an automatic structure is decidable, check
always terminates with a definite answer — a theorem prover for the fragment of
mathematics these structures capture. evaluate goes further and returns the
automaton of all satisfying assignments, which you can enumerate or reuse. The
arithmetic & algebra notebook walks
through Presburger and Büchi arithmetic in full.
Computer algebra over infinite domains#
Structures need not be finitely generated. The localizations ℤ[1/p] — the rationals whose denominator is a power of p — are infinite, non-finitely-generated groups, yet each has an exact automatic presentation:
from autstr.algebra import z1p_localization
z2 = z1p_localization(2) # (ℤ[1/2], +)
x = z2.from_fraction(1, 2)
y = z2.from_fraction(3, 4)
z2.check('A(x,y,z)', x=x, y=y, z=z2.add(x, y)) # 1/2 + 3/4 = 5/4 — True
z2.check('all x.(exists y.(A(y,y,x)))') # 2-divisible? — True
z2.check('all x.(exists y.(exists w.(A(y,y,w) and A(w,y,x))))') # 3-divisible? — False
The first-order divisibility theory even distinguishes the localizations: every element of ℤ[1/2] is 2-divisible but not 3-divisible, and vice versa for ℤ[1/3].
Uniformly automatic classes: one automaton for a whole family#
A uniformly automatic class presents not one structure but an entire family, by giving every automaton one extra tape that reads an advice string synchronously with the elements. Fixing the advice instantiates one member; a query is compiled once for the class and then decides any member by running its advice word through the resulting automaton.
autstr.graphs, autstr.algebra, and autstr.groups ship ready-made classes:
# Finite abelian groups — advice is the cyclic decomposition
from autstr.groups import FiniteAbelianGroups
ab = FiniteAbelianGroups()
ab.check('A(x,y,z)', [2, 3], x=(1, 1), y=(1, 2), z=(0, 0)) # (1,1)+(1,2)=(0,0) in Z2⊕Z3
# Non-abelian groups — dihedral, quaternion, semidihedral, modular, ...
from autstr.groups import IndexTwoCyclicGroups
G = IndexTwoCyclicGroups()
G.check('M(x,y,z)', G.dicyclic(4), x=(0, 1), y=(1, 0), z=(1, 1)) # i·j = k in Q₈
# Extraspecial p-groups — nilpotency class 2, order p^(1+2n)
from autstr.groups import ExtraspecialGroups
H = ExtraspecialGroups(3)
H.check('M(x,y,z)', 2, x=(1, (0, 0), (0, 0)), y=(0, (1, 0), (0, 0)),
z=(1, (1, 0), (0, 0)))
The generic machinery in autstr.uniform turns any advice-indexed family of
automata into a class with relativized query evaluation, sentence checking, member
instantiation (get_structure), and a first-order define for bootstrapping
complex relations from primitives.
The same machinery runs over trees rather than words. Where an automatic
presentation encodes elements as strings and a word automaton reads them, a
tree-automatic presentation encodes them as finite trees read by a bottom-up
tree automaton — exactly the step from Büchi’s theorem to Rabin’s.
autstr.tree_uniform hosts the classes whose advice is naturally a tree: a tree
decomposition (bounded tree-width) or a k-expression (bounded clique-width), and
Skolem arithmetic (ℕ, ·) in autstr.buildin.tree_presentations, where a number
is the tree of its prime exponents.
The graphs and groups notebooks build these classes and query them; the arithmetic & algebra notebook covers the Boolean algebras, ℤ[1/p], and the MSO0 finite-powerset structure.
Composing presentations#
Automatic structures over a shared signature are closed under disjoint union and
direct products; uniformly automatic classes of finite structures are closed
under union and under taking all finite direct products of their members.
autstr.composition builds the new presentation for you.
from autstr.composition import (
class_union, direct_product_closure, blocks, tagged_advice,
)
from autstr.groups import ExtraspecialGroups, IndexTwoCyclicGroups
from autstr.uniform import UniformlyAutomaticClass
cyclic, extra = IndexTwoCyclicGroups(), ExtraspecialGroups(3)
def reduct(uniform): # the signature the two classes share
return UniformlyAutomaticClass(
{'U': uniform.class_automata['U'], 'M': uniform.class_automata['M']})
# Members of either family ...
both = class_union(reduct(cyclic.cls), reduct(extra.cls))
# ... and every finite direct product of them.
groups = direct_product_closure(both)
z4 = tagged_advice(cyclic.cyclic(4), '<l>') # Z4, abelian
heis = tagged_advice(extra.advice(1), '<r>') # extraspecial 3^(1+2)
abelian = 'all x.(all y.(all z.(M(x,y,z) -> M(y,x,z))))'
groups.check(abelian, blocks(z4, z4)) # True — Z4 × Z4
groups.check(abelian, blocks(z4, heis)) # False — one nonabelian factor
operation |
on |
construction |
|---|---|---|
|
structures |
tag each element with the side it came from |
|
structures |
|
|
structures |
|
|
classes |
tag the advice, so the advice languages are disjoint |
|
classes |
advice |
Two of these are worth a word. The direct product encodes a pair over the
pair alphabet, where a letter carries one letter of each factor; each factor is
then embedded by a variable renaming into its half of the bits, and the two
products are Boolean combinations of the embeddings. That is affordable only
because the pair alphabet has |Σ_A|·|Σ_B| letters but bits_A + bits_B
variables — letters multiply, bits add, which is precisely what the decision
diagrams buy.
The product closure concatenates advices with a separator. Since an element of
a finite member is never longer than its advice, the blocks line up across every
tape, so a relation of the product is the original relation holding in every block
— one automaton with one extra state, where an interleaved encoding would need
one copy per component. FiniteAbelianGroups is this construction applied to the
cyclic groups, and it predates the module. The
composition notebook walks through all five
operations.
Bounded rank-width: groups and graphs from one linear algebra#
The width notions above bound how much combinatorial structure crosses a cut.
Rank-width bounds the linear-algebraic rank of what crosses, and one body of
machinery — interface bases, streamed basis changes, and a two-sided factorization
of the sibling block (autstr.chain_ring) — presents both groups and graphs of
bounded rank-width uniformly.
Class-2 groups (autstr.groups.CutRankGroups,
autstr.tree_groups.CutRankTreeGroups): a member is a central extension of
(ℤ/pᵈ)ⁿ by (ℤ/pᵈ)ᵏ given by commutator labels; the advice spells out, cut by cut, a
rank-≤r factorization of the crossing block of the commutation form, and the
multiplication automaton carries r linear functionals instead of the digits it has
read. With d = 1 this is the field F_p; with d > 1 the center has exponent pᵈ
and the width is measured over the chain ring ℤ/pᵈ. Bounded pathwidth, bounded
vertex cover, the extraspecial matching and the complete graph are all special
layouts; the tree class recovers the word class on spine layouts.
autstr.cocycle_groups.CocycleRankWidthGroups generalizes further to distributed
centers — central generators scattered through the layout — via a six-register
protocol whose advice is microcode.
When a flat advice alphabet would be astronomical (it grows like q^(r²+r+kr)), the classes switch to factored letters: one letter per ring entry of the factorization, streamed through an accumulator — the advice alphabet drops to q+1 letters, and width r ≥ 2 over ℤ/4 goes from unrepresentable to explicitly buildable this way.
Graphs (autstr.tree_graphs.RankWidthClass): the advice is a rank
decomposition annotated with the GF(2) factorization of its cuts; adjacency of two
vertices is a bilinear form applied to their r-bit interface vectors at the node
where their subtrees meet. Rank-width lower-bounds clique-width and stays bounded
on dense graphs (cliques have rank-width 1), and the class answers MSO queries like
every other graph class:
from autstr.tree_graphs import RankWidthClass, RankWidthGraph
rw = RankWidthClass(1)
two_col = ('exists a.(all x.(all y.((not E(x,y)) or '
'((Subset(x,a) and (not Subset(y,a))) or '
'(Subset(y,a) and (not Subset(x,a)))))))')
rw.check(two_col, RankWidthGraph.complete_bipartite(2, 3)) # True (rank-width 1)
rw.check(two_col, RankWidthGraph.clique(3)) # False (odd cycle)
The bounded-rank-width groups appear in the groups notebook and the graphs in the graphs notebook.
Implicit evaluation: members whose automata cannot be built#
For the heavy ring members the base multiplication automaton is already
infeasible to construct — an O(|Σ|⁴) product over a huge advice alphabet. The
autstr.implicit layer decides first-order formulas on such members anyway, by
never building anything: Boolean connectives keep composite states and step the
base automata on the fly, exists is an on-the-fly powerset over the (tiny)
element alphabet, not flips acceptance. Because the advice is fixed input, the
cost is driven by quantifier alternation, not by alphabet size.
from autstr.groups import CutRankGroups
G = CutRankGroups(2, d=3) # commutation forms over Z/8 — automata unbuildable
advice = G.advice(3, G.clique_form(3))
x = ((5,), (3, 1, 6))
# model checking, implicitly:
G.check_implicit('exists y.(M(x,y,u))', advice, x=x, u=G.identity(3)) # True
# the satisfying SET, implicitly: exact count without enumeration, lazy iteration
inv = G.evaluate_implicit('M(x,y,u)', advice, x=x, u=G.identity(3))
len(inv) # 1 — the unique inverse
next(iter(inv))['y'] # ... and here it is, as a (b, a) tuple
Every uniform class offers check_implicit (model checking) and evaluate_implicit
(the satisfying assignments of a formula with open free variables, with the exact
solution count computed by dynamic programming — no enumeration — and lazy
iteration). ImplicitClass / ImplicitTreeClass package functional atoms and an
element alphabet as a first-class fully implicit presentation. The
implicit-evaluation notebook also times the
implicit path against a compiled one.
How it works#
An automatic presentation encodes a countable structure so that its domain is a regular language and each relation is recognized by a synchronous multi-tape automaton reading its arguments letter-by-letter in lockstep. The foundational fact is that this recognizability is closed under first-order definability: Boolean combinations correspond to product automata, and quantifiers to projection followed by determinization. Consequently the first-order theory of any automatic structure is decidable, and every definable relation is again automatic.
A concrete encoding#
In the arithmetic package an integer is written sign-magnitude, least-significant
bit first: the first symbol is a sign bit (0 for non-negative, 1 for
negative), followed by the binary digits of the magnitude from the lowest bit
upward, with * padding the shorter arguments of a multi-tape relation so all
tapes advance in lockstep. Under this encoding a definable set is a genuinely small
automaton — here, for instance, is the recognizer for the integers greater than 1
that are divisible by neither 2 nor 3:
The start state consumes the sign bit; the remaining states scan the magnitude bits
from least to most significant. Because “greater than 1” and “not divisible by 2 or
3” are both regular properties of that bit stream, a handful of states suffice —
doubled circles are accepting, and * marks the padding that ends the word.
Advice and trees#
Allowing the automata to read an additional fixed advice word widens the reach to structures like (ℚ, +) and, using a set of advices, to whole parameterized classes of finite structures. Deciding the first-order theory of a uniformly automatic class reduces to the monadic second-order theory of its advice language (Abu Zaid–Grädel–Reinhardt 2017; Abu Zaid 2018). The same programme runs over finite trees read by bottom-up tree automata, which reaches structures no string encoding captures naturally — (ℕ, ·) with a number written as the tree of its prime exponents, and classes whose advice is inherently a tree.
Why it is fast — and where it is hard#
Evaluating a fixed formula on a structure is one linear pass of its advice through the query automaton, so on any class of bounded width every fixed MSO property is decided in linear time — a constructive, streaming form of Courcelle’s theorem. The cost lives entirely in compiling the automaton.
A transition is not a symbol -> target table but a decision diagram over the
symbol’s digits, hash-consed and shared across states and automata
(autstr.mtbdd) — the representation MONA uses, for the same reason. A transition
that ignores a tape never tests that tape’s variables, so cylindrification is a
variable renaming rather than a duplication of every row once per letter of every
new tape, complementation touches no diagram at all, and the alphabet’s width
stops driving the cost. What remains is the subset explosion of determinizing an
existential quantifier: element quantifiers are cheap, and set quantifiers (MSO
proper) determinize over subsets of the intermediate automaton’s states.
Connectedness and bipartiteness compile in seconds; 3-colourability — the minimal
NP-hard MSO query — is a genuinely large one-time compile. Once compiled, an
automaton can be serialized (diagrams and all) and reused forever.
Around the diagrams the engine is batched NumPy: frontier-batched constructions, hashed partition refinement, and a subset construction that runs in a collectable scratch store. JAX is an optional accelerator used only for bulk word processing.