From 3a2e0e13b8169044541d4edd47f9b09f7570f554 Mon Sep 17 00:00:00 2001 From: Faried Abu Zaid Date: Thu, 13 Aug 2026 20:35:34 +0200 Subject: [PATCH 1/9] Every structure declares its own vocabulary; dissolve `buildin` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes that turn out to be one. `.symbolic()` could be called with no arguments on nine of the library's structures, because each declares a `default_signature` naming its operators and the codec that turns Python values into elements. It could not on Büchi arithmetic or MSO0: those were built by plain functions returning a bare presentation, with nowhere to hang a signature. `autstr.arithmetic` existed to fill that gap -- for ONE of the three, which is what made it look like a second, rival interface. So the signature moves onto the presentation, where the rest of the library keeps it, and the module that worked around its absence goes away. All three gain their codec, verified against the automata rather than assumed: - N: binary, least significant bit first (checked against A(x,y,z)), - Z: sign symbol then magnitude (as before), - MSO0: a set as a bitmask, position i set iff i is a member, in CANONICAL form -- the universe rejects trailing zeros, so {0} is `1` and the empty set is the empty word (checked against Subset over all subsets of {0,1,2}). MSO0 therefore gains an interface it never had: union, intersection and difference as `+`, `*`, `-`, and solutions that come back as Python sets. `buildin` was the other half of the same confusion: every structure the library ships is built in, so the name carved nothing. Its contents move to where their subject already lives -- `arithmetic` (Buechi N and Z), `powerset` (MSO0), `tree_arithmetic` (Skolem, matching algebra/tree_algebra), and the generic automaton constructors to `utils.automata_tools`, which was already importing `one` from across the package boundary. `CompiledPresentation` holds the part the presentations share. The precompiled `.autstr` artifacts go too. They saved 0.03s, 0.23s and 0.44s against building from scratch, and the built presentations are identical to the loaded ones relation by relation. Serialization stays fully supported; it just stops shipping artifacts nobody needed. BREAKING: `autstr.buildin.*` is gone, as are `autstr.arithmetic.integers` and `signature`; encode/decode are now classmethods on the presentations. Co-Authored-By: Claude Opus 5 --- .github/workflows/publish.yaml | 2 +- autstr/arithmetic.py | 398 ++++++++++--- autstr/buildin/__init__.py | 0 autstr/buildin/automata.py | 138 ----- autstr/buildin/bin/buechi.autstr | Bin 12816 -> 0 bytes autstr/buildin/bin/buechiZ.autstr | Bin 102128 -> 0 bytes autstr/buildin/bin/mso0.autstr | Bin 70412 -> 0 bytes autstr/buildin/presentations.py | 522 ------------------ autstr/infinite_graphs.py | 8 +- autstr/ordinals.py | 2 +- autstr/powerset.py | 319 +++++++++++ autstr/presentations.py | 25 +- autstr/symbolic/backends.py | 2 +- autstr/tree_algebra.py | 2 +- ...ee_presentations.py => tree_arithmetic.py} | 23 +- autstr/uniform.py | 5 +- autstr/utils/automata_tools.py | 209 ++++++- autstr/utils/tree_automata_tools.py | 2 +- docs/source/overview.md | 22 +- notebooks/arithmetic_and_algebra.ipynb | 16 +- pyproject.toml | 2 - scripts/gen_builtin_presentations.py | 43 -- scripts/gen_readme_media.py | 4 +- tests/test_arithmetic.py | 83 ++- tests/test_infinite_graphs.py | 6 +- tests/test_interpretations.py | 6 +- tests/test_ordinals.py | 2 +- tests/test_presentations.py | 7 +- tests/test_serialization.py | 2 +- tests/test_skolem.py | 2 +- tests/test_symbolic.py | 12 +- tests/test_symbolic_tree.py | 2 +- tests/test_tree_interpretations.py | 2 +- tests/test_tree_presentations.py | 2 +- 34 files changed, 1034 insertions(+), 836 deletions(-) delete mode 100644 autstr/buildin/__init__.py delete mode 100644 autstr/buildin/automata.py delete mode 100644 autstr/buildin/bin/buechi.autstr delete mode 100644 autstr/buildin/bin/buechiZ.autstr delete mode 100644 autstr/buildin/bin/mso0.autstr delete mode 100644 autstr/buildin/presentations.py create mode 100644 autstr/powerset.py rename autstr/{buildin/tree_presentations.py => tree_arithmetic.py} (91%) delete mode 100644 scripts/gen_builtin_presentations.py diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 40e5592..3e59519 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -63,7 +63,7 @@ jobs: /tmp/smoke/bin/pip install --quiet dist/*.whl /tmp/smoke/bin/python -c " import autstr - from autstr.buildin.presentations import BuechiArithmeticZ + from autstr.arithmetic import BuechiArithmeticZ assert BuechiArithmeticZ().check('exists x.(A(x,x,x))') print('smoke test passed for', autstr.__version__) " diff --git a/autstr/arithmetic.py b/autstr/arithmetic.py index d33a0f9..6f67499 100644 --- a/autstr/arithmetic.py +++ b/autstr/arithmetic.py @@ -1,85 +1,335 @@ -"""Büchi arithmetic over the integers, as a symbolic structure. +"""Büchi arithmetic, over the naturals and over the integers. -:math:`(\\mathbb{Z}, +, <, \\mid_2)` presented in base 2, with the generic -symbolic interface of `autstr.symbolic` on top: build terms and formulas with -Python operators, then evaluate, model check, test for emptiness or -finiteness, and enumerate solutions. +:math:`(\\mathbb{N}, +, <, \\mid_2)` and :math:`(\\mathbb{Z}, +, <, \\mid_2)` +presented in base 2, where ``x \\mid_2 y`` says that `y` is a power of two +dividing `x`. Adding that predicate to Presburger arithmetic is what makes the +structure *Büchi* arithmetic: it can talk about the binary expansion of a +number, and it remains decidable. - >>> Z = integers() + >>> Z = BuechiArithmeticZ().symbolic() >>> x, y, z = Z.vars("x y z") - >>> phi = (x + y).eq(z) & z.lt(100) - >>> phi.check() + >>> ((x + y).eq(z) & z.lt(100)).check() True - >>> phi.evaluate().contains(x=3, y=4, z=7) - True - -Integers are written directly wherever a term is expected -- ``x + 5``, -``x.lt(100)`` -- and solutions come back as Python integers. -The signature is that of the underlying presentation: ``A`` (addition graph), -``Lt``/``Gt``/``Eq``, ``Neg`` (the graph of negation), ``N0`` (non-negative), -``Z`` (zero), ``Pt`` (powers of two) and ``B``, where ``B(x, y)`` holds iff -``y`` is a power of two dividing ``x``. Anything the operators do not cover is -reachable through `SymbolicContext.rel`. +Both presentations are compiled from a handful of small automata the first time +they are constructed; the derived relations (order, equality, negation) are +first-order definitions over those. """ -from __future__ import annotations - -from functools import lru_cache from typing import List -from autstr.buildin.presentations import BuechiArithmeticZ -from autstr.symbolic import FunctionCodec, Signature, SymbolicContext - -#: Base-2 encoding: a sign symbol followed by the magnitude, least significant -#: bit first. This is the convention of `lsbf_Z_automaton`. -SIGN_POSITIVE = '0' -SIGN_NEGATIVE = '1' -PADDING = '*' - - -def encode(n: int) -> List[str]: - """The word encoding an integer: sign symbol, then magnitude bits LSB - first.""" - sign = SIGN_POSITIVE if n >= 0 else SIGN_NEGATIVE - return [sign] + list(format(abs(n), 'b')[::-1]) - - -def decode(word) -> int: - """The integer encoded by a word, ignoring padding.""" - word = ''.join(word).replace(PADDING, '') - if not word: - raise ValueError("empty encoding") - magnitude = int(word[1:][::-1] or '0', base=2) - return magnitude if word[0] == SIGN_POSITIVE else -magnitude - - -def signature() -> Signature: - """The signature of Büchi arithmetic: ``+`` and unary ``-`` as functions, - the order and equality as relational methods, and the integer codec.""" - sig = Signature(codec=FunctionCodec(encode, decode)) - sig.function('+', graph='A', out=2) - sig.function('neg', graph='Neg', out=1) - sig.operator('+', '+') - sig.operator('-', 'neg') - sig.operator('eq', 'Eq') - sig.operator('lt', 'Lt') - sig.operator('gt', 'Gt') - # B(x, y): y is a power of two dividing x. Spelled out as a method rather - # than bound to `|`, which on formulas already means union. - sig.operator('divided_by_power', 'B') - return sig - - -@lru_cache(maxsize=1) -def integers() -> SymbolicContext: - """The symbolic interface to Büchi arithmetic over :math:`\\mathbb{Z}`. - - The presentation is built once and shared; expressions are immutable, so - sharing it is safe and avoids rebuilding the base automata per term. +import itertools as it + +from autstr.presentations import ( + AutomaticPresentation, CompiledPresentation, +) +from autstr.utils.automata_tools import create_sparse_dfa + + +def _build_buechi_arithmetic() -> AutomaticPresentation: + """Büchi arithmetic over the natural numbers, compiled from scratch.""" + # Universe automaton - only accepts valid binary numbers (no '*') + universe = create_sparse_dfa( + states={'i', '0', '0+', '1', '*'}, + input_symbols={('0',), ('1',), ('*',)}, + transitions={ + 'i': {('0',): '0', ('1',): '1', ('*',): '*'}, + '0': {('0',): '0+', ('1',): '1', ('*',): '*'}, + '0+': {('0',): '0+', ('1',): '1', ('*',): '*'}, + '1': {('0',): '0+', ('1',): '1', ('*',): '*'}, + '*': {('0',): '*', ('1',): '*', ('*',): '*'}, + }, + initial_state='i', + final_states={'0', '1'} + ) + + addition = create_sparse_dfa( + states={0, 1, 2}, + input_symbols={('0', '0', '0'), ('0', '0', '1'), ('0', '0', '*'), ('0', '1', '0'), ('0', '1', '1'), + ('0', '1', '*'), ('0', '*', '0'), ('0', '*', '1'), ('0', '*', '*'), + ('1', '0', '0'), ('1', '0', '1'), ('1', '0', '*'), ('1', '1', '0'), ('1', '1', '1'), + ('1', '1', '*'), ('1', '*', '0'), ('1', '*', '1'), ('1', '*', '*'), + ('*', '0', '0'), ('*', '0', '1'), ('*', '0', '*'), ('*', '1', '0'), ('*', '1', '1'), + ('*', '1', '*'), ('*', '*', '0'), ('*', '*', '1'), ('*', '*', '*'), + }, + transitions={ + 0: { + ('0', '0', '0'): 0, ('0', '0', '1'): 2, ('0', '0', '*'): 2, ('0', '1', '0'): 2, ('0', '1', '1'): 0, + ('0', '1', '*'): 2, ('0', '*', '0'): 0, ('0', '*', '1'): 2, ('0', '*', '*'): 2, + ('1', '0', '0'): 2, ('1', '0', '1'): 0, ('1', '0', '*'): 2, ('1', '1', '0'): 1, ('1', '1', '1'): 2, + ('1', '1', '*'): 2, ('1', '*', '0'): 2, ('1', '*', '1'): 0, ('1', '*', '*'): 2, + ('*', '0', '0'): 0, ('*', '0', '1'): 2, ('*', '0', '*'): 2, ('*', '1', '0'): 2, ('*', '1', '1'): 0, + ('*', '1', '*'): 2, ('*', '*', '0'): 2, ('*', '*', '1'): 2, ('*', '*', '*'): 2, + }, + 1: { + ('0', '0', '0'): 2, ('0', '0', '1'): 0, ('0', '0', '*'): 2, ('0', '1', '0'): 1, ('0', '1', '1'): 2, + ('0', '1', '*'): 2, ('0', '*', '0'): 2, ('0', '*', '1'): 0, ('0', '*', '*'): 2, + ('1', '0', '0'): 1, ('1', '0', '1'): 2, ('1', '0', '*'): 2, ('1', '1', '0'): 2, ('1', '1', '1'): 1, + ('1', '1', '*'): 2, ('1', '*', '0'): 1, ('1', '*', '1'): 2, ('1', '*', '*'): 2, + ('*', '0', '0'): 2, ('*', '0', '1'): 0, ('*', '0', '*'): 2, ('*', '1', '0'): 1, ('*', '1', '1'): 2, + ('*', '1', '*'): 2, ('*', '*', '0'): 2, ('*', '*', '1'): 0, ('*', '*', '*'): 2, + }, + 2: { + ('0', '0', '0'): 2, ('0', '0', '1'): 2, ('0', '0', '*'): 2, ('0', '1', '0'): 2, ('0', '1', '1'): 2, + ('0', '1', '*'): 2, ('0', '*', '0'): 2, ('0', '*', '1'): 2, ('0', '*', '*'): 2, + ('1', '0', '0'): 2, ('1', '0', '1'): 2, ('1', '0', '*'): 2, ('1', '1', '0'): 2, ('1', '1', '1'): 2, + ('1', '1', '*'): 2, ('1', '*', '0'): 2, ('1', '*', '1'): 2, ('1', '*', '*'): 2, + ('*', '0', '0'): 2, ('*', '0', '1'): 2, ('*', '0', '*'): 2, ('*', '1', '0'): 2, ('*', '1', '1'): 2, + ('*', '1', '*'): 2, ('*', '*', '0'): 2, ('*', '*', '1'): 2, ('*', '*', '*'): 2, + }, + }, + initial_state=0, + final_states={0} + ) + + input_symbols = {a for a in it.product(['0', '1', '*'], repeat=2)} + weak_div = create_sparse_dfa( + states={'0', '1', 'e'}, + input_symbols=input_symbols, + transitions={ + '0': { + a: '0' if a == ('0', '0') else '1' if a == ('0', '1') or a == ('1', '1') else 'e' for a in input_symbols + }, + '1': { + a: 'e' if a[1] != '*' else '1' for a in input_symbols + }, + 'e': {a: 'e' for a in input_symbols} + }, + initial_state='0', + final_states={'1'} + ) + + + # Create presentation + presentation = AutomaticPresentation({'U': universe, 'A': addition, 'B': weak_div}) + + # Add bootstrap remaining relations + presentation.update(Z='A(x,x,x)') + presentation.update(Eq='exists z.(Z(z) and A(x,z,y))') + presentation.update(Pt='B(x,x)') + presentation.update(Lt='exists z.(not Z(z) and A(x, z, y))') + presentation.update(Gt='exists z.(not Z(z) and A(y, z, x))') + + return presentation + +def _build_buechi_arithmetic_Z() -> AutomaticPresentation: + """Büchi arithmetic over the integers, compiled from scratch.""" + # Universe automaton + universe_states = ['-1', 'i+', 'i', '0', '0+', '1', '*'] + universe_symbols = {('0',), ('1',), ('*',)} + universe_trans = { + '-1': {('0',): 'i', ('1',): 'i+', ('*',): '*'}, + 'i+': {('0',): '0+', ('1',): '1', ('*',): '*'}, + 'i': {('0',): '0', ('1',): '1', ('*',): '*'}, + '0': {('0',): '0+', ('1',): '1', ('*',): '*'}, + '0+': {('0',): '0+', ('1',): '1', ('*',): '*'}, + '1': {('0',): '0+', ('1',): '1', ('*',): '*'}, + '*': {('0',): '*', ('1',): '*', ('*',): '*'}, + } + universe = create_sparse_dfa( + universe_states, universe_symbols, universe_trans, '-1', {'0', '1'} + ) + + # Addition automaton (intermediate) + add_states = [-1, 0, 1, 2] + add_symbols = set(it.product(['0', '1', '*'], repeat=3)) + add_trans = { + -1: {a: 0 if '*' not in a else 2 for a in add_symbols}, + 0: { + ('0','0','0'): 0, ('0','0','1'): 2, ('0','0','*'): 2, + ('0','1','0'): 2, ('0','1','1'): 0, ('0','1','*'): 2, + ('0','*','0'): 0, ('0','*','1'): 2, ('0','*','*'): 2, + ('1','0','0'): 2, ('1','0','1'): 0, ('1','0','*'): 2, + ('1','1','0'): 1, ('1','1','1'): 2, ('1','1','*'): 2, + ('1','*','0'): 2, ('1','*','1'): 0, ('1','*','*'): 2, + ('*','0','0'): 0, ('*','0','1'): 2, ('*','0','*'): 2, + ('*','1','0'): 2, ('*','1','1'): 0, ('*','1','*'): 2, + ('*','*','0'): 2, ('*','*','1'): 2, ('*','*','*'): 2, + }, + 1: { + ('0','0','0'): 2, ('0','0','1'): 0, ('0','0','*'): 2, + ('0','1','0'): 1, ('0','1','1'): 2, ('0','1','*'): 2, + ('0','*','0'): 2, ('0','*','1'): 0, ('0','*','*'): 2, + ('1','0','0'): 1, ('1','0','1'): 2, ('1','0','*'): 2, + ('1','1','0'): 2, ('1','1','1'): 1, ('1','1','*'): 2, + ('1','*','0'): 1, ('1','*','1'): 2, ('1','*','*'): 2, + ('*','0','0'): 2, ('*','0','1'): 0, ('*','0','*'): 2, + ('*','1','0'): 1, ('*','1','1'): 2, ('*','1','*'): 2, + ('*','*','0'): 2, ('*','*','1'): 0, ('*','*','*'): 2, + }, + 2: {s: 2 for s in add_symbols} + } + addition_intermediate = create_sparse_dfa(add_states, add_symbols, add_trans, -1, {0}) + + # Weak division automaton + div_states = ['-1', '0', '1', 'e'] + div_symbols = set(it.product(['0', '1', '*'], repeat=2)) + div_trans = { + '-1': {a: '0' if a[1] == '0' else 'e' for a in div_symbols}, + '0': { + a: '0' if a == ('0','0') + else '1' if a in {('0','1'), ('1','1')} + else 'e' for a in div_symbols + }, + '1': { + a: 'e' if a[1] != '*' else '1' for a in div_symbols + }, + 'e': {a: 'e' for a in div_symbols} + } + weak_div = create_sparse_dfa(div_states, div_symbols, div_trans, '-1', {'1'}) + + # N0 automaton + n0_states = [-1, 0, 1] + n0_symbols = {('0',), ('1',), ('*',)} + n0_trans = { + -1: {('0',): 1, ('1',): 0, ('*',): 0}, + 0: {s: 0 for s in n0_symbols}, + 1: {s: 1 for s in n0_symbols} + } + N0 = create_sparse_dfa(n0_states, n0_symbols, n0_trans, -1, {1}) + + # Create presentation + presentation = AutomaticPresentation({ + 'U': universe, + 'A0': addition_intermediate, + 'B': weak_div, + 'N0': N0 + }) + presentation.update(Z='A0(x,x,x)') + + # Define addition formula + c000 = '(N0(x) and N0(y) and N0(z) and A0(x, y, z))' + c001 = '(N0(x) and N0(y) and not N0(z) and exists a z0.(Z(z0) and A0(x,y,a) and A0(a,z,z0)))' + c010 = '(N0(x) and not N0(y) and N0(z) and A0(z, y, x))' + c011 = '(N0(x) and not N0(y) and not N0(z) and A0(z, x, y))' + c100 = '(not N0(x) and N0(y) and N0(z) and A0(x, z, y))' + c101 = '(not N0(x) and N0(y) and not N0(z) and A0(z, y, x))' + c110 = '(not N0(x) and not N0(y) and N0(z) and exists a z0.(Z(z0) and A0(x,y,a) and A0(a,z,z0)))' + c111 = '(not N0(x) and not N0(y) and not N0(z) and A0(x,y,z))' + phi_A = ' or '.join([c000, c001, c010, c011, c100, c101, c110, c111]) + presentation.update(A=phi_A) + + presentation.update(Eq='exists z.(Z(z) and A(x,z,y))') + presentation.update(Pt='B(x,x) and N0(x)') + presentation.update(Lt='exists z.(N0(z) and not Z(z) and A(x, z, y))') + presentation.update(Gt='exists z.(N0(z) and not Z(z) and A(y, z, x))') + presentation.update(Neg='exists z.(Z(z) and A(x,y,z))') + + # Delete auxiliary relation + del presentation.automata['A0'] + + return presentation + + +# -------------------------------------------------------------------------- +# The presentations. Each declares its own vocabulary, so `symbolic()` takes +# no argument; see `CompiledPresentation`. +# -------------------------------------------------------------------------- + +class BuechiArithmetic(CompiledPresentation): + """Büchi arithmetic over the natural numbers: :math:`(\\mathbb{N}, +, <, + \\mid_2)` in base 2. + + >>> N = BuechiArithmetic() + >>> x, y = N.symbolic().vars("x y") + >>> ((x + y).eq(12) & x.lt(y)).check() + True + + ``B(x, y)`` holds iff `y` is a power of two dividing `x`; it is spelled + ``.divided_by_power`` rather than bound to ``|``, which on formulas already + means union. """ - return BuechiArithmeticZ().symbolic(signature()) + + _BUILD = staticmethod(_build_buechi_arithmetic) + + #: base-2 encoding: the magnitude, least significant bit first + PADDING = '*' + + @staticmethod + def encode(n: int) -> List[str]: + """The word encoding a natural number: binary, least significant bit + first.""" + if n < 0: + raise ValueError(f"not a natural number: {n}") + return list(format(n, 'b')[::-1]) + + @staticmethod + def decode(word) -> int: + """The natural number encoded by a word, ignoring padding.""" + digits = ''.join(word).replace(BuechiArithmetic.PADDING, '') + return int(digits[::-1] or '0', base=2) + + def default_signature(self): + """``+`` as addition, with the order, equality and divisibility as + methods, and naturals written as Python integers.""" + from autstr.symbolic import FunctionCodec, Signature + signature = Signature(codec=FunctionCodec(self.encode, self.decode)) + signature.function('+', graph='A', out=2) + signature.operator('+', '+') + signature.operator('eq', 'Eq') + signature.operator('lt', 'Lt') + signature.operator('gt', 'Gt') + signature.operator('divided_by_power', 'B') + return signature + + +class BuechiArithmeticZ(CompiledPresentation): + """Büchi arithmetic over the integers: :math:`(\\mathbb{Z}, +, <, \\mid_2)` + in base 2. + + >>> Z = BuechiArithmeticZ() + >>> x, y, z = Z.symbolic().vars("x y z") + >>> ((x + y).eq(z) & z.lt(100)).check() + True + >>> (3, 4, 7) in (x + y).eq(z) + True + + Integers are written directly wherever a term is expected -- ``x + 5``, + ``x.lt(100)`` -- and solutions come back as Python integers. Anything the + operators do not cover is reachable through `SymbolicContext.rel`. + """ + + _BUILD = staticmethod(_build_buechi_arithmetic_Z) + + #: base-2 encoding: a sign symbol, then the magnitude, least significant + #: bit first + SIGN_POSITIVE = '0' + SIGN_NEGATIVE = '1' + PADDING = '*' + + @staticmethod + def encode(n: int) -> List[str]: + """The word encoding an integer: sign symbol, then magnitude bits least + significant first.""" + sign = (BuechiArithmeticZ.SIGN_POSITIVE if n >= 0 + else BuechiArithmeticZ.SIGN_NEGATIVE) + return [sign] + list(format(abs(n), 'b')[::-1]) + + @staticmethod + def decode(word) -> int: + """The integer encoded by a word, ignoring padding.""" + word = ''.join(word).replace(BuechiArithmeticZ.PADDING, '') + if not word: + raise ValueError("empty encoding") + magnitude = int(word[1:][::-1] or '0', base=2) + return (magnitude if word[0] == BuechiArithmeticZ.SIGN_POSITIVE + else -magnitude) + + def default_signature(self): + """``+`` as addition and unary ``-`` as negation, with the order, + equality and divisibility as methods, and integers written as Python + integers.""" + from autstr.symbolic import FunctionCodec, Signature + signature = Signature(codec=FunctionCodec(self.encode, self.decode)) + signature.function('+', graph='A', out=2) + signature.function('neg', graph='Neg', out=1) + signature.operator('+', '+') + signature.operator('-', 'neg') + signature.operator('eq', 'Eq') + signature.operator('lt', 'Lt') + signature.operator('gt', 'Gt') + # B(x, y): y is a power of two dividing x. Spelled out as a method + # rather than bound to `|`, which on formulas already means union. + signature.operator('divided_by_power', 'B') + return signature -def variables(names) -> tuple: - """Shorthand for ``integers().vars(names)``.""" - return integers().vars(names) diff --git a/autstr/buildin/__init__.py b/autstr/buildin/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/autstr/buildin/automata.py b/autstr/buildin/automata.py deleted file mode 100644 index f0844f6..0000000 --- a/autstr/buildin/automata.py +++ /dev/null @@ -1,138 +0,0 @@ -import itertools as it -from typing import Optional, Set -from autstr.sparse_automata import SparseDFA -import numpy as np - -from autstr.utils.misc import encode_symbol - -def length_automaton(n: int, base_alphabet: Set[int]) -> SparseDFA: - """ - Creates an automaton that recognizes all words over base_alphabet with length exactly n. - - :param n: The exact word length - :param base_alphabet: Set of integer symbols - :return: SparseDFA recognizing words of length n - """ - # States: 0 (start), 1, 2, ..., n (accepting), n+1 (dead) - num_states = n + 2 - # Default transitions: move to next state or dead state - default_states = np.array([i + 1 for i in range(n)] + [n + 1, n + 1], dtype=np.int32) - # No exceptions needed (same behavior for all symbols) - exception_symbols = np.full((num_states, 0), -1, dtype=np.int32) - exception_states = np.full((num_states, 0), -1, dtype=np.int32) - # Only state n is accepting - is_accepting = np.array([False] * n + [True, False]) - start_state = 0 - - return SparseDFA( - num_states=num_states, - default_states=default_states, - exception_symbols=exception_symbols, - exception_states=exception_states, - is_accepting=is_accepting, - start_state=start_state, - symbol_arity=1, - base_alphabet=base_alphabet - ) - -def k_longer_automaton(k: int, r: int, base_alphabet: Set[int], padding_symbol: int) -> SparseDFA: - """ - Creates an automaton recognizing (r+1)-tuples where the last word is at least k letters - longer than the other r words. - - :param k: Minimal length difference - :param r: Number of reference words - :param base_alphabet: Set of integer symbols - :param padding_symbol: Padding symbol integer - :return: SparseDFA for the k-longer condition - """ - # State mapping: [-1, 0, 1, ..., k] -> [0, 1, 2, ..., k+1] - state_mapping = {s: i for i, s in enumerate(range(-1, k+1))} - num_states = len(state_mapping) - sorted_alphabet = sorted(base_alphabet) - arity = r + 1 - - # Precompute all symbol tuples and their encodings - symbol_tuples = list(it.product(sorted_alphabet, repeat=arity)) - symbol_encodings = [encode_symbol(t, base_alphabet) for t in symbol_tuples] - - # Initialize DFA components - default_states = np.full(num_states, state_mapping[-1], dtype=np.int32) # Default to dead state - exception_list = [[] for _ in range(num_states)] - - # Build transitions - for state in range(-1, k+1): - state_idx = state_mapping[state] - for t, enc in zip(symbol_tuples, symbol_encodings): - # Compute next state - if state == -1: - next_state = -1 # Stay in dead state - else: - if all(x == padding_symbol for x in t[:-1]) and t[-1] != padding_symbol: - next_state = min(state + 1, k) # Count extra length - elif t[-1] == padding_symbol: - next_state = -1 # Reject if padding the last word - elif state == 0: - next_state = 0 # Wait for other words to end - else: - next_state = -1 # Reject otherwise - - next_state_idx = state_mapping[next_state] - if next_state_idx != state_mapping[-1]: - exception_list[state_idx].append((enc, next_state_idx)) - - # Find max exceptions needed - max_exceptions = max(len(ex_list) for ex_list in exception_list) if exception_list else 0 - - # Build exception arrays - exception_symbols = np.full((num_states, max_exceptions), -1, dtype=np.int32) - exception_states = np.full((num_states, max_exceptions), -1, dtype=np.int32) - - for i, ex_list in enumerate(exception_list): - if ex_list: - syms, states = zip(*ex_list) - exception_symbols[i, :len(syms)] = syms - exception_states[i, :len(states)] = states - - # Final states: state k (meaning we've counted k extra symbols) - is_accepting = np.array([i == state_mapping[k] for i in range(num_states)]) - - return SparseDFA( - num_states=num_states, - default_states=default_states, - exception_symbols=exception_symbols, - exception_states=exception_states, - is_accepting=is_accepting, - start_state=state_mapping[0], - symbol_arity=arity, - base_alphabet=base_alphabet - ) - - -def zero(symbol_arity: int = 1, base_alphabet: Optional[Set[int]] = None) -> SparseDFA: - """Automaton that rejects all inputs.""" - base_alphabet = base_alphabet or {0} - return SparseDFA( - num_states=1, - default_states=np.array([0], dtype=np.int32), - exception_symbols=np.full((1, 0), -1, dtype=np.int32), - exception_states=np.full((1, 0), -1, dtype=np.int32), - is_accepting=np.array([False]), - start_state=0, - symbol_arity=symbol_arity, - base_alphabet=base_alphabet - ) - -def one(symbol_arity: int = 1, base_alphabet: Optional[Set[int]] = None) -> SparseDFA: - """Automaton that accepts all inputs.""" - base_alphabet = base_alphabet or {0} - return SparseDFA( - num_states=1, - default_states=np.array([0], dtype=np.int32), - exception_symbols=np.full((1, 0), -1, dtype=np.int32), - exception_states=np.full((1, 0), -1, dtype=np.int32), - is_accepting=np.array([True]), - start_state=0, - symbol_arity=symbol_arity, - base_alphabet=base_alphabet - ) \ No newline at end of file diff --git a/autstr/buildin/bin/buechi.autstr b/autstr/buildin/bin/buechi.autstr deleted file mode 100644 index 24326b354d965a453e411ec7c2cd7b0d2dd9ef56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12816 zcmeI1!EO^V5QZ0?WR)YzA$B%R0;eJ&E*vTl7Z9qT4X6^N(3V40^?i5{UWhkfoHWko z;ORPevustQDxJ)Z$N$XyGj@{wgO{%!6h-mt;g2VUEs9FypQb3TdSBlkAD_%W9iCr) z`EdHV_qgmm>h;UsZ2s}|>}Ym)bUHsjIlq|Ak1ple#o71S^;Pe+h;Jvuez`a4m*au_ zAN5PyFGX7ZIb9{s$42zSs$ZJQKNCT${aY@hEGxZ@;ex3=aV{&dUi=q%xRM3WAG+yM zHx)eIqX-*s{Uc594MZR~>g~uSB7CWPzG1Gur62B3SyJK6sE;{ioU!MrI{Mkze+}WC zP-?%9HO9ySGnN`!BQM5P%sLu*w`LCkN?m=h-a<44)EINfz`H0jL6TdaIO8P|Opv-U z%q-0;%`CG)D{V*Gq;jljyCCWywl1LS0*2I#-$EIcC8@sen_X9P>Bh3>q+wqN^Vo18 zTb|{XV>_p+oHZOzI;h>?U~np?X0wJ8XglU)Hft@ggq z($BXX&>D^$pG~g~&c$2Ck)H-4>?T_QlHi1HI_4aIVT#xW>`_j?U{Z067`|cO1T+oa zU_a_}0?Jt%+_f1^7r=D^Luzu_U7AdWr24&Yc52_Lz1BvCf;J&U9znq|%2TVUx`&gC zEYl$E0tLHLN7P)#()MeE1+hH>bUZ);HpEu8p$qGSvxkHUj<=n(nF^zg9Hj1{GB(H$ z&&b{6iX`H0sd2Pn=f3k>{c#S(W%otm)7mc*hqBlXoZCv+X~q(}9dLA#vuwK{N#J-; z(423exmk|P?9dMbqfKfh`6-3$;q*`S6|+n6uf-Gd#}C=W%h+iX^AXb#Cw) zzlK}W$M#sZ&>EfVlQ(sJ;(oSqKjYYeyf})F35#W53RRhSb9zK;Lh9F%E@p>+8_SJ< zxFN)BZ-R5_d-km^f8uX3U)+~Wb|5ci!Y!B&CX;bc?!=H`0AvFP0BvGPjOjFAG20y7 zHv5ptT*vrmlZuP$DW|SawbL-i)=M1gP#nDvF6!uQCQ|f~J3uQpDEj4;B+wYW-i14g z%ywvVJSf@lg<%WEY-2jVScKSfCZW#umi4YqFY4>$%40^ZShBBmSINbVXLWz!#fKj< z)e1IMDe`mI^50}RmiZhVF^~ad81uk9voLJ46rVYmV{5m?81r!#b#2Do6hC5l)_lio zo7T0NL-t0~aisbq2KF#}p8ZUjAJ$m26rZX1QtdP>)KgCN+jTq6V68Sy#jWw92@IO= zHuBHBQD)xA?;K3xXZgn_ak9HBJ+HkhS;1%B?}?Qp%nSwL|Ep5;2Afl(gte?5@1ff9 zK`N2zq?_iGYZIVtWuoELFmC0%!^UmK-4s7!>7?&n@+7u*n>l1}#IUrXBL=NMf*Pzo zz{*1QIgb*}C$C>k$3~2liHrKC_y1S^cuiq^tPS-!ILQxfU)FdV4D?MOIe1goYB;GU Tk5c`{?3VW=db{bCch|oGP-eS+ diff --git a/autstr/buildin/bin/buechiZ.autstr b/autstr/buildin/bin/buechiZ.autstr deleted file mode 100644 index f121de438d4144b13a116945c49a50d90e6330a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102128 zcmeI5&2B756~~h|vAjfHOttN?ogGMs4T=zm4N-(3xu8gpKoS-R@d`WzPr`d3@dj*o z0P1wdC*_*+YyWGy+B5cTY3_A(b)C;Tb?Ve-|L|u&|HX$NeDJ}4|Mlzx(Rz&))s|v#-AV=8JE> zefQ;Of6ec{{rWHO{{FYiU*_Z|_t#e!`}?blyIuZ!dv%c?b87mfUAnqRH~Ar5Kji=J zt}cf0zsCam$3!kNI9Qd)I6Nn13jXkr-(25ZUEB=c{mtp?$8=_Cd{^dW9xU&nj-5jwZoKs{Y1RJ>)IItHHj%8jQ)=V{VI!WV>yC=;UG#GQ*Wp=yforlp8g21=&O zvUEN0lDc711LFtyNRRhYMOI z){+}Xnu#>h4pE8Xj{{Ybmk<-Gat3m(16DINsDadjLfQBr{2?l23J`_ZbEc}EYw}lQ ztk>jQuTjxrU_h3kjw^eYL(PcuDZTko@u3Nwh;O!w%&BCjTe(B?+WUJ~ z4!VMK*MqI;hCiE5i(gPDG++oD;>nq~J`-cI3Biwd&&>!9x0&<77OWp~uQ=T1M7q!J z&e<-z5FUXQ()XVdG@c3quAeZ|6!lox6Alt4sF>$_A_PCo6w7-tPaL8ObJi{&5!=_R zZ|~5D45BxkH-irhxwm{6#XX6eN~eI6((OyfWia~uVv^Qp~Q1TObyX2WOo9vimf3nvp-DGd#uS(^VM$y0fDk zlsLJ4w#wUD-UX}#7atwGm)%x$yW?+dk8A7G1*`-YA07Oy>c7&%n~`^Ixq8_{YkR5W zqQ~y_*vjYH`dJ&lE08o?<@fSqz3^1*`QzI7ozRqUHJ+-z)|S@=tOOSy9lV!*)|Pi| z{H_pDfxYxrk#jbB;cxYyYx8p^a=Dnj?5Y8stX=-C z>bX}vF7#t=2CaD3uJ^U^yWms8$-Py3J2!bxrRPcy{@9D3UhUc$s}jBVY;EtSqFaei zk7wI{&s=Y=PqJGtFeRSVb8Go?+b6R&|7+uKZPyh){wVFs%f_vibHi~8Q$mF&`)sx! zakD7VqrHSw9(V8JZbjVDzEo9Dwh@-qfCpeyig?hVSu1!qgvtt0M-lPv10GimEW%$?Igw z#0}YpZ4)jFdKxu(>qV->_uGrB7g&vU7P~4tJu-1>D7Pp%IsIFb{1z>OP$9c}GJ!ow zju2S2dy;%lb&gHkjyi?)VYj}AKh-cv+&g?heJbkuj`VOxdQh3aBef9oc6KaO7cy_x z>xU3%4|Zum2%$=KPii4n?nw$?djub_h(~it1VXHvnPZ*zaf5yj3F(#F@fyen1Mx}`_7YVH)XR%Jj3I-VShMZkcQ>+l90L`WiMCJZ^*4RFk9Cmlmd$wmU<8&!qq6fp=* z9EFPGlC-BN1i1-iG(GQ8HTEr_5RB^?<5IR^3``m1j-*%XEMaVl`nw}#s5BWfg&(Zu z`>aFXdLY%{y`o6OkyeR> zL29zAE2`4d#2wo{QU?4hhAj-3& ztV;|%2Mj$24AKXj6h#+2pk)uN9KQP(qt5}OPt}A6j770}aW-_oP;|gc6LB~?;M7^d zXl1}ylxu4F{o)vyq6RT8#n^4YKy<+NIkX3&0|uf4&V7y-1cjb-rlK$uMV(jfQO%Es5Nsyq#$yolC+5XN5Od<; z3-%_tcnC4$7yx2Ge$9c0M9hDOFPJ)tj*h^lwnHMuZ}FMV;5+yIV8e%YQ8xHNvL^y5rUX&4qu?9SO*Y7pkHg5+vlk(e0i!IV9)n=tYb#SB2=hh4tKEbxRD~qx`iTs4 z__NV}i0O_LfqubUOo$5gaJN(sB<^^tVI=POZM^6O`p5CpdJzQDCN-eq?nw=(I2+lA zsjxqTonz}%AiN2aLHPd1E?*+sW8t}lf!M1EXP3L#J_$;Ry=2x?`# zzAOY$y};;pLkuigWXYye8>)t=62^by>Qo4WjcK^8CiChX?b z+C-(F^4?mOx5yQLlL5v z27W`WCZRKFZfN5WJmjFcLG&~98Kc3qrE!QI3X(A7#eFx_GI8>t+05E8jYH^IK>QTr ziyJep&xefX%=U(Rgyw8LCuAVaEZXNFQ&3;RIF4*6WF%(_#Ks~7pD=t&c%mp_Kuc8G z`_F{&X2Ro1X~h~N$&xMJ(91Ait+9lsjL1e39xF9-o-InYauniB*w-fU|}hp9){E_PLmXBt8S$(aKlY;CVuqS20Nz*9rx z=b1u6N5L!{-wk+PJ!H}JO>S$znayL20z5)bBJFJP=@9astVJyfCHOo@4abl<(h2i( zW|&X-)&7LwY0CnY=Yt-k*_kDJkkvNYhf1Jj_+H9vXVtuQ$f`N(@I}|50QJYV=RTy~ zZ5r72(T5bsaPY(&hDx}uZbSW$cKcL+JB{`^;eG>D+btgoPD(#wZpjX9hBRy$DtvJS z5#$#qmu!$9Qn#x`!WTyOvo<~tF%bYx z=-9k(h{*=%;R{@H!%fAcL1K2i>m#I%5ZK`m?QsDpq=8mHUgi;kjO#by8`bVnFdI3C zn0fakK@bhf=k1}WR6d1+M|#FZACQom1rCi#l%>_6!jCt6#KPdYo@A#g0frjq$>qc| zJPBjMr96)pfrNTObHpP(<8>n;uEb}0Fw&N-BinPR1@$Z5A;%e`3g5fj%wk)%TuCi& zqa4p&*~YCCC<3q3>1_ht@tvEER^O8P2?Z@i4;s)(~;i;QOWsRbF1eKat7f8n!BtT#k0P$_P%RA(cP8 zJ4r>*yByyC)SXbCMGZru-h?)M@gNwt3U3OF6}IVR)62AfX$zR6Sv6d>-Wty9Y&bKA z%}>(;aAV_UVXxD|^5WZh#ele#?99gzas!~{IvGJ{MbKuWQ}LtIJvYj)AOc^vv(BP9h_H?u{E4!4QHDukhNCt>Ry%;_-&ptpZ-A z0yaHwdfp;3j>z8p0_Qdp0hQl35?{KGy!15Yx%>I0iwz8A9MUu10?Y%ZRuhg|`q7e0ZA=599UGgNYOK##{Jyq5K(3!rQTu zT{05>lHETPzrkzv5}th+TT+Y-4^iQ@t9FY|hzq|thj;$M*@#cCE-ruk=gW^SKCydm z?3Q8EleMAz=%l~fyExXBF`KMqc)gYCn$Gw)r|rUoF`fB2$jhzO z^6E>JGnzwf9p+3jd44ISi<=YFEF7>be3c``y#Jcr$rA|Dpnx0zD z5myPKUlsF5l^&`dm&L7nZ;Coic^w3rp_X|MrR8v6IIh=|gl`h$q3g5dNIji1uHhdI{ zW{Z&g!I_skGykC48%!ryosdM>gx$Ya3EZ)nX8@|A_GnWXKl}FCsSLw9jQ2Z^t1UL0 zVPC}a9-w3sgLE&Q3EN%w&Q6!vj1v2V`7o!-rvlE{apfI+ZTwD1Oh`>%i{wXCB%ex$ z`h(J%O3|piN{{%Zc%`+x%7 zgOlD`%P-W=v-}XB3+cL;E8%}ut~2M~_2Ytg&U~VmK6~kBAs!dMmt3U1Uif?AAwFFp zRG`W~Rr$5`&ms)Km7oGYeMmW#`mNb^BN)dDDR#O zM6DwhiyR1wh1;Z`OmLR+-H{^jbC>pk(h`HG(RbLlPQ6r_K*{k-qV{pq@P`nnE8FHB zq9Xb3>4jX;A-SA+-|R8)3!CJQ?Q^Fg`Fi)b7eaUZKzcxvc_0J8%CUuL=cG&^jq!`8 zWl2w?U5?)z^n|9;|P{y>QYG*1~DG4Ld8D!J~m}T@MGa${CZ=VG&Ys1rW zLnFZw@8#nNQw$_o&lqB|()g5+$c?Hh*KL(h;ni|fBjlDUcKlJL=Kxhgg(rT=FR6Gd zcKlI=XDcJmk6U+^wle3vN~oe>TYR#BTOki0Rf-{S&Q#&KRi0XxxC;$8)$_VIb?@z_ zNBVBZWM|?+Yaa06qIrP>tf~7{Q657gzQN5uFx*M8C!7F612?8qEPKf{H*%RV}&BG?!y_?mPY;yl~of6Ns zFj++Tzh{<~LWbk{xI7;9%rmdkX)2sf&1gye6Ee(1(^~qKGAJ0zjk|B zh~LE_d0X-K!sCop39q0kzk2jyXTp!Ow-P^RE3Y%=8kkSCmhYv{UU*2lSK5wToVQvp zE?|{d$sSydgui0r+48M+RpIx?c)laASsnSp8Fva(;z)D|Mgw13ZKGi?5~yC#c$)aY z-82rvnE)^1AYZ&9gmuNo!)HJ!(qa&dNU81d&i{m4ofsGCc!uHTwvWqU z$QGU(P#Zaa$xy|>ywRA2RmO)N0OO=SqF3BUOO~c=c9e-4LaDXa7&5b(U zrSw#IwH$8x#FmVAMo_C%S*bMZk1D)ej%)yxQ zqh!)JHADa>J5!Y5%psEuuSwemCmLuxj`$>fdyWJE`7!c;E2NY+mZyV%|5!s`6$n zSF!%|sKRgMwjOSc9&WR%&8{}PnwuKw+dt9_yW9?6X+P5YXgE!D%^eNLnTj1@~`t0 zEi>)23?+%dcFhiUM{E9TyD-PAMO~h9godkoWo13toojpSZ1vZ69*=)>+7@lbbY|!v zFVAOMUcJ*UqshNY!-qLj0nabRtYFJWFJFog7AP}wx)nc_C%P3m=d%@$16By>k@z7z zlu!6u%dahOE1$$qg~!FD@~wKV@cW~*?vNX}TFx1F3R6ObCwbkeRCuZUQhMayRlK|k zL>j_r0GzBR*`K)4QeR90yI<|%N{_0jj3G*TDqgBO!b>JZ6-szYUduGvE-qfSa)h3TSjBH4AWQh7 zlvGKpwa8nw$!_q>0dDV6c_B+sM~USBRrucJsD<>VLp^wJIvYNU)^ZIM-Sqa diff --git a/autstr/buildin/bin/mso0.autstr b/autstr/buildin/bin/mso0.autstr deleted file mode 100644 index c5274f1aa0ebcf9fdd26a0c283e51118084ae955..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70412 zcmeI5&u$#Y5ys_L2#`CWW2^&b|B&3=atI6qF%U%XDKG?^W()xi6^j%Q!|=Q1A@Vpm zqV`TRlD1RbN$CS6BD+%>L=Ge|!1avuDqK{QZAF|KIOEd$zLj zf9&61p8xCH@4tV2`@^fdpa1#Yn;)P5@$C5@p1(MIeslYeH*c?RUR}Sry?cFke{*~N zv)#LY`|jq~U!H$$#lK%IUz}ZBy*Rr(w}00!&g{}k{a?4R8{KmC;%sq#VWlg(e`yyF z_wB}e+IZo$R_G#;G4OjEyOVgYjNkTOD=#0)CU~!I)7NbbqBV_too6PmSWgWj@grq# z?uAE)?Us{y;c&?ADlv#Z({bFss)bM{KpXyc7y-{+%G#B$)$Rd zTTAZ%_OTUfYUwj3-kfj}J=GH*vX2AE7}Dj(&c9Y)X-2T&s?w2swLRG%!-|+##uyd* zp5)a9Q(Hvrg^XSbM(YkemQ0AORNHoB1eJkG*HQMFCXnLaf|Op6naJP^_7sv$W??nR zI~Jt)vZ%FHqT*@QT5P0tf=X8ul8}ibi*OF*OYNz*Xo*F9s+UeNX(6VuzGF|+0%~Ly z%W}_%J=l>ucVtrxMX2*r$e@sbJzcK`jc@V|wX0(H# z-9|1h+Y2TYoPKA%N|yCL=>cJ&?YLc%gD%m-XpUJBtDW707%XASWV_R0&oK_|b9CpH z$}Cb|-d$Oe(XDM-?Em*LXd)t>J07SF+78qq8y&rx`!?LK$tZ~++ARTn0PTg0p!QPa z<78eC1xa(7mS$~Pk$hE7bd9Kj4X=3VWs*mHR2;;Y5*L-PO6Lk@1+_ilr&R{kXX5=* z#X%`CMk_Vf27izzvZ3uKnh3oEvVyF)h#PZ#S~iw5+J1NZ`ufnEv4y`f9^HirSe)B% zzcd&2Q5=Uq?@WX~Z9HR`wf9HVeofygUhnYp&I&-|EpHer_sdQ;5`ULCHh&lNcwuaP z$L8N8Rsac>4M5|DEKpR(DrJF~1w$af z-Fe*3l%bGBG0$n8H&NfWmUH3e8v$)8c>}%V$IC4w@9aA#u*?iGN1sY>B=Vo=mP^4&?u2_@!WAGrdaZ zz}*skf$Fh;X1+KWY!7r0vIvGiY=9zYg)~&D)9T=1q>#H)mrx&4lAet&zm$8B0FJ-B zfK47DdRpE?NOA2MOqzvd3)%o z!O9G_vHuq7srVvI%jsyQN9rh&4-N*GZT!%{Kox^~vC`D=dYXeMfOdObTr_Oc97 zbY^Fq705L*GQU)Dd4sH=wkNNtHLy?fABY2c8v2bonsAJ*C&?u{W@hqBwcor!R=}aO zi1}A+v>?-9TB%~hKAmd)0luW>_teK~`>E(?tW1UHB$xJ$&BKJj6-RF@AB|lpL&L2p z++f9*1L3erTIv4zr&q%J#X(#0ya;nzS=(CvL{D<5p73Pw{IZtb8}hfb=nCWJDe%`#A9&0R>UMR3XEkHy-F8($Fka< zB17b*lvR0TFzOSkr@4^QdC~!1Ba#x1tQSn)8LT&*~7)Yx%O*%Vy}1j&ZIAt&Bcj&)_iRKE{^a!L0@<>Jegd-bO2scEI1kI zHLzb?s5%v?bPn7l^aZL%{lsl+$|h)Xr|gFEXyQNE9@v3o5e#YAfNl)NPWcWBq~2GN zsC1dr`6U_K39^DJ5A_P+J+dYP#W^F=Q(4AG05Qj{MK~IA8j#eLypu*lUN$0hrkuT; zvgqGTEoQAVv-ec}%!6OFV`IOQ?Lb46_q1u2El)HlS^q+N=VP0urv&@A@0y=7b@D2o zDQE`3UWMU$P!JLH^-qR}V2~X$`X|GwPFE_O11E*Dda{QqkH&bbOnvY)j|L&nCyUf) z>LgmVEw#t}5rWPSD#i7fia2vFza(2bK~_-Zk%dSe8ECY87osTP!@X$C>_(S;`EoxN(;(+ZrafA#v{ z3mA*-C!5mi*=fRBoLjIy?l&-^<*e+J69x~@KO9$XJ#e$R7 z;`D|^w)>T}^!pt8nvrliypj03I_ku#(m8OK&==J9q%S1~w5Cwy(V78M8t{8ey6R8W zsL*kdfgzf=U&svHq8#oml4Tw5m(;W9G1Ne|4|Em2fq~Gce+5Had!a4C6y7;mVX%oo z9?d4?Oq7IVaQ$*D+~^s&2&>fy8M$s*0T1S8bbhJYhTpcJPNm@S$O|17wgo)s&T6(y zh}HIu>1Yy@v-QS!rjpB%-7F;d#ZZ@=6 zedvP&+aKYz9CKw3-(6S$-G0~JS;hf^JGf45V0Ti!;U~!U1MhxKZz8XckllJ#h$v&d zH;k40WhWbn|GvbmhU8{s&2bL2o5PdBsWu&NS-GMvp)XK9*-ynq3p`2N^-CEy0mRJ#7m*c^ z5e^v3QQf6Y6jy24l0FRe{u3uVBYRU88D1GkZR3nHB!#mm>4DM<)4Y8ONZCW-!ehX`PbT^*3J$%Q-m(3qMw^yt?bjv4{QCTmE2Z*YVni( z)g_clNB&Or=%?Z`pX^C%Ns>ofE0p!h_8`UIr`cK8;O2@fl5Bzx!pSJOE>WT28A;y6 zCdZaVQbv!@1W9}TG8;KddX+ls&Hhpak3VR;B(v~u1jn#WIDZ9zCp1hy_QpKft|7J{+xK+ME_C;1`_zN-vwn)G9Q$)or2rjn30dh&> z;F9`3ePW^r7Y`#{70IoV;S#F!%Sc6T)U)a3x?%$y&R3-P6)C>6r51&yFSi%}(k@!@ z#?D?_q}Y(+7)v&!Iri*^G*?HkN*5K2Wg6&_CYDusWNng1cA&TtF~#y&_@azh9!nlR zx5Gz+Vb?lPFsm=;KGDn-$ zB(3I6NAYJWgOg`Xg-4Z3V@Pe^nvSNl)^g^~j|0-Y$E9jlnnu<3Q_*3LmgfH>h^tfC z^;45eS1rgXBpI2m2hn)Zrvpi4?x zFG+vdT3rv{s}b{G4z)jsErR>o*5KoYSReT#NKAE-^p0?6t&Pv5 z;=*R<;n)}l(ThD)tTZjWo^o>6v@bY4^=ec#Ya1_}56LC|we*CCoX>$pwEJZzYlSBh z(VRc&JGR`h`IBC5zzhIglAmPs&bC#*55)gucxG+~he!QbYv)=%wfr4$R`3qZ>glo- zeXYK&=QmQLh%$&YvW=RT@VkC;Gtt|fzQji1^F+wz>dOW z9`z?iaDek304??Locbkcj&SjChB_)#x;QV!vf7@K&d4j4Rlc!JEK{h9G_g$6Aod{y zw#YMkUQv@hgNh zOfj{o6ww7}&{Ai||Neawhq#h3q?ajop#-lkgMH zvGKIx--=JIeY}G*0OF-LC%+}XBAl7r=KRS|Tu=t!W7mu9Tx*BSEPhG+ouDtcd0W|Y zApR%AL-rYn-_6^~o~>}U!bA3nl12gUHyqVf`}*RRNLZ-I(#^WPs&x1_~d-NBXy7b z@*d>+{1FoB3Oq%p+J)R4kKEvyI$0u(pxTjw$G7lE9gYqBj3+)VSG8r4WCrSLJVS>K zD3sJ6p(7Eh74SeE^*KJ;#`AReSXRYIP;i?DPuJm&4<4rF%_Evt@N^xjLEeBnTG$8p z5l`GsBT)F(AKI4p_#md@Cp__0M2su^RwKy9FL)v-{DddBps~p<==nXizz<2|XOu9+ z;1`)tI3C@=Pk5q#;!gmfyLIQ}x@eQ`oaR(D5~*~w6j0lvYT(W&59sNf0rC;g$+W_~ zv(+6zz26OwwN`#|%Z_A`vGm{*mlSh$o*hJMQv($`zAZuv-zYqmRe`ZhEaT_u@lYs! zt~s)e%#dhY*7=xB#(s{Y5l+o=E@i(JYXk43+^>%nVlEnv-EKiV{(JMI%cnpcm* zFHik@>fe4x_vFvz{P|>;r?5+RZg$RA^Zg}$0~UW51%G-Ef5i`fDG+~j5PymWe{B&z z+JYYgg7?Uu*8y^u%88@SRCtpnV;0n9*mKPBs#M7w3Y!rdV*YvGEk8TV*S`#3z&cWcrpP zns%D8)YgHMLRmf8qr|&ZN2>|ij-%BrZEp~L2}`LC_eb&hHdzD@c2WZ}4R#UP0CgxS zT&g39_!yB?dgRpRhr4VEapt9dNnzCqvOsSYg^=9|cx3=}7^NKfSwmQr0UK0Ks> z+jh0c@ia22O7$gmi*;xN SJX6dsS int: - """Encode a symbol tuple into an integer using base conversion.""" - base = len(base_alphabet) - mapping = {sym: idx for idx, sym in enumerate(sorted(base_alphabet))} - enc = 0 - for char in tup: - enc = enc * base + mapping[char] - return enc - -# Helper function to create a SparseDFA from a traditional DFA description -def create_sparse_dfa(states: List[str], input_symbols: Set[Tuple[str]], - transitions: Dict[str, Dict[Tuple[str], str]], - initial_state: str, final_states: Set[str]) -> SparseDFA: - """Convert a traditional DFA description to a SparseDFA.""" - # Map states to integers - state_to_index = {s: i for i, s in enumerate(states)} - num_states = len(states) - - # Determine base alphabet and arity - base_alphabet = set() - for sym in input_symbols: - for char in sym: - base_alphabet.add(char) - arity = len(next(iter(input_symbols))) if input_symbols else 0 - - # Create state arrays - default_states = [] - exception_symbols = [] - exception_states = [] - is_accepting = np.array([state in final_states for state in states], dtype=bool) - - # Build symbol mapping - symbol_map = {} - for symbol in input_symbols: - symbol_map[symbol] = encode_symbol(symbol, base_alphabet) - - # Process each state - for state in states: - # Find most common transition - next_states = [transitions[state][sym] for sym in input_symbols] - default_target = max(set(next_states), key=next_states.count) - default_states.append(state_to_index[default_target]) - - # Collect exceptions - exceptions = [] - for symbol, next_state in transitions[state].items(): - if next_state != default_target: - sym_enc = symbol_map[symbol] - next_idx = state_to_index[next_state] - exceptions.append((sym_enc, next_idx)) - - # Sort exceptions by symbol for consistency - exceptions.sort(key=lambda x: x[0]) - exception_symbols.append([e[0] for e in exceptions]) - exception_states.append([e[1] for e in exceptions]) - - # Pad exceptions - max_exceptions = max(len(e) for e in exception_symbols) if exception_symbols else 0 - padded_ex_syms = np.full((num_states, max_exceptions), -1, dtype=np.int32) - padded_ex_states = np.full((num_states, max_exceptions), -1, dtype=np.int32) - - for i in range(num_states): - if exception_symbols[i]: - padded_ex_syms[i, :len(exception_symbols[i])] = exception_symbols[i] - padded_ex_states[i, :len(exception_states[i])] = exception_states[i] - - return SparseDFA( - num_states=num_states, - default_states=np.array(default_states, dtype=np.int32), - exception_symbols=padded_ex_syms, - exception_states=padded_ex_states, - is_accepting=is_accepting, - start_state=state_to_index[initial_state], - symbol_arity=arity, - base_alphabet=base_alphabet - ).minimize() - -def BuechiArithmetic() -> AutomaticPresentation: - """Load the serialized Büchi arithmetic presentation.""" - current_dir = Path(__file__).parent - bin_path = current_dir / 'bin' / 'buechi.autstr' - return AutomaticPresentation.automatic_presentation_from_file(str(bin_path)) - -def buechi_arithmetic() -> AutomaticPresentation: - """Sparse version of Büchi arithmetic over natural numbers. Compiles definable relation from scratch.""" - # Universe automaton - only accepts valid binary numbers (no '*') - universe = create_sparse_dfa( - states={'i', '0', '0+', '1', '*'}, - input_symbols={('0',), ('1',), ('*',)}, - transitions={ - 'i': {('0',): '0', ('1',): '1', ('*',): '*'}, - '0': {('0',): '0+', ('1',): '1', ('*',): '*'}, - '0+': {('0',): '0+', ('1',): '1', ('*',): '*'}, - '1': {('0',): '0+', ('1',): '1', ('*',): '*'}, - '*': {('0',): '*', ('1',): '*', ('*',): '*'}, - }, - initial_state='i', - final_states={'0', '1'} - ) - - addition = create_sparse_dfa( - states={0, 1, 2}, - input_symbols={('0', '0', '0'), ('0', '0', '1'), ('0', '0', '*'), ('0', '1', '0'), ('0', '1', '1'), - ('0', '1', '*'), ('0', '*', '0'), ('0', '*', '1'), ('0', '*', '*'), - ('1', '0', '0'), ('1', '0', '1'), ('1', '0', '*'), ('1', '1', '0'), ('1', '1', '1'), - ('1', '1', '*'), ('1', '*', '0'), ('1', '*', '1'), ('1', '*', '*'), - ('*', '0', '0'), ('*', '0', '1'), ('*', '0', '*'), ('*', '1', '0'), ('*', '1', '1'), - ('*', '1', '*'), ('*', '*', '0'), ('*', '*', '1'), ('*', '*', '*'), - }, - transitions={ - 0: { - ('0', '0', '0'): 0, ('0', '0', '1'): 2, ('0', '0', '*'): 2, ('0', '1', '0'): 2, ('0', '1', '1'): 0, - ('0', '1', '*'): 2, ('0', '*', '0'): 0, ('0', '*', '1'): 2, ('0', '*', '*'): 2, - ('1', '0', '0'): 2, ('1', '0', '1'): 0, ('1', '0', '*'): 2, ('1', '1', '0'): 1, ('1', '1', '1'): 2, - ('1', '1', '*'): 2, ('1', '*', '0'): 2, ('1', '*', '1'): 0, ('1', '*', '*'): 2, - ('*', '0', '0'): 0, ('*', '0', '1'): 2, ('*', '0', '*'): 2, ('*', '1', '0'): 2, ('*', '1', '1'): 0, - ('*', '1', '*'): 2, ('*', '*', '0'): 2, ('*', '*', '1'): 2, ('*', '*', '*'): 2, - }, - 1: { - ('0', '0', '0'): 2, ('0', '0', '1'): 0, ('0', '0', '*'): 2, ('0', '1', '0'): 1, ('0', '1', '1'): 2, - ('0', '1', '*'): 2, ('0', '*', '0'): 2, ('0', '*', '1'): 0, ('0', '*', '*'): 2, - ('1', '0', '0'): 1, ('1', '0', '1'): 2, ('1', '0', '*'): 2, ('1', '1', '0'): 2, ('1', '1', '1'): 1, - ('1', '1', '*'): 2, ('1', '*', '0'): 1, ('1', '*', '1'): 2, ('1', '*', '*'): 2, - ('*', '0', '0'): 2, ('*', '0', '1'): 0, ('*', '0', '*'): 2, ('*', '1', '0'): 1, ('*', '1', '1'): 2, - ('*', '1', '*'): 2, ('*', '*', '0'): 2, ('*', '*', '1'): 0, ('*', '*', '*'): 2, - }, - 2: { - ('0', '0', '0'): 2, ('0', '0', '1'): 2, ('0', '0', '*'): 2, ('0', '1', '0'): 2, ('0', '1', '1'): 2, - ('0', '1', '*'): 2, ('0', '*', '0'): 2, ('0', '*', '1'): 2, ('0', '*', '*'): 2, - ('1', '0', '0'): 2, ('1', '0', '1'): 2, ('1', '0', '*'): 2, ('1', '1', '0'): 2, ('1', '1', '1'): 2, - ('1', '1', '*'): 2, ('1', '*', '0'): 2, ('1', '*', '1'): 2, ('1', '*', '*'): 2, - ('*', '0', '0'): 2, ('*', '0', '1'): 2, ('*', '0', '*'): 2, ('*', '1', '0'): 2, ('*', '1', '1'): 2, - ('*', '1', '*'): 2, ('*', '*', '0'): 2, ('*', '*', '1'): 2, ('*', '*', '*'): 2, - }, - }, - initial_state=0, - final_states={0} - ) - - input_symbols = {a for a in it.product(['0', '1', '*'], repeat=2)} - weak_div = create_sparse_dfa( - states={'0', '1', 'e'}, - input_symbols=input_symbols, - transitions={ - '0': { - a: '0' if a == ('0', '0') else '1' if a == ('0', '1') or a == ('1', '1') else 'e' for a in input_symbols - }, - '1': { - a: 'e' if a[1] != '*' else '1' for a in input_symbols - }, - 'e': {a: 'e' for a in input_symbols} - }, - initial_state='0', - final_states={'1'} - ) - - - # Create presentation - presentation = AutomaticPresentation({'U': universe, 'A': addition, 'B': weak_div}) - - # Add bootstrap remaining relations - presentation.update(Z='A(x,x,x)') - presentation.update(Eq='exists z.(Z(z) and A(x,z,y))') - presentation.update(Pt='B(x,x)') - presentation.update(Lt='exists z.(not Z(z) and A(x, z, y))') - presentation.update(Gt='exists z.(not Z(z) and A(y, z, x))') - - return presentation - -def BuechiArithmeticZ() -> AutomaticPresentation: - """Load the serialized Büchi arithmetic presentation.""" - current_dir = Path(__file__).parent - bin_path = current_dir / 'bin' / 'buechiZ.autstr' - return AutomaticPresentation.automatic_presentation_from_file(str(bin_path)) - -def buechi_arithmetic_Z() -> AutomaticPresentation: - """Sparse version of Büchi arithmetic over integers.""" - # Universe automaton - universe_states = ['-1', 'i+', 'i', '0', '0+', '1', '*'] - universe_symbols = {('0',), ('1',), ('*',)} - universe_trans = { - '-1': {('0',): 'i', ('1',): 'i+', ('*',): '*'}, - 'i+': {('0',): '0+', ('1',): '1', ('*',): '*'}, - 'i': {('0',): '0', ('1',): '1', ('*',): '*'}, - '0': {('0',): '0+', ('1',): '1', ('*',): '*'}, - '0+': {('0',): '0+', ('1',): '1', ('*',): '*'}, - '1': {('0',): '0+', ('1',): '1', ('*',): '*'}, - '*': {('0',): '*', ('1',): '*', ('*',): '*'}, - } - universe = create_sparse_dfa( - universe_states, universe_symbols, universe_trans, '-1', {'0', '1'} - ) - - # Addition automaton (intermediate) - add_states = [-1, 0, 1, 2] - add_symbols = set(it.product(['0', '1', '*'], repeat=3)) - add_trans = { - -1: {a: 0 if '*' not in a else 2 for a in add_symbols}, - 0: { - ('0','0','0'): 0, ('0','0','1'): 2, ('0','0','*'): 2, - ('0','1','0'): 2, ('0','1','1'): 0, ('0','1','*'): 2, - ('0','*','0'): 0, ('0','*','1'): 2, ('0','*','*'): 2, - ('1','0','0'): 2, ('1','0','1'): 0, ('1','0','*'): 2, - ('1','1','0'): 1, ('1','1','1'): 2, ('1','1','*'): 2, - ('1','*','0'): 2, ('1','*','1'): 0, ('1','*','*'): 2, - ('*','0','0'): 0, ('*','0','1'): 2, ('*','0','*'): 2, - ('*','1','0'): 2, ('*','1','1'): 0, ('*','1','*'): 2, - ('*','*','0'): 2, ('*','*','1'): 2, ('*','*','*'): 2, - }, - 1: { - ('0','0','0'): 2, ('0','0','1'): 0, ('0','0','*'): 2, - ('0','1','0'): 1, ('0','1','1'): 2, ('0','1','*'): 2, - ('0','*','0'): 2, ('0','*','1'): 0, ('0','*','*'): 2, - ('1','0','0'): 1, ('1','0','1'): 2, ('1','0','*'): 2, - ('1','1','0'): 2, ('1','1','1'): 1, ('1','1','*'): 2, - ('1','*','0'): 1, ('1','*','1'): 2, ('1','*','*'): 2, - ('*','0','0'): 2, ('*','0','1'): 0, ('*','0','*'): 2, - ('*','1','0'): 1, ('*','1','1'): 2, ('*','1','*'): 2, - ('*','*','0'): 2, ('*','*','1'): 0, ('*','*','*'): 2, - }, - 2: {s: 2 for s in add_symbols} - } - addition_intermediate = create_sparse_dfa(add_states, add_symbols, add_trans, -1, {0}) - - # Weak division automaton - div_states = ['-1', '0', '1', 'e'] - div_symbols = set(it.product(['0', '1', '*'], repeat=2)) - div_trans = { - '-1': {a: '0' if a[1] == '0' else 'e' for a in div_symbols}, - '0': { - a: '0' if a == ('0','0') - else '1' if a in {('0','1'), ('1','1')} - else 'e' for a in div_symbols - }, - '1': { - a: 'e' if a[1] != '*' else '1' for a in div_symbols - }, - 'e': {a: 'e' for a in div_symbols} - } - weak_div = create_sparse_dfa(div_states, div_symbols, div_trans, '-1', {'1'}) - - # N0 automaton - n0_states = [-1, 0, 1] - n0_symbols = {('0',), ('1',), ('*',)} - n0_trans = { - -1: {('0',): 1, ('1',): 0, ('*',): 0}, - 0: {s: 0 for s in n0_symbols}, - 1: {s: 1 for s in n0_symbols} - } - N0 = create_sparse_dfa(n0_states, n0_symbols, n0_trans, -1, {1}) - - # Create presentation - presentation = AutomaticPresentation({ - 'U': universe, - 'A0': addition_intermediate, - 'B': weak_div, - 'N0': N0 - }) - presentation.update(Z='A0(x,x,x)') - - # Define addition formula - c000 = '(N0(x) and N0(y) and N0(z) and A0(x, y, z))' - c001 = '(N0(x) and N0(y) and not N0(z) and exists a z0.(Z(z0) and A0(x,y,a) and A0(a,z,z0)))' - c010 = '(N0(x) and not N0(y) and N0(z) and A0(z, y, x))' - c011 = '(N0(x) and not N0(y) and not N0(z) and A0(z, x, y))' - c100 = '(not N0(x) and N0(y) and N0(z) and A0(x, z, y))' - c101 = '(not N0(x) and N0(y) and not N0(z) and A0(z, y, x))' - c110 = '(not N0(x) and not N0(y) and N0(z) and exists a z0.(Z(z0) and A0(x,y,a) and A0(a,z,z0)))' - c111 = '(not N0(x) and not N0(y) and not N0(z) and A0(x,y,z))' - phi_A = ' or '.join([c000, c001, c010, c011, c100, c101, c110, c111]) - presentation.update(A=phi_A) - - presentation.update(Eq='exists z.(Z(z) and A(x,z,y))') - presentation.update(Pt='B(x,x) and N0(x)') - presentation.update(Lt='exists z.(N0(z) and not Z(z) and A(x, z, y))') - presentation.update(Gt='exists z.(N0(z) and not Z(z) and A(y, z, x))') - presentation.update(Neg='exists z.(Z(z) and A(x,y,z))') - - # Delete auxiliary relation - del presentation.automata['A0'] - - return presentation - -def MSO0() -> AutomaticPresentation: - """Load the serialized MSO0 presentation.""" - current_dir = Path(__file__).parent - bin_path = current_dir / 'bin' / 'mso0.autstr' - return AutomaticPresentation.automatic_presentation_from_file(str(bin_path)) - -def finite_powerset() -> AutomaticPresentation: - # Base alphabet for the presentation - base_alphabet = {'0', '1', '*'} - - # 1. Universe Automaton (U) - Includes empty set - universe = create_sparse_dfa( - states={'start', 'empty', 'zero', 'one', 'pad_one', 'pad_zero', 'reject'}, - input_symbols={'0', '1', '*'}, - transitions={ - 'start': { - '*': 'empty', - '0': 'zero', - '1': 'one' - }, - 'empty': { - '*': 'empty', - '0': 'reject', - '1': 'reject' - }, - 'zero': { - '*': 'pad_zero', - '0': 'zero', - '1': 'one' - }, - 'one': { - '*': 'pad_one', - '0': 'zero', - '1': 'one' - }, - 'pad_zero': { - '*': 'pad_zero', - '0': 'reject', - '1': 'reject' - }, - 'pad_one': { - '*': 'pad_one', - '0': 'reject', - '1': 'reject' - }, - 'reject': { - '0': 'reject', - '1': 'reject', - '*': 'reject' - } - }, - initial_state='start', - final_states={'start', 'one','empty', 'pad_one'} # Accepts empty set and non-empty sets - ) - - # 2. Subset Automaton (Subset) - Empty set is subset of all sets - subset = create_sparse_dfa( - states={'start', 'error'}, - input_symbols=set(it.product(base_alphabet, repeat=2)), - transitions={ - 'start': { - ('0','0'): 'start', - ('0','1'): 'start', - ('0','*'): 'start', - ('1','1'): 'start', - ('*','0'): 'start', - ('*','1'): 'start', - ('*','*'): 'start', - ('1','0'): 'error', - ('1','*'): 'error' - }, - 'error': {k: 'error' for k in it.product(base_alphabet, repeat=2)} - }, - initial_state='start', - final_states={'start'} - ) - - # 3. Singleton Automaton (Sing) - Empty set is not a singleton - sing = create_sparse_dfa( - states={'start', 'one', 'one_pad', 'many', 'reject'}, - input_symbols=base_alphabet, - transitions={ - 'start': { - '0': 'start', - '1': 'one', - '*': 'reject' # Reject empty set immediately - }, - 'one': { - '0': 'reject', - '1': 'many', - '*': 'one_pad' - }, - 'one_pad': { - '0': 'many', - '1': 'many', - '*': 'one_pad' - }, - 'many': { - '0': 'many', - '1': 'many', - '*': 'many' - }, - 'reject': { - '0': 'reject', - '1': 'reject', - '*': 'reject' - } - }, - initial_state='start', - final_states={'one','one_pad'} # Only non-empty singletons - ) - - # 4. Successor Automaton (Succ) - Empty set has no successor - succ = create_sparse_dfa( - states={'start', 'after_x', 'after_y', 'error'}, - input_symbols=set(it.product(base_alphabet, repeat=2)), - transitions={ - 'start': { - ('0','0'): 'start', - ('1','0'): 'after_x', - ('0','1'): 'error', - ('1','1'): 'error', - ('*','*'): 'error', # Reject empty set - ('0','*'): 'error', - ('1','*'): 'error', - ('*','0'): 'error', - ('*','1'): 'error' - }, - 'after_x': { - ('*','1'): 'after_y', - ('0','0'): 'error', - ('0','1'): 'error', - ('1','0'): 'error', - ('1','1'): 'error', - ('*','0'): 'error', - ('0','*'): 'error', - ('1','*'): 'error', - ('*','*'): 'error' - }, - 'after_y': { - ('*','*'): 'after_y', - ('0','0'): 'error', - ('0','1'): 'error', - ('1','0'): 'error', - ('1','1'): 'error', - ('*','0'): 'error', - ('*','1'): 'error', - ('0','*'): 'error', - ('1','*'): 'error' - }, - 'error': {k: 'error' for k in it.product(base_alphabet, repeat=2)} - }, - initial_state='start', - final_states={'after_y'} # Only consecutive singletons - ) - - # 5. Less-Than on Singletons Automaton (Lt_sing) - Empty set not involved - lt_sing = create_sparse_dfa( - states={'init', 'x_first', 'x_first_accept', 'error'}, - input_symbols=set(it.product(base_alphabet, repeat=2)), - transitions={ - 'init': { - ('0','0'): 'init', - ('0','1'): 'error', - ('1','0'): 'x_first', - ('1','1'): 'error', - ('*','*'): 'error', - ('0','*'): 'error', - ('*','0'): 'error', - ('1','*'): 'error', - ('*','1'): 'error' - }, - 'x_first': { - ('0','0'): 'x_first', - ('0','1'): 'x_first_accept', - ('1','0'): 'error', - ('1','1'): 'error', - ('*','*'): 'error', - ('0','*'): 'error', - ('*','0'): 'x_first', - ('1','*'): 'error', - ('*','1'): 'x_first_accept' - }, - 'x_first_accept': { - ('*','*'): 'x_first_accept', - ('0','0'): 'error', - ('0','1'): 'error', - ('1','0'): 'error', - ('1','1'): 'error', - ('*','0'): 'error', - ('*','1'): 'error', - ('0','*'): 'error', - ('1','*'): 'error' - }, - 'error': {k: 'error' for k in it.product(base_alphabet, repeat=2)} - }, - initial_state='init', - final_states={'x_first_accept'} # Only when first singleton < second - ) - - # Create the presentation with base automata - presentation = AutomaticPresentation({ - 'U': universe, - 'Subset': subset, - 'Sing': sing, - 'Succ': succ, - 'Lt_sing': lt_sing - }) - - # Define additional relations using formulas - presentation.update( - In="Sing(x) and Subset(x, y)", - Eq_set="Subset(x, y) and Subset(y, x)", - Leq_sing="Lt_sing(x, y) or (Eq_set(x, y) and Sing(x) and Sing(y))", - Gt_sing="not Leq_sing(x, y) and Sing(x) and Sing(y)", - Min="(forall z. not Subset(z, x)) or " # Empty set case - "(Sing(y) and Subset(y, x) and forall z. (-(Sing(z) and Subset(z, x)) or Leq_sing(y, z)))", - Max="Sing(y) and Subset(y, x) and forall z. (-(Sing(z) and Subset(z, x)) or Leq_sing(z, y))", - Intersect="forall a. (-Sing(a) or ((Subset(a, z) and (Subset(a, x) and Subset(a, y))) or (-Subset(a, z) and -(Subset(a, x) and Subset(a, y)))))", - Union="forall a. (-Sing(a) or ((Subset(a, z) and (Subset(a, x) or Subset(a, y))) or (-Subset(a, z) and -(Subset(a, x) or Subset(a, y)))))", - SetMinus="forall a. (-Sing(a) or ((Subset(a, z) and (Subset(a, x) and not Subset(a, y))) or (-Subset(a, z) and -(Subset(a, x) and not Subset(a, y)))))" - ) - - presentation.update( - Geq_sing="not Lt_sing(x, y) and Sing(x) and Sing(y)", - ) - - return presentation - diff --git a/autstr/infinite_graphs.py b/autstr/infinite_graphs.py index 791af59..74b8ae1 100644 --- a/autstr/infinite_graphs.py +++ b/autstr/infinite_graphs.py @@ -135,7 +135,7 @@ def _path(): """The two-way integer path as a minimal presentation carrying only the domain, the ±1 edge, and equality — so the product folds those and nothing else.""" - from autstr.buildin.presentations import BuechiArithmeticZ + from autstr.arithmetic import BuechiArithmeticZ from autstr.presentations import AutomaticPresentation z = BuechiArithmeticZ() z.update(E=f'exists o.(({_ONE}) & (A(x,o,y) | A(y,o,x)))') @@ -145,7 +145,8 @@ def _path(): # -- element codec: an n-tuple <-> its nested-pair convolution ------ def encode(self, point: Sequence[int]) -> list: - from autstr.arithmetic import encode as encode_int + from autstr.arithmetic import BuechiArithmeticZ + encode_int = BuechiArithmeticZ.encode point = tuple(point) if len(point) != self.n: raise ValueError(f"expected a {self.n}-tuple, got {point!r}") @@ -161,7 +162,8 @@ def encode(self, point: Sequence[int]) -> list: return word def decode(self, word) -> Tuple[int, ...]: - from autstr.arithmetic import decode as decode_int + from autstr.arithmetic import BuechiArithmeticZ + decode_int = BuechiArithmeticZ.decode coordinates = [] current = list(word) for _ in range(self.n - 1): diff --git a/autstr/ordinals.py b/autstr/ordinals.py index 776fd11..283f3b4 100644 --- a/autstr/ordinals.py +++ b/autstr/ordinals.py @@ -43,7 +43,7 @@ from typing import Dict, List, Mapping, Optional, Sequence, Tuple, Union -from autstr.buildin.presentations import BuechiArithmetic +from autstr.arithmetic import BuechiArithmetic from autstr.interpretations import interpret from autstr.sparse_tree_automata import SparseTreeAutomaton, Tree from autstr.symbolic import FunctionCodec, order_signature diff --git a/autstr/powerset.py b/autstr/powerset.py new file mode 100644 index 0000000..2f35394 --- /dev/null +++ b/autstr/powerset.py @@ -0,0 +1,319 @@ +"""The finite subsets of the naturals -- the structure MSO0. + +Finite subsets of :math:`\\mathbb{N}` under :math:`\\subseteq`, with singletons, +the successor on singletons and their order. A finite set is a +:math:`\\{0,1\\}`-word, position `i` set iff `i` is a member, so by Büchi's +theorem first-order logic over this structure is exactly **monadic second-order +logic** over :math:`(\\mathbb{N}, <)`: the definable sets of naturals are +precisely the regular ones. + + >>> M = MSO0().symbolic() + >>> x, y = M.vars("x y") + >>> ({0, 2}, {0, 1, 2}) in x.subset(y) + True + +That correspondence is why the structure is called MSO0, and why quantifying +over sets here costs no more than quantifying over elements elsewhere. +""" +from typing import List + +import itertools as it + +from autstr.presentations import ( + AutomaticPresentation, CompiledPresentation, +) +from autstr.utils.automata_tools import create_sparse_dfa + + +def _build_finite_powerset() -> AutomaticPresentation: + """The finite subsets of the naturals, compiled from scratch.""" + # Base alphabet for the presentation + base_alphabet = {'0', '1', '*'} + + # 1. Universe Automaton (U) - Includes empty set + universe = create_sparse_dfa( + states={'start', 'empty', 'zero', 'one', 'pad_one', 'pad_zero', 'reject'}, + input_symbols={'0', '1', '*'}, + transitions={ + 'start': { + '*': 'empty', + '0': 'zero', + '1': 'one' + }, + 'empty': { + '*': 'empty', + '0': 'reject', + '1': 'reject' + }, + 'zero': { + '*': 'pad_zero', + '0': 'zero', + '1': 'one' + }, + 'one': { + '*': 'pad_one', + '0': 'zero', + '1': 'one' + }, + 'pad_zero': { + '*': 'pad_zero', + '0': 'reject', + '1': 'reject' + }, + 'pad_one': { + '*': 'pad_one', + '0': 'reject', + '1': 'reject' + }, + 'reject': { + '0': 'reject', + '1': 'reject', + '*': 'reject' + } + }, + initial_state='start', + final_states={'start', 'one','empty', 'pad_one'} # Accepts empty set and non-empty sets + ) + + # 2. Subset Automaton (Subset) - Empty set is subset of all sets + subset = create_sparse_dfa( + states={'start', 'error'}, + input_symbols=set(it.product(base_alphabet, repeat=2)), + transitions={ + 'start': { + ('0','0'): 'start', + ('0','1'): 'start', + ('0','*'): 'start', + ('1','1'): 'start', + ('*','0'): 'start', + ('*','1'): 'start', + ('*','*'): 'start', + ('1','0'): 'error', + ('1','*'): 'error' + }, + 'error': {k: 'error' for k in it.product(base_alphabet, repeat=2)} + }, + initial_state='start', + final_states={'start'} + ) + + # 3. Singleton Automaton (Sing) - Empty set is not a singleton + sing = create_sparse_dfa( + states={'start', 'one', 'one_pad', 'many', 'reject'}, + input_symbols=base_alphabet, + transitions={ + 'start': { + '0': 'start', + '1': 'one', + '*': 'reject' # Reject empty set immediately + }, + 'one': { + '0': 'reject', + '1': 'many', + '*': 'one_pad' + }, + 'one_pad': { + '0': 'many', + '1': 'many', + '*': 'one_pad' + }, + 'many': { + '0': 'many', + '1': 'many', + '*': 'many' + }, + 'reject': { + '0': 'reject', + '1': 'reject', + '*': 'reject' + } + }, + initial_state='start', + final_states={'one','one_pad'} # Only non-empty singletons + ) + + # 4. Successor Automaton (Succ) - Empty set has no successor + succ = create_sparse_dfa( + states={'start', 'after_x', 'after_y', 'error'}, + input_symbols=set(it.product(base_alphabet, repeat=2)), + transitions={ + 'start': { + ('0','0'): 'start', + ('1','0'): 'after_x', + ('0','1'): 'error', + ('1','1'): 'error', + ('*','*'): 'error', # Reject empty set + ('0','*'): 'error', + ('1','*'): 'error', + ('*','0'): 'error', + ('*','1'): 'error' + }, + 'after_x': { + ('*','1'): 'after_y', + ('0','0'): 'error', + ('0','1'): 'error', + ('1','0'): 'error', + ('1','1'): 'error', + ('*','0'): 'error', + ('0','*'): 'error', + ('1','*'): 'error', + ('*','*'): 'error' + }, + 'after_y': { + ('*','*'): 'after_y', + ('0','0'): 'error', + ('0','1'): 'error', + ('1','0'): 'error', + ('1','1'): 'error', + ('*','0'): 'error', + ('*','1'): 'error', + ('0','*'): 'error', + ('1','*'): 'error' + }, + 'error': {k: 'error' for k in it.product(base_alphabet, repeat=2)} + }, + initial_state='start', + final_states={'after_y'} # Only consecutive singletons + ) + + # 5. Less-Than on Singletons Automaton (Lt_sing) - Empty set not involved + lt_sing = create_sparse_dfa( + states={'init', 'x_first', 'x_first_accept', 'error'}, + input_symbols=set(it.product(base_alphabet, repeat=2)), + transitions={ + 'init': { + ('0','0'): 'init', + ('0','1'): 'error', + ('1','0'): 'x_first', + ('1','1'): 'error', + ('*','*'): 'error', + ('0','*'): 'error', + ('*','0'): 'error', + ('1','*'): 'error', + ('*','1'): 'error' + }, + 'x_first': { + ('0','0'): 'x_first', + ('0','1'): 'x_first_accept', + ('1','0'): 'error', + ('1','1'): 'error', + ('*','*'): 'error', + ('0','*'): 'error', + ('*','0'): 'x_first', + ('1','*'): 'error', + ('*','1'): 'x_first_accept' + }, + 'x_first_accept': { + ('*','*'): 'x_first_accept', + ('0','0'): 'error', + ('0','1'): 'error', + ('1','0'): 'error', + ('1','1'): 'error', + ('*','0'): 'error', + ('*','1'): 'error', + ('0','*'): 'error', + ('1','*'): 'error' + }, + 'error': {k: 'error' for k in it.product(base_alphabet, repeat=2)} + }, + initial_state='init', + final_states={'x_first_accept'} # Only when first singleton < second + ) + + # Create the presentation with base automata + presentation = AutomaticPresentation({ + 'U': universe, + 'Subset': subset, + 'Sing': sing, + 'Succ': succ, + 'Lt_sing': lt_sing + }) + + # Define additional relations using formulas + presentation.update( + In="Sing(x) and Subset(x, y)", + Eq_set="Subset(x, y) and Subset(y, x)", + Leq_sing="Lt_sing(x, y) or (Eq_set(x, y) and Sing(x) and Sing(y))", + Gt_sing="not Leq_sing(x, y) and Sing(x) and Sing(y)", + Min="(forall z. not Subset(z, x)) or " # Empty set case + "(Sing(y) and Subset(y, x) and forall z. (-(Sing(z) and Subset(z, x)) or Leq_sing(y, z)))", + Max="Sing(y) and Subset(y, x) and forall z. (-(Sing(z) and Subset(z, x)) or Leq_sing(z, y))", + Intersect="forall a. (-Sing(a) or ((Subset(a, z) and (Subset(a, x) and Subset(a, y))) or (-Subset(a, z) and -(Subset(a, x) and Subset(a, y)))))", + Union="forall a. (-Sing(a) or ((Subset(a, z) and (Subset(a, x) or Subset(a, y))) or (-Subset(a, z) and -(Subset(a, x) or Subset(a, y)))))", + SetMinus="forall a. (-Sing(a) or ((Subset(a, z) and (Subset(a, x) and not Subset(a, y))) or (-Subset(a, z) and -(Subset(a, x) and not Subset(a, y)))))" + ) + + presentation.update( + Geq_sing="not Lt_sing(x, y) and Sing(x) and Sing(y)", + ) + + return presentation + + + + +# -------------------------------------------------------------------------- +# The presentation. It declares its own vocabulary, so `symbolic()` takes no +# argument; see `CompiledPresentation`. +# -------------------------------------------------------------------------- + +class MSO0(CompiledPresentation): + """The finite subsets of :math:`\\mathbb{N}` under :math:`\\subseteq`, with + singletons, successor and the order on singletons. + + >>> M = MSO0() + >>> x, y = M.symbolic().vars("x y") + >>> ({0, 2} , {0, 1, 2}) in x.subset(y) + True + >>> (x + y).eq({0, 1}).check() # union + True + + By Büchi's theorem, first-order logic over this structure is exactly + monadic second-order logic over :math:`(\\mathbb{N}, <)`, so the definable + sets are precisely the regular ones. + """ + + _BUILD = staticmethod(_build_finite_powerset) + + #: a set as a bitmask, position i set iff i is a member. The universe + #: rejects trailing zeros, so the encoding is the CANONICAL one: {0} is + #: `1`, {0, 2} is `101`, and the empty set is the empty word. + PADDING = '*' + + @staticmethod + def encode(s) -> List[str]: + """The word encoding a finite set of naturals: position `i` carries + ``1`` iff `i` is a member, up to the largest one.""" + s = set(s) + if not s: + return [] + if min(s) < 0: + raise ValueError(f"not a set of naturals: {s}") + return ['1' if i in s else '0' for i in range(max(s) + 1)] + + @staticmethod + def decode(word) -> set: + """The set encoded by a word, ignoring padding.""" + digits = ''.join(word).replace(MSO0.PADDING, '') + return {i for i, c in enumerate(digits) if c == '1'} + + def default_signature(self): + """Union as ``+``, intersection as ``*``, difference as binary ``-``, + and the relations of the structure as methods, with sets written as + Python sets.""" + from autstr.symbolic import FunctionCodec, Signature + signature = Signature(codec=FunctionCodec(self.encode, self.decode)) + signature.function('union', graph='Union', out=2) + signature.function('intersect', graph='Intersect', out=2) + signature.function('minus', graph='SetMinus', out=2) + signature.operator('+', 'union') + signature.operator('*', 'intersect') + signature.operator('-', 'minus') + signature.operator('eq', 'Eq_set') + signature.operator('subset', 'Subset') + signature.operator('sing', 'Sing') + # In(x, y): x is a singleton contained in y -- membership, for the + # singletons that stand in for the elements of N. + signature.operator('member_of', 'In') + signature.operator('lt_sing', 'Lt_sing') + return signature diff --git a/autstr/presentations.py b/autstr/presentations.py index f48cbb8..eda24a2 100644 --- a/autstr/presentations.py +++ b/autstr/presentations.py @@ -3,7 +3,7 @@ from autstr.sparse_automata import SparseDFA, SparseDFASerializer from autstr.utils.automata_tools import pad, unpad, projection, expand, stack -from autstr.buildin.automata import zero, one +from autstr.utils.automata_tools import zero, one from autstr.utils.logic import get_free_elementary_vars, optimize_query import json @@ -498,4 +498,25 @@ def _build_automaton(self, phi: logic.Expression, verbose=False, init=True) -> S print(f'{str(phi)}: {result.num_states} states') return result - raise ValueError(f"Unsupported expression type: {type(phi)}") \ No newline at end of file + raise ValueError(f"Unsupported expression type: {type(phi)}") + + +class CompiledPresentation(AutomaticPresentation): + """A presentation whose automata are compiled by a builder function. + + Subclasses set `_BUILD` to that function and declare their vocabulary in + `default_signature`, so the structure can be constructed with no arguments + and addressed symbolically with no setup. The builder's automata are + adopted as they are rather than passed to `AutomaticPresentation.__init__`, + which would restrict and pad relations the builder already restricted and + padded. + """ + + #: the builder, as a staticmethod on the subclass + _BUILD = None + + def __init__(self) -> None: + built = type(self)._BUILD() + self.padding_symbol = built.padding_symbol + self.automata = built.automata + self.sigma = built.sigma diff --git a/autstr/symbolic/backends.py b/autstr/symbolic/backends.py index 5ce4185..58dab12 100644 --- a/autstr/symbolic/backends.py +++ b/autstr/symbolic/backends.py @@ -9,7 +9,7 @@ from typing import Dict, List, Optional, Sequence -from autstr.buildin.automata import k_longer_automaton +from autstr.utils.automata_tools import k_longer_automaton from autstr.sparse_tree_automata import Tree, convolve_trees, tree_to_arrays from autstr.utils.tree_automata_tools import ( canonical as tree_canonical, iterate_trees, k_deeper_automaton, diff --git a/autstr/tree_algebra.py b/autstr/tree_algebra.py index 5eb152a..cea2ed4 100644 --- a/autstr/tree_algebra.py +++ b/autstr/tree_algebra.py @@ -106,7 +106,7 @@ def default_signature(self): The lattice operations are terms, not connectives: ``&``, ``|`` and ``~`` already mean conjunction, disjunction and negation of *formulas*, so binding them here would make ``x & y`` ambiguous — the same reason - `autstr.arithmetic` spells its divisibility relation + Büchi arithmetic spells its divisibility relation ``divided_by_power``. """ signature = Signature(codec=FunctionCodec(self.encode, self.decode)) diff --git a/autstr/buildin/tree_presentations.py b/autstr/tree_arithmetic.py similarity index 91% rename from autstr/buildin/tree_presentations.py rename to autstr/tree_arithmetic.py index ca264a9..385f191 100644 --- a/autstr/buildin/tree_presentations.py +++ b/autstr/tree_arithmetic.py @@ -1,10 +1,19 @@ -"""Built-in tree-automatic presentations. - -The flagship example is Skolem arithmetic, the multiplicative monoid -(N_{>0}, ·): multiplication of naturals is not string-automatic, but the -prime-factorization encoding makes it tree-automatic — the multiplicative -structure is the direct sum of countably many copies of (N, +), one per -prime, and a tree bundles the finitely many nonzero summands. +"""Skolem arithmetic: the multiplicative monoid of the naturals. + +:math:`(\\mathbb{N}_{>0}, \\cdot)` is *not* string-automatic, but it is +tree-automatic, and the prime factorization is why: multiplicatively the +structure is the direct sum of countably many copies of :math:`(\\mathbb{N}, ++)`, one per prime, and a tree bundles the finitely many nonzero summands. So +multiplication becomes addition of exponents, carried out in parallel across +the primes. + + >>> S = skolem_arithmetic().symbolic() + >>> x, y = S.vars("x y") + >>> (6, 35, 210) in (x * y).eq(S.vars("z")[0]) + True + +This is the tree-engine counterpart of `autstr.arithmetic`, whose additive +structures the string engine can present directly. """ from itertools import product as iter_product from typing import List, Optional diff --git a/autstr/uniform.py b/autstr/uniform.py index 8eb2e22..c34fbbc 100644 --- a/autstr/uniform.py +++ b/autstr/uniform.py @@ -19,8 +19,9 @@ from autstr.presentations import AutomaticPresentation, DeferredRelations from autstr.sparse_automata import SparseDFA -from autstr.buildin.automata import one -from autstr.buildin.presentations import create_sparse_dfa, encode_symbol +from autstr.utils.automata_tools import one +from autstr.utils.automata_tools import create_sparse_dfa +from autstr.utils.misc import encode_symbol from autstr.utils.automata_tools import expand, pad, permute_tapes, projection, word_automaton from autstr.utils.logic import get_free_elementary_vars from autstr.utils.misc import get_unique_id diff --git a/autstr/utils/automata_tools.py b/autstr/utils/automata_tools.py index 1882435..27fd973 100644 --- a/autstr/utils/automata_tools.py +++ b/autstr/utils/automata_tools.py @@ -1,14 +1,13 @@ import heapq import numpy as np from collections import deque -from typing import Callable, Dict, Generator, List, Set +from typing import Callable, Dict, Generator, List, Optional, Set, Tuple import itertools as it from autstr.mtbdd import NONE, var_tables from autstr.sparse_automata import ( SparseDFA, SparseNFA, _determinize_set_nfa, reduce_set_nfa, ) -from autstr.buildin.automata import one from autstr.utils.misc import decode_symbol, encode_symbol, complement @@ -801,3 +800,209 @@ def partial_dfa(base_alphabet: Set, arity: int, symbol_arity=arity, base_alphabet=set(base_alphabet), ).minimize() + + +# -------------------------------------------------------------------------- +# Basic automata the constructions above build on: fixed and relative word +# lengths, and the two constant languages. +# -------------------------------------------------------------------------- + +def length_automaton(n: int, base_alphabet: Set[int]) -> SparseDFA: + """ + Creates an automaton that recognizes all words over base_alphabet with length exactly n. + + :param n: The exact word length + :param base_alphabet: Set of integer symbols + :return: SparseDFA recognizing words of length n + """ + # States: 0 (start), 1, 2, ..., n (accepting), n+1 (dead) + num_states = n + 2 + # Default transitions: move to next state or dead state + default_states = np.array([i + 1 for i in range(n)] + [n + 1, n + 1], dtype=np.int32) + # No exceptions needed (same behavior for all symbols) + exception_symbols = np.full((num_states, 0), -1, dtype=np.int32) + exception_states = np.full((num_states, 0), -1, dtype=np.int32) + # Only state n is accepting + is_accepting = np.array([False] * n + [True, False]) + start_state = 0 + + return SparseDFA( + num_states=num_states, + default_states=default_states, + exception_symbols=exception_symbols, + exception_states=exception_states, + is_accepting=is_accepting, + start_state=start_state, + symbol_arity=1, + base_alphabet=base_alphabet + ) + +def k_longer_automaton(k: int, r: int, base_alphabet: Set[int], padding_symbol: int) -> SparseDFA: + """ + Creates an automaton recognizing (r+1)-tuples where the last word is at least k letters + longer than the other r words. + + :param k: Minimal length difference + :param r: Number of reference words + :param base_alphabet: Set of integer symbols + :param padding_symbol: Padding symbol integer + :return: SparseDFA for the k-longer condition + """ + # State mapping: [-1, 0, 1, ..., k] -> [0, 1, 2, ..., k+1] + state_mapping = {s: i for i, s in enumerate(range(-1, k+1))} + num_states = len(state_mapping) + sorted_alphabet = sorted(base_alphabet) + arity = r + 1 + + # Precompute all symbol tuples and their encodings + symbol_tuples = list(it.product(sorted_alphabet, repeat=arity)) + symbol_encodings = [encode_symbol(t, base_alphabet) for t in symbol_tuples] + + # Initialize DFA components + default_states = np.full(num_states, state_mapping[-1], dtype=np.int32) # Default to dead state + exception_list = [[] for _ in range(num_states)] + + # Build transitions + for state in range(-1, k+1): + state_idx = state_mapping[state] + for t, enc in zip(symbol_tuples, symbol_encodings): + # Compute next state + if state == -1: + next_state = -1 # Stay in dead state + else: + if all(x == padding_symbol for x in t[:-1]) and t[-1] != padding_symbol: + next_state = min(state + 1, k) # Count extra length + elif t[-1] == padding_symbol: + next_state = -1 # Reject if padding the last word + elif state == 0: + next_state = 0 # Wait for other words to end + else: + next_state = -1 # Reject otherwise + + next_state_idx = state_mapping[next_state] + if next_state_idx != state_mapping[-1]: + exception_list[state_idx].append((enc, next_state_idx)) + + # Find max exceptions needed + max_exceptions = max(len(ex_list) for ex_list in exception_list) if exception_list else 0 + + # Build exception arrays + exception_symbols = np.full((num_states, max_exceptions), -1, dtype=np.int32) + exception_states = np.full((num_states, max_exceptions), -1, dtype=np.int32) + + for i, ex_list in enumerate(exception_list): + if ex_list: + syms, states = zip(*ex_list) + exception_symbols[i, :len(syms)] = syms + exception_states[i, :len(states)] = states + + # Final states: state k (meaning we've counted k extra symbols) + is_accepting = np.array([i == state_mapping[k] for i in range(num_states)]) + + return SparseDFA( + num_states=num_states, + default_states=default_states, + exception_symbols=exception_symbols, + exception_states=exception_states, + is_accepting=is_accepting, + start_state=state_mapping[0], + symbol_arity=arity, + base_alphabet=base_alphabet + ) + + +def zero(symbol_arity: int = 1, base_alphabet: Optional[Set[int]] = None) -> SparseDFA: + """Automaton that rejects all inputs.""" + base_alphabet = base_alphabet or {0} + return SparseDFA( + num_states=1, + default_states=np.array([0], dtype=np.int32), + exception_symbols=np.full((1, 0), -1, dtype=np.int32), + exception_states=np.full((1, 0), -1, dtype=np.int32), + is_accepting=np.array([False]), + start_state=0, + symbol_arity=symbol_arity, + base_alphabet=base_alphabet + ) + +def one(symbol_arity: int = 1, base_alphabet: Optional[Set[int]] = None) -> SparseDFA: + """Automaton that accepts all inputs.""" + base_alphabet = base_alphabet or {0} + return SparseDFA( + num_states=1, + default_states=np.array([0], dtype=np.int32), + exception_symbols=np.full((1, 0), -1, dtype=np.int32), + exception_states=np.full((1, 0), -1, dtype=np.int32), + is_accepting=np.array([True]), + start_state=0, + symbol_arity=symbol_arity, + base_alphabet=base_alphabet + ) + + +def create_sparse_dfa(states: List[str], input_symbols: Set[Tuple[str]], + transitions: Dict[str, Dict[Tuple[str], str]], + initial_state: str, final_states: Set[str]) -> SparseDFA: + """Convert a traditional DFA description to a SparseDFA.""" + # Map states to integers + state_to_index = {s: i for i, s in enumerate(states)} + num_states = len(states) + + # Determine base alphabet and arity + base_alphabet = set() + for sym in input_symbols: + for char in sym: + base_alphabet.add(char) + arity = len(next(iter(input_symbols))) if input_symbols else 0 + + # Create state arrays + default_states = [] + exception_symbols = [] + exception_states = [] + is_accepting = np.array([state in final_states for state in states], dtype=bool) + + # Build symbol mapping + symbol_map = {} + for symbol in input_symbols: + symbol_map[symbol] = encode_symbol(symbol, frozenset(base_alphabet)) + + # Process each state + for state in states: + # Find most common transition + next_states = [transitions[state][sym] for sym in input_symbols] + default_target = max(set(next_states), key=next_states.count) + default_states.append(state_to_index[default_target]) + + # Collect exceptions + exceptions = [] + for symbol, next_state in transitions[state].items(): + if next_state != default_target: + sym_enc = symbol_map[symbol] + next_idx = state_to_index[next_state] + exceptions.append((sym_enc, next_idx)) + + # Sort exceptions by symbol for consistency + exceptions.sort(key=lambda x: x[0]) + exception_symbols.append([e[0] for e in exceptions]) + exception_states.append([e[1] for e in exceptions]) + + # Pad exceptions + max_exceptions = max(len(e) for e in exception_symbols) if exception_symbols else 0 + padded_ex_syms = np.full((num_states, max_exceptions), -1, dtype=np.int32) + padded_ex_states = np.full((num_states, max_exceptions), -1, dtype=np.int32) + + for i in range(num_states): + if exception_symbols[i]: + padded_ex_syms[i, :len(exception_symbols[i])] = exception_symbols[i] + padded_ex_states[i, :len(exception_states[i])] = exception_states[i] + + return SparseDFA( + num_states=num_states, + default_states=np.array(default_states, dtype=np.int32), + exception_symbols=padded_ex_syms, + exception_states=padded_ex_states, + is_accepting=is_accepting, + start_state=state_to_index[initial_state], + symbol_arity=arity, + base_alphabet=base_alphabet + ).minimize() diff --git a/autstr/utils/tree_automata_tools.py b/autstr/utils/tree_automata_tools.py index 21b7523..3971758 100644 --- a/autstr/utils/tree_automata_tools.py +++ b/autstr/utils/tree_automata_tools.py @@ -532,7 +532,7 @@ def k_deeper_automaton(k: int, references: int, base_alphabet, reference tape: some root-to-leaf path carries `k` nodes at which all `references` reference tapes are padding and the last tape is not. - The tree counterpart of `buildin.automata.k_longer_automaton`, and it + The tree counterpart of `automata_tools.k_longer_automaton`, and it supports exists-infinity for the same pumping reason. A tree's domain is closed under parents, so the nodes outside every reference's domain form a suffix of each root-to-leaf path; `k` of them make a body automaton with diff --git a/docs/source/overview.md b/docs/source/overview.md index e01ddf0..8bd4eac 100644 --- a/docs/source/overview.md +++ b/docs/source/overview.md @@ -17,7 +17,7 @@ An `AutomaticPresentation` bundles automata for a domain and its relations, and decides first-order statements about the presented structure: ```python -from autstr.buildin.presentations import BuechiArithmeticZ +from autstr.arithmetic import BuechiArithmeticZ Z = BuechiArithmeticZ() # (ℤ, +, <, |) as automata @@ -41,10 +41,10 @@ relation symbols and function symbols that compose with ordinary Python operators. ```python -from autstr.arithmetic import integers +from autstr.arithmetic import BuechiArithmeticZ -Z = integers() -x, y, z = Z.vars('x y z') +Z = BuechiArithmeticZ().symbolic() # no arguments: the structure knows its own +x, y, z = Z.vars('x y z') # vocabulary phi = (x + y).eq(z) & z.lt(100) # a relation over x, y, z phi.check() # satisfiable? — True @@ -61,10 +61,11 @@ restores the names in results. What a structure offers is declared in a `Signature`: which relations are function graphs, which operators they bind to, and how Python values encode as -elements. +elements. Every structure in the library declares its own, which is why +`symbolic()` above needed no argument. For a presentation you built yourself, +write one: ```python -from autstr.arithmetic import encode, decode from autstr.symbolic import Signature, FunctionCodec signature = (Signature(codec=FunctionCodec(encode, decode)) @@ -75,6 +76,11 @@ signature = (Signature(codec=FunctionCodec(encode, decode)) S = my_presentation.symbolic(signature) ``` +`encode` and `decode` are the structure's own — a Python value to the word that +represents it, and back — so results come out as the values you put in. Give a +presentation a `default_signature` method returning this, and its `symbolic()` +takes no argument either. + Results carry their tape order as variable *names*, so membership and enumeration are keyed by name rather than position: @@ -149,7 +155,7 @@ presentation encodes elements as strings and a word automaton reads them, a 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 +Skolem arithmetic (ℕ, ·) in `autstr.tree_arithmetic`, where a number is the tree of its prime exponents. The {doc}`graphs ` and {doc}`groups ` @@ -309,7 +315,7 @@ is **decidable**, and every definable relation is again automatic. ### A concrete encoding -In the arithmetic package an integer is written **sign-magnitude, least-significant +In Büchi arithmetic 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 diff --git a/notebooks/arithmetic_and_algebra.ipynb b/notebooks/arithmetic_and_algebra.ipynb index 341fa6f..a2d2057 100644 --- a/notebooks/arithmetic_and_algebra.ipynb +++ b/notebooks/arithmetic_and_algebra.ipynb @@ -35,9 +35,9 @@ "source": [ "## Presburger arithmetic over the integers\n", "\n", - "$(\\mathbb{Z}, +, <)$ is automatic in base 2. `autstr.arithmetic` exposes it through\n", - "a small term algebra, so an *infinite* definable set is an ordinary Python value you\n", - "can intersect, complement and iterate." + "$(\\mathbb{Z}, +, <)$ is automatic in base 2. Calling `.symbolic()` on the presentation\n", + "hands out variables that compose with Python operators, so an *infinite* definable set\n", + "is an ordinary Python value you can intersect, complement and iterate." ] }, { @@ -47,10 +47,10 @@ "metadata": {}, "outputs": [], "source": [ - "from autstr.arithmetic import integers\n", + "from autstr.arithmetic import BuechiArithmeticZ\n", "from time import time\n", "\n", - "Z = integers()\n", + "Z = BuechiArithmeticZ().symbolic() # the structure declares its own vocabulary\n", "x = Z.var('x')" ] }, @@ -212,7 +212,7 @@ "metadata": {}, "outputs": [], "source": [ - "from autstr.buildin.presentations import BuechiArithmetic\n", + "from autstr.arithmetic import BuechiArithmetic\n", "\n", "ba = BuechiArithmetic()\n", "ba.automata['A'].show_diagram() # the addition relation A(x, y, z): x + y = z" @@ -288,7 +288,7 @@ "metadata": {}, "outputs": [], "source": [ - "from autstr.buildin.tree_presentations import skolem_arithmetic\n", + "from autstr.tree_arithmetic import skolem_arithmetic\n", "\n", "skolem = skolem_arithmetic()\n", "print('360 = 2^3 * 3^2 * 5 round-trips to:', skolem.decode(skolem.encode(360)))\n", @@ -425,7 +425,7 @@ "metadata": {}, "outputs": [], "source": [ - "from autstr.buildin.presentations import MSO0\n", + "from autstr.powerset import MSO0\n", "\n", "mso0 = MSO0()\n", "print('relations:', mso0.get_relation_symbols())\n", diff --git a/pyproject.toml b/pyproject.toml index 17b795c..1110853 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,8 +73,6 @@ testpaths = ["tests"] [tool.setuptools.packages.find] include = ["autstr*"] -[tool.setuptools.package-data] -"autstr.buildin" = ["bin/*.autstr"] [tool.poetry-dynamic-versioning] enable = true diff --git a/scripts/gen_builtin_presentations.py b/scripts/gen_builtin_presentations.py deleted file mode 100644 index 22ac213..0000000 --- a/scripts/gen_builtin_presentations.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Regenerate the serialized built-in presentations in `autstr/buildin/bin`. - -The loaders (`BuechiArithmetic`, `BuechiArithmeticZ`, `MSO0`) deserialize these -artifacts with ``enforce_consistency=False`` — the stored automata are taken to -be already restricted to the universe. So whenever the construction of a -built-in presentation changes, or the consistency preparation itself does, the -artifacts have to be rebuilt from their generators: - - python scripts/gen_builtin_presentations.py [name ...] - -Rewriting an artifact also re-encodes it in the current serializer format, -which is bulkier than the one some of them were written with — so rebuild only -the ones whose content actually changed. -""" -import sys -from pathlib import Path - -from autstr.buildin.presentations import ( - buechi_arithmetic, buechi_arithmetic_Z, finite_powerset, -) - -BIN = Path(__file__).resolve().parent.parent / 'autstr' / 'buildin' / 'bin' - -TARGETS = { - 'buechi.autstr': buechi_arithmetic, - 'buechiZ.autstr': buechi_arithmetic_Z, - 'mso0.autstr': finite_powerset, -} - - -def main(names=()) -> None: - for filename, build in TARGETS.items(): - if names and filename not in names and Path(filename).stem not in names: - continue - presentation = build() - path = BIN / filename - presentation.automatic_presentation_to_file(str(path)) - print(f'{filename}: {len(presentation.automata)} automata ' - f'({", ".join(sorted(presentation.automata))})') - - -if __name__ == '__main__': - main(sys.argv[1:]) diff --git a/scripts/gen_readme_media.py b/scripts/gen_readme_media.py index 1cb6b52..337bd18 100755 --- a/scripts/gen_readme_media.py +++ b/scripts/gen_readme_media.py @@ -12,14 +12,14 @@ """ from pathlib import Path -from autstr.arithmetic import integers +from autstr.arithmetic import BuechiArithmeticZ MEDIA = Path(__file__).resolve().parent.parent / "docs" / "media" def coprime_automaton(primes): """The DFA for {x > 1 : no p in `primes` divides x}.""" - Z = integers() + Z = BuechiArithmeticZ().symbolic() x = Z.var("x") s = x.gt(1) for p in primes: diff --git a/tests/test_arithmetic.py b/tests/test_arithmetic.py index ce308b7..3dbc98d 100644 --- a/tests/test_arithmetic.py +++ b/tests/test_arithmetic.py @@ -3,12 +3,25 @@ import pytest -from autstr.arithmetic import decode, encode, integers +from autstr.arithmetic import BuechiArithmetic, BuechiArithmeticZ +from autstr.powerset import MSO0 + +encode, decode = BuechiArithmeticZ.encode, BuechiArithmeticZ.decode @pytest.fixture(scope='module') def Z(): - return integers() + return BuechiArithmeticZ().symbolic() + + +@pytest.fixture(scope='module') +def N(): + return BuechiArithmetic().symbolic() + + +@pytest.fixture(scope='module') +def sets(): + return MSO0().symbolic() def bounded(formula, limit=6): @@ -159,3 +172,69 @@ def test_enumeration_yields_integers(Z): x, y = Z.vars('x y') formula = (x + y).eq(4) & x.gt(0) & x.lt(y) assert sorted(islice(iter(formula), 5)) == [(1, 3)] + + +# ---------------------------------------------------------------------- +# the naturals: the same interface, without negation +# ---------------------------------------------------------------------- + +@pytest.mark.parametrize('n', [0, 1, 2, 7, 8, 255]) +def test_natural_encode_decode_roundtrip(n): + assert BuechiArithmetic.decode(BuechiArithmetic.encode(n)) == n + + +def test_naturals_reject_negatives(): + with pytest.raises(ValueError): + BuechiArithmetic.encode(-1) + + +def test_naturals_add(N): + x, y = N.vars('x y') + relation = (x + y).eq(12).evaluate() + assert relation.contains(x=5, y=7) + assert not relation.contains(x=5, y=8) + + +def test_naturals_have_a_least_element(N): + x, y = N.vars('x y') + # unbounded above, but 0 has nothing below it -- the difference from Z + assert x.lt(y).drop(y).all(x).check() + assert not y.lt(x).drop(y).all(x).check() + + +# ---------------------------------------------------------------------- +# MSO0: finite sets as Python sets +# ---------------------------------------------------------------------- + +@pytest.mark.parametrize('s', [set(), {0}, {1}, {0, 2}, {0, 1, 2}, {5}]) +def test_set_encode_decode_roundtrip(s): + assert MSO0.decode(MSO0.encode(s)) == s + + +def test_set_encoding_is_canonical(): + # the universe rejects trailing zeros, so {0} is `1`, not `100` + assert MSO0.encode({0}) == ['1'] + assert MSO0.encode({0, 2}) == ['1', '0', '1'] + assert MSO0.encode(set()) == [] + + +def test_set_operations(sets): + x, y, z = sets.vars('x y z') + assert ({0, 1}, {1, 2}, {0, 1, 2}) in (x + y).eq(z) # union + assert ({0, 1}, {1, 2}, {1}) in (x * y).eq(z) # intersection + assert ({0, 1}, {1, 2}, {0}) in (x - y).eq(z) # difference + + +def test_subset_and_singletons(sets): + x, y = sets.vars('x y') + assert ({0, 2}, {0, 1, 2}) in x.subset(y) + assert ({0, 2}, {0, 1}) not in x.subset(y) + assert ({1},) in x.sing() + assert ({0, 1},) not in x.sing() + assert ({1}, {0, 1, 2}) in x.member_of(y) + + +def test_enumeration_yields_sets(sets): + x, y = sets.vars('x y') + solutions = list(islice(iter((x + y).eq({0, 1}) & x.subset(y)), 3)) + assert all(isinstance(a, set) and a | b == {0, 1} for a, b in solutions) diff --git a/tests/test_infinite_graphs.py b/tests/test_infinite_graphs.py index 1233921..d370990 100644 --- a/tests/test_infinite_graphs.py +++ b/tests/test_infinite_graphs.py @@ -8,8 +8,10 @@ """ import pytest -from autstr.arithmetic import decode, encode -from autstr.buildin.presentations import BuechiArithmeticZ +from autstr.arithmetic import BuechiArithmeticZ + +encode, decode = BuechiArithmeticZ.encode, BuechiArithmeticZ.decode +from autstr.arithmetic import BuechiArithmeticZ from autstr.infinite_graphs import InfiniteGraph from autstr.symbolic import FunctionCodec diff --git a/tests/test_interpretations.py b/tests/test_interpretations.py index 0cbdef2..4fd7b50 100644 --- a/tests/test_interpretations.py +++ b/tests/test_interpretations.py @@ -3,8 +3,10 @@ import pytest -from autstr.arithmetic import decode, encode -from autstr.buildin.presentations import BuechiArithmeticZ +from autstr.arithmetic import BuechiArithmeticZ + +encode, decode = BuechiArithmeticZ.encode, BuechiArithmeticZ.decode +from autstr.arithmetic import BuechiArithmeticZ from autstr.infinite_graphs import IntegerGrid, InfiniteGraph from autstr.interpretations import interpret from autstr.symbolic import FunctionCodec diff --git a/tests/test_ordinals.py b/tests/test_ordinals.py index af2a9f2..24400ce 100644 --- a/tests/test_ordinals.py +++ b/tests/test_ordinals.py @@ -132,7 +132,7 @@ def test_repr(self, omega_squared): def test_omega_is_the_naturals(self, omega): # dimension 1: the interpretation is a definable reduct of ℕ, so the # order relation is the source's own, minimized - from autstr.buildin.presentations import BuechiArithmetic + from autstr.arithmetic import BuechiArithmetic assert omega.presentation.automata['Lt'].num_states == \ BuechiArithmetic().automata['Lt'].num_states diff --git a/tests/test_presentations.py b/tests/test_presentations.py index 5298437..18c28a1 100644 --- a/tests/test_presentations.py +++ b/tests/test_presentations.py @@ -10,10 +10,9 @@ import pytest -from autstr.buildin.automata import zero -from autstr.buildin.presentations import ( - BuechiArithmetic, BuechiArithmeticZ, MSO0, -) +from autstr.utils.automata_tools import zero +from autstr.arithmetic import BuechiArithmetic, BuechiArithmeticZ +from autstr.powerset import MSO0 from autstr.interpretations import interpret from autstr.presentations import AutomaticPresentation diff --git a/tests/test_serialization.py b/tests/test_serialization.py index bc863cf..6ebfcbf 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -1,6 +1,6 @@ import numpy as np -from autstr.buildin.presentations import BuechiArithmeticZ +from autstr.arithmetic import BuechiArithmeticZ from autstr.sparse_automata import SparseDFA, SparseDFASerializer diff --git a/tests/test_skolem.py b/tests/test_skolem.py index e49e080..3c9a8e5 100644 --- a/tests/test_skolem.py +++ b/tests/test_skolem.py @@ -3,7 +3,7 @@ import pytest -from autstr.buildin.tree_presentations import SkolemArithmetic, _prime +from autstr.tree_arithmetic import SkolemArithmetic, _prime from autstr.sparse_tree_automata import Tree enc = SkolemArithmetic.encode diff --git a/tests/test_symbolic.py b/tests/test_symbolic.py index 4f0abe1..54ff991 100644 --- a/tests/test_symbolic.py +++ b/tests/test_symbolic.py @@ -3,7 +3,7 @@ import pytest -from autstr.buildin.presentations import BuechiArithmeticZ +from autstr.arithmetic import BuechiArithmeticZ from autstr.symbolic import ( CompileError, FunctionCodec, Signature, SymbolicSymbolError, graph_signature, order_signature, relational_signature, @@ -330,11 +330,19 @@ def test_bare_structure_needs_no_signature(): def test_constants_need_a_codec(): - S = BuechiArithmeticZ().symbolic() + # a signature that declares no codec cannot turn a Python value into an + # element, even over a structure whose own signature would + S = BuechiArithmeticZ().symbolic(Signature()) with pytest.raises(SymbolicSymbolError, match='no element codec'): S.const(3) +def test_the_structures_own_signature_brings_a_codec(): + S = BuechiArithmeticZ().symbolic() + x, = S.vars('x') + assert (3,) in x.eq(3) + + # ---------------------------------------------------------------------- # uniformly automatic classes # ---------------------------------------------------------------------- diff --git a/tests/test_symbolic_tree.py b/tests/test_symbolic_tree.py index 5c7fbf8..82f0300 100644 --- a/tests/test_symbolic_tree.py +++ b/tests/test_symbolic_tree.py @@ -8,7 +8,7 @@ import pytest -from autstr.buildin.tree_presentations import SkolemArithmetic +from autstr.tree_arithmetic import SkolemArithmetic from autstr.sparse_tree_automata import convolve_trees from autstr.symbolic import ( CompileError, FunctionCodec, Signature, SymbolicSymbolError, diff --git a/tests/test_tree_interpretations.py b/tests/test_tree_interpretations.py index 789a9b6..f47d4fa 100644 --- a/tests/test_tree_interpretations.py +++ b/tests/test_tree_interpretations.py @@ -16,7 +16,7 @@ import pytest -from autstr.buildin.tree_presentations import SkolemArithmetic +from autstr.tree_arithmetic import SkolemArithmetic from autstr.interpretations import _EQUIV, _tree_representatives, interpret from autstr.sparse_tree_automata import Tree, convolve_trees from autstr.tree_presentations import TreeAutomaticPresentation diff --git a/tests/test_tree_presentations.py b/tests/test_tree_presentations.py index d2019e6..e3965b3 100644 --- a/tests/test_tree_presentations.py +++ b/tests/test_tree_presentations.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from autstr.buildin.presentations import BuechiArithmeticZ +from autstr.arithmetic import BuechiArithmeticZ from autstr.sparse_automata import SparseDFA from autstr.sparse_tree_automata import Tree from autstr.tree_presentations import TreeAutomaticPresentation From dec437a93cd8199b9cd02615bc76df39f97f6ccd Mon Sep 17 00:00:00 2001 From: Faried Abu Zaid Date: Thu, 13 Aug 2026 20:40:02 +0200 Subject: [PATCH 2/9] Notebooks: sets as sets, and a formula that says what its label claims The abelian-groups cell was labelled "has an element of order 4" and asked for an element of order 2: `A(x,x,x)` spells x = 0, so `A(x,x,z) and A(z,z,z)` says 2x = 0. Z/2 alone satisfied it. The corrected formula asks for y = 2x, z = 2y = 0 with x and y both nonzero, which separates the two groups of order 4 -- Z/4 has such an element, Z/2 + Z/2 does not. That cell is also where the symbolic interface earns its place: with `+` and `.eq` the query reads as the mathematics, and a class has no constant symbols, so the identity is named by what defines it (t + t = t). MSO0 gains its codec's payoff: union, intersection and difference as `+`, `*`, `-`, membership and subset as methods, and solutions that come back as Python sets -- the eight ways to split {0,1,2} in two, enumerated from the automaton. Co-Authored-By: Claude Opus 5 --- notebooks/arithmetic_and_algebra.ipynb | 48 ++++++++++++++++++++++++- notebooks/groups.ipynb | 49 ++++++++++++++++++++++---- 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/notebooks/arithmetic_and_algebra.ipynb b/notebooks/arithmetic_and_algebra.ipynb index a2d2057..ec53ce8 100644 --- a/notebooks/arithmetic_and_algebra.ipynb +++ b/notebooks/arithmetic_and_algebra.ipynb @@ -415,7 +415,11 @@ "and order on singletons, form the structure MSO0. A finite set is a $\\{0,1\\}$-word\n", "(position $i$ set iff $i$ is a member), and — by Büchi's theorem — first-order logic\n", "over MSO0 is precisely **monadic second-order** logic over $(\\mathbb{N}, <)$: the\n", - "definable sets are exactly the regular ones." + "definable sets are exactly the regular ones.\n", + "\n", + "The encoding is *canonical*: the universe rejects trailing zeros, so $\\{0\\}$ is the\n", + "word `1` and $\\emptyset$ is the empty word. The structure's codec knows this, so\n", + "ordinary Python sets go in and come out." ] }, { @@ -447,6 +451,48 @@ " mso0.check('exists x.(Sing(x) and all y.((not Sing(y)) or Leq_sing(x,y)))'))" ] }, + { + "cell_type": "markdown", + "id": "4fa93591", + "metadata": {}, + "source": [ + "### Sets as Python sets\n", + "\n", + "Symbolically the Boolean operations are the arithmetic ones: union is `+`, intersection\n", + "`*`, and set difference binary `-`. Membership, subset and singleton-hood are methods." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72e189ba", + "metadata": {}, + "outputs": [], + "source": [ + "S = mso0.symbolic()\n", + "x, y, z = S.vars('x y z')\n", + "\n", + "print('{0,1} u {1,2} = {0,1,2}:', ({0, 1}, {1, 2}, {0, 1, 2}) in (x + y).eq(z))\n", + "print('{0,1} n {1,2} = {1}: ', ({0, 1}, {1, 2}, {1}) in (x * y).eq(z))\n", + "print('{0,1} \\\\ {1,2} = {0}: ', ({0, 1}, {1, 2}, {0}) in (x - y).eq(z))\n", + "print('{0,2} subset {0,1,2}: ', ({0, 2}, {0, 1, 2}) in x.subset(y))\n", + "\n", + "# Solutions come back as sets: the ways to split {0,1,2} into two disjoint pieces.\n", + "disjoint_cover = (x + y).eq({0, 1, 2}) & (x * y).eq(set())\n", + "for a, b in sorted(disjoint_cover, key=lambda p: (len(p[0]), sorted(p[0]))):\n", + " print(f' {a or set()} + {b or set()}')" + ] + }, + { + "cell_type": "markdown", + "id": "e25efbf0", + "metadata": {}, + "source": [ + "Each of those is a first-order query over MSO0, which is to say a **monadic\n", + "second-order** query over $(\\mathbb{N}, <)$ — quantifying over sets here costs no more\n", + "than quantifying over elements does elsewhere." + ] + }, { "cell_type": "markdown", "id": "3da49763", diff --git a/notebooks/groups.ipynb b/notebooks/groups.ipynb index 151f760..a271364 100644 --- a/notebooks/groups.ipynb +++ b/notebooks/groups.ipynb @@ -49,14 +49,51 @@ "from autstr.groups import FiniteAbelianGroups\n", "\n", "ab = FiniteAbelianGroups()\n", - "print('A((1,1),(1,2),(0,0)) in Z2+Z3:', ab.check('A(x,y,z)', [2, 3], x=(1, 1), y=(1, 2), z=(0, 0)))\n", + "print('A((1,1),(1,2),(0,0)) in Z2+Z3:', ab.check('A(x,y,z)', [2, 3], x=(1, 1), y=(1, 2), z=(0, 0)))" + ] + }, + { + "cell_type": "markdown", + "id": "fbb35bc7", + "metadata": {}, + "source": [ + "### The same queries without formula strings\n", "\n", - "# One compiled automaton, evaluated over many groups: \"has an element of order 4\".\n", - "phi = 'exists x.((not A(x,x,x)) and (exists z.(A(x,x,z) and A(z,z,z))))'\n", - "dfa, _ = ab.evaluate(phi)\n", - "for orders in ([4], [3], [2, 3], [5, 6]):\n", + "`symbolic()` hands out variables that compose with Python operators. For an abelian\n", + "class that is `+` and `.eq`; the class has no constants of its own, so the identity is\n", + "named by what defines it, $t + t = t$. One formula compiles **once for the class** and\n", + "then decides any member." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d985b332", + "metadata": {}, + "outputs": [], + "source": [ + "S = ab.symbolic()\n", + "x, y, z = S.vars('x y z')\n", + "\n", + "is_zero = lambda t: (t + t).eq(t) # a class has no constant symbols\n", + "\n", + "# \"has an element of order 4\": y = 2x, z = 2y = 0, with x and y both nonzero\n", + "order_four = ((x + x).eq(y) & (y + y).eq(z) & is_zero(z)\n", + " & ~is_zero(x) & ~is_zero(y))\n", + "\n", + "for orders in ([4], [2, 2], [3], [2, 3], [8]):\n", " print(f'orders={orders}: has an order-4 element =',\n", - " dfa.accepts([(s,) for s in ab.advice(orders)]))" + " S.check_member(order_four, ab.advice(orders)))" + ] + }, + { + "cell_type": "markdown", + "id": "5b736df5", + "metadata": {}, + "source": [ + "The two groups of order 4 come apart here: $\\mathbb{Z}_4$ has such an element and\n", + "$\\mathbb{Z}_2 \\oplus \\mathbb{Z}_2$ does not — decided by running each group's advice\n", + "word through one automaton." ] }, { From e291b40241768832c720219481679b6a033ebd0e Mon Sep 17 00:00:00 2001 From: Faried Abu Zaid Date: Thu, 13 Aug 2026 20:59:40 +0200 Subject: [PATCH 3/9] Interpretations, and three bugs that writing the notebook found The composition notebook becomes "building new structures from old": the integers constructed from the naturals as a 2-dimensional quotient interpretation, which is small enough to read and real enough to be worth saving. It carries the codec story -- the construction is correct but illegible until a codec says that the pair (a, b) denotes a - b -- and closes on the shipped example of the same idea, `autstr.ordinals`. A new notebook, `infinite_structures`, covers what had none: the integer grid, the regular tree, Turing configuration graphs and level 2 collapsible pushdown graphs. The last two are the point. Both present computation as a graph and both have a decidable first-order theory, but reachability is the halting problem for one and a relation of the other -- so `Reach` is there to be queried, and transitivity over all runs of all lengths is decided in under a second. Writing those two turned up three real defects, none of which any test saw: - ENUMERATION WAS BROKEN for every interpreted structure of dimension > 1, including the shipped `Ordinal(2)`. `iterate_language` built each tape by string-concatenating letters, which holds only while a letter is one character; over a product alphabet a letter is a tuple, and str() flattened ('0','1') into text no codec could read. Tapes are tuples of letters now. Membership worked throughout, which is why this went unseen. - SERIALIZING SUCH A PRESENTATION could not be reloaded: JSON has no tuples, so the alphabet came back as a set of lists (unhashable) and the padding symbol as a list. Both are restored on load. - The v2 back-compat loader lost its only test when the `.autstr` artifacts went away -- `test_the_legacy_flat_payload_still_loads` was left building a presentation and querying it, touching no legacy payload at all. It now constructs a v2 payload by hand and reads it. Also: the abelian-groups notebook cell and `is_deterministic`'s docstring, which still said reachability was not built. Co-Authored-By: Claude Opus 5 --- .claude/settings.json | 460 ++++++++++++++++++++++++++++ autstr/collapsible.py | 5 +- autstr/presentations.py | 8 +- autstr/sparse_automata.py | 15 +- autstr/utils/automata_tools.py | 16 +- notebooks/composition.ipynb | 216 ++++++++++++- notebooks/infinite_structures.ipynb | 275 +++++++++++++++++ tests/test_serialization.py | 84 ++++- 8 files changed, 1060 insertions(+), 19 deletions(-) create mode 100644 .claude/settings.json create mode 100644 notebooks/infinite_structures.ipynb diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..be571f1 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,460 @@ +{ + "permissions": { + "allow": [ + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr timeout 1200 /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv-nojax/bin/python -c ' *)", + "WebSearch", + "Bash(PYTHONPATH=. timeout 1800 /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv-nojax/bin/python -m pytest tests/ -q)", + "Bash(git add *)", + "Bash(git commit -m ' *)", + "Bash(git push *)", + "Bash(awk '{print $2}')", + "Read(//home/faried/.config/pypoetry/**)", + "Bash(poetry config *)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv-nojax/bin/pip install *)", + "Bash(git tag *)", + "Bash(git checkout *)", + "Bash(git merge *)", + "Bash(poetry publish *)", + "Bash(curl -s https://pypi.org/pypi/autstr/json)", + "Bash(python3 -m venv /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv-pypi)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv-pypi/bin/pip install *)", + "Bash(ls /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv-nojax/bin/python)", + "Bash(ulimit -v 6000000)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr timeout 600 /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv-nojax/bin/python -u /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/probe_3col.py)", + "Bash(mkdir -p /var/home/faried/projects/AutStr/benchmarks)", + "Read(//tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/tasks/**)", + "Bash(grep -v \"^$\")", + "Bash(rm -f abelian_even_order_curve.png pathwidth_curves.png)", + "Bash(rm -rf __pycache__)", + "Bash(systemd-run --user --scope -p MemoryMax=5G -p MemorySwapMax=0 --quiet env PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv-nojax/bin/python -u /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/readme_examples.py)", + "Bash(git reset *)", + "Bash(git restore *)", + "Bash(git commit -q -m ' *)", + "Bash(python3 -m venv /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv-nojax)", + "Bash(awk '{print $3, $4, $11, $12, $13}')", + "Bash(pkill -f \"pytest /var/home/faried/projects/AutStr/tests/test_tree_automata\")", + "Bash(systemd-run --user --scope -p MemoryMax=4G --quiet env PYTHONPATH=/var/home/faried/projects/AutStr timeout -s ABRT 25 /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv-nojax/bin/python -X faulthandler -c ' *)", + "Bash(timeout 20 /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv-nojax/bin/python -c ' *)", + "Bash(env PYTHONPATH=/var/home/faried/projects/AutStr timeout 45 /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv-nojax/bin/python -m pytest /var/home/faried/projects/AutStr/tests/test_tree_automata.py -x -v)", + "Bash(systemd-run --user --scope -p MemoryMax=4G --quiet env PYTHONPATH=/var/home/faried/projects/AutStr timeout 600 /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv-nojax/bin/python -u -c ' *)", + "Bash(awk '{print $3, $4}')", + "Bash(awk '{print $3, $4, $10}')", + "Bash(python3 -)", + "Bash(systemd-run --user --scope -q -p MemoryMax=4G -p MemorySwapMax=0 /tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a-a4d7-45bc-8b9e-c7c87eb6ef00/scratchpad/venv-nojax/bin/python -u -c ' *)", + "Bash(grep -v \"def \\\\|class \\\\|-> \\\\|:.*SparseTreeAutomaton$\")", + "Read(//tmp/claude-1000/-var-home-faried-projects-AutStr/**)", + "Bash(python3 -c \"import numpy, nltk, pytest; print\\('ok'\\)\")", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/200ddc4a*/scratchpad/venv/bin/python -c \"import numpy,nltk,pytest;print\\('venv ok', numpy.__version__\\)\")", + "Bash(./venv/bin/python -c \"import numpy,nltk,pytest;print\\('venv ok',numpy.__version__\\)\")", + "Bash(PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pytest tests/test_tree_automata.py tests/test_tree_tools.py tests/test_tree_presentations.py tests/test_skolem.py tests/test_tree_uniform.py tests/test_tree_graphs.py tests/test_tree_groups.py -q)", + "Bash(PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pytest tests/test_skolem.py -q)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/pip install *)", + "Bash(cat head.py body.py)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr ./venv/bin/python -c ' *)", + "Bash(PYTHONPATH=. timeout 300 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pytest tests/test_tree_automata.py -q)", + "Bash(PYTHONPATH=. timeout 600 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pytest tests/test_tree_tools.py -q)", + "Bash(PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pytest tests/test_tree_tools.py --collect-only -q)", + "Bash(PYTHONPATH=. timeout 900 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pytest tests/test_tree_presentations.py tests/test_skolem.py -q)", + "Bash(PYTHONPATH=. timeout 900 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pytest tests/test_tree_uniform.py tests/test_tree_graphs.py tests/test_tree_groups.py -q)", + "Bash(PYTHONPATH=. timeout 900 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pytest tests/ -q)", + "Bash(systemd-run --user --scope -q -p MemoryMax=4G -p MemorySwapMax=0 env PYTHONPATH=. AUTSTR_HEAVY=1 timeout 1200 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pytest tests/test_tree_graphs.py -q)", + "Bash(sed -i 's/len\\(grp.presentation.base_alphabet\\)/len\\(sta.base_alphabet\\)/' /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/func3.py)", + "Bash(systemd-run --user --scope -q -p MemoryMax=5G -p MemorySwapMax=0 env PYTHONPATH=. timeout 540 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/func3.py)", + "Bash(pkill -f heavy.py)", + "Bash(pkill -f prof.py)", + "Bash(echo \"exit=$?\")", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pyflakes autstr/sparse_automata.py)", + "Bash(PYTHONPATH=. timeout 600 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pytest tests/test_arithmetic.py -q)", + "Bash(PYTHONPATH=. timeout 1200 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pytest tests/ -q)", + "Bash(PYTHONPATH=. timeout 1200 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pytest tests/ -q --durations=8)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pyflakes autstr/mtbdd.py autstr/sparse_automata.py autstr/sparse_tree_automata.py autstr/utils/automata_tools.py autstr/utils/tree_automata_tools.py)", + "Bash(PYTHONPATH=. timeout 600 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pytest tests/test_tree_tools.py tests/test_tree_presentations.py tests/test_tree_uniform.py tests/test_tree_graphs.py -q)", + "Bash(git commit *)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pyflakes autstr/mtbdd.py autstr/sparse_automata.py)", + "Bash(./venv/bin/python -c \"import numpy,nltk,pytest,graphviz,networkx;print\\('venv rebuilt'\\)\")", + "Bash(sed -n '/def _quotient_tree_nfa/,/^ # ---- rebuild/p' autstr/utils/tree_automata_tools.py)", + "Bash(PYTHONPATH=. timeout 600 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/congr.py)", + "Bash(PYTHONPATH=. timeout 900 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/min2.py)", + "Bash(systemd-run --user --scope -q -p MemoryMax=3G -p MemorySwapMax=0 env PYTHONPATH=. timeout 540 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pytest tests/test_clique_width.py -q)", + "Bash(PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -c ' *)", + "Bash(systemd-run --user --scope -q -p MemoryMax=3G -p MemorySwapMax=0 env PYTHONPATH=.. timeout 540 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python tree_class_benchmark.py --profile light --max-exp 4)", + "Read(//var/home/faried/projects/**)", + "Bash(PYTHONPATH=.. timeout 420 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python uniform_graph_benchmark.py --profile light --max-exp 3)", + "Bash(sed -n '/^## Compiling 3-colourability/,/^## /p' benchmarks/README.md)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -)", + "Bash(PYTHONPATH=.. timeout 900 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/jupyter nbconvert --to notebook --execute --inplace tree_classes.ipynb)", + "Bash(python3 -c ' *)", + "Bash(distrobox list *)", + "Bash(podman ps *)", + "Bash(timeout 300 distrobox enter devbox -- sh -c 'command -v dot || \\(sudo dnf install -y graphviz >/dev/null 2>&1; command -v dot\\)')", + "Bash(timeout 300 distrobox enter devbox -- sh -c 'PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -c \" *)", + "Bash(timeout 1800 distrobox enter devbox -- sh -c 'cd __TRACKED_VAR__/notebooks && for nb in arithmetic mso0 treedepth_graphs; do echo \"--- $nb\"; PYTHONPATH=.. timeout 900 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/jupyter nbconvert --to notebook --execute --inplace $nb.ipynb > /tmp/nbout 2>&1 && echo '\\\\'' ok'\\\\'' || { echo '\\\\'' FAILED'\\\\''; tail -5 /tmp/nbout; }; done')", + "Bash(timeout 600 distrobox enter devbox -- env PYTHONPATH=.. /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/jupyter nbconvert --to notebook --execute --inplace arithmetic.ipynb)", + "Bash(echo \"rc=$?\")", + "Read(//tmp/**)", + "Bash(pkill -9 -f jupyter-nbconvert)", + "Bash(pkill -9 -f ipykernel_launcher)", + "Bash(git status *)", + "Bash(PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -c 'import autstr; print\\('\\\\''autstr'\\\\'', autstr.__version__\\)')", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pyflakes autstr/mtbdd.py autstr/sparse_automata.py tests/test_closure.py)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pyflakes autstr/closure.py tests/test_closure.py)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pyflakes autstr/closure.py autstr/presentations.py)", + "Bash(git mv *)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -m pyflakes autstr/algebra.py autstr/groups.py autstr/composition.py tests/test_algebra.py tests/test_groups.py tests/test_composition.py)", + "Bash(PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -c \"import autstr; print\\('version', autstr.__version__\\)\")", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/sphinx-apidoc -f -o docs/source/api autstr)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/sphinx-build -b html docs/source docs/build/html)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/python -c ' *)", + "Bash(env -u PYTHONPATH timeout 1200 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/pytest -q)", + "Bash(git check-ignore *)", + "Bash(git fetch *)", + "Bash(git rev-list *)", + "Bash(git ls-remote *)", + "Bash(env -u PYTHONPATH /tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/venv/bin/twine check dist/*)", + "Bash(grep -E \"LICENSE|pyproject|composition|\\\\.autstr$\")", + "Bash(env)", + "Bash(command -v gh)", + "Bash(gh auth *)", + "Bash(rm -rf /tmp/smoke)", + "Bash(env -u PYTHONPATH bash -e /tmp/smoke_step.sh)", + "WebFetch(domain:docs.github.com)", + "Bash(git ls-tree *)", + "Bash(timeout 200 distrobox enter devbox -- sh -c 'ls ~/.config/gh 2>/dev/null && cat ~/.config/gh/hosts.yml 2>/dev/null | sed \"s/oauth_token:.*/oauth_token: /\"')", + "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(' latest:', d['info']['version'], '| 3.0.0 present:', '3.0.0' in d['releases']\\)\")", + "Bash(timeout 200 distrobox enter devbox -- sh -c 'cd '\\\\''__TRACKED_VAR__'\\\\'' && gh repo view --json nameWithOwner,defaultBranchRef -q '\\\\''.nameWithOwner + \" default=\" + .defaultBranchRef.name'\\\\'' && echo '\\\\''--- existing releases:'\\\\'' && gh release list --limit 5 && echo '\\\\''--- release for v3.0.0?'\\\\'' && \\(gh release view v3.0.0 >/dev/null 2>&1 && echo EXISTS || echo '\\\\''none \\(good\\)'\\\\''\\)')", + "Bash(timeout 300 distrobox enter devbox -- sh -c 'cd '\\\\''__TRACKED_VAR__'\\\\'' && gh release create v3.0.0 --verify-tag --title '\\\\''AutStr 3.0.0 — tree-automatic structures, MTBDD transitions, composition'\\\\'' --notes-file '\\\\''/tmp/claude-1000/-var-home-faried-projects-AutStr/2d526722-67e6-40eb-831e-8c7cfd3aa6ed/scratchpad/RELEASE_NOTES_3.0.0.md'\\\\''')", + "Bash(timeout 200 distrobox enter devbox -- sh -c 'cd '\\\\''__TRACKED_VAR__'\\\\'' && gh run list --workflow=publish.yaml --limit 3')", + "Bash(timeout 200 distrobox enter devbox -- sh -c 'cd '\\\\''__TRACKED_VAR__'\\\\'' && gh run list --limit 6 --json databaseId,name,event,status,conclusion,headBranch --template '\\\\''{{range .}}{{.databaseId}} {{.name}} [{{.event}}] {{.status}} {{.conclusion}}{{\"\\\\n\"}}{{end}}'\\\\''')", + "Bash(timeout 250 distrobox enter devbox -- sh -c 'cd '\\\\''__TRACKED_VAR__'\\\\'' && echo '\\\\''--- runs with event=release:'\\\\'' && gh run list --event release --limit 5 && echo '\\\\''--- workflows known to GitHub:'\\\\'' && gh workflow list --all && echo '\\\\''--- release object:'\\\\'' && gh release view v3.0.0 --json isDraft,isPrerelease,publishedAt,tagName -q '\\\\''.tagName + \" draft=\" + \\(.isDraft|tostring\\) + \" prerelease=\" + \\(.isPrerelease|tostring\\) + \" publishedAt=\" + \\(.publishedAt|tostring\\)'\\\\''')", + "Bash(timeout 250 distrobox enter devbox -- sh -c 'cd '\\\\''__TRACKED_VAR__'\\\\'' && echo '\\\\''--- workflow object:'\\\\'' && gh api repos/fariedabuzaid/AutStr/actions/workflows/310732983 -q '\\\\''.name + \" state=\" + .state + \" path=\" + .path'\\\\'' && echo '\\\\''--- any run at all for it:'\\\\'' && gh api '\\\\''repos/fariedabuzaid/AutStr/actions/workflows/310732983/runs'\\\\'' -q '\\\\''.total_count'\\\\'' && echo '\\\\''--- most recent 3 runs overall \\(any workflow\\):'\\\\'' && gh api '\\\\''repos/fariedabuzaid/AutStr/actions/runs?per_page=3'\\\\'' -q '\\\\''.workflow_runs[] | .name + \" | \" + .event + \" | \" + .conclusion'\\\\''')", + "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(' latest:', d['info']['version'], '| 3.0.0 present:', '3.0.0' in d['releases']\\)\")", + "Bash(awk '/refs\\\\/tags\\\\/v3.0.0\\\\^\\\\{\\\\}/{print \" \" substr\\($1,1,8\\)}')", + "Bash(curl -s https://pypi.org/simple/autstr/)", + "Bash(curl -s -H 'Cache-Control: no-cache' \"https://pypi.org/pypi/autstr/json\")", + "WebFetch(domain:csl2027.github.io)", + "WebFetch(domain:www.fsttcs.org.in)", + "Bash(./venv/bin/python -c \"import numpy, nltk, pytest; print\\('venv ok'\\)\")", + "Bash(PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad/venv/bin/python -m pytest tests/test_groups.py -k CutRank -x -q)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad/venv/bin/pip install *)", + "Bash(PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad/venv/bin/python -m pytest -q)", + "Bash(PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad/venv/bin/python -m pytest tests/test_tree_groups.py -k CutRank -x -q)", + "Bash(echo \"exit: $?\")", + "Bash(PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad/venv/bin/python -m sphinx -b html -W --keep-going docs/source /tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad/docsbuild)", + "Bash(distrobox enter *)", + "Bash(podman pod *)", + "Bash(podman images *)", + "Bash(curl -sfL -o lipics.zip \"https://submission.dagstuhl.de/styles/download-tag/lipics/v2021.1.3/authors/zip\")", + "Bash(unzip -o -q lipics.zip)", + "Bash(curl -sfL -o lipics.tgz \"https://drops.dagstuhl.de/styles/lipics-v2021/lipics-v2021-authors.tgz\")", + "Bash(tar tzf *)", + "Bash(pdfinfo main.pdf)", + "Bash(python3 -m venv /tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad/venv)", + "Bash(PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad/venv/bin/python -m pytest tests/test_cocycle_groups.py -x -q)", + "Bash(PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad/venv/bin/python -m pytest tests/test_cocycle_groups.py -q --durations=6)", + "Bash(PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad/venv/bin/python -m pytest tests/test_cocycle_groups.py -q)", + "Bash(podman run *)", + "Bash(pdftotext main.pdf -)", + "Bash(PYTHONPATH=. /tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad/venv/bin/python -c ' *)", + "Bash(PYTHONPATH=. timeout 590 /tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad/venv/bin/python -m pytest tests/test_cocycle_groups.py -k Automaton -x -q --durations=3)", + "Bash(ps -eo pid,rss,comm,args --no-headers)", + "Bash(awk '$3 ~ /python/ && $0 ~ /pytest|cocycle/ {print $1, $2, $3}')", + "Bash(SP=/tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad && cat $SP/../tasks/bvtn4phrx.output 2>/dev/null | tail -5; echo ---; tail -4 $SP/heavy_run.log 2>/dev/null)", + "Read(//tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/tasks/**)", + "Bash(systemctl --user list-units --type=scope --no-legend)", + "Bash(SP=/tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad && cat $SP/heavy_run.log; echo \"===\"; cat $SP/../tasks/bvtn4phrx.output 2>/dev/null | tail -3; cat $SP/../tasks/bp55opj0b.output 2>/dev/null | tail -3)", + "Bash(grep -n 'every layout compatible\\\\|total rank of the alternating\\\\|inverses are\\\\|suppressing structural\\\\|row bases of $F_y\\\\|a row of $F_g\\(J\\)$\\\\|hat Q'\\\\''\\\\|co-located\\\\|p + 2$ letters\\\\|prod\\\\\\\\text{-terms\\\\|the run is the word\\\\|dihedral and quaternion\\\\|finitely supported\\\\|right inverse\\\\|n/2 - O\\\\|n/2 -\\\\|width $\\\\\\\\ge n/2' main.tex)", + "Bash(pdftotext -f 1 -l 1 main.pdf -)", + "Bash([ -s $SP/../tasks/b5t7mh2n0.output ])", + "Bash(SP=/tmp/claude-1000/-var-home-faried-projects-AutStr/5da92948-4a90-4ba2-a6be-bb695acdc5d4/scratchpad && cat $SP/../tasks/b5t7mh2n0.output)", + "Bash(pkill -f twosided_search.py)", + "Bash(pdfinfo scratch-chainring.pdf)", + "Bash(pdftotext scratch-chainring.pdf -)", + "Bash(cd /var/home/faried/projects/AutStr/paper && rm -f main.aux main.bbl main.blg main.out main.pdf main.fdb_latexmk main.fls main.log main.vtc && echo \"cleaned\")", + "Bash(grep -viE \"^$\")", + "Bash(timeout 150 poetry run python -c ' *)", + "Bash(timeout 60 poetry run python -c ' *)", + "Bash(poetry run *)", + "Bash(echo \"EXIT=$? DONE\")", + "Bash(grep -nE \"^#{1,4} \" README.md)", + "Bash(sed -n '/\\\\[build-system\\\\]/,/^\\\\[/p' pyproject.toml)", + "Bash(sed -n '/dependencies = \\\\[/,/\\\\]/p' pyproject.toml)", + "Bash(git archive *)", + "Bash(timeout 200 poetry run python benchmarks/implicit_vs_explicit.py --budget-seconds 130 --mem-gb 16 --explicit-timeout 18 --implicit-timeout 15 --max-n-word 3 --max-n-tree 2 --out /tmp/claude-1000/-var-home-faried-projects-AutStr/bf14bf1b-57bf-4218-a192-43611006ec2b/scratchpad/bench_smoke3.md)", + "Bash(git commit -q -m 'Benchmark: sweep width r, center k and depth d; add 4-quantifier query *)", + "Bash(git -C /var/home/faried/projects/AutStr status)", + "Bash(python -m pytest tests/test_groups.py tests/test_tree_groups.py tests/test_cocycle_groups.py -x -q)", + "Bash(ls /var/home/faried/projects/AutStr/.venv/bin 2>/dev/null | head -5; which python3; ls ~/.venvs 2>/dev/null)", + "Read(//home/faried/**)", + "Bash(ls -d /tmp/claude-1000/-var-home-faried-projects-AutStr/*/scratchpad/venv 2>/dev/null | head; ls /tmp/claude-1000/-var-home-faried-projects-AutStr/ 2>/dev/null | head)", + "Bash(python3 -m venv /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/pip -q install nltk pytest numpy)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -c \"import numpy, nltk, pytest; print\\('deps ok'\\)\")", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/test_groups.py tests/test_tree_groups.py tests/test_cocycle_groups.py -q)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/pip -q install graphviz)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -c \"import ast; ast.parse\\(open\\('autstr/cocycle_groups.py'\\).read\\(\\)\\); print\\('syntax ok'\\)\")", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/bytecompat.py)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/test_cocycle_groups.py -q)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/ringcheck.py)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -)", + "Bash(ps -eo pid,etime,pcpu,args --no-headers)", + "Bash(awk '$4 ~ /python/ && /scratchpad/ {print $1, $2, $3}')", + "Bash(timeout 280 /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -u -)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/ -q)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/pip -q install networkx)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/test_groups.py tests/test_implicit.py -q)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/test_groups.py::TestCutRankGroups::test_validation -q)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/test_groups.py -q)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/test_tree_groups.py tests/test_cocycle_groups.py tests/test_implicit.py -q)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/test_tree_groups.py::TestCutRankTreeGroups::test_width_guard -q)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/test_tree_groups.py -q)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/ -q -W error::Warning --co -q)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/test_arithmetic.py -q)", + "Bash(git -C /var/home/faried/projects/AutStr status --short)", + "Bash(git -C /var/home/faried/projects/AutStr diff --stat)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/test_implicit.py -q)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/test_rank_width.py -q)", + "Bash(timeout 200 /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -u -)", + "Bash(grep -v \"^\\\\s*$\")", + "Bash(ps -eo pid,etime,pcpu,comm --no-headers)", + "Bash(awk '/sphinx|python3/ && $3 > 50 {print}')", + "Bash(grep -n '`s\\\\b\\\\|`s ' autstr/implicit.py)", + "Bash(git -C /var/home/faried/projects/AutStr remote -v)", + "Bash(git -C /var/home/faried/projects/AutStr branch -a)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -c \"import yaml,sys; d=yaml.safe_load\\(open\\('.github/workflows/run_docs.yaml'\\)\\); print\\('yaml ok — jobs:', list\\(d['jobs']\\)\\); print\\('perms:', d['true'] if 'true' in d else d.get\\('permissions'\\)\\)\")", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/pip -q install pyyaml)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -c ' *)", + "Bash(pdfinfo paper/main.pdf)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -c \"import re; d=open\\('paper/main.log','rb'\\).read\\(\\).decode\\('utf8','ignore'\\); import re; m=re.findall\\(r'Output written on main.pdf \\\\\\(\\(\\\\d+\\) page', d\\); print\\('pages:', m\\)\")", + "Bash(git -C /var/home/faried/projects/AutStr add paper/main.tex)", + "Bash(git -C /var/home/faried/projects/AutStr commit -m ' *)", + "Bash(git -C /var/home/faried/projects/AutStr log --oneline -1)", + "Bash(timeout 500 /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -u -)", + "Bash(timeout 400 /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -u -)", + "Bash(timeout 250 /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -u -)", + "Bash(git -C /var/home/faried/projects/AutStr add paper/main.tex autstr/groups.py tests/test_groups.py FINALIZING.md)", + "Bash(timeout 550 /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -u -)", + "Bash(/tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -u -)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/test_tree_groups.py tests/test_cocycle_groups.py tests/test_implicit.py tests/test_groups.py -q)", + "Bash(git -C /var/home/faried/projects/AutStr add autstr/tree_groups.py tests/test_tree_groups.py FINALIZING.md)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/ -q -x --ignore=tests/test_cocycle_groups.py)", + "Bash(git -C /var/home/faried/projects/AutStr add autstr/cocycle_groups.py tests/test_cocycle_groups.py paper/main.tex FINALIZING.md)", + "Bash(git -C /var/home/faried/projects/AutStr add README.md)", + "Bash(git -C /var/home/faried/projects/AutStr log --oneline -12)", + "Bash(git -C /var/home/faried/projects/AutStr ls-files paper/)", + "Bash(mkdir -p /var/home/faried/projects/rankwidth-groups-paper)", + "Bash(cp /var/home/faried/projects/AutStr/paper/.gitignore /var/home/faried/projects/AutStr/paper/cc-by.pdf /var/home/faried/projects/AutStr/paper/lipics-logo-bw.pdf /var/home/faried/projects/AutStr/paper/lipics-v2021.cls /var/home/faried/projects/AutStr/paper/main.tex /var/home/faried/projects/AutStr/paper/orcid.pdf /var/home/faried/projects/AutStr/paper/refs.bib /var/home/faried/projects/AutStr/paper/scratch-chainring.tex /var/home/faried/projects/AutStr/paper/theorem3-notes.md /var/home/faried/projects/rankwidth-groups-paper/)", + "Bash(echo \"scratch-chainring.pdf\")", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/a44e92b8-fc14-40a7-8f08-315be4b8cd46/scratchpad/venv/bin/python -m pytest tests/test_groups.py tests/test_chain_ring.py -q)", + "Bash(git -C /var/home/faried/projects/AutStr rm --cached -q .claude/settings.json)", + "Bash(git -C /var/home/faried/projects/AutStr commit --amend --no-edit -q)", + "Bash(git -C /var/home/faried/projects/AutStr show --stat HEAD)", + "Bash(git -C /var/home/faried/projects/AutStr add autstr/cocycle_groups.py)", + "Bash(distrobox enter devbox -- sh -c 'cd ~/projects/AutStr && git add poetry.lock uv.lock pyproject.toml && git commit -q -m \"Rebuild lock files; reconcile sphinx-autodoc-typehints pin *)", + "Bash(python -)", + "Bash(python -c ' *)", + "Bash(python -m pytest tests/test_chain_ring.py tests/test_cocycle_groups.py tests/test_tree_groups.py tests/test_rank_width.py -q)", + "Bash(/var/home/faried/projects/AutStr/.venv/bin/python -m pytest tests/test_chain_ring.py tests/test_cocycle_groups.py tests/test_tree_groups.py tests/test_rank_width.py -q)", + "Bash(~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_chain_ring.py tests/test_cocycle_groups.py tests/test_tree_groups.py tests/test_rank_width.py tests/test_tree_graphs.py -q)", + "Bash(~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_chain_ring.py -q)", + "Bash(~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_tree_groups.py tests/test_rank_width.py -q -x)", + "Bash(git config *)", + "Read(//var/home/faried/**)", + "Bash(flatpak list *)", + "Bash(rpm -q code)", + "Bash(git -C /var/home/faried/projects/AutStr branch --show-current)", + "Bash(timeout 300 .venv/bin/python -c ' *)", + "Bash(python3 -c \"import numpy\")", + "Read(//var/home/faried/.cache/pypoetry/virtualenvs/**)", + "Bash(timeout 600 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -c ' *)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -c ' *)", + "Bash(timeout 120 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -c ' *)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/b85bf2ed-1f4c-4f2e-8358-3348f99e2d8d/scratchpad/t1.py)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -u /tmp/claude-1000/-var-home-faried-projects-AutStr/b85bf2ed-1f4c-4f2e-8358-3348f99e2d8d/scratchpad/t1.py)", + "Bash(timeout 1800 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_symbolic.py -q)", + "Bash(timeout 600 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -u -c ' *)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -u -c ' *)", + "Bash(timeout 1800 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_arithmetic.py tests/test_symbolic.py -q)", + "Bash(timeout 3000 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/ -q -x --timeout=600)", + "Bash(timeout 3000 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/ -q)", + "Bash(timeout 60 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -c ' *)", + "Bash(timeout 60 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_symbolic.py -q)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_symbolic.py tests/test_arithmetic.py -q)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python scripts/gen_readme_media.py)", + "Bash(awk '{print \"still running, cpu-time:\", $10}')", + "Bash(awk '{print \"cpu-time:\", $10}')", + "Bash(timeout 300 uv sync --dry-run)", + "Bash(timeout 900 uv sync)", + "Bash(timeout 900 uv sync --python 3.13)", + "Bash(.venv/bin/python -c ' *)", + "Bash(timeout 900 .venv/bin/python -m pytest tests/test_symbolic.py tests/test_arithmetic.py -q)", + "Bash(.venv/bin/python -c \"import jax\")", + "Bash(command -v dot)", + "Bash(rm -rf /tmp/uvcheck)", + "Bash(.venv/bin/python -c \"import sys,numpy,nltk;print\\('python',sys.version.split\\(\\)[0],'| numpy',numpy.__version__\\)\")", + "Bash(timeout 60 .venv/bin/python -c ' *)", + "Bash(timeout 900 .venv/bin/python -u -c ' *)", + "Bash(chmod +x drive.sh)", + "Bash(./drive.sh)", + "Bash(tee sweep.csv)", + "Bash(python3 -c \"import autstr\")", + "Bash(chmod +x drive_b.sh)", + "Bash(./drive_b.sh)", + "Bash(tee sweep_b.csv)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 180 env PYTHONPATH=/var/home/faried/projects/AutStr /var/home/faried/projects/AutStr/.venv/bin/python3 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d9a6131-5471-4026-977d-846f68bb33e8/scratchpad/verify_instance.py)", + "Bash(python3 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d9a6131-5471-4026-977d-846f68bb33e8/scratchpad/edit_nb.py)", + "Bash(.venv/bin/python3 -m pytest --version)", + "Bash(.venv/bin/python3 -c \"import pytest_timeout; print\\('pytest-timeout available'\\)\")", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 2400 .venv/bin/python3 -m pytest tests/ -v --durations=20 -p no:cacheprovider)", + "Bash(echo \"EXIT=$?\")", + "Bash(.venv/bin/python3 -c \"import nbstripout; print\\('installed'\\)\")", + "Bash(timeout 180 .venv/bin/python3 -m pre_commit run --all-files)", + "Read(//var/home/linuxbrew/.linuxbrew/Cellar/git-lfs/3.7.1/bin/**)", + "Read(//home/faried/.linuxbrew/bin/**)", + "Bash(git-lfs version *)", + "Bash(git lfs *)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 600 .venv/bin/python3 -m pytest tests/test_symbolic_tree.py -x -q -p no:cacheprovider)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 900 .venv/bin/python3 -m pytest tests/test_symbolic_tree.py -q -p no:cacheprovider)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 2400 .venv/bin/python3 -m pytest tests/ -q -p no:cacheprovider)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 900 .venv/bin/python3 -m pytest tests/test_tree_automata.py -q -p no:cacheprovider)", + "Bash(.venv/bin/python3 -c ' *)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=2G -- timeout 300 .venv/bin/python3 -m pytest tests/test_symbolic_tree.py -q -p no:cacheprovider)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=2G -- timeout 300 .venv/bin/python3 -m pytest tests/test_symbolic_tree.py tests/test_tree_automata.py -q -p no:cacheprovider)", + "Bash(sed -i 's/ The tree engine has no counterpart yet for enumeration, which raises with\\\\n the reason rather than quietly answering a different question./X/' autstr/symbolic/backends.py)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 600 .venv/bin/python3 -c ' *)", + "Bash(sed -i '52s/.*/from autstr.sparse_tree_automata import SparseTreeAutomaton, Tree/' autstr/utils/tree_automata_tools.py)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 300 .venv/bin/python3 -c ' *)", + "Bash(sed -i 's/^from autstr.sparse_tree_automata import \\(/from autstr.utils.tree_automata_tools import _shortlex_key, iterate_trees\\\\nfrom autstr.sparse_tree_automata import \\(/' tests/test_tree_automata.py)", + "Bash(sed -i 's/ for letter in sorted\\(alphabet\\) if letter != padding_symbol/ for letter in sorted\\(alphabet\\)/' autstr/utils/tree_automata_tools.py)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 600 .venv/bin/python3 -m pytest tests/test_symbolic_tree.py -q -p no:cacheprovider)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=2G -- timeout 300 .venv/bin/python3 -m pytest tests/test_tree_tools.py -q -p no:cacheprovider)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 900 .venv/bin/python3 -m pytest tests/test_tree_uniform.py -q -p no:cacheprovider)", + "Bash(sed -i 's/^from autstr.sparse_tree_automata import SparseTreeAutomaton, Tree$/from autstr.sparse_tree_automata import SparseTreeAutomaton, Tree\\\\nfrom autstr.symbolic import FunctionCodec, Signature/' tests/test_tree_uniform.py)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 900 .venv/bin/python3 -c ' *)", + "Bash(.venv/bin/python3 -c \"from autstr.symbolic import operation_signature, EQUALITY_SYMBOLS; print\\('exports ok', EQUALITY_SYMBOLS\\)\")", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 900 .venv/bin/python3 -m pytest tests/test_symbolic.py tests/test_symbolic_tree.py -q -p no:cacheprovider)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 1500 .venv/bin/python3 -c ' *)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -- timeout 600 .venv/bin/python3 -c ' *)", + "Bash(.venv/bin/python3 -c \"from autstr.symbolic import operation_signature, EQUALITY_SYMBOL; print\\('ok', EQUALITY_SYMBOL\\)\")", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -- timeout 900 .venv/bin/python3 -c ' *)", + "Bash(.venv/bin/python3 -c \"import autstr.tree_graphs, autstr.graphs, autstr.algebra; print\\('modules import ok'\\)\")", + "Bash(.venv/bin/python3 -c \"import autstr.tree_graphs, autstr.graphs, autstr.algebra, autstr.groups; print\\('modules import ok'\\)\")", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 900 .venv/bin/python3 -m pytest tests/test_equality.py -q -p no:cacheprovider)", + "Bash(.venv/bin/python3 -c \"import sphinx, myst_nb; print\\('sphinx', sphinx.__version__\\)\")", + "Bash(.venv/bin/python3 -c \"import sphinx; print\\('sphinx', sphinx.__version__\\)\")", + "Bash(.venv/bin/python3 -)", + "Bash(which uv *)", + "Read(//home/linuxbrew/.linuxbrew/bin/**)", + "Bash(uv venv *)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 900 env PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/2d9a6131-5471-4026-977d-846f68bb33e8/scratchpad/docsvenv/bin/python -m sphinx.ext.apidoc -f -o /tmp/claude-1000/-var-home-faried-projects-AutStr/2d9a6131-5471-4026-977d-846f68bb33e8/scratchpad/apidoc autstr)", + "Bash(echo \"apidoc EXIT=$?\")", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 1200 env PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/2d9a6131-5471-4026-977d-846f68bb33e8/scratchpad/docsvenv/bin/python -m sphinx -b html --keep-going -D nb_execution_mode=off docs/source /tmp/claude-1000/-var-home-faried-projects-AutStr/2d9a6131-5471-4026-977d-846f68bb33e8/scratchpad/docsbuild)", + "Bash(PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/2d9a6131-5471-4026-977d-846f68bb33e8/scratchpad/docsvenv/bin/python -c 'import autstr.algebra')", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -- timeout 900 env PYTHONPATH=/var/home/faried/projects/AutStr /tmp/claude-1000/-var-home-faried-projects-AutStr/2d9a6131-5471-4026-977d-846f68bb33e8/scratchpad/docsvenv/bin/python -m sphinx -b html --keep-going -D nb_execution_mode=off docs/source /tmp/claude-1000/-var-home-faried-projects-AutStr/2d9a6131-5471-4026-977d-846f68bb33e8/scratchpad/docsbuild3)", + "Bash(grep WARNING __TRACKED_VAR__/docsbuild3.log)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 900 .venv/bin/python3 -m pytest tests/test_infinite_extraspecial.py -q -p no:cacheprovider)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -- timeout 600 .venv/bin/python3 -m pytest tests/test_infinite_extraspecial.py -q -p no:cacheprovider)", + "WebFetch(domain:arxiv.org)", + "WebFetch(domain:www.irif.fr)", + "WebFetch(domain:d-nb.info)", + "Bash(pdftotext webfetch-1786476185248-qqkzxh.pdf -)", + "Bash(pdftotext webfetch-1786476190531-ui46ls.pdf -)", + "Bash(git switch *)", + "Bash(git pull *)", + "Bash(.venv/bin/python3 -c \"from autstr.symbolic import graph_signature; print\\('exported:', graph_signature.__name__\\)\")", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 600 .venv/bin/python3 -m pytest tests/test_infinite_graphs.py -q -p no:cacheprovider)", + "Bash(.venv/bin/python3 -c \"from autstr.arithmetic import encode, decode; print\\('codec fns ok:', encode\\(7\\), decode\\(encode\\(7\\)\\)\\)\")", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 900 .venv/bin/python3 -m pytest tests/test_integer_grid.py -q -p no:cacheprovider)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 600 .venv/bin/python3 -m pytest tests/test_integer_grid.py -q -p no:cacheprovider)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 900 .venv/bin/python3 -m pytest tests/test_interpretations.py -q -p no:cacheprovider)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -- timeout 500 .venv/bin/python3 -c ' *)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -- timeout 400 .venv/bin/python3 -c ' *)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 900 .venv/bin/python3 -m pytest tests/test_interpretations.py tests/test_automata_tools.py -q -p no:cacheprovider)", + "Bash(kill %1)", + "Bash(pkill -f \"pytest tests/test_interpretations\")", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 400 .venv/bin/python3 -m pytest tests/test_interpretations.py tests/test_automata_tools.py -q -p no:cacheprovider)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -- timeout 300 .venv/bin/python3 -c ' *)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -p MemorySwapMax=0 -- timeout 600 .venv/bin/python3 -m pytest tests/test_interpretations.py tests/test_automata_tools.py tests/test_integer_grid.py -q -p no:cacheprovider)", + "Bash(systemd-run --user --scope --quiet -p MemoryMax=3G -- timeout 300 .venv/bin/python3 /tmp/claude-1000/-var-home-faried-projects-AutStr/2d9a6131-5471-4026-977d-846f68bb33e8/scratchpad/fold_diag_repro.py)", + "Bash(ls /tmp/claude-1000/-var-home-faried-projects-AutStr/*/scratchpad/ 2>/dev/null)", + "Bash(python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/batch_repro.py)", + "Read(//var/home/faried/.local/share/uv/python/cpython-3.13.14-linux-x86_64-gnu/bin/**)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/batch_repro.py)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/kdim_repro.py)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/kdim_quot.py)", + "Bash(sed -i 's/from autstr.buildin.presentations import BuechiArithmeticN, BuechiArithmeticZ/from autstr.buildin.presentations import BuechiArithmetic/; s/^SRC = SRC$/SRC = BuechiArithmetic/' /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/kdim_quot.py)", + "Bash(sed -i 's/^SRC = BuechiArithmeticN$/SRC = BuechiArithmetic/' /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/kdim_quot.py)", + "Bash(.venv/bin/python -m pytest tests/ -q -p no:randomly)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/scan.py)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/scan2.py 2418 2456)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/scan2.py 2380 2418)", + "Bash(.venv/bin/python -)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/scan2.py 2316 2362)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/diag_bug.py)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/diag_bug2.py)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/witness.py)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/witness2.py)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/src_level.py)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/universe.py)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/kdim_fresh.py)", + "Bash(.venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/compare.py)", + "Bash(.venv/bin/python scripts/gen_builtin_presentations.py)", + "Bash(timeout 600 .venv/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/cbe3da38-e60d-426a-b319-7761c21ba7e9/scratchpad/quot2.py)", + "Bash(timeout 900 .venv/bin/python -m pytest tests/test_presentations.py tests/test_interpretations.py -q -p no:cacheprovider)", + "Bash(timeout 2400 .venv/bin/python -m pytest tests/ -q -p no:cacheprovider)", + "Read(//home/faried/.claude/**)", + "Bash(~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/ -q -x --timeout=600)", + "Bash(~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/ -q)", + "Bash(~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -c ' *)", + "Bash(time ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -c ' *)", + "Bash(time ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_ordinals.py -q)", + "Bash(~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_ordinals.py -q)", + "Bash(timeout 300 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_regular_tree.py -q)", + "Bash(timeout 600 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_presentations.py -q)", + "Bash(~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -)", + "Bash(timeout 300 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -c ' *)", + "Bash(timeout 600 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_turing.py -q)", + "Bash(timeout 300 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_regular_tree.py tests/test_integer_grid.py tests/test_infinite_graphs.py -q)", + "Bash(ps -eo pid,stat,etime,time,%cpu,%mem,rss,comm --sort=-%mem)", + "Bash(timeout 600 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_automata_tools.py -q)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python /tmp/claude-1000/-var-home-faried-projects-AutStr/8ea117be-ee5c-4752-bcca-214821a7c09a/scratchpad/probe_sentences.py)", + "Bash(~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pyflakes autstr/interpretations.py)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_tree_tools.py -q)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_tree_interpretations.py -q)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_tree_interpretations.py tests/test_interpretations.py tests/test_tree_presentations.py tests/test_symbolic_tree.py -q)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_tree_presentations.py -q)", + "WebFetch(domain:eiche.theoinf.tu-ilmenau.de)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_tree_interpretations.py tests/test_interpretations.py -q)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_ordinals.py -q)", + "Bash(timeout 900 ~/.cache/pypoetry/virtualenvs/autstr-Ff0G8Q62-py3.13/bin/python -m pytest tests/test_tree_algebra.py -q)", + "Bash(git branch *)", + "Bash(gh pr *)", + "WebFetch(domain:ar5iv.labs.arxiv.org)", + "Bash(curl -sL -o k.tar.gz \"https://arxiv.org/e-print/1303.2453\")", + "Bash(mkdir -p kartzow)", + "Bash(tar xzf *)", + "Bash(git rebase *)", + "Bash(ps -eo pid,etime,args --no-headers)", + "Bash(awk '$3 ~ /pytest|python/ && $0 !~ /awk/')", + "Bash(pdftotext -layout kw.pdf kw.txt)", + "Bash(git cherry-pick *)", + "Bash(awk -F: '$1 > 1600 && $1 < 2400')", + "Bash(curl -sL -o thesis.tar.gz \"https://arxiv.org/e-print/1202.0137\")", + "Bash(mkdir -p thesis)", + "Bash(awk -F: '$1 > 639 && $1 < 2396')", + "Bash(timeout 300 distrobox enter devbox -- bash -lc 'PYTHONPATH=. .venv/bin/python -u /tmp/claude-1000/-var-home-faried-projects-AutStr/f653607e-ac8b-43c1-8960-acd739422fab/scratchpad/summary_check.py')", + "Bash(timeout 900 distrobox enter devbox -- bash -lc 'PYTHONPATH=. .venv/bin/python -u /tmp/claude-1000/-var-home-faried-projects-AutStr/f653607e-ac8b-43c1-8960-acd739422fab/scratchpad/random_check.py')", + "Bash(sed -i 's/for trial in range\\(60\\):/for trial in range\\(200\\):/; s/60 systems/200 systems/' random_check.py)", + "Bash(timeout 1800 distrobox enter devbox -- bash -lc 'PYTHONPATH=. .venv/bin/python -u /tmp/claude-1000/-var-home-faried-projects-AutStr/f653607e-ac8b-43c1-8960-acd739422fab/scratchpad/random_check2.py')", + "Bash(awk '{print} /missing/ && !/0 missing, 0 extra/ {bad++} END {print \"systems with disagreements:\", bad+0}')", + "Bash(timeout 2400 distrobox enter devbox -- bash -lc 'PYTHONPATH=. .venv/bin/python -u /tmp/claude-1000/-var-home-faried-projects-AutStr/f653607e-ac8b-43c1-8960-acd739422fab/scratchpad/d_check.py')" + ], + "additionalDirectories": [ + "/var/home/faried/projects/AutStr", + "/var/home/faried/projects/AutStr/paper", + "/dev", + "/var/home/faried/projects/rankwidth-groups-paper" + ] + } +} diff --git a/autstr/collapsible.py b/autstr/collapsible.py index aadfc2b..7bed383 100644 --- a/autstr/collapsible.py +++ b/autstr/collapsible.py @@ -1028,8 +1028,9 @@ def get_relation_symbols(self): return self.presentation.get_relation_symbols() def is_deterministic(self) -> bool: - """Whether every configuration has at most one successor — first-order, - and so decidable, even though reachability is not built here.""" + """Whether every configuration has at most one successor — a + first-order property of the one-step relation, so it costs nothing + beyond `E`.""" return self.presentation.check( 'all x.(all y.(all z.((E(x,y) & E(x,z)) -> Eq(y,z))))') diff --git a/autstr/presentations.py b/autstr/presentations.py index eda24a2..c0fe5e5 100644 --- a/autstr/presentations.py +++ b/autstr/presentations.py @@ -105,10 +105,14 @@ def _parse_payload(cls, payload: bytes): dfa_bytes = bytes(dfa_bytes_list) automata[name] = SparseDFASerializer.from_bytes(dfa_bytes) - # Reconstruct presentation + # Reconstruct presentation. Over a product alphabet the padding symbol + # is a tuple, and JSON has no tuples, so it comes back as a list. + padding_symbol = metadata["padding_symbol"] + if isinstance(padding_symbol, list): + padding_symbol = tuple(padding_symbol) return AutomaticPresentation( automata, - padding_symbol=metadata["padding_symbol"], + padding_symbol=padding_symbol, enforce_consistency=False ) diff --git a/autstr/sparse_automata.py b/autstr/sparse_automata.py index 3cf8a94..2c4b065 100644 --- a/autstr/sparse_automata.py +++ b/autstr/sparse_automata.py @@ -49,6 +49,17 @@ def step(states, symbols): # - Acceptance array # - Transition diagrams: var / lo / hi / term arrays, then one root per state +def _alphabet_from_json(raw) -> set: + """The base alphabet as stored in a payload. + + JSON has no tuples, so a product alphabet's letters -- which is what an + interpreted structure of dimension > 1 has -- come back as lists. Restore + them, or the set() below raises on the first unhashable letter. + """ + return {tuple(letter) if isinstance(letter, list) else letter + for letter in json.loads(raw)} + + class SparseDFASerializer: VERSION = 3 # diagram payload (v2 stored flat exception rows) HEADER_FORMAT = "4sB3sII" @@ -132,7 +143,7 @@ def _parse_payload(cls, payload: bytes, version: int = None) -> 'SparseDFA': num_states, num_nodes, start_state, symbol_arity, alphabet_len = \ struct.unpack(cls.METADATA_FORMAT, payload[:cls.METADATA_SIZE]) offset = cls.METADATA_SIZE - base_alphabet = set(json.loads(payload[offset:offset + alphabet_len])) + base_alphabet = _alphabet_from_json(payload[offset:offset + alphabet_len]) offset += alphabet_len is_accepting = np.frombuffer(payload, dtype=np.uint8, count=num_states, @@ -157,7 +168,7 @@ def _parse_payload_v2(cls, payload: bytes) -> 'SparseDFA': num_states, max_exceptions, start_state, symbol_arity, alphabet_len = \ struct.unpack(cls.METADATA_FORMAT, payload[:cls.METADATA_SIZE]) offset = cls.METADATA_SIZE - base_alphabet = set(json.loads(payload[offset:offset + alphabet_len])) + base_alphabet = _alphabet_from_json(payload[offset:offset + alphabet_len]) offset += alphabet_len defaults = np.frombuffer(payload, dtype=np.uint32, count=num_states, diff --git a/autstr/utils/automata_tools.py b/autstr/utils/automata_tools.py index 27fd973..f80ecf7 100644 --- a/autstr/utils/automata_tools.py +++ b/autstr/utils/automata_tools.py @@ -538,15 +538,19 @@ def iterate_language(dfa: SparseDFA, decoder: Callable = None, heap = LengthLexHeap() for state in start_set: if state in nonempty: - # Represent words as tuple of empty strings - heap.push((tuple(["" for _ in range(arity)]), state)) - + # One tape per argument, each a tuple of letters. Letters are + # kept as they are rather than concatenated into a string: an + # interpreted structure's alphabet is a product alphabet, whose + # letters are themselves tuples, and str() would flatten them + # into unparseable text. + heap.push((tuple([() for _ in range(arity)]), state)) + def cat(word, symbol): - """Concatenate symbol to word based on direction.""" + """Extend a tape by one letter, on the side we are building from.""" if backward: - return str(symbol) + word + return (symbol,) + word else: - return word + str(symbol) + return word + (symbol,) def push(heap, word_tuple, sym_enc, next_state): """Push a new word onto the heap with the given extension symbol and next state.""" diff --git a/notebooks/composition.ipynb b/notebooks/composition.ipynb index 122edbf..00d94f5 100644 --- a/notebooks/composition.ipynb +++ b/notebooks/composition.ipynb @@ -5,14 +5,17 @@ "id": "dc56966b", "metadata": {}, "source": [ - "# Composing presentations\n", + "# Building new structures from old\n", "\n", "Automatic structures over a shared signature are closed under **disjoint union**\n", "and **direct products**; uniformly automatic classes are closed under **union**\n", - "and under taking all **finite direct products** of their members.\n", + "and under taking all **finite direct products** of their members. They are also\n", + "closed under **first-order interpretation** — defining one structure inside\n", + "another by formulas.\n", "\n", - "`autstr.composition` performs these constructions: each takes presentations and\n", - "returns a presentation, so the result is queried exactly like any other." + "`autstr.composition` and `autstr.interpretations` perform these constructions:\n", + "each takes presentations and returns a presentation, so the result is queried\n", + "exactly like any other." ] }, { @@ -181,6 +184,211 @@ "the cyclic groups; it predates the module, which is a good sign the abstraction\n", "is the right one." ] + }, + { + "cell_type": "markdown", + "id": "a0f1d87e", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## First-order interpretations\n", + "\n", + "An interpretation defines a structure *inside* another one. You give\n", + "\n", + "- a **domain formula** $\\delta(\\bar x)$ saying which tuples of the source are\n", + " elements of the new structure — so an element is a $k$-tuple, and $k$ is the\n", + " interpretation's **dimension**;\n", + "- a formula per **relation**, over $k$ coordinates per argument;\n", + "- optionally an **equivalence** $\\varepsilon(\\bar x, \\bar y)$, and then the\n", + " elements are its classes.\n", + "\n", + "`interpret` does not merely record this: it *computes* the presentation, so the\n", + "result carries real automata and answers queries at full speed.\n", + "\n", + "The textbook example is the construction of the integers from the naturals. A\n", + "pair $(a, b)$ stands for $a - b$, two pairs denote the same integer when\n", + "$a + d = c + b$, and the order is $a + d < c + b$. That is a 2-dimensional\n", + "quotient interpretation of $(\\mathbb{N}, +, <)$ — and here it is." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff03cfd1", + "metadata": {}, + "outputs": [], + "source": [ + "from autstr.arithmetic import BuechiArithmetic\n", + "from autstr.interpretations import interpret\n", + "\n", + "N = BuechiArithmetic() # (N, +, <, |2); A(x,y,z) is x + y = z\n", + "pair = ['x0', 'x1', 'y0', 'y1']\n", + "\n", + "SAME = 'exists z.(A(x0,y1,z) and A(y0,x1,z))' # a + d = c + b\n", + "LESS = ('exists s.(exists t.(A(x0,y1,s) and A(y0,x1,t) '\n", + " 'and Lt(s,t)))') # a + d < c + b\n", + "\n", + "Z = interpret(\n", + " N,\n", + " domain=('Eq(x0,x0) and Eq(x1,x1)', ['x0', 'x1']), # every pair is an element\n", + " relations={'Lt': (LESS, pair), 'Eq': (SAME, pair)},\n", + " dimension=2,\n", + " quotient=(SAME, pair),\n", + ")\n", + "\n", + "print('relations:', Z.get_relation_symbols())\n", + "print('no least element (unlike N):', Z.check('exists x.(all y.((not Lt(y,x))))'))\n", + "print('discrete, not dense: ',\n", + " not Z.check('all x.(all y.(Lt(x,y) -> exists z.(Lt(x,z) and Lt(z,y))))'))" + ] + }, + { + "cell_type": "markdown", + "id": "a772b548", + "metadata": {}, + "source": [ + "### A codec says what the elements are\n", + "\n", + "The construction is now correct but illegible: an element of `Z` is a *pair of\n", + "naturals*, and queries hand back the words encoding those pairs. A **codec** is\n", + "the missing half — a pair of functions between Python values and the words that\n", + "represent them. Give one in a `Signature`, and the structure speaks integers.\n", + "\n", + "Every structure the library ships declares its own this way, which is why\n", + "`symbolic()` needs no argument on them. For a structure you build yourself,\n", + "this is what you write." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53a41a45", + "metadata": {}, + "outputs": [], + "source": [ + "from autstr.symbolic import FunctionCodec, Signature\n", + "\n", + "PAD = '*'\n", + "\n", + "def encode(n):\n", + " \"\"\"n as the pair (a, b) with n = a - b, folded into pair letters.\"\"\"\n", + " a, b = (n, 0) if n >= 0 else (0, -n)\n", + " wa, wb = N.encode(a), N.encode(b) # the source structure's own codec\n", + " width = max(len(wa), len(wb))\n", + " return [(wa[i] if i < len(wa) else PAD, wb[i] if i < len(wb) else PAD)\n", + " for i in range(width)]\n", + "\n", + "def decode(word):\n", + " return (N.decode([letter[0] for letter in word])\n", + " - N.decode([letter[1] for letter in word]))\n", + "\n", + "S = Z.symbolic(Signature(codec=FunctionCodec(encode, decode))\n", + " .operator('lt', 'Lt')\n", + " .operator('eq', 'Eq'))\n", + "x, y = S.vars('x y')\n", + "\n", + "print('-2 < 3 :', (-2, 3) in x.lt(y))\n", + "print(' 3 < -2:', (3, -2) in x.lt(y))\n", + "print('solutions of x < 2:', [t for t, _ in zip(iter(x.lt(2)), range(7))])" + ] + }, + { + "cell_type": "markdown", + "id": "87aa2f42", + "metadata": {}, + "source": [ + "The quotient picks one representative per class — over words, the shortlex-least\n", + "member — so each integer is enumerated once. Over *trees* no order is\n", + "well-founded, and the representative is instead the least **description** of the\n", + "class (Kuske & Weidner); `interpret` handles both, since a tree presentation\n", + "goes in and a tree presentation comes out.\n", + "\n", + "This is how `autstr.ordinals` is built: the ordinals below $\\omega^n$ are three\n", + "formulas over Büchi arithmetic — the reverse-lexicographic order on Cantor\n", + "coefficients — with no automaton authored by hand, and a codec that writes an\n", + "ordinal as its coefficient tuple." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8051d42", + "metadata": {}, + "outputs": [], + "source": [ + "from autstr.ordinals import Ordinal\n", + "\n", + "w2 = Ordinal(2) # the ordinals below omega^2\n", + "a, b = w2.symbolic().vars('a b')\n", + "\n", + "# an ordinal is its Cantor coefficients, most significant first:\n", + "# (1, 0) is omega, (0, 5) is 5, (1, 1) is omega + 1\n", + "print('5 < omega: ', ((0, 5), (1, 0)) in a.lt(b))\n", + "print('omega < 5: ', ((1, 0), (0, 5)) in a.lt(b))\n", + "print('omega+1 < omega*2:', ((1, 1), (2, 0)) in a.lt(b))\n", + "print('below omega + 2: ', [t for t, _ in zip(iter(a.lt((1, 2))), range(5))])" + ] + }, + { + "cell_type": "markdown", + "id": "3123dc4e", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Saving what you built\n", + "\n", + "Every construction on this page *computes* automata, and some of them are not\n", + "cheap. A presentation therefore serializes: `automatic_presentation_to_file`\n", + "writes the whole thing — universe, every relation, the alphabet — and\n", + "`automatic_presentation_from_file` reads it back.\n", + "\n", + "The payload stores each automaton's **transition diagrams** rather than a flat\n", + "`symbol → target` table, so a relation over a convolution alphabet too wide to\n", + "enumerate still writes out in the size of its diagrams." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46b0fc0c", + "metadata": {}, + "outputs": [], + "source": [ + "import tempfile, os\n", + "from autstr.presentations import AutomaticPresentation\n", + "\n", + "path = os.path.join(tempfile.mkdtemp(), 'integers.autstr')\n", + "Z.automatic_presentation_to_file(path)\n", + "print(f'{os.path.getsize(path)} bytes on disk')\n", + "\n", + "reloaded = AutomaticPresentation.automatic_presentation_from_file(path)\n", + "print('relations:', reloaded.get_relation_symbols())\n", + "\n", + "# the reloaded structure answers the same questions\n", + "R = reloaded.symbolic(Signature(codec=FunctionCodec(encode, decode))\n", + " .operator('lt', 'Lt').operator('eq', 'Eq'))\n", + "a, b = R.vars('a b')\n", + "print('-2 < 3 after a round trip:', (-2, 3) in a.lt(b))\n", + "print('still discrete:',\n", + " not reloaded.check('all x.(all y.(Lt(x,y) -> exists z.(Lt(x,z) and Lt(z,y))))'))" + ] + }, + { + "cell_type": "markdown", + "id": "2bda71d3", + "metadata": {}, + "source": [ + "What a file does **not** carry is the signature: a codec is Python code, not\n", + "data, so a reloaded presentation is a bare structure until you hand it one\n", + "again. Structures that ship with the library declare their own, which is why\n", + "`BuechiArithmeticZ().symbolic()` needs no argument while this one does.\n", + "\n", + "Serialization is a *string*-engine facility. Tree presentations — Skolem\n", + "arithmetic, the atomless Boolean algebra, the collapsible pushdown graphs —\n", + "have no file format yet; rebuild those from their constructors." + ] } ], "metadata": { diff --git a/notebooks/infinite_structures.ipynb b/notebooks/infinite_structures.ipynb new file mode 100644 index 0000000..def8d32 --- /dev/null +++ b/notebooks/infinite_structures.ipynb @@ -0,0 +1,275 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "722dd063", + "metadata": {}, + "source": [ + "# Infinite graphs, and how far first-order logic reaches\n", + "\n", + "The structures in the other notebooks are infinite but tame: arithmetic, orders,\n", + "lattices. This one is about infinite **graphs** — the Cayley graph of\n", + "$\\mathbb{Z}^n$, the infinite $k$-regular tree, the configuration graph of a\n", + "Turing machine, and the configuration graph of a level 2 collapsible pushdown\n", + "system.\n", + "\n", + "The last two are the interesting pair. Both present *computation* as a graph:\n", + "vertices are machine configurations, edges are single steps. Both have a\n", + "decidable first-order theory. But **reachability** — is there a run of any\n", + "length from here to there — is undecidable for the Turing machine and decidable\n", + "for the collapsible pushdown system, and that difference is visible in what the\n", + "library can hand you.\n", + "\n", + "> **Variable naming.** The formula parser treats a name as a variable only if it\n", + "> matches `[a-df-z][0-9]*` — a single lowercase letter (never `e`, which is the\n", + "> parser's event marker), optionally followed by digits." + ] + }, + { + "cell_type": "markdown", + "id": "c6515bc9", + "metadata": {}, + "source": [ + "## The integer grid\n", + "\n", + "$\\mathbb{Z}^n$ with its Cayley graph: vertices are integer points, and two are\n", + "adjacent when they differ by one in a single coordinate. It is built as an\n", + "**asynchronous product** of $n$ copies of $(\\mathbb{Z}, +)$ — the product that\n", + "advances exactly one coordinate and holds the rest, which is precisely a grid\n", + "step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c26dad9f", + "metadata": {}, + "outputs": [], + "source": [ + "from autstr.infinite_graphs import IntegerGrid\n", + "\n", + "grid = IntegerGrid(2)\n", + "S = grid.symbolic()\n", + "x, y = S.vars('x y')\n", + "\n", + "print('(0,0) — (0,1):', ((0, 0), (0, 1)) in x.adj(y))\n", + "print('(0,0) — (1,1):', ((0, 0), (1, 1)) in x.adj(y), ' (diagonals are not edges)')\n", + "print('symmetric: ', grid.is_symmetric())\n", + "\n", + "neighbours = [b for (a, b), _ in zip(iter(x.eq((0, 0)) & x.adj(y)), range(4))]\n", + "print('neighbours of the origin:', neighbours)" + ] + }, + { + "cell_type": "markdown", + "id": "1b5bde03", + "metadata": {}, + "source": [ + "Vertices are Python tuples because the structure ships a **codec**: the points\n", + "go in and come out as `(a, b)`, while underneath each is a word over a product\n", + "alphabet." + ] + }, + { + "cell_type": "markdown", + "id": "bc7fa921", + "metadata": {}, + "source": [ + "## The regular tree\n", + "\n", + "The infinite $k$-regular tree $T_k$: vertices are finite words over $k$ letters,\n", + "each with $k$ children. It carries the child relation, the successors `S0`…`S{k-1}`\n", + "individually, and the **prefix order** — an ancestor relation, which is what makes\n", + "it more than a graph." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a3213a81", + "metadata": {}, + "outputs": [], + "source": [ + "from autstr.infinite_graphs import RegularTree\n", + "\n", + "tree = RegularTree(2)\n", + "print('relations:', tree.get_relation_symbols())\n", + "\n", + "T = tree.symbolic()\n", + "u, v = T.vars('u v')\n", + "print('root — its 0-child: ', ((), (0,)) in u.adj(v))\n", + "print('root is an ancestor: ', tree.check('exists u.(all v.(Prefix(u,v)))'))\n", + "print('nobody is their own child:',\n", + " tree.check('all u.(all v.(Child(u,v) -> (not Eq(u,v))))'))" + ] + }, + { + "cell_type": "markdown", + "id": "19a84ad5", + "metadata": {}, + "source": [ + "## Turing machines: where first-order logic stops\n", + "\n", + "A configuration is a state, a tape and a head position; one step is an edge. The\n", + "step relation is automatic — it rewrites a bounded window of the tape — so the\n", + "whole configuration graph is an automatic structure and its first-order theory\n", + "is decidable.\n", + "\n", + "What you may **not** ask is whether one configuration reaches another. That is\n", + "the halting problem, and no amount of cleverness puts it inside first-order logic\n", + "over this graph. So `autstr.turing` gives you `E` and `Halt`, and stops there." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eacf10c5", + "metadata": {}, + "outputs": [], + "source": [ + "from autstr.turing import TuringMachine, Configuration\n", + "\n", + "# two states, flipping a bit and stepping right, then back\n", + "flipper = TuringMachine({('a', '0'): ('b', '1', 'R'),\n", + " ('a', '1'): ('b', '0', 'R'),\n", + " ('b', '0'): ('a', '0', 'L'),\n", + " ('b', '1'): ('a', '1', 'L')}, blank='0')\n", + "\n", + "machine = flipper.configuration_graph()\n", + "print('relations: ', machine.get_relation_symbols())\n", + "print('deterministic:', machine.is_deterministic())\n", + "\n", + "start = Configuration('a', ('1', '0'), 0)\n", + "after = flipper.step(start)\n", + "print('one step:', start.state, start.tape, start.head, '->',\n", + " after.state, after.tape, after.head)\n", + "\n", + "M = machine.symbolic()\n", + "p, q = M.vars('p q')\n", + "print('and that step is an edge:', (start, after) in p.adj(q))" + ] + }, + { + "cell_type": "markdown", + "id": "74a3bcf4", + "metadata": {}, + "source": [ + "Determinism *is* first-order — \"every configuration has at most one\n", + "successor\" quantifies over one step, not over runs — so it costs nothing beyond\n", + "`E`. Reachability would quantify over runs of unbounded length, and that is\n", + "exactly the line first-order logic does not cross." + ] + }, + { + "cell_type": "markdown", + "id": "c742b33d", + "metadata": {}, + "source": [ + "## Collapsible pushdown graphs: where reachability comes back\n", + "\n", + "A **level 2 collapsible pushdown system** has a stack of stacks. It can clone\n", + "the topmost word, push a letter carrying a *collapse link* back to the stack it\n", + "was created over, pop, and **collapse** — jump straight back to that stack,\n", + "however far away it now is. These systems generate the level 2 graphs of the\n", + "pushdown hierarchy.\n", + "\n", + "Their configuration graphs are **tree**-automatic rather than string-automatic\n", + "(Kartzow), and that is not a technicality: their MSO theory is undecidable, so\n", + "the tree encoding is the only automatic route to them. A stack of stacks is a\n", + "tree of blocks, and each stack operation is a bounded rewrite at the end of the\n", + "tree's last path.\n", + "\n", + "The payoff is the contrast with the Turing machine above. Here **reachability is\n", + "first-order definable** — a relation of the graph like any other — so you may\n", + "ask about runs of unbounded length." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e6a9790", + "metadata": {}, + "outputs": [], + "source": [ + "from autstr.collapsible import Level2CPS\n", + "\n", + "# clone the topmost word, then pop it back off\n", + "system = Level2CPS([('0', None, 'c', '1', 'clone'),\n", + " ('1', None, 'o', '0', 'pop 2')], symbols=('a',))\n", + "graph = system.configuration_graph()\n", + "\n", + "print('relations:', graph.get_relation_symbols())" + ] + }, + { + "cell_type": "markdown", + "id": "82d062e7", + "metadata": {}, + "source": [ + "`Reach` is in that list but not yet built: it is **exponential in the number of\n", + "control states**, so it is declared up front and compiled the first time a query\n", + "mentions it. That deferral is the opt-out — nothing pays for it until it is\n", + "asked for.\n", + "\n", + "Kartzow's construction is a decomposition rather than a fixpoint. Every run\n", + "splits into four stretches — whole words come off the stack, then letters come\n", + "off, then letters go back on, then words go back on — and each stretch is its\n", + "own automaton. All four relations are *reflexive*, so composing them excludes\n", + "nothing, and reachability is the first-order formula\n", + "\n", + "$$\\mathrm{Reach}(x,y) \\;=\\; \\exists d\\, \\exists f\\, \\exists g.\\;\n", + "A(x,d) \\wedge B(d,f) \\wedge C(f,g) \\wedge D(g,y)$$\n", + "\n", + "over the four — no fifth automaton, and no fixpoint over trees." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1dc4b5b", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "t = time.perf_counter()\n", + "reach = graph.presentation.relation('Reach') # built on first use\n", + "print(f'Reach: {reach.num_states} states, built in {time.perf_counter() - t:.1f}s')\n", + "\n", + "# properties of runs of ANY length -- the questions the Turing graph cannot answer\n", + "print('reflexive: ', graph.check('all x.(Reach(x,x))'))\n", + "print('contains one step:', graph.check('all x.(all y.(E(x,y) -> Reach(x,y)))'))\n", + "print('transitive: ', graph.check(\n", + " 'all x.(all y.(all z.((Reach(x,y) & Reach(y,z)) -> Reach(x,z))))'))" + ] + }, + { + "cell_type": "markdown", + "id": "daec2974", + "metadata": {}, + "source": [ + "Transitivity is worth pausing on. It is a statement about *all* configurations\n", + "and *all* runs between them, decided by automata operations in well under a\n", + "second — and it is not a test that was passed, it is a theorem that was proved.\n", + "\n", + "Constraining the labels a run may read is `reach_along`, which builds Kartzow's\n", + "$\\mathrm{Reach}_L$ by putting the label automaton into a **product system**\n", + "whose ordinary reachability is the answer. An $\\varepsilon$-contraction — any\n", + "number of silent steps followed by one visible one — is the special case of that\n", + "with a two-state label automaton." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/test_serialization.py b/tests/test_serialization.py index 6ebfcbf..c6fdf72 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -1,3 +1,7 @@ +import json +import struct +import zlib + import numpy as np from autstr.arithmetic import BuechiArithmeticZ @@ -52,6 +56,80 @@ def test_round_trip_of_a_multi_tape_automaton(self): assert np.array_equal(reloaded.dense_next(), dfa.dense_next()) def test_the_legacy_flat_payload_still_loads(self): - """BuechiArithmeticZ ships a version-2 file (flat exception rows).""" - presentation = BuechiArithmeticZ() - assert presentation.check('exists x.(A(x,x,x))') # 0 + 0 = 0 + """Version 2 stored a default target plus flat exception rows, before + transitions became shared diagrams. Files written by those versions + must still load, so the payload is built here by hand -- the library + no longer ships one to read. + """ + # a two-state automaton over {0, 1, *}: accepts words ending in 1 + alphabet = sorted({'*', '0', '1'}) + code = {letter: i for i, letter in enumerate(alphabet)} + num_states, max_exceptions = 2, 1 + defaults = np.array([0, 0], dtype=np.uint32) # on anything else + ex_symbols = np.array([[code['1']], [code['1']]], dtype=np.int32) + ex_states = np.array([[1], [1]], dtype=np.int32) + is_accepting = np.array([0, 1], dtype=np.uint8) + + alphabet_json = json.dumps(alphabet).encode('utf-8') + payload = b''.join([ + struct.pack("IIIII", num_states, max_exceptions, 0, 1, + len(alphabet_json)), + alphabet_json, + defaults.tobytes(), + ex_symbols.tobytes(), + ex_states.tobytes(), + is_accepting.tobytes(), + ]) + data = struct.pack("4sB3sII", b'SDFA', 2, b'\0\0\0', + zlib.crc32(payload), len(payload)) + payload + + dfa = SparseDFASerializer.from_bytes(data) + + assert dfa.num_states == 2 + assert dfa.base_alphabet == {'*', '0', '1'} + assert dfa.accepts([('1',)]) + assert dfa.accepts([('0',), ('1',)]) + assert not dfa.accepts([('1',), ('0',)]) + + +class TestProductAlphabet: + """A presentation whose letters are tuples. + + An interpretation of dimension k > 1 encodes an element as a word over the + k-fold product alphabet, so both the alphabet's letters and the padding + symbol are tuples. JSON has no tuples: without restoring them on load, the + alphabet is a set of lists (unhashable) and the padding symbol is a list. + """ + + def _integers_from_naturals(self): + from autstr.arithmetic import BuechiArithmetic + from autstr.interpretations import interpret + pair = ['x0', 'x1', 'y0', 'y1'] + same = 'exists z.(A(x0,y1,z) and A(y0,x1,z))' + less = 'exists s.(exists t.(A(x0,y1,s) and A(y0,x1,t) and Lt(s,t)))' + return interpret( + BuechiArithmetic(), + domain=('Eq(x0,x0) and Eq(x1,x1)', ['x0', 'x1']), + relations={'Lt': (less, pair), 'Eq': (same, pair)}, + dimension=2, quotient=(same, pair)) + + def test_round_trip_through_a_file(self, tmp_path): + from autstr.presentations import AutomaticPresentation + presentation = self._integers_from_naturals() + path = str(tmp_path / 'integers.autstr') + presentation.automatic_presentation_to_file(path) + + reloaded = AutomaticPresentation.automatic_presentation_from_file(path) + + assert set(reloaded.get_relation_symbols()) == \ + set(presentation.get_relation_symbols()) + assert isinstance(reloaded.padding_symbol, tuple) + assert all(isinstance(letter, tuple) for letter in reloaded.sigma) + # the order it presents is still the order of the integers: + # no least element, and discrete rather than dense + assert not reloaded.check( + 'exists x.(all y.((not Lt(y,x)) and (not Eq(x,y))))') + assert not reloaded.check( + 'all x.(all y.(Lt(x,y) -> exists z.(Lt(x,z) and Lt(z,y))))') + assert _same_language(reloaded.automata['Lt'], + presentation.automata['Lt'], max_length=3) From 5a74b35bdd600380c5a9d746a90884710d5ee17e Mon Sep 17 00:00:00 2001 From: Faried Abu Zaid Date: Thu, 13 Aug 2026 21:03:04 +0200 Subject: [PATCH 4/9] Notebooks: the atomless Boolean algebra, and MSO without the paren soup The countable atomless Boolean algebra had no notebook coverage. It sits next to the finite Boolean algebras, where the contrast is the point: finite ones are powersets and full of atoms, and dropping finiteness leaves exactly one countable algebra in which every nonzero element splits forever. Its elements are trees, so it is the tree engine's turn. Bipartiteness in the graphs notebook now appears twice: as the formula string, and built from `symbolic()` with `.implies`, `.all` and `.drop`. A set variable is just a variable, which is what makes the query monadic second-order, and the symbolic form makes that legible instead of nesting six parentheses. Checked to agree with the string form on all three of that cell's graphs. `infinite_structures` joins the documentation's notebook toctree. Co-Authored-By: Claude Opus 5 --- docs/source/index.rst | 1 + notebooks/arithmetic_and_algebra.ipynb | 57 ++++++++++++++++++++++++++ notebooks/graphs.ipynb | 31 ++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index 522f437..de3e520 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -18,6 +18,7 @@ reference is generated from the docstrings. :caption: Notebooks notebooks/arithmetic_and_algebra + notebooks/infinite_structures notebooks/graphs notebooks/groups notebooks/composition diff --git a/notebooks/arithmetic_and_algebra.ipynb b/notebooks/arithmetic_and_algebra.ipynb index ec53ce8..a824141 100644 --- a/notebooks/arithmetic_and_algebra.ipynb +++ b/notebooks/arithmetic_and_algebra.ipynb @@ -348,6 +348,63 @@ " dfa.accepts([(s,) for s in advice]))" ] }, + { + "cell_type": "markdown", + "id": "77fa08cf", + "metadata": {}, + "source": [ + "## The countable atomless Boolean algebra\n", + "\n", + "Every *finite* Boolean algebra is a powerset, so it is full of **atoms** —\n", + "minimal nonzero elements. Drop finiteness and something else becomes possible: a\n", + "Boolean algebra in which every nonzero element splits, forever. There is exactly\n", + "one countable such algebra up to isomorphism, and it is the clopen algebra of\n", + "Cantor space.\n", + "\n", + "Its elements are *sets of binary strings* — the cylinders a clopen set contains —\n", + "so an element is a finite tree, and the presentation lives on the **tree**\n", + "engine. Meet, join and complement are `*`, `+` and unary `-`; the lattice order\n", + "is `.leq`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26a35f81", + "metadata": {}, + "outputs": [], + "source": [ + "from autstr.tree_algebra import AtomlessBooleanAlgebra\n", + "\n", + "B = AtomlessBooleanAlgebra()\n", + "S = B.symbolic()\n", + "x, y, z = S.vars('x y z')\n", + "\n", + "# the cylinder \"strings starting 0\" and its complement partition the space\n", + "print('0 join 1 = everything:', ({'0'}, {'1'}, {''}) in (x + y).eq(z))\n", + "print('complement of 0 is 1: ', ({'0'}, {'1'}) in (-x).eq(y))\n", + "print('00 <= 0: ', ({'00'}, {'0'}) in x.leq(y))" + ] + }, + { + "cell_type": "markdown", + "id": "de601520", + "metadata": {}, + "source": [ + "The defining property is a *sentence*, so it is something to decide rather than\n", + "to assert: every nonzero element strictly contains a nonzero element." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e249058", + "metadata": {}, + "outputs": [], + "source": [ + "print('atomless:', B.is_atomless())" + ] + }, { "cell_type": "markdown", "id": "f9cf091f", diff --git a/notebooks/graphs.ipynb b/notebooks/graphs.ipynb index 97f7b9a..87c5dc1 100644 --- a/notebooks/graphs.ipynb +++ b/notebooks/graphs.ipynb @@ -115,6 +115,37 @@ " print(f'{name:30s} bipartite = {cls.check(bipartite, TreeDepthGraph.from_networkx(G))}')" ] }, + { + "cell_type": "markdown", + "id": "b4aff901", + "metadata": {}, + "source": [ + "The same query without the parenthesis soup: `symbolic()` hands out variables and\n", + "relation symbols, and `.implies`, `.all` and `.drop` build the quantifiers. A set\n", + "variable is just a variable — `Subset(x, c)` reads it as a set, which is what makes\n", + "this monadic *second*-order rather than first." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41259a4b", + "metadata": {}, + "outputs": [], + "source": [ + "S = cls.symbolic()\n", + "c, x, y = S.vars('c x y')\n", + "E, Subset = S.rel('E'), S.rel('Subset')\n", + "\n", + "bipartite_sym = (E(x, y).implies((Subset(x, c) & ~Subset(y, c))\n", + " | (~Subset(x, c) & Subset(y, c)))\n", + " ).all([x, y]).drop(c)\n", + "\n", + "for name, G in cases.items():\n", + " print(f'{name:30s} bipartite = '\n", + " f'{S.check_member(bipartite_sym, cls.advice(TreeDepthGraph.from_networkx(G)))}')" + ] + }, { "cell_type": "markdown", "id": "5826682f", From af1b5a417768b506ad718fcbc977fcae35c4eda8 Mon Sep 17 00:00:00 2001 From: Faried Abu Zaid Date: Thu, 13 Aug 2026 21:09:01 +0200 Subject: [PATCH 5/9] v4.0.0: the changelog moves to a file of its own The README carried the whole release history, which made it long and made the history shallow -- there was never room to say what a release actually changed. CHANGELOG.md takes it over, with v4.0.0 written out properly: what was added, what moved, what was fixed, and an upgrade path from 3.x. The docs build copies it in, so it has a page on the site too. What stays in the README is what only the README can say: the AI-assisted algorithm engineering narrative, the gource animation, the highlights of the newest version, and a link to the rest. The quick start now opens on the symbolic interface -- which is the honest entry point, and was also the one broken example in the file, since it still imported a class that the arithmetic rewrite deleted a release ago. Version 4.0.0 rather than 3.2: `autstr.buildin` is gone, `autstr.arithmetic` holds something else, and the old term-algebra front end has no replacement of the same shape. Co-Authored-By: Claude Opus 5 --- .gitignore | 1 + CHANGELOG.md | 270 ++++++++++++++++++++++++++++++++++++++++++ README.md | 151 ++++++++++------------- autstr/_version.py | 2 +- docs/source/conf.py | 4 + docs/source/index.rst | 6 + pyproject.toml | 2 +- 7 files changed, 345 insertions(+), 91 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.gitignore b/.gitignore index 89c79c8..bcc1ca9 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,4 @@ docs/source/api/ docs/source/notebooks/ docs/source/jupyter_execute/ docs/source/_media/ +docs/source/changelog.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d0d8c39 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,270 @@ +# Changelog + +All notable changes to AutStr are recorded here. Versions follow +[semantic versioning](https://semver.org): the major number changes when +existing code has to be edited to keep working. + +Each release since v2.0 is also a snapshot of an experiment in AI-assisted +algorithm engineering — see the [README](https://github.com/fariedabuzaid/AutStr#readme) for that story. + +--- + +## v4.0.0 — August 2026 + +**The symbolic release.** Queries stop being strings, structures start +declaring their own vocabulary, and the package stops pretending some of its +structures are more "built in" than others. + +### Added + +**A generic first-order layer — `autstr.symbolic`.** +Variables, relation symbols and function symbols that compose with ordinary +Python operators, compiled against either engine and against uniformly +automatic classes: + +```python +x, y, z = BuechiArithmeticZ().symbolic().vars('x y z') +phi = (x + y).eq(z) & z.lt(100) +phi.check() # satisfiable? +(3, 4, 7) in phi # membership +list(phi.drop(z)) # enumerate, smallest first +``` + +- `Signature` declares which relations are function graphs, which Python + operator each binds to, and — via an `ElementCodec` — how Python values + encode as elements. Terms nest (`((x + y) + z).eq(10)`), arities come from + the automata, and variable names survive into results. +- Quantifiers (`.all`, `.drop`, `.exinf`), Boolean connectives, `is_finite`, + membership, and lazy enumeration in length-lexicographic order. +- The same expressions compile against a class, where `check_member` and + `evaluate_member` decide one member — explicitly or implicitly. +- Ready-made signature builders: `operation_signature`, `order_signature`, + `graph_signature`, `relational_signature`. + +**Every structure declares its own vocabulary.** +`symbolic()` now takes no argument on every structure the library ships: +each provides `default_signature`, so operators and a codec are there from the +start. Büchi arithmetic over ℕ and over ℤ and the MSO0 powerset structure +gained theirs in this release; MSO0's is new capability rather than +convenience — union, intersection and difference as `+`, `*`, `-`, with +solutions returned as Python sets. + +**First-order interpretations — `autstr.interpretations`.** +`interpret` *computes* the interpreted presentation from a domain formula, a +formula per relation, an optional dimension *k* (elements are *k*-tuples) and +an optional definable equivalence: + +- quotients over words take the shortlex-least member of a class as its + representative; +- quotients over **trees** take the least *description*, since no order on + trees is well-founded (Kuske & Weidner) — the same idea made to work without + a well-order; +- `representatives` exposes the chosen representative of each class. + +**Infinite graphs — `autstr.infinite_graphs`, `autstr.turing`.** +`InfiniteGraph` is the shared surface (edge relation, symmetry check, codec); +concrete members are `IntegerGrid` (the Cayley graph of ℤⁿ, built as an +asynchronous product), `RegularTree` (T_k with successors and the prefix +order), and `ConfigurationGraph` for a Turing machine — whose first-order +theory is decidable while reachability, being halting, is not. + +**Ordinals — `autstr.ordinals`.** The ordinals below ω^ω on the string engine +and below ω^(ω^n) on the tree engine, each *interpreted* in arithmetic rather +than authored: three formulas over Büchi arithmetic, with the +reverse-lexicographic order on Cantor coefficients. + +**The countable atomless Boolean algebra — `autstr.tree_algebra`.** The clopen +algebra of Cantor space, with meet, join and complement as terms, and +`is_atomless` as a decided sentence rather than an assertion. + +**Level 2 collapsible pushdown graphs — `autstr.collapsible`.** +Stacks of stacks with collapse links, presented by Kartzow's tree encoding: +blocks hang off each other as right children, links are recovered from the +tree's shape rather than stored, and every stack operation is a bounded rewrite +at the end of the last path. MSO over these graphs is undecidable, so the tree +encoding is the only automatic route to them. + +**Reachability for those graphs — `autstr.collapsible_reach`.** +`Reach` is a relation of the configuration graph, so a first-order formula may +ask about runs of *any* length — the question a Turing machine's configuration +graph cannot answer. Following Kartzow's decomposition, every run splits into +four stretches and reachability is the first-order formula + + Reach(x,y) = ∃d ∃f ∃g. A(x,d) ∧ B(d,f) ∧ C(f,g) ∧ D(g,y) + +over four reflexive relations — no fifth automaton and no fixpoint over trees. +Underneath, `Summaries` computes per-word returns and loops by saturation +(the paper's own effectiveness argument routes through µ-calculus model +checking, which is not needed), and the nondeterministic guesses of Kartzow's +automata are carried on annotation tapes that are projected away — the move +MONA makes. `reach_along` builds the label-constrained `Reach_L` by putting +the label automaton into a product system; ε-contraction is a special case. + +**Deferred relations.** A presentation may declare a relation now and build it +on first use. `get_relation_symbols()` lists it, a query that mentions it +triggers the build. This is how `Reach` avoids costing anything until asked +for, and it is the opt-out for every expensive relation. + +**Smaller additions.** `partial_dfa` / `partial_tree_automaton`; +`restrict_alphabet`; `fold_tapes` for tree automata; `tree_order` and +`domain_within`; sentences usable as operands of a connective; an `Eq` +relation on every presentation. + +### Changed + +- **`autstr.buildin` is gone.** Every structure the library ships is built in, + so the name carved nothing. Its contents moved to where their subject lives: + + | was | now | + |---|---| + | `autstr.buildin.presentations` | `autstr.arithmetic` (Büchi ℕ and ℤ) and `autstr.powerset` (MSO0) | + | `autstr.buildin.tree_presentations` | `autstr.tree_arithmetic` (Skolem arithmetic) | + | `autstr.buildin.automata` | `autstr.utils.automata_tools` | + +- **`autstr.arithmetic` is a different module.** It holds the two Büchi + presentations themselves; the old term-algebra front end + (`VariableETerm` and friends) is replaced by the symbolic layer. +- `encode` / `decode` are now static methods on the presentations that own + them. +- Documentation builds execute every notebook, and the repository keeps them + output-free. + +### Fixed + +- **Enumeration over a product alphabet.** Every interpreted structure of + dimension > 1 — including the shipped `Ordinal(2)` — produced unusable + results from `iter(...)`: tapes were built by string-concatenating letters, + which holds only while a letter is a single character, so a tuple letter was + flattened into text no codec could read. Membership was unaffected, which is + why it went unseen. +- **Serializing such a presentation.** JSON has no tuples, so a reloaded + product alphabet was a set of lists (unhashable) and the padding symbol a + list. Both are restored on load. +- A universal quantifier that silently became an existential. +- Sentence markers that dropped the structure's alphabet. +- Relation automata are restricted to the universe on *every* tape, not all but + the last: a relation that still held of a non-element made universal + sentences false on encodings that were not elements at all. +- Spliced relations are restored even when a query fails. + +### Removed + +- **The precompiled `.autstr` artifacts.** They saved 0.03 s, 0.23 s and + 0.44 s against building from scratch, and the built presentations are + identical to the loaded ones relation by relation. Serialization itself is + unchanged and fully supported — see the composition notebook. +- `scripts/gen_builtin_presentations.py`, which generated them. + +### Upgrading from 3.x + +```python +# 3.x +from autstr.buildin.presentations import BuechiArithmeticZ, MSO0 +from autstr.buildin.tree_presentations import skolem_arithmetic +from autstr.arithmetic import integers, encode, decode + +# 4.0 +from autstr.arithmetic import BuechiArithmeticZ # .encode / .decode on it +from autstr.powerset import MSO0 +from autstr.tree_arithmetic import skolem_arithmetic +Z = BuechiArithmeticZ().symbolic() # was integers() +``` + +--- + +## v3.1.0 — July 2026 + +The rank-width release: new mathematics on top of the v3 engines. + +### Added + +- **Class-2 groups of bounded rank-width.** `CutRankGroups` (linear layouts), + `CutRankTreeGroups` (tree layouts) and `CocycleRankWidthGroups` (distributed + centers, microcode advice) — the advice spells out rank-≤r factorizations of + the commutation form's crossing blocks, cut by cut. +- **The chain-ring extension** (`autstr.chain_ring`). Everything generalizes + from F_p to R = ℤ/pᵈ: Smith normal form, saturated interfaces, the two-sided + factorization lemma. Centers of exponent pᵈ, widths measured as module + cut-rank, byte-identical at d = 1. +- **Factored advice letters.** Beyond ~20000 flat letters the cut-rank classes + stream one ring entry per letter through accumulator states, making width + r ≥ 2 over the ring representable (q+1 advice letters). +- **Implicit evaluation** (`autstr.implicit`). `check_implicit` / + `evaluate_implicit` on every class: first-order model checking and + satisfying-set computation that never build a query — or even a base — + automaton, reaching members (ℤ/8 and ℤ/9 words, ℤ/4 trees, the + distributed-center protocol) whose automata are infeasible. `ImplicitClass` / + `ImplicitTreeClass` are presentations given purely by transition functions. +- **Graphs of bounded rank-width.** `RankWidthClass` — rank decompositions as + advice, adjacency as a bilinear form on r-bit interface vectors, full MSO. + +### Changed + +- `show_diagram` no longer opens an external image viewer by default; pass + `view=True` for the old behavior. + +--- + +## v3.0.0 — July 2026 + +From strings to trees, with the transition representation replaced underneath +both engines. + +### Added + +- **Tree-automatic structures.** `autstr.sparse_tree_automata` (bottom-up tree + automata), `autstr.tree_presentations` and `autstr.tree_uniform` — the tree + counterparts of the whole stack. New members: **Skolem arithmetic** (ℕ, ·), + graphs of bounded **tree-width** and **clique-width** with full MSO, and + tree-indexed **extraspecial p-groups**. Cross-validated by embedding the + string engine's Büchi arithmetic into the tree engine and re-deciding every + sentence through both. +- **Composing presentations** (`autstr.composition`): disjoint union, + synchronous and asynchronous direct products of automatic structures, union + of uniformly automatic classes, and the direct-product closure of a class. + Composed, they present every finite direct product of index-≤2 cyclic groups + and extraspecial p-groups, from either family, and decide that such a product + is abelian exactly when all of its factors are. + +### Changed + +- **Transitions are shared multi-terminal BDDs** over the symbol's digits, in + both engines (`autstr.mtbdd`). `expand` became a variable renaming, + `complement` stopped touching diagrams at all, and `minimize` became one + `apply` per state per round. Queries impossible for lack of alphabet width + now compile: an arity-5 relation over a 14-letter alphabet (14⁵ = 537 824 + flat symbols) went from *infeasible* to 0.2 s; tree-depth-4 bipartiteness + from 17 s to 0.4 s. The test suite went from ~2 min to ~35 s. + +--- + +## v2.0.0 — July 2026 + +### Added + +- The whole uniformly-automatic layer: the generic advice machinery, bounded + tree-depth and pathwidth graphs with MSO, finite Boolean algebras, finite + abelian groups, the ℤ[1/p] presentations, and the non-abelian group classes — + each verified against exhaustive or exact ground-truth oracles. +- The [benchmark suite](https://github.com/fariedabuzaid/AutStr/tree/main/benchmarks) and the documentation site. + +### Changed + +- The automata core was profiled and rewritten as batched, sparsity-aware + NumPy: a **10²–10³× speedup** (the reference query dropped from 85 s to + 0.03 s), with linear memory. +- JAX went from a hard dependency to an optional accelerator; NumPy is the + canonical core. + +--- + +## v1.x — July 2025 + +Added the sparse-DFA backend, serialization, and the MSO0 finite-powerset +structure; modernized packaging. + +--- + +## v1.0 — 2022 + +The original library and its arithmetic front end. diff --git a/README.md b/README.md index cfeb484..4d70a81 100644 --- a/README.md +++ b/README.md @@ -43,27 +43,39 @@ from different angles. pip install autstr ``` -The friendliest entry point is the arithmetic package: relations over the -integers are first-class, exactly-represented **infinite** objects that you -combine with relational algebra. +Ask any structure for its **symbolic interface** and it hands you variables that +compose with ordinary Python operators. Relations are first-class, +exactly-represented **infinite** objects. ```python -from autstr.arithmetic import VariableETerm as Var +from autstr.arithmetic import BuechiArithmeticZ -x, y, z = Var('x'), Var('y'), Var('z') +Z = BuechiArithmeticZ().symbolic() # (ℤ, +, <, |₂); no setup required +x, y, z = Z.vars('x y z') R = (x + y + 3).lt(2 * x) # the infinite set { (x, y) : x + y + 3 < 2x } -R.isempty() # False +R.is_empty() # False (0, 4) in R # False — membership test band = (x + y).eq(z) & z.gt(0) & z.lt(3) # x + y = z ∧ 0 < z < 3 -solutions = band.drop(['z']) # project z away (an ∃ quantifier) -for s, _ in zip(solutions, range(3)): # enumerate lazily, smallest-first - print(s) # (0, 1), (1, 0), (1, 1), ... +for s, _ in zip(band.drop(z), range(3)): # ∃z, then enumerate smallest-first + print(s) # (0, 1), (1, 0), (1, 1) ``` Nothing is materialized until you iterate; `& | ~` are exact operations on -infinite sets. +infinite sets. Integers go in and come out because the structure ships a +**codec** along with its operators — and so does every other structure here, so +the same expressions work elsewhere unchanged: + +```python +from autstr.powerset import MSO0 + +a, b, c = MSO0().symbolic().vars('a b c') # finite sets of naturals +({0, 1}, {1, 2}, {0, 1, 2}) in (a + b).eq(c) # union — True +``` + +Where a formula reads better as text, `check` and `evaluate` still take +strings; the two are interchangeable. --- @@ -82,7 +94,7 @@ fragment these structures capture — and `evaluate` returns the automaton of *a satisfying assignments, which you can enumerate or reuse. ```python -from autstr.buildin.presentations import BuechiArithmeticZ +from autstr.arithmetic import BuechiArithmeticZ Z = BuechiArithmeticZ() # (ℤ, +, <, |) as automata Z.check('all x.(exists y.(A(x,y,x)))') # ∀x ∃y: x+y=x — True @@ -129,7 +141,7 @@ batch: | package | structures | |---------|------------| -| `autstr.arithmetic`, `autstr.buildin` | Presburger and Büchi arithmetic (ℤ, +, <, \|₂), Skolem arithmetic (ℕ, ·), the MSO0 finite-powerset structure | +| `autstr.arithmetic`, `autstr.tree_arithmetic`, `autstr.powerset` | Presburger and Büchi arithmetic (ℤ and ℕ, +, <, \|₂), Skolem arithmetic (ℕ, ·) on the tree engine, the MSO0 finite-powerset structure | | `autstr.algebra`, `autstr.tree_algebra` | the localizations **ℤ[1/p]**, finite **Boolean algebras**, and the countable **atomless Boolean algebra** | | `autstr.infinite_graphs`, `autstr.ordinals`, `autstr.turing` | the **integer grid** ℤⁿ, the **regular tree** T_k, the **ordinals** below ω^ω and — on the tree engine — below ω^(ω^ω), **Turing-machine configuration graphs** | | `autstr.collapsible`, `autstr.collapsible_reach` | **level 2 collapsible pushdown graphs** — tree-automatic by Kartzow's encoding, and the one automatic route to them, since their MSO theory is undecidable. **Reachability** is a relation of the graph, so first-order formulas may ask about runs of any length — the question a Turing machine's configuration graph cannot answer | @@ -162,8 +174,8 @@ Four capabilities cut across all of these: Büchi's theorem to Rabin's. The executable notebooks in [`notebooks/`](notebooks/) work through all of it, -one per area — arithmetic & algebra, graphs, groups, composition, and implicit -evaluation. +one per area — arithmetic & algebra, infinite structures, graphs, groups, +composition & interpretations, and implicit evaluation. --- @@ -184,7 +196,7 @@ Requires Python 3.10–3.14. The core depends only on NumPy, nltk, and graphviz. --- -## Changelog & an experiment in AI-assisted algorithm engineering +## An experiment in AI-assisted algorithm engineering AutStr began in 2022 as a summer project — a hands-on realization of the automatic structures its author had studied during his PhD in algorithmic model theory. @@ -197,80 +209,41 @@ mathematical direction and review kept firmly human. alt="Gource animation of the AutStr commit history, showing the file tree growing across the human, DeepSeek and Claude phases">

-- **v1.0 (2022) — human.** The original library and arithmetic front-end. -- **v1.x (July 2025) — DeepSeek.** A vibe-coding session (with extensive human - testing and supervision) that added the sparse-DFA backend, serialization, and - the MSO0 finite-powerset structure, and modernized packaging. -- **v2.0 (July 2026) — Claude, Anthropic's Fable 5 model.** An intensive two-day - pair-programming session inside Claude Code that: - - profiled and rewrote the entire automata core as batched, sparsity-aware - NumPy — a **10²–10³× speedup** (the reference query dropped from 85 s to - 0.03 s), with linear memory; - - migrated the library from a hard JAX dependency to a NumPy-canonical core with - JAX as an optional accelerator; - - built the whole uniformly-automatic layer — the generic advice machinery, - bounded tree-depth and pathwidth graphs with MSO, finite Boolean algebras, - finite abelian groups, the ℤ[1/p] presentations, and the non-abelian group - classes — each verified against exhaustive or exact ground-truth oracles; - - added the [benchmark suite](benchmarks/) and these docs. - - The ideas realized in v2 include constructions the author had sketched a decade - earlier; several went from a whiteboard description to running, tested code - within hours. The code is the model's; the theory, the choices, and the - verification protocol were human. - -- **v3.0 (July 2026) — Claude, (various models)** A second session, in - the same protocol, that took the library from strings to trees and replaced the - transition representation underneath both: - - **tree-automatic structures.** `autstr.sparse_tree_automata` (bottom-up tree - automata), `autstr.tree_presentations`, and `autstr.tree_uniform` — the tree - counterparts of the whole stack. New members: **Skolem arithmetic** (ℕ, ·), - graphs of bounded **tree-width** and bounded **clique-width** with full MSO, - and tree-indexed **extraspecial p-groups**. Cross-validated by embedding the - string engine's Büchi arithmetic into the tree engine and re-deciding every - sentence through both. - - **transitions are shared multi-terminal BDDs** over the symbol's digits, in - both engines. `expand` became a variable renaming, `complement` stopped - touching diagrams at all, and `minimize` became one `apply` per state per - round. Queries that had been impossible for lack of alphabet width now - compile: an arity-5 relation over a 14-letter alphabet (14⁵ = 537 824 flat - symbols) went from *infeasible* to 0.2 s; tree-depth-4 bipartiteness from - 17 s to 0.4 s. The test suite went from ~2 min to ~35 s. - - - **composing presentations.** `autstr.composition`: disjoint union and - synchronous/asynchronous direct products of automatic structures, union of - uniformly automatic classes, and the direct-product closure of a class. - Composed, they present every finite direct product of index-≤2 cyclic groups - and extraspecial p-groups, drawn from either family — and decide that such a - product is abelian exactly when all of its factors are. - -- **v3.1 (July 2026) — Claude, Fable 5.** The rank-width release: new - mathematics on top of the v3 engines, in the same human-directed protocol. - - **class-2 groups of bounded rank-width.** `CutRankGroups` (linear layouts), - `CutRankTreeGroups` (tree layouts) and `CocycleRankWidthGroups` - (distributed centers, microcode advice) — the advice spells out rank-≤r - factorizations of the commutation form's crossing blocks, cut by cut. - - **the chain-ring extension.** Everything generalizes from F_p to - R = ℤ/pᵈ (`autstr.chain_ring`: Smith normal form, saturated interfaces, - the two-sided factorization lemma) — centers of exponent pᵈ, widths - measured as module cut-rank, byte-identical at d = 1. - - **factored advice letters.** Beyond ~20000 flat letters the cut-rank - classes stream one ring entry per letter through accumulator states, - making width r ≥ 2 over the ring representable (q+1 advice letters). - - **implicit evaluation.** `check_implicit` / `evaluate_implicit` on every - class: first-order model checking and satisfying-set computation that - never build a query — or even a base — automaton, reaching members (ℤ/8 - and ℤ/9 words, ℤ/4 trees, the distributed-center protocol) whose - automata are infeasible; `ImplicitClass` / `ImplicitTreeClass` are - presentations given purely by transition functions. - - **graphs of bounded rank-width.** `RankWidthClass` — rank decompositions - as advice, adjacency as a bilinear form on r-bit interface vectors, - full MSO; the graph face of the same linear algebra. - - Docs are built by CI with all notebooks **executed during the build** - (the repository keeps them output-free). - - One behavior change to a 3.0 API: `show_diagram` no longer opens an - external image viewer by default (headless builds must not spawn one); - pass `view=True` for the old behavior. +- **v1.0 (2022) — human.** The original library and arithmetic front end. +- **v1.x (2025) — DeepSeek.** A vibe-coding session, with extensive human + testing and supervision. +- **v2.0–v3.1 (2026) — Claude.** The automata core rewritten as batched, + sparsity-aware NumPy (10²–10³× faster); the uniformly-automatic layer; the + step from strings to trees; MTBDD transitions; the rank-width families. +- **v4.0 (2026) — Claude.** This release. + +Several of the constructions realized here were sketched by the author a decade +earlier and had never been implemented; some went from a whiteboard description +to running, tested code within hours. The code is the model's; the theory, the +choices, and the verification protocol are human. + +### What is new in v4.0 + +- **A symbolic first-order layer.** Write `(x + y).eq(z) & z.lt(100)` instead of + a formula string, over any structure or class, on either engine. Every + structure declares its own operators and its own **codec**, so Python values + go in and come out and `symbolic()` needs no arguments. +- **First-order interpretations** that *compute* the interpreted presentation, + quotients included — over trees too, where the representative of a class is + its least description rather than its least member. +- **Infinite graphs**: the integer grid, the regular tree, and Turing-machine + configuration graphs, where first-order logic stops exactly at reachability. +- **Level 2 collapsible pushdown graphs, with reachability.** Tree-automatic by + Kartzow's encoding, and `Reach` is a relation of the graph — so a first-order + formula may ask about runs of any length, which is precisely what the Turing + graph cannot be asked. +- **The countable atomless Boolean algebra**, and the ordinals below ω^ω and + ω^(ω^n). +- `autstr.buildin` is gone: every structure the library ships is built in, so + its contents moved to modules named for their subject. See the changelog for + the upgrade path. + +📜 **[Full changelog](CHANGELOG.md)** — every release, in detail. --- diff --git a/autstr/_version.py b/autstr/_version.py index e276047..0951e03 100644 --- a/autstr/_version.py +++ b/autstr/_version.py @@ -1,2 +1,2 @@ # autstr/_version.py -__version__ = "3.1.0" +__version__ = "4.0.0" diff --git a/docs/source/conf.py b/docs/source/conf.py index 0d0e027..bcdbe02 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -59,6 +59,10 @@ for _img in sorted((_here.parent / 'media').glob('*.png')): shutil.copyfile(_img, _media_dst / _img.name) +# Copy the changelog into the source tree so it has a page of its own; it +# lives at the repository root, where GitHub and PyPI look for it. +shutil.copyfile(_here.parent.parent / 'CHANGELOG.md', _here / 'changelog.md') + nb_execution_mode = 'force' nb_execution_timeout = 900 nb_execution_raise_on_error = True diff --git a/docs/source/index.rst b/docs/source/index.rst index de3e520..1e365c5 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -30,6 +30,12 @@ reference is generated from the docstrings. api/modules +.. toctree:: + :maxdepth: 1 + :caption: Project + + changelog + Indices and tables ================== diff --git a/pyproject.toml b/pyproject.toml index 1110853..9be0573 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "autstr" -version = "3.1.0" +version = "4.0.0" description = "Library for working with finitely presentable infinite structures" readme = "README.md" authors = [ From 0e89e50db53a125c5c506b0dd8f5d3e972f36536 Mon Sep 17 00:00:00 2001 From: Faried Abu Zaid Date: Thu, 13 Aug 2026 21:10:07 +0200 Subject: [PATCH 6/9] Regenerate the history animation through v4.0.0 Gource over the full commit history, now reaching the symbolic layer, the interpretations, the infinite graphs and the collapsible pushdown work. The sieve automaton is unchanged, byte for byte -- the example it renders did not move. Co-Authored-By: Claude Opus 5 --- docs/media/history.gif | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/media/history.gif b/docs/media/history.gif index 8fb7f47..1803b84 100644 --- a/docs/media/history.gif +++ b/docs/media/history.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e059a611435f7f5fc6b8705ad9d8c360ab217e8e1bb7ff6f4147cacd307f017d -size 2418080 +oid sha256:f1d687abb50e728c2c957280a88847079bbfd731df6d09e4d11e0c07572fcb3e +size 2831196 From c91796abafd8942f96582845b62de47ead1bbd85 Mon Sep 17 00:00:00 2001 From: Faried Abu Zaid Date: Thu, 13 Aug 2026 22:02:06 +0200 Subject: [PATCH 7/9] Serialization for the tree engine The string side had it and the tree side did not, which put the expensive constructions on the wrong side of the line: a tree relation is where the cost lives, and `Reach` for a collapsible pushdown graph is exponential in the system's control states. It turned out to be a short walk. A tree automaton's compiled form is a sorted table of child pairs with one decision-diagram root each, so the payload is the pair keys plus the shared sub-DAG below those roots -- and `STORE.export` / `import_nodes` are generic over roots, so the diagram half needed no new code. A relation over a convolution alphabet too wide to enumerate still writes out in the size of its diagrams, exactly as on the string side. Two deliberate differences from the string presentation serializer: each automaton's payload is stored as raw bytes with a length prefix rather than as a JSON list of integers (which costs about four bytes of file per byte of data), and the JSON-has-no-tuples fix moved to `utils.misc` instead of being copied, since both engines need it now. Measured on the case it exists for: the collapsible `Reach` presentation is 22709 bytes, reloads in 0.01s against 0.5s to rebuild, and the reloaded relation is still reflexive, still contains the edge relation, and is still transitive. Skolem arithmetic round-trips and answers identical queries; a flipped payload byte is caught by the checksum. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 11 ++- autstr/sparse_automata.py | 19 ++--- autstr/sparse_tree_automata.py | 126 +++++++++++++++++++++++++++- autstr/tree_presentations.py | 103 ++++++++++++++++++++++- autstr/utils/misc.py | 14 +++- notebooks/composition.ipynb | 8 +- notebooks/infinite_structures.ipynb | 32 +++++++ tests/test_serialization.py | 82 ++++++++++++++++++ 8 files changed, 374 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0d8c39..53d7793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,15 @@ automata are carried on annotation tapes that are projected away — the move MONA makes. `reach_along` builds the label-constrained `Reach_L` by putting the label automaton into a product system; ε-contraction is a special case. +**Serialization for the tree engine.** `SparseTreeAutomatonSerializer` and +`TreeAutomaticPresentationSerializer`, with the same method names as the string +side (`automatic_presentation_to_file` / `..._from_file`). A tree automaton's +compiled form is a sorted table of child pairs with one decision-diagram root +each, so the payload is the pair keys plus the shared sub-DAG below those +roots — a relation over an alphabet too wide to enumerate still writes out in +the size of its diagrams. This is where it earns its keep: `Reach` is +exponential in the control states, and a reloaded copy is still reachability. + **Deferred relations.** A presentation may declare a relation now and build it on first use. `get_relation_symbols()` lists it, a query that mentions it triggers the build. This is how `Reach` avoids costing anything until asked @@ -139,7 +148,7 @@ relation on every presentation. why it went unseen. - **Serializing such a presentation.** JSON has no tuples, so a reloaded product alphabet was a set of lists (unhashable) and the padding symbol a - list. Both are restored on load. + list. Both are restored on load, on both engines. - A universal quantifier that silently became an existential. - Sentence markers that dropped the structure's alphabet. - Relation automata are restricted to the universe on *every* tape, not all but diff --git a/autstr/sparse_automata.py b/autstr/sparse_automata.py index 2c4b065..6636780 100644 --- a/autstr/sparse_automata.py +++ b/autstr/sparse_automata.py @@ -32,7 +32,9 @@ def step(states, symbols): from autstr.mtbdd import ( NONE, STORE, TOP, ComputedTable, NodeStore, bits_of, num_bits, var_tables, ) -from autstr.utils.misc import decode_symbol, encode_symbol +from autstr.utils.misc import ( + alphabet_from_json, decode_symbol, encode_symbol, +) @@ -49,17 +51,6 @@ def step(states, symbols): # - Acceptance array # - Transition diagrams: var / lo / hi / term arrays, then one root per state -def _alphabet_from_json(raw) -> set: - """The base alphabet as stored in a payload. - - JSON has no tuples, so a product alphabet's letters -- which is what an - interpreted structure of dimension > 1 has -- come back as lists. Restore - them, or the set() below raises on the first unhashable letter. - """ - return {tuple(letter) if isinstance(letter, list) else letter - for letter in json.loads(raw)} - - class SparseDFASerializer: VERSION = 3 # diagram payload (v2 stored flat exception rows) HEADER_FORMAT = "4sB3sII" @@ -143,7 +134,7 @@ def _parse_payload(cls, payload: bytes, version: int = None) -> 'SparseDFA': num_states, num_nodes, start_state, symbol_arity, alphabet_len = \ struct.unpack(cls.METADATA_FORMAT, payload[:cls.METADATA_SIZE]) offset = cls.METADATA_SIZE - base_alphabet = _alphabet_from_json(payload[offset:offset + alphabet_len]) + base_alphabet = alphabet_from_json(payload[offset:offset + alphabet_len]) offset += alphabet_len is_accepting = np.frombuffer(payload, dtype=np.uint8, count=num_states, @@ -168,7 +159,7 @@ def _parse_payload_v2(cls, payload: bytes) -> 'SparseDFA': num_states, max_exceptions, start_state, symbol_arity, alphabet_len = \ struct.unpack(cls.METADATA_FORMAT, payload[:cls.METADATA_SIZE]) offset = cls.METADATA_SIZE - base_alphabet = _alphabet_from_json(payload[offset:offset + alphabet_len]) + base_alphabet = alphabet_from_json(payload[offset:offset + alphabet_len]) offset += alphabet_len defaults = np.frombuffer(payload, dtype=np.uint32, count=num_states, diff --git a/autstr/sparse_tree_automata.py b/autstr/sparse_tree_automata.py index 3a1c4a9..929d9d8 100644 --- a/autstr/sparse_tree_automata.py +++ b/autstr/sparse_tree_automata.py @@ -20,13 +20,16 @@ acceptance and touches no diagram at all, and hash-consing makes two states with the same transition function share one node. """ +import json +import struct +import zlib from collections import defaultdict, deque from typing import Dict, List, Optional, Sequence, Set, Tuple import numpy as np from autstr.mtbdd import NONE, STORE, num_bits, var_tables -from autstr.utils.misc import encode_symbol +from autstr.utils.misc import alphabet_from_json, encode_symbol # ==================================================================== @@ -615,3 +618,124 @@ def __repr__(self): return (f"SparseTreeAutomaton({self.num_states} states, " f"{len(self.pair_keys)} pairs, {self.num_nodes} nodes, " f"default={self.default_state})") + + +# ==================================================================== +# Serialization +# ==================================================================== +# +# [Header] magic 'STAU', version, reserved, CRC32 of payload, payload size +# [Payload] metadata (24 bytes), base alphabet (JSON), acceptance array, +# the packed pair keys, then the shared sub-DAG of the transition +# diagrams: var / lo / hi / term arrays and one root per pair. +# +# The diagrams are what makes this worth storing: a relation over a +# convolution alphabet too wide to enumerate still writes out in the size of +# its decision diagrams, exactly as on the string side. + +class SparseTreeAutomatonSerializer: + """Binary serialization for `SparseTreeAutomaton`. + + The tree counterpart of `autstr.sparse_automata.SparseDFASerializer`, and + the same payload idea: the compiled form is a sorted table of child pairs + with one diagram root each, so storing it is storing the pair keys plus + the sub-DAG below their roots. + """ + + MAGIC = b'STAU' + VERSION = 1 + HEADER_FORMAT = "4sB3sII" + HEADER_SIZE = struct.calcsize(HEADER_FORMAT) + #: num_states, num_nodes, num_pairs, default_state, symbol_arity, alphabet + METADATA_FORMAT = "IIIIII" + METADATA_SIZE = struct.calcsize(METADATA_FORMAT) + + @classmethod + def serialize(cls, automaton: 'SparseTreeAutomaton', filename: str) -> None: + with open(filename, 'wb') as handle: + handle.write(cls.to_bytes(automaton)) + + @classmethod + def deserialize(cls, filename: str) -> 'SparseTreeAutomaton': + with open(filename, 'rb') as handle: + return cls.from_bytes(handle.read()) + + @classmethod + def to_bytes(cls, automaton: 'SparseTreeAutomaton') -> bytes: + payload = cls._create_payload(automaton) + header = struct.pack(cls.HEADER_FORMAT, cls.MAGIC, cls.VERSION, + b'\0\0\0', zlib.crc32(payload), len(payload)) + return header + payload + + @classmethod + def from_bytes(cls, data: bytes) -> 'SparseTreeAutomaton': + if len(data) < cls.HEADER_SIZE: + raise ValueError("Data too short for header") + magic, version, _, checksum, payload_size = struct.unpack( + cls.HEADER_FORMAT, data[:cls.HEADER_SIZE]) + if magic != cls.MAGIC: + raise ValueError("Invalid SparseTreeAutomaton format") + if version != cls.VERSION: + raise ValueError( + f"Unsupported SparseTreeAutomaton version: {version}") + payload = data[cls.HEADER_SIZE:cls.HEADER_SIZE + payload_size] + if len(payload) != payload_size: + raise ValueError("Payload size mismatch") + if zlib.crc32(payload) != checksum: + raise ValueError("SparseTreeAutomaton data corruption detected") + return cls._parse_payload(payload) + + @classmethod + def _create_payload(cls, automaton: 'SparseTreeAutomaton') -> bytes: + var, lo, hi, term, roots = STORE.export(automaton.pair_nodes.tolist()) + alphabet_json = json.dumps( + sorted(automaton.base_alphabet_frozen)).encode('utf-8') + metadata = struct.pack( + cls.METADATA_FORMAT, + automaton.num_states, + len(var), + len(automaton.pair_keys), + automaton.default_state, + automaton.symbol_arity, + len(alphabet_json), + ) + return b''.join([ + metadata, + alphabet_json, + np.asarray(automaton.is_accepting, dtype=np.uint8).tobytes(), + np.asarray(automaton.pair_keys, dtype=np.int64).tobytes(), + var.astype(np.int64).tobytes(), + lo.astype(np.int64).tobytes(), + hi.astype(np.int64).tobytes(), + term.astype(np.int64).tobytes(), + np.asarray(roots, dtype=np.int64).tobytes(), + ]) + + @classmethod + def _parse_payload(cls, payload: bytes) -> 'SparseTreeAutomaton': + (num_states, num_nodes, num_pairs, default_state, symbol_arity, + alphabet_len) = struct.unpack(cls.METADATA_FORMAT, + payload[:cls.METADATA_SIZE]) + offset = cls.METADATA_SIZE + base_alphabet = alphabet_from_json(payload[offset:offset + alphabet_len]) + offset += alphabet_len + + is_accepting = np.frombuffer(payload, dtype=np.uint8, count=num_states, + offset=offset).astype(bool) + offset += num_states + + arrays = [] + for count in (num_pairs, num_nodes, num_nodes, num_nodes, num_nodes, + num_pairs): + arrays.append(np.frombuffer(payload, dtype=np.int64, count=count, + offset=offset)) + offset += count * 8 + pair_keys, var, lo, hi, term, roots = arrays + + return SparseTreeAutomaton( + num_states, default_state, + is_accepting=is_accepting, + symbol_arity=symbol_arity, + base_alphabet=base_alphabet, + pair_keys=pair_keys, + pair_nodes=STORE.import_nodes(var, lo, hi, term, roots)) diff --git a/autstr/tree_presentations.py b/autstr/tree_presentations.py index c4100cb..e89fe3a 100644 --- a/autstr/tree_presentations.py +++ b/autstr/tree_presentations.py @@ -11,12 +11,17 @@ `project` — which produces the canonical (trimmed) language — is followed by re-saturation, the tree analog of the string pipeline's pad/unpad dance. """ +import json +import struct +import zlib from typing import Dict, Optional, Union from nltk.sem import logic from autstr.presentations import DeferredRelations -from autstr.sparse_tree_automata import SparseTreeAutomaton +from autstr.sparse_tree_automata import ( + SparseTreeAutomaton, SparseTreeAutomatonSerializer, +) from autstr.utils.logic import get_free_elementary_vars, optimize_query from autstr.utils.tree_automata_tools import ( attach_padding, expand, minimize, project, @@ -35,6 +40,87 @@ def tree_zero(symbol_arity: int = 1, base_alphabet=None) -> SparseTreeAutomaton: symbol_arity, base_alphabet or {0}) +class TreeAutomaticPresentationSerializer: + """Binary serialization for `TreeAutomaticPresentation`. + + Mirrors `autstr.presentations.AutomaticPresentationSerializer`, but stores + each automaton's payload as raw bytes with a length prefix rather than as + a JSON list of integers -- which costs roughly four bytes of file per byte + of data. + """ + + MAGIC = b'TPRS' + VERSION = 1 + HEADER_FORMAT = "4sB3sII" + HEADER_SIZE = struct.calcsize(HEADER_FORMAT) + + @classmethod + def serialize(cls, presentation, filename: str) -> None: + payload = cls._create_payload(presentation) + header = struct.pack(cls.HEADER_FORMAT, cls.MAGIC, cls.VERSION, + b'\0\0\0', zlib.crc32(payload), len(payload)) + with open(filename, 'wb') as handle: + handle.write(header) + handle.write(payload) + + @classmethod + def deserialize(cls, filename: str) -> 'TreeAutomaticPresentation': + with open(filename, 'rb') as handle: + data = handle.read() + magic, version, _, checksum, payload_size = struct.unpack( + cls.HEADER_FORMAT, data[:cls.HEADER_SIZE]) + if magic != cls.MAGIC: + raise ValueError("Invalid file format (bad magic number)") + if version != cls.VERSION: + raise ValueError(f"Unsupported version: {version}") + payload = data[cls.HEADER_SIZE:cls.HEADER_SIZE + payload_size] + if len(payload) != payload_size: + raise ValueError("Payload size mismatch") + if zlib.crc32(payload) != checksum: + raise ValueError("Data corruption detected (checksum mismatch)") + return cls._parse_payload(payload) + + @classmethod + def _create_payload(cls, presentation) -> bytes: + names = list(presentation.automata) + metadata = json.dumps({ + "padding_symbol": presentation.padding_symbol, + "max_states": presentation.max_states, + "names": names, + }).encode('utf-8') + chunks = [struct.pack("I", len(metadata)), metadata] + for name in names: + blob = SparseTreeAutomatonSerializer.to_bytes( + presentation.automata[name]) + chunks.append(struct.pack("I", len(blob))) + chunks.append(blob) + return b''.join(chunks) + + @classmethod + def _parse_payload(cls, payload: bytes) -> 'TreeAutomaticPresentation': + metadata_len, = struct.unpack("I", payload[:4]) + offset = 4 + metadata = json.loads(payload[offset:offset + metadata_len]) + offset += metadata_len + + automata = {} + for name in metadata["names"]: + size, = struct.unpack("I", payload[offset:offset + 4]) + offset += 4 + automata[name] = SparseTreeAutomatonSerializer.from_bytes( + payload[offset:offset + size]) + offset += size + + # over a product alphabet the padding symbol is a tuple, and JSON has + # no tuples + padding_symbol = metadata["padding_symbol"] + if isinstance(padding_symbol, list): + padding_symbol = tuple(padding_symbol) + return TreeAutomaticPresentation( + automata, padding_symbol=padding_symbol, + enforce_consistency=False, max_states=metadata["max_states"]) + + class TreeAutomaticPresentation(DeferredRelations): """A presentation of a structure by tree automata. @@ -82,6 +168,21 @@ def _domain_product(self, arity: int) -> SparseTreeAutomaton: minimize(expand(self.automata['U'], arity, [i])))) return domain + def automatic_presentation_to_file(self, filename: str) -> None: + """Write the presentation -- domain, every built relation, alphabet -- + to a file. Deferred relations that have not been built are not stored; + call `materialize` first to include them.""" + TreeAutomaticPresentationSerializer.serialize(self, filename) + + @classmethod + def automatic_presentation_from_file(cls, filename: str): + """Read a presentation written by `automatic_presentation_to_file`. + + The signature is not stored -- a codec is Python code, not data -- so + the result is a bare structure until `symbolic` is given one. + """ + return TreeAutomaticPresentationSerializer.deserialize(filename) + def _install_relation(self, name, definition): self.update(**{name: definition() if callable(definition) else definition}) diff --git a/autstr/utils/misc.py b/autstr/utils/misc.py index a8577cd..37d515a 100644 --- a/autstr/utils/misc.py +++ b/autstr/utils/misc.py @@ -1,3 +1,4 @@ +import json from functools import cmp_to_key from heapq import heapify, heappop, heappush from typing import FrozenSet, Set, List, Tuple, Union @@ -113,4 +114,15 @@ def complement(values, min_val: int, max_val: int) -> np.ndarray: values = np.asarray(values) if values.size == 0: return np.array([], dtype=values.dtype) - return np.setdiff1d(np.arange(min_val, max_val + 1), values) \ No newline at end of file + return np.setdiff1d(np.arange(min_val, max_val + 1), values) + + +def alphabet_from_json(raw) -> set: + """The base alphabet as stored in a serialized payload. + + JSON has no tuples, so a product alphabet's letters -- which is what an + interpreted structure of dimension > 1 has -- come back as lists. Restore + them, or the set() built from them raises on the first unhashable letter. + """ + return {tuple(letter) if isinstance(letter, list) else letter + for letter in json.loads(raw)} diff --git a/notebooks/composition.ipynb b/notebooks/composition.ipynb index 00d94f5..6fcf36e 100644 --- a/notebooks/composition.ipynb +++ b/notebooks/composition.ipynb @@ -385,9 +385,11 @@ "again. Structures that ship with the library declare their own, which is why\n", "`BuechiArithmeticZ().symbolic()` needs no argument while this one does.\n", "\n", - "Serialization is a *string*-engine facility. Tree presentations — Skolem\n", - "arithmetic, the atomless Boolean algebra, the collapsible pushdown graphs —\n", - "have no file format yet; rebuild those from their constructors." + "Tree presentations serialize the same way and with the same method names. There\n", + "the payload matters more, because tree relations are where the expensive\n", + "constructions live — the reachability relation of a collapsible pushdown graph\n", + "is exponential in the system's control states, and once built it is worth\n", + "keeping." ] } ], diff --git a/notebooks/infinite_structures.ipynb b/notebooks/infinite_structures.ipynb index def8d32..7ab7e9d 100644 --- a/notebooks/infinite_structures.ipynb +++ b/notebooks/infinite_structures.ipynb @@ -258,6 +258,38 @@ "number of silent steps followed by one visible one — is the special case of that\n", "with a two-state label automaton." ] + }, + { + "cell_type": "markdown", + "id": "5ab0a6b5", + "metadata": {}, + "source": [ + "### Keeping what was expensive\n", + "\n", + "`Reach` cost real work to build, and nothing about it depends on the session.\n", + "Tree presentations serialize exactly as string ones do, so it can be written\n", + "out and read back — the reloaded relation is still reachability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b4ac295", + "metadata": {}, + "outputs": [], + "source": [ + "import tempfile, os\n", + "from autstr.tree_presentations import TreeAutomaticPresentation\n", + "\n", + "path = os.path.join(tempfile.mkdtemp(), 'collapsible.autstr')\n", + "graph.presentation.automatic_presentation_to_file(path)\n", + "print(f'{os.path.getsize(path)} bytes on disk')\n", + "\n", + "reloaded = TreeAutomaticPresentation.automatic_presentation_from_file(path)\n", + "print('Reach is there:', 'Reach' in reloaded.get_relation_symbols())\n", + "print('and still transitive:', reloaded.check(\n", + " 'all x.(all y.(all z.((Reach(x,y) & Reach(y,z)) -> Reach(x,z))))'))" + ] } ], "metadata": { diff --git a/tests/test_serialization.py b/tests/test_serialization.py index c6fdf72..584eb0b 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -3,6 +3,7 @@ import zlib import numpy as np +import pytest from autstr.arithmetic import BuechiArithmeticZ from autstr.sparse_automata import SparseDFA, SparseDFASerializer @@ -133,3 +134,84 @@ def test_round_trip_through_a_file(self, tmp_path): 'all x.(all y.(Lt(x,y) -> exists z.(Lt(x,z) and Lt(z,y))))') assert _same_language(reloaded.automata['Lt'], presentation.automata['Lt'], max_length=3) + + +class TestTreeSerialization: + """The tree engine's counterpart. + + The compiled form of a tree automaton is a sorted table of child pairs with + one decision-diagram root each, so storing it is storing the pair keys plus + the shared sub-DAG below those roots -- the same payload idea as the string + side, and the same reason for it: a relation over a convolution alphabet + too wide to enumerate still writes out in the size of its diagrams. + """ + + def test_automaton_round_trip(self): + from autstr.sparse_tree_automata import SparseTreeAutomatonSerializer + from autstr.tree_arithmetic import skolem_arithmetic + + original = skolem_arithmetic().automata['M'] # x * y = z + reloaded = SparseTreeAutomatonSerializer.from_bytes( + SparseTreeAutomatonSerializer.to_bytes(original)) + + assert reloaded.num_states == original.num_states + assert reloaded.default_state == original.default_state + assert reloaded.symbol_arity == original.symbol_arity + assert reloaded.base_alphabet_frozen == original.base_alphabet_frozen + assert list(reloaded.is_accepting) == list(original.is_accepting) + assert list(reloaded.pair_keys) == list(original.pair_keys) + + def test_corruption_is_detected(self): + from autstr.sparse_tree_automata import SparseTreeAutomatonSerializer + from autstr.tree_arithmetic import skolem_arithmetic + + data = bytearray(SparseTreeAutomatonSerializer.to_bytes( + skolem_arithmetic().automata['U'])) + data[-1] ^= 0xFF # flip a byte of the payload + with pytest.raises(ValueError, match='corruption'): + SparseTreeAutomatonSerializer.from_bytes(bytes(data)) + + def test_presentation_round_trip_answers_the_same_questions(self, tmp_path): + from autstr.tree_arithmetic import skolem_arithmetic + from autstr.tree_presentations import TreeAutomaticPresentation + + skolem = skolem_arithmetic() + path = str(tmp_path / 'skolem.autstr') + skolem.automatic_presentation_to_file(path) + + reloaded = TreeAutomaticPresentation.automatic_presentation_from_file(path) + + assert set(reloaded.get_relation_symbols()) == \ + set(skolem.get_relation_symbols()) + assert reloaded.padding_symbol == skolem.padding_symbol + for phi in ('exists x.(all y.(M(x,y,y)))', # a multiplicative unit + 'all x.(exists y.(M(x,y,x)))'): + assert reloaded.check(phi) == skolem.check(phi) + + # the signature is not stored, so hand the reloaded structure the + # original's and check that elements still mean what they meant + S = reloaded.symbolic(skolem.default_signature()) + x, y, z = S.vars('x y z') + assert (6, 35, 210) in (x * y).eq(z) + assert (6, 35, 211) not in (x * y).eq(z) + + def test_an_expensive_relation_survives(self, tmp_path): + """The case this exists for: `Reach` costs real time to build, and a + reloaded copy must still be the reachability relation.""" + from autstr.collapsible import Level2CPS + from autstr.tree_presentations import TreeAutomaticPresentation + + system = Level2CPS([('0', None, 'c', '1', 'clone'), + ('1', None, 'o', '0', 'pop 2')], symbols=('a',)) + presentation = system.configuration_graph().presentation + presentation.relation('Reach') # build it + + path = str(tmp_path / 'reach.autstr') + presentation.automatic_presentation_to_file(path) + reloaded = TreeAutomaticPresentation.automatic_presentation_from_file(path) + + assert 'Reach' in reloaded.get_relation_symbols() + assert reloaded.check('all x.(Reach(x,x))') + assert reloaded.check('all x.(all y.(E(x,y) -> Reach(x,y)))') + assert reloaded.check( + 'all x.(all y.(all z.((Reach(x,y) & Reach(y,z)) -> Reach(x,z))))') From a4987f7e329078af6799f9c6d7fa07aa28933b5a Mon Sep 17 00:00:00 2001 From: Faried Abu Zaid Date: Thu, 13 Aug 2026 22:02:17 +0200 Subject: [PATCH 8/9] References: four in the README, all of them in the documentation The README carried eleven references and the documentation none, which is backwards -- a reader of the docs meets the constructions and has nowhere to follow them. `references.md` now holds the bibliography, grouped and annotated with what each work IS in this library, and the README keeps the four it leans on hardest plus a link. Citing where it was due exposed a gap: the overview had no section on any of the v4 material. It gains two -- interpretations (with the tree quotient's least description, and the ordinals as the payoff) and infinite graphs (the grid, the tree, Turing configuration graphs, and the collapsible pushdown graphs where reachability comes back). Each names its source at the point of use: Kuske & Weidner and Colcombet & Loeding on tree quotients, Delhomme on why `Ordinal` takes an exponent, Kartzow on the encoding and the run decomposition, Courcelle on the linear-time claim, Abu Zaid/Graedel/Reinhardt on advice. Three entries had no bibliographic details anywhere in the repository and could not be verified offline (Delhomme 2004, Oum & Seymour 2006, Colcombet & Loeding 2007); they carry author, title and venue but no DOI, pending a check. Co-Authored-By: Claude Opus 5 --- README.md | 96 +++++++---------------- docs/source/index.rst | 1 + docs/source/overview.md | 85 +++++++++++++++++++-- docs/source/references.md | 157 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 262 insertions(+), 77 deletions(-) create mode 100644 docs/source/references.md diff --git a/README.md b/README.md index 4d70a81..75fd0b0 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,8 @@ choices, and the verification protocol are human. graph cannot be asked. - **The countable atomless Boolean algebra**, and the ordinals below ω^ω and ω^(ω^n). +- **Serialization for the tree engine**, so an expensive relation is built once + and kept. - `autstr.buildin` is gone: every structure the library ships is built in, so its contents moved to modules named for their subject. See the changelog for the upgrade path. @@ -249,81 +251,37 @@ choices, and the verification protocol are human. ## References -1. **Abu Zaid, F.** *Algorithmic Solutions via Model Theoretic Interpretations.* - Dissertation, RWTH Aachen University, 2016. - DOI: [10.18154/RWTH-2017-07663](https://doi.org/10.18154/RWTH-2017-07663) +AutStr implements a line of work that runs from Büchi's theorem to uniformly +automatic classes. The four below are the ones it leans on most; the +**[full bibliography](https://fariedabuzaid.github.io/AutStr/references.html)** +in the documentation lists the rest — Rabin, Courcelle, Delhommé, Kuske & +Weidner, Kartzow and others — and says for each what in the library it is. -2. **Abu Zaid, F.** *Uniformly Automatic Classes of Finite Structures.* - FSTTCS 2018, LIPIcs vol. 122, pp. 10:1–10:21. - DOI: [10.4230/LIPIcs.FSTTCS.2018.10](https://doi.org/10.4230/LIPIcs.FSTTCS.2018.10) - *The meta-theorems for finite Boolean algebras, finite groups, and graphs of - bounded tree-depth implemented by `autstr.uniform`, `autstr.graphs`, - `autstr.algebra`, and `autstr.groups`.* - -3. **Abu Zaid, F., Grädel, E., & Reinhardt, F.** *Advice Automatic Structures and - Uniformly Automatic Classes.* CSL 2017, LIPIcs vol. 82, pp. 35:1–35:20. - DOI: [10.4230/LIPIcs.CSL.2017.35](https://doi.org/10.4230/LIPIcs.CSL.2017.35) - *Introduces automatic presentations with advice — the foundation of the uniform - classes here; the ℤ[1/p] presentation follows its blueprint for (ℚ, +).* - -4. **Blumensath, A., & Grädel, E.** *Automatic Structures.* LICS 2000, pp. 51–62. - [Proceedings](https://lics.siglog.org/2000/Grdel-AutomaticStructures.html) +1. **Büchi, J. R.** *Weak Second-Order Arithmetic and Finite Automata.* + Zeitschrift für math. Logik und Grundlagen der Mathematik 6 (1960), 66–92. + DOI: [10.1002/malq.19600060105](https://doi.org/10.1002/malq.19600060105) + *Every `evaluate` call in this library is this construction.* -5. **Khoussainov, B., & Nerode, A.** *Automatic presentations of structures.* +2. **Khoussainov, B., & Nerode, A.** *Automatic presentations of structures.* LCC 1994, LNCS vol. 960, Springer. DOI: [10.1007/3-540-60178-3_93](https://doi.org/10.1007/3-540-60178-3_93) -6. **Khoussainov, B., Rubin, S., & Stephan, F.** *Automatic Structures: Richness - and Limitations.* LMCS 3(2), 2007. - arXiv: [cs/0703064](https://arxiv.org/abs/cs/0703064) · - DOI: [10.2168/LMCS-3(2:2)2007](https://doi.org/10.2168/LMCS-3%282%3A2%292007) - -### Foundations - -The idea that a logic can be decided by translating formulas into automata long -predates the term *automatic structure*; this library is a late implementation of -a line of work that runs through: +3. **Blumensath, A., & Grädel, E.** *Automatic Structures.* LICS 2000, pp. 51–62. + [Proceedings](https://lics.siglog.org/2000/Grdel-AutomaticStructures.html) + *Closure under first-order definability — the decidability `check` rests on.* -7. **Büchi, J. R.** *Weak Second-Order Arithmetic and Finite Automata.* - Zeitschrift für math. Logik und Grundlagen der Mathematik 6 (1960), 66–92. - DOI: [10.1002/malq.19600060105](https://doi.org/10.1002/malq.19600060105) - *Monadic second-order logic over (ℕ, +1) is decidable, by translation into - finite automata. Every `evaluate` call in this library is this construction.* - -8. **Rabin, M. O.** *Decidability of Second-Order Theories and Automata on - Infinite Trees.* Transactions of the AMS 141 (1969), 1–35. - DOI: [10.2307/1995086](https://doi.org/10.2307/1995086) - *The same programme over trees. `autstr.sparse_tree_automata` and the - tree-automatic presentations are the finite-tree fragment of this.* - -9. **Courcelle, B.** *The Monadic Second-Order Logic of Graphs I: Recognizable - Sets of Finite Graphs.* Information and Computation 85(1), 1990, 12–75. - DOI: [10.1016/0890-5401(90)90043-H](https://doi.org/10.1016/0890-5401%2890%2990043-H) - *MSO properties of graphs of bounded tree-width are decidable in linear time. - `autstr.tree_graphs.TreeWidthClass` builds the automaton the theorem promises.* - -10. **Courcelle, B., & Olariu, S.** *Upper Bounds to the Clique Width of Graphs.* - Discrete Applied Mathematics 101 (2000), 77–114. - DOI: [10.1016/S0166-218X(99)00184-5](https://doi.org/10.1016/S0166-218X%2899%2900184-5) - *The k-expressions that `autstr.tree_graphs.CliqueWidthClass` reads as advice.* - -11. **Makowsky, J. A.** *Algorithmic Uses of the Feferman–Vaught Theorem.* - Annals of Pure and Applied Logic 126 (2004), 159–213. - DOI: [10.1016/j.apal.2003.11.002](https://doi.org/10.1016/j.apal.2003.11.002) - *The composition method behind meta-theorems of this shape.* +4. **Abu Zaid, F.** *Uniformly Automatic Classes of Finite Structures.* + FSTTCS 2018, LIPIcs vol. 122, pp. 10:1–10:21. + DOI: [10.4230/LIPIcs.FSTTCS.2018.10](https://doi.org/10.4230/LIPIcs.FSTTCS.2018.10) + *The meta-theorems behind `autstr.uniform`, `autstr.graphs`, + `autstr.algebra` and `autstr.groups`.* ### Related tools -- **[MONA](https://www.brics.dk/mona/)** (Klarlund, Møller, Henriksen et al.) - decides WS1S and WS2S by translating formulas to automata whose transitions are - shared multi-terminal BDDs over the symbol's bits. AutStr's - [`autstr.mtbdd`](autstr/mtbdd.py) adopts exactly that representation, for - exactly MONA's reason: over a convolution alphabet, the flat - `symbol -> target` table is the bottleneck. -- **[Walnut](https://cs.uwaterloo.ca/~shallit/walnut.html)** (Mousavi, Shallit) - proves theorems about automatic sequences by deciding first-order statements - over (ℕ, +) with automata — the same decision procedure, aimed at combinatorics - on words rather than at presenting structures. - -Both are mature and fast, and neither targets *uniformly* automatic classes or -arbitrary automatic presentations, which is where AutStr sits. +**[MONA](https://www.brics.dk/mona/)** decides WS1S and WS2S with automata whose +transitions are shared multi-terminal BDDs over the symbol's bits; +[`autstr.mtbdd`](autstr/mtbdd.py) adopts exactly that representation, for +exactly MONA's reason. **[Walnut](https://cs.uwaterloo.ca/~shallit/walnut.html)** +proves theorems about automatic sequences by deciding first-order statements +over (ℕ, +). Both are mature and fast, and neither targets *uniformly* automatic +classes or arbitrary automatic presentations, which is where AutStr sits. diff --git a/docs/source/index.rst b/docs/source/index.rst index 1e365c5..38fbc99 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -35,6 +35,7 @@ reference is generated from the docstrings. :caption: Project changelog + references Indices and tables ================== diff --git a/docs/source/overview.md b/docs/source/overview.md index 8bd4eac..43574b7 100644 --- a/docs/source/overview.md +++ b/docs/source/overview.md @@ -25,8 +25,9 @@ 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 +Because the first-order theory of an automatic structure is decidable +({ref}`Khoussainov & Nerode `, {ref}`Blumensath & Grädel +`), `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 {doc}`arithmetic & algebra notebook ` walks @@ -118,9 +119,10 @@ element of ℤ[1/2] is 2-divisible but not 3-divisible, and vice versa for ℤ[1 ## 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 +A **uniformly automatic class** ({ref}`Abu Zaid, Grädel & Reinhardt +`) 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. @@ -152,7 +154,8 @@ 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. +tree automaton — exactly the step from {ref}`Büchi's theorem ` to +{ref}`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.tree_arithmetic`, where a number @@ -221,6 +224,70 @@ cyclic groups, and it predates the module. The {doc}`composition notebook ` walks through all five operations. +## Defining one structure inside another + +Automatic structures are closed under **first-order interpretation**, and +`autstr.interpretations` does not merely record that — it *computes* the +interpreted presentation. You give a domain formula δ(x̄), a formula per +relation, a dimension *k* (an element is a *k*-tuple of source elements), and +optionally a definable equivalence ε whose classes become the elements: + +```python +Z = interpret(BuechiArithmetic(), # the integers from the naturals + domain=('Eq(x0,x0) and Eq(x1,x1)', ['x0', 'x1']), + relations={'Lt': (LESS, pair), 'Eq': (SAME, pair)}, + dimension=2, quotient=(SAME, pair)) +``` + +A quotient needs one representative per class. Over words that is the +shortlex-least member, and shortlex is a well-order, so it exists. Over +**trees** no automatic order is well-founded — growing a tree where two members +differ makes it *smaller* — so a class need have no least member at all, and +the representative is instead the least *description* +({ref}`Kuske & Weidner `; existence is +{ref}`Colcombet & Löding `). That construction is provably +exponential in the worst case, which is why `max_states` is worth passing. + +The **ordinals** are the payoff example. By Cantor normal form an ordinal below +ω^n is an n-tuple of naturals under reverse-lexicographic order, so +`autstr.ordinals` is three formulas over Büchi arithmetic and no automaton +authored by hand. How far this reaches is exactly +{ref}`Delhommé's theorem `: the word-automatic ordinals are those +below ω^ω and the tree-automatic ones those below ω^(ω^ω) — which is why +`Ordinal` and `TreeOrdinal` take an exponent instead of being single +structures. + +## Infinite graphs, and where first-order logic stops + +`autstr.infinite_graphs` presents the Cayley graph of ℤⁿ (an asynchronous +product) and the infinite k-regular tree with its prefix order. +`autstr.turing` presents a Turing machine's **configuration graph**: one step +is a bounded rewrite, so the graph is automatic and its first-order theory is +decidable. What you may not ask is whether one configuration *reaches* another — +that is halting, and no first-order formula over this graph can express it. + +Level 2 **collapsible pushdown systems** are where that changes. Their stacks +are stacks of stacks whose letters carry collapse links, and their configuration +graphs are tree-automatic by {ref}`Kartzow's encoding ` — blocks +hang off each other as a tree and the links are recovered from its shape rather +than stored. MSO over these graphs is undecidable, so this is the only +automatic route to them. + +For these graphs **reachability is a relation like any other**. Every run +decomposes into four stretches — words leaving the stack, letters leaving, +letters returning, words returning — each of them reflexive, so + +``` +Reach(x, y) = ∃d ∃f ∃g. A(x, d) ∧ B(d, f) ∧ C(f, g) ∧ D(g, y) +``` + +is a first-order formula over four automata rather than a fixpoint over trees. +It is exponential in the number of control states, so it is *declared* up front +and built the first time a query mentions it. The +{doc}`infinite structures notebook ` builds one +and decides that its reachability is transitive — a statement about all runs of +all lengths. + ## Bounded rank-width: groups and graphs from one linear algebra The width notions above bound how much *combinatorial* structure crosses a cut. @@ -357,12 +424,14 @@ exponents, and classes whose advice is inherently a tree. 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 +decided in linear time — a constructive, streaming form of +{ref}`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 +(`autstr.mtbdd`) — the representation {doc}`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* diff --git a/docs/source/references.md b/docs/source/references.md new file mode 100644 index 0000000..57c1ab0 --- /dev/null +++ b/docs/source/references.md @@ -0,0 +1,157 @@ +# References + +Every construction in AutStr comes from somewhere. This page collects those +sources and says, for each, what in the library it is. The +{doc}`overview ` links here wherever a named theorem or construction +is used. + +## Foundations + +The idea that a logic can be decided by translating formulas into automata long +predates the term *automatic structure*. + +(buechi1960)= +**Büchi, J. R.** *Weak Second-Order Arithmetic and Finite Automata.* +Zeitschrift für math. Logik und Grundlagen der Mathematik 6 (1960), 66–92. +DOI: [10.1002/malq.19600060105](https://doi.org/10.1002/malq.19600060105) +: Monadic second-order logic over (ℕ, +1) is decidable, by translation into + finite automata. **Every `evaluate` call in this library is this + construction**, and `autstr.powerset.MSO0` is the structure that makes the + correspondence literal: first-order logic over it *is* MSO over (ℕ, <). + +(rabin1969)= +**Rabin, M. O.** *Decidability of Second-Order Theories and Automata on +Infinite Trees.* Transactions of the AMS 141 (1969), 1–35. +DOI: [10.2307/1995086](https://doi.org/10.2307/1995086) +: The same programme over trees. `autstr.sparse_tree_automata` and every + tree-automatic presentation here are the finite-tree fragment of this. + +(courcelle1990)= +**Courcelle, B.** *The Monadic Second-Order Logic of Graphs I: Recognizable +Sets of Finite Graphs.* Information and Computation 85(1), 1990, 12–75. +DOI: [10.1016/0890-5401(90)90043-H](https://doi.org/10.1016/0890-5401%2890%2990043-H) +: MSO properties of graphs of bounded tree-width are decidable in linear time. + `autstr.tree_graphs.TreeWidthClass` builds the automaton the theorem + promises — and, because the class is compiled once and each member is one + linear pass, in a constructive and streaming form. + +(makowsky2004)= +**Makowsky, J. A.** *Algorithmic Uses of the Feferman–Vaught Theorem.* +Annals of Pure and Applied Logic 126 (2004), 159–213. +DOI: [10.1016/j.apal.2003.11.002](https://doi.org/10.1016/j.apal.2003.11.002) +: The composition method behind meta-theorems of this shape, and behind + `autstr.composition`. + +## Automatic structures + +(khoussainov1994)= +**Khoussainov, B., & Nerode, A.** *Automatic presentations of structures.* +LCC 1994, LNCS vol. 960, Springer. +DOI: [10.1007/3-540-60178-3_93](https://doi.org/10.1007/3-540-60178-3_93) +: The definition the whole library implements: a structure presented by + automata for its domain and relations. + +(blumensath2000)= +**Blumensath, A., & Grädel, E.** *Automatic Structures.* LICS 2000, pp. 51–62. +[Proceedings](https://lics.siglog.org/2000/Grdel-AutomaticStructures.html) +: Closure under first-order definability, and hence the decidability that + `check` relies on. `autstr.interpretations` computes the closure under + interpretations that this theory guarantees. + +(khoussainov2007)= +**Khoussainov, B., Rubin, S., & Stephan, F.** *Automatic Structures: Richness +and Limitations.* LMCS 3(2), 2007. +arXiv: [cs/0703064](https://arxiv.org/abs/cs/0703064) · +DOI: [10.2168/LMCS-3(2:2)2007](https://doi.org/10.2168/LMCS-3%282%3A2%292007) +: Where the boundaries are — which structures cannot be automatic, and why + some of the constructions here need the tree engine. + +(delhomme2004)= +**Delhommé, C.** *Automaticité des ordinaux et des graphes homogènes.* +Comptes Rendus Mathématique 339(1), 2004, 5–10. +: The two lines `autstr.ordinals` sits between: the word-automatic ordinals are + exactly those below ω^ω, and the tree-automatic ones exactly those below + ω^(ω^ω). `Ordinal(n)` and `TreeOrdinal(n)` take an exponent rather than being + single structures precisely because neither boundary ordinal is reachable. + +(colcombet2007)= +**Colcombet, T., & Löding, C.** *Transforming structures by set +interpretations.* LMCS 3(2), 2007. +: Existence of injective presentations for tree-automatic quotients — the + theorem that makes `interpret(..., quotient=ε)` well posed over trees. + +(kuske2011)= +**Kuske, D., & Weidner, T.** *Size and computation of injective tree automatic +presentations.* MFCS 2011, LNCS vol. 6907. +: The construction `autstr.interpretations` uses for tree quotients. No + automatic order on trees is well-founded, so a class need have no least + member; the representative is instead the least *description*. §4 proves the + exponential blowup unavoidable, which is why `max_states` is worth passing. + +## Uniformly automatic classes + +(abuzaid2016)= +**Abu Zaid, F.** *Algorithmic Solutions via Model Theoretic Interpretations.* +Dissertation, RWTH Aachen University, 2016. +DOI: [10.18154/RWTH-2017-07663](https://doi.org/10.18154/RWTH-2017-07663) + +(abuzaid2017)= +**Abu Zaid, F., Grädel, E., & Reinhardt, F.** *Advice Automatic Structures and +Uniformly Automatic Classes.* CSL 2017, LIPIcs vol. 82, pp. 35:1–35:20. +DOI: [10.4230/LIPIcs.CSL.2017.35](https://doi.org/10.4230/LIPIcs.CSL.2017.35) +: Introduces automatic presentations with advice — the foundation of + `autstr.uniform` and every class in the library. The ℤ[1/p] presentation in + `autstr.algebra` follows its blueprint for (ℚ, +). + +(abuzaid2018)= +**Abu Zaid, F.** *Uniformly Automatic Classes of Finite Structures.* +FSTTCS 2018, LIPIcs vol. 122, pp. 10:1–10:21. +DOI: [10.4230/LIPIcs.FSTTCS.2018.10](https://doi.org/10.4230/LIPIcs.FSTTCS.2018.10) +: The meta-theorems for finite Boolean algebras, finite groups, and graphs of + bounded tree-depth implemented by `autstr.uniform`, `autstr.graphs`, + `autstr.algebra` and `autstr.groups`. + +## Width parameters + +(courcelle2000)= +**Courcelle, B., & Olariu, S.** *Upper Bounds to the Clique Width of Graphs.* +Discrete Applied Mathematics 101 (2000), 77–114. +DOI: [10.1016/S0166-218X(99)00184-5](https://doi.org/10.1016/S0166-218X%2899%2900184-5) +: The k-expressions that `autstr.tree_graphs.CliqueWidthClass` reads as advice. + +(oum2006)= +**Oum, S., & Seymour, P.** *Approximating clique-width and branch-width.* +Journal of Combinatorial Theory Series B 96(4), 2006, 514–528. +: Rank-width and rank decompositions — the advice of + `autstr.tree_graphs.RankWidthClass`, and the width measure the class-2 group + families in `autstr.groups`, `autstr.tree_groups` and + `autstr.cocycle_groups` are graded by. + +## Higher-order pushdown graphs + +(kartzow2013)= +**Kartzow, A.** *Collapsible Pushdown Graphs of Level 2 are Tree-Automatic.* +Logical Methods in Computer Science 9(1), 2013. +arXiv: [1303.2453](https://arxiv.org/abs/1303.2453) +: The encoding `autstr.collapsible` implements — blocks as a tree, collapse + links recovered from its shape rather than stored — and the reachability + decomposition `autstr.collapsible_reach` builds: §4 splits every run into + four stretches, §5 turns that into a relation of the graph. Since MSO over + these graphs is undecidable, this is the only automatic route to them. + +## Related tools + +**[MONA](https://www.brics.dk/mona/)** (Klarlund, Møller, Henriksen et al.) +: Decides WS1S and WS2S by translating formulas to automata whose transitions + are shared multi-terminal BDDs over the symbol's bits. + {py:mod}`autstr.mtbdd` adopts exactly that representation, for exactly MONA's + reason: over a convolution alphabet, the flat `symbol -> target` table is the + bottleneck. + +**[Walnut](https://cs.uwaterloo.ca/~shallit/walnut.html)** (Mousavi, Shallit) +: Proves theorems about automatic sequences by deciding first-order statements + over (ℕ, +) with automata — the same decision procedure, aimed at + combinatorics on words rather than at presenting structures. + +Both are mature and fast, and neither targets *uniformly* automatic classes or +arbitrary automatic presentations, which is where AutStr sits. From 58bf5e47bec8848886656bb9ec3949f64f1cec63 Mon Sep 17 00:00:00 2001 From: Faried Abu Zaid Date: Thu, 13 Aug 2026 22:24:50 +0200 Subject: [PATCH 9/9] Look up the references I could not verify offline All four now carry a DOI, and one annotation was wrong. Delhomme 2004 -> 10.1016/j.crma.2004.03.035 (CR Math. 339(1), 5-10) Oum & Seymour -> 10.1016/j.jctb.2005.10.006 (JCTB 96(4), 514-528) Colcombet & -> 10.2168/LMCS-3(2:4)2007 (LMCS 3(2:4), 1-36) Loeding Kuske & -> 10.1007/978-3-642-22993-0_39 (LNCS 6907, 424-435) Weidner Kartzow -> 10.2168/LMCS-9(1:12)2013 (issue was 9(1:12), not 9(1)) Oum & Seymour is the one that needed care. A publisher's abstract page claims rank-width is not defined there, which would have made it the wrong citation for `RankWidthClass`; Oum's own later survey settles it -- "Rank-width was introduced by Oum and Seymour [71]", where [71] is exactly this paper. The entry now says so, as the branch-width of the cut-rank function. Colcombet and Loeding's role is stated more carefully too: the existence of injective presentations for tree-automatic structures, which is what makes a quotient interpretation well posed over trees, with Kuske and Weidner making it effective. Co-Authored-By: Claude Opus 5 --- docs/source/references.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/source/references.md b/docs/source/references.md index 57c1ab0..ad3e50c 100644 --- a/docs/source/references.md +++ b/docs/source/references.md @@ -69,6 +69,7 @@ DOI: [10.2168/LMCS-3(2:2)2007](https://doi.org/10.2168/LMCS-3%282%3A2%292007) (delhomme2004)= **Delhommé, C.** *Automaticité des ordinaux et des graphes homogènes.* Comptes Rendus Mathématique 339(1), 2004, 5–10. +DOI: [10.1016/j.crma.2004.03.035](https://doi.org/10.1016/j.crma.2004.03.035) : The two lines `autstr.ordinals` sits between: the word-automatic ordinals are exactly those below ω^ω, and the tree-automatic ones exactly those below ω^(ω^ω). `Ordinal(n)` and `TreeOrdinal(n)` take an exponent rather than being @@ -76,13 +77,17 @@ Comptes Rendus Mathématique 339(1), 2004, 5–10. (colcombet2007)= **Colcombet, T., & Löding, C.** *Transforming structures by set -interpretations.* LMCS 3(2), 2007. -: Existence of injective presentations for tree-automatic quotients — the - theorem that makes `interpret(..., quotient=ε)` well posed over trees. +interpretations.* Logical Methods in Computer Science 3(2:4), 2007, 1–36. +DOI: [10.2168/LMCS-3(2:4)2007](https://doi.org/10.2168/LMCS-3%282%3A4%292007) +: Set interpretations, and their application to tree-automatic structures. + Every tree-automatic structure admits an *injective* presentation, which is + what makes `interpret(..., quotient=ε)` well posed over trees; Kuske and + Weidner below make that effective. (kuske2011)= -**Kuske, D., & Weidner, T.** *Size and computation of injective tree automatic -presentations.* MFCS 2011, LNCS vol. 6907. +**Kuske, D., & Weidner, T.** *Size and Computation of Injective Tree Automatic +Presentations.* MFCS 2011, LNCS vol. 6907, pp. 424–435. +DOI: [10.1007/978-3-642-22993-0_39](https://doi.org/10.1007/978-3-642-22993-0_39) : The construction `autstr.interpretations` uses for tree quotients. No automatic order on trees is well-founded, so a class need have no least member; the representative is instead the least *description*. §4 proves the @@ -122,7 +127,9 @@ DOI: [10.1016/S0166-218X(99)00184-5](https://doi.org/10.1016/S0166-218X%2899%290 (oum2006)= **Oum, S., & Seymour, P.** *Approximating clique-width and branch-width.* Journal of Combinatorial Theory Series B 96(4), 2006, 514–528. -: Rank-width and rank decompositions — the advice of +DOI: [10.1016/j.jctb.2005.10.006](https://doi.org/10.1016/j.jctb.2005.10.006) +: The paper that introduces **rank-width**, as the branch-width of the cut-rank + function. Rank decompositions are the advice of `autstr.tree_graphs.RankWidthClass`, and the width measure the class-2 group families in `autstr.groups`, `autstr.tree_groups` and `autstr.cocycle_groups` are graded by. @@ -131,8 +138,9 @@ Journal of Combinatorial Theory Series B 96(4), 2006, 514–528. (kartzow2013)= **Kartzow, A.** *Collapsible Pushdown Graphs of Level 2 are Tree-Automatic.* -Logical Methods in Computer Science 9(1), 2013. -arXiv: [1303.2453](https://arxiv.org/abs/1303.2453) +Logical Methods in Computer Science 9(1:12), 2013. +arXiv: [1303.2453](https://arxiv.org/abs/1303.2453) · +DOI: [10.2168/LMCS-9(1:12)2013](https://doi.org/10.2168/LMCS-9%281%3A12%292013) : The encoding `autstr.collapsible` implements — blocks as a tree, collapse links recovered from its shape rather than stored — and the reachability decomposition `autstr.collapsible_reach` builds: §4 splits every run into