Skip to content

Add co-lexicographic (Wheeler) ordering - #9

Merged
Autoplectic merged 6 commits into
mainfrom
cursor/wheeler-automata
Sep 19, 2026
Merged

Autoplectic merged 6 commits into
mainfrom
cursor/wheeler-automata

Conversation

@Autoplectic

Copy link
Copy Markdown
Member

Adds Wheeler (co-lexicographic) ordering to sofic as a structural layer: recognition and canonical sorting, a succinct BWT index, an interval fast path for the existing synchronization algorithms, and a stochastic layer for ε-machines.

A labeled graph is Wheeler when its states admit a total order in which each state's set of incoming words is a co-lex interval. Because co-lex compares strings from the last symbol backwards, it is the recency order on pasts — so an ε-machine is Wheeler exactly when its causal-state partition of history space is an interval partition under recency.

What's here

Topological core (sofic/automata/wheeler.py) — the three Gagie–Manzini–Sirén axioms, is_input_consistent as a cheap necessary condition, wheeler_order, minimum_wdfa, wnfa_to_wdfa, and colex_width. Since "is this presentation Wheeler?" is meaningful for any state machine, is_wheeler / wheeler_order / colex_width live on the StateMachine base class rather than only on UnifilarAutomaton, so sofic shifts and ε-machines get them too.

Interval fast path (sofic/generators/synchronization.py) — when a Wheeler order exists, reachable subsets of the power automaton are contiguous, so states become (lo, hi) pairs bounded by n(n+1)/2 instead of 2ⁿ. The four synchronization entry points inherit this with no behavioral change, and the tests assert both paths agree.

Succinct index (sofic/automata/wheeler_index.py) — the GMS representation with numpy-backed rank/select, forward_search, contains, and colex rank_word / unrank_word. No new dependencies.

Shifts (sofic/shifts/wheeler.py) — wheeler_cover, plus a WheelerCover class. The Krieger cover stubs in cover_construction.py are left alone; the Wheeler cover doesn't subsume enough of that machinery to fill them honestly.

Stochastic layer (sofic/generators/wheeler_epsilon.py) — original work. No literature exists on weighted or probabilistic Wheeler automata, so every public symbol carries a docstring saying no canonical source is known, per the repo's literature-references rule.

Presentations versus languages

These are kept as distinct APIs throughout. is_wheeler asks about this presentation. Whether the language is Wheeler is strictly weaker and much more expensive — O(mn) for a DFA, PSPACE-complete for an NFA — and that search over presentations is wheeler_cover / wheeler_presentation.

A result worth review

Beyond the expected C_W ≥ C_μ, there's a second independent bound: a Wheeler presentation is input consistent, so its state determines the symbol that entered it, giving C_W ≥ H[X₀] for a stationary process. Together, C_W ≥ max(C_μ, H[X₀]). The fair coin is the sharp case — nothing to predict, C_μ = 0, yet sortability costs a full bit.

This holds for irreducible machines using the stationary symbol entropy; it fails for UncoupledGMPs, which isn't strongly connected. The docstring and test both scope it accordingly.

Verification

996 tests pass, ruff clean, docs build with all citations resolving. Includes known-answer tests (golden mean Wheeler with order A < B; even, Nemo, and butterfly not), the enumeration counts 2/3/12/49/256 for k=2, n=1..5, hypothesis properties, index round-trips, and equivalence of the interval and subset power-automaton paths. The power-automaton comparison was mutation-tested to confirm it catches an introduced bug rather than passing vacuously.

Adds a 5-state Wheeler ε-machine with infinite Markov order as the discriminating example that Wheeler is not a restatement of finite memory, and nine references to docs/references.bib.

The final commit relocks uv.lock: pyproject.toml declares matplotlib>=3.5 under viz but the lock predated that, so every uv invocation was re-resolving before running. No package versions change.

Made with Cursor

Ryan James and others added 6 commits September 17, 2026 18:02
Wheeler order is the total order on states in which the words reaching
each state form an interval of the co-lexicographically sorted prefixes
of the language. Co-lex compares words from the last symbol backwards,
so a Wheeler presentation is one whose states partition history space
into bands of recency.

Topological core in automata/wheeler.py: axiom checking, order search by
partition refinement then linearization of the maximum co-lex relation,
co-lex width, minimum WDFA, and WNFA determinization. Every graph-backed
model inherits is_wheeler/wheeler_order/colex_width from StateMachine.

Path coherence gives synchronization.py an interval power automaton:
n(n+1)/2 states instead of 2^n, so the Markov order, cryptic order, and
reset threshold stay polynomial. automata/wheeler_index.py stores the
GMS arrays for O(|w| log |A|) membership and co-lex rank/unrank.

shifts/wheeler.py searches higher block presentations for a Wheeler
cover; generators/wheeler_epsilon.py proposes C_W, the entropy of the
states of a Wheeler presentation, bounded below by both C_mu and H[X_0].
That layer is original and every docstring says no source exists.

Wheeler is not finite memory: wheeler_infinite_order_process is Wheeler
with infinite Markov order, while the even process is non-Wheeler in
every presentation because Wheeler languages are star-free.
Becker et al. is 2023, not 2024, and the PSPACE-completeness of deciding
whether an NFA's language is Wheeler is D'Agostino, Martincigh, and Policriti,
not Alanko et al. Add the missing DAgostino2023 entry and correct the claim in
both the is_wheeler docstring and the automata docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
pyproject declares matplotlib>=3.5 under viz, but uv.lock predated that, so
every uv invocation re-resolved the project before running. No package
versions change; the lock only records the viz dependency and its specifier.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	docs/references.bib
#	sofic/examples/__init__.py
@Autoplectic
Autoplectic merged commit 85046d5 into main Sep 19, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant