morphata 2.2.2__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. morphata-2.3.0/.agents/plans/2026-10-16-canonical-rfsa.md +456 -0
  2. {morphata-2.2.2 → morphata-2.3.0}/.envrc +5 -5
  3. {morphata-2.2.2 → morphata-2.3.0}/.gitignore +2 -0
  4. {morphata-2.2.2 → morphata-2.3.0}/AGENTS.md +12 -0
  5. {morphata-2.2.2 → morphata-2.3.0}/CHANGELOG.md +241 -0
  6. {morphata-2.2.2 → morphata-2.3.0}/PKG-INFO +6 -4
  7. {morphata-2.2.2 → morphata-2.3.0}/bench/README.md +17 -0
  8. morphata-2.3.0/bench/test_distance_perf.py +217 -0
  9. {morphata-2.2.2 → morphata-2.3.0}/bench/test_equivalence_perf.py +1 -2
  10. morphata-2.3.0/bench/test_matrix_operator_perf.py +412 -0
  11. morphata-2.3.0/bench/test_transforms_perf.py +239 -0
  12. {morphata-2.2.2 → morphata-2.3.0}/docs/api/modules.rst +1 -0
  13. morphata-2.3.0/docs/api/morphata.operators.rst +26 -0
  14. morphata-2.3.0/docs/api/morphata.storage.rst +36 -0
  15. {morphata-2.2.2 → morphata-2.3.0}/docs/api/morphata.symbolic.rst +11 -1
  16. morphata-2.3.0/docs/bibliography.rst +4 -0
  17. {morphata-2.2.2 → morphata-2.3.0}/docs/concepts/operators.rst +62 -15
  18. {morphata-2.2.2 → morphata-2.3.0}/docs/concepts/symbolic.rst +55 -0
  19. {morphata-2.2.2 → morphata-2.3.0}/docs/concepts/weights.rst +13 -4
  20. {morphata-2.2.2 → morphata-2.3.0}/docs/conf.py +27 -17
  21. {morphata-2.2.2 → morphata-2.3.0}/docs/index.rst +10 -1
  22. {morphata-2.2.2 → morphata-2.3.0}/docs/quick-start.rst +22 -8
  23. morphata-2.3.0/docs/refs.bib +516 -0
  24. {morphata-2.2.2 → morphata-2.3.0}/mypy.ini +2 -1
  25. morphata-2.3.0/pixi.lock +5078 -0
  26. {morphata-2.2.2 → morphata-2.3.0}/pyproject.toml +72 -46
  27. {morphata-2.2.2 → morphata-2.3.0}/pytest.toml +2 -0
  28. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/__init__.py +2 -1
  29. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/_version.py +2 -2
  30. morphata-2.3.0/src/morphata/alphabet.py +369 -0
  31. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/builder.py +9 -27
  32. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/hoa/__init__.py +2 -0
  33. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/logic/__init__.py +8 -1
  34. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/logic/_common.py +15 -7
  35. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/logic/psl.py +2 -2
  36. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/operators/__init__.py +4 -1
  37. morphata-2.3.0/src/morphata/operators/jax.py +811 -0
  38. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/operators/matrix.py +159 -92
  39. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/operators/polynomial.py +16 -19
  40. morphata-2.3.0/src/morphata/storage/__init__.py +8 -0
  41. morphata-2.3.0/src/morphata/storage/serialize.py +554 -0
  42. morphata-2.3.0/src/morphata/storage/signature.py +207 -0
  43. morphata-2.3.0/src/morphata/symbolic/__init__.py +31 -0
  44. morphata-2.3.0/src/morphata/symbolic/automaton.py +1601 -0
  45. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/symbolic/bdd.py +251 -36
  46. morphata-2.3.0/src/morphata/symbolic/distance.py +952 -0
  47. morphata-2.3.0/src/morphata/symbolic/transforms.py +1355 -0
  48. morphata-2.3.0/src/morphata/utils/__init__.py +9 -0
  49. morphata-2.2.2/src/morphata/utils.py → morphata-2.3.0/src/morphata/utils/expressions.py +2 -78
  50. morphata-2.3.0/src/morphata/utils/memory.py +75 -0
  51. morphata-2.3.0/src/morphata/utils/sampling.py +728 -0
  52. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/test_exporter.py +27 -21
  53. {morphata-2.2.2 → morphata-2.3.0}/tests/logic/test_psl.py +2 -1
  54. morphata-2.3.0/tests/operators/test_jax.py +81 -0
  55. morphata-2.3.0/tests/operators/test_matrix.py +534 -0
  56. {morphata-2.2.2 → morphata-2.3.0}/tests/operators/test_polynomial.py +4 -3
  57. morphata-2.3.0/tests/operators/test_reductions.py +116 -0
  58. morphata-2.3.0/tests/storage/test_serialize.py +463 -0
  59. morphata-2.3.0/tests/storage/test_signature.py +498 -0
  60. {morphata-2.2.2 → morphata-2.3.0}/tests/symbolic/test_bdd_helpers.py +196 -0
  61. {morphata-2.2.2 → morphata-2.3.0}/tests/symbolic/test_compose.py +21 -20
  62. morphata-2.3.0/tests/symbolic/test_distance.py +584 -0
  63. {morphata-2.2.2 → morphata-2.3.0}/tests/symbolic/test_from_hoaf.py +5 -3
  64. {morphata-2.2.2 → morphata-2.3.0}/tests/symbolic/test_language_equivalence.py +14 -15
  65. {morphata-2.2.2 → morphata-2.3.0}/tests/symbolic/test_ltlf2dfa_equivalence.py +3 -4
  66. morphata-2.3.0/tests/symbolic/test_rfsa.py +529 -0
  67. {morphata-2.2.2 → morphata-2.3.0}/tests/symbolic/test_symbolic_automaton.py +193 -42
  68. {morphata-2.2.2 → morphata-2.3.0}/tests/symbolic/test_to_hoa.py +5 -5
  69. morphata-2.3.0/tests/symbolic/test_transforms.py +1139 -0
  70. morphata-2.3.0/tests/test_alphabet.py +123 -0
  71. {morphata-2.2.2 → morphata-2.3.0}/tests/test_builder.py +24 -30
  72. morphata-2.3.0/uv.lock +3453 -0
  73. morphata-2.2.2/bench/test_transforms_perf.py +0 -120
  74. morphata-2.2.2/docs/api/morphata.operators.rst +0 -12
  75. morphata-2.2.2/pixi.lock +0 -7844
  76. morphata-2.2.2/pixi.toml +0 -127
  77. morphata-2.2.2/src/morphata/alphabet.py +0 -92
  78. morphata-2.2.2/src/morphata/symbolic/__init__.py +0 -14
  79. morphata-2.2.2/src/morphata/symbolic/automaton.py +0 -847
  80. morphata-2.2.2/src/morphata/symbolic/utils.py +0 -723
  81. morphata-2.2.2/tests/operators/test_matrix.py +0 -328
  82. morphata-2.2.2/tests/symbolic/test_transforms.py +0 -530
  83. morphata-2.2.2/tests/test_alphabet.py +0 -50
  84. morphata-2.2.2/uv.lock +0 -3335
  85. {morphata-2.2.2 → morphata-2.3.0}/.basedpyright/baseline.json +0 -0
  86. {morphata-2.2.2 → morphata-2.3.0}/.dockerignore +0 -0
  87. {morphata-2.2.2 → morphata-2.3.0}/.editorconfig +0 -0
  88. {morphata-2.2.2 → morphata-2.3.0}/.envrc.recommended +0 -0
  89. {morphata-2.2.2 → morphata-2.3.0}/.fdignore +0 -0
  90. {morphata-2.2.2 → morphata-2.3.0}/.gitattributes +0 -0
  91. {morphata-2.2.2 → morphata-2.3.0}/.mailmap +0 -0
  92. {morphata-2.2.2 → morphata-2.3.0}/.python-version +0 -0
  93. {morphata-2.2.2 → morphata-2.3.0}/.rstcheck.cfg +0 -0
  94. {morphata-2.2.2 → morphata-2.3.0}/CLAUDE.md +0 -0
  95. {morphata-2.2.2 → morphata-2.3.0}/CONTRIBUTING.md +0 -0
  96. {morphata-2.2.2 → morphata-2.3.0}/LICENSE +0 -0
  97. {morphata-2.2.2 → morphata-2.3.0}/README.rst +0 -0
  98. {morphata-2.2.2 → morphata-2.3.0}/bench/__init__.py +0 -0
  99. {morphata-2.2.2 → morphata-2.3.0}/bench/conftest.py +0 -0
  100. {morphata-2.2.2 → morphata-2.3.0}/bench/test_hoa_exporter_perf.py +0 -0
  101. {morphata-2.2.2 → morphata-2.3.0}/bench/test_hoa_parser_perf.py +0 -0
  102. {morphata-2.2.2 → morphata-2.3.0}/bench/test_sere_nlm_perf.py +0 -0
  103. {morphata-2.2.2 → morphata-2.3.0}/ci/make-release +0 -0
  104. {morphata-2.2.2 → morphata-2.3.0}/docs/Makefile +0 -0
  105. {morphata-2.2.2 → morphata-2.3.0}/docs/_static/navigation.html +0 -0
  106. {morphata-2.2.2 → morphata-2.3.0}/docs/api/morphata.acc_expr.rst +0 -0
  107. {morphata-2.2.2 → morphata-2.3.0}/docs/api/morphata.acceptance.rst +0 -0
  108. {morphata-2.2.2 → morphata-2.3.0}/docs/api/morphata.alphabet.rst +0 -0
  109. {morphata-2.2.2 → morphata-2.3.0}/docs/api/morphata.builder.rst +0 -0
  110. {morphata-2.2.2 → morphata-2.3.0}/docs/api/morphata.hoa.rst +0 -0
  111. {morphata-2.2.2 → morphata-2.3.0}/docs/api/morphata.logic.rst +0 -0
  112. {morphata-2.2.2 → morphata-2.3.0}/docs/changelog.md +0 -0
  113. {morphata-2.2.2 → morphata-2.3.0}/docs/concepts/acceptance.rst +0 -0
  114. {morphata-2.2.2 → morphata-2.3.0}/docs/concepts/hoa.rst +0 -0
  115. {morphata-2.2.2 → morphata-2.3.0}/docs/concepts/index.rst +0 -0
  116. {morphata-2.2.2 → morphata-2.3.0}/justfile +0 -0
  117. {morphata-2.2.2 → morphata-2.3.0}/project.utf-8.add +0 -0
  118. {morphata-2.2.2 → morphata-2.3.0}/project.utf-8.add.spl +0 -0
  119. {morphata-2.2.2 → morphata-2.3.0}/pyrightconfig.json +0 -0
  120. {morphata-2.2.2 → morphata-2.3.0}/rules/no-derivative-multiarg.yml +0 -0
  121. {morphata-2.2.2 → morphata-2.3.0}/rules/no-typing-as-ty.yml +0 -0
  122. {morphata-2.2.2 → morphata-2.3.0}/sgconfig.yml +0 -0
  123. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/acc_expr.py +0 -0
  124. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/acceptance.py +0 -0
  125. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/hoa/__main__.py +0 -0
  126. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/hoa/hoa.lark +0 -0
  127. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/hoa/parser.py +0 -0
  128. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/logic/__init__.pyi +0 -0
  129. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/logic/ltl.py +0 -0
  130. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/logic/sere.py +0 -0
  131. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/operators/_backend.py +0 -0
  132. {morphata-2.2.2 → morphata-2.3.0}/src/morphata/py.typed +0 -0
  133. {morphata-2.2.2 → morphata-2.3.0}/tests/conftest.py +0 -0
  134. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/aut1.hoa +0 -0
  135. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/aut11.hoa +0 -0
  136. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/aut2.hoa +0 -0
  137. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/aut3.2.hoa +0 -0
  138. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/aut3.hoa +0 -0
  139. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/aut4.hoa +0 -0
  140. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/aut5.hoa +0 -0
  141. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/aut6.hoa +0 -0
  142. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/aut7.hoa +0 -0
  143. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/aut8.hoa +0 -0
  144. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/finite_aut.hoa +0 -0
  145. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/gen_buchi_3.hoa +0 -0
  146. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/gen_cobuchi_2.hoa +0 -0
  147. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/rabin_2pair.hoa +0 -0
  148. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/streett_2pair.hoa +0 -0
  149. {morphata-2.2.2 → morphata-2.3.0}/tests/hoa/test_parser.py +0 -0
  150. {morphata-2.2.2 → morphata-2.3.0}/tests/logic/__init__.py +0 -0
  151. {morphata-2.2.2 → morphata-2.3.0}/tests/logic/test_ltl.py +0 -0
  152. {morphata-2.2.2 → morphata-2.3.0}/tests/logic/test_sere.py +0 -0
  153. {morphata-2.2.2 → morphata-2.3.0}/tests/test_acceptance_validation.py +0 -0
@@ -0,0 +1,456 @@
1
+ # Canonical RFSA Implementation Plan
2
+
3
+ > **For implementation:** Execute the dependency chain in order and update each
4
+ > checkbox. All tasks are sequential. Agent delegation, commits, and remote
5
+ > actions are not authorized. Perform a separate whole-change review before
6
+ > handing the local diff back to the author.
7
+
8
+ **Goal:** Construct the canonical finite-word RFSA without constructing a full
9
+ DFA or minimizing a DFA in the production path.
10
+
11
+ **Architecture:** Implement the cover-aware subset construction from Section 5
12
+ of Denis, Lemay, and Terlutte, using an online basis of non-coverable reachable
13
+ state subsets. Apply it twice, with language reversal before each pass. Keep
14
+ the algorithm in the existing symbolic transforms module and expose one
15
+ `to_min_rfsa` operation alongside `to_min_dfa`.
16
+
17
+ **Tech Stack:** Python 3.12+, existing `SymbolicAutomaton`, Pollywog-backed BDD
18
+ guards, `Finite` acceptance, pytest, Hypothesis, Ruff, Zuban, BasedPyright,
19
+ Sphinx. No new dependencies.
20
+
21
+ **Spec:** This plan, based on the author's request for the canonical RFSA
22
+ construction without full determinization and the verified literature below.
23
+
24
+ **Assumptions:**
25
+
26
+ - The public name is `to_min_rfsa`, matching `to_min_dfa`.
27
+ - Finite AFAs are accepted through the existing dealternation performed by
28
+ `reverse_language`; this normalization can itself grow exponentially.
29
+ - Canonical means unique up to state renaming, with prime residual states and
30
+ saturated transitions. It does not promise canonical integer IDs or bytes.
31
+ - `initial`, `predicate_exploration_order`, and `abort_on_max_size` follow the
32
+ existing transform conventions, with the bound accounting specified below.
33
+ - Public RFSA diagnostics, arbitrary NFA minimization, experiment tooling, and
34
+ changes to existing transform behavior are outside this plan.
35
+
36
+ ## Global Constraints
37
+
38
+ - "Local changes only."
39
+ - "No scheduled, recurring, background, or looping execution, and no spawning
40
+ agents without my approval or an agreed plan that authorizes their work."
41
+ - "If `.jj` exists in the project root, use `jj`."
42
+ - "Morphata must not depend on Automata Encoders."
43
+ - "Use NumPy/numpydoc-style sections for Python docstrings. Use RST math markup
44
+ inside those sections and in the main docstring body."
45
+ - Use the activated environment directly. Do not install dependencies or use
46
+ environment wrappers. Stop and report a missing tool.
47
+ - This is a planning artifact. Implementation requires the author's subsequent
48
+ instruction; the repository defaults to read-only work.
49
+ - Keep this plan local and ignored. Preserve existing changes, including
50
+ `.envrc.recommended` and `CHANGELOG.md`; do not include them in this work.
51
+ - Do not introduce new production modules, public intermediate constructions,
52
+ compatibility layers, or refactors of `explore_and_transform`.
53
+
54
+ ## Review Focus
55
+
56
+ - A subset discovered early becomes coverable later: Task 1 late-cover test
57
+ and basis invariant. Removed states must leave no output references.
58
+ - Skipping expansion must preserve all reachable subset generators: Task 1
59
+ exhaustive two-state oracle and closure proof.
60
+ - Set inclusion is different from BDD implication between state disjunctions:
61
+ Task 1 uses `frozenset[int]` and ordinary subset comparison throughout.
62
+ - Language equality alone does not establish canonicality: Task 2 compares
63
+ residual-labelled states, initial/final sets, and all transition guards
64
+ against a separate small-DFA reference.
65
+ - No hidden full DFA construction or alphabet enumeration: Task 2 sentinel
66
+ tests, compressed-language fixtures, and whole-change review.
67
+ - Partial transitions, constants, empty language, alphabet restrictions, and
68
+ selected initial formulas: Tasks 1 and 2 named boundary tests.
69
+ - Resource accounting and exponential cases: Task 1 admission-bound tests and
70
+ Task 2 public documentation. No polynomial-time or output-sensitive claim.
71
+
72
+ ---
73
+
74
+ ## Verified sources and current repository
75
+
76
+ - Zotero key `denis2002residual`, Item ID
77
+ `http://zotero.org/users/5809761/items/8MQULFLR`; verified PDF:
78
+ `/home/anand/Zotero/storage/7RJJJLZ5/Denis et al. - 2002 - Residual finite state automata.pdf`.
79
+ Definition 4.5 specifies canonical states and saturation. Section 5,
80
+ Lemma 5.1 and Theorem 5.2 establish the two-pass construction. The end of
81
+ Section 5 and Proposition 6.2 establish the relevant exponential limits.
82
+ - `src/morphata/symbolic/transforms.py:582-675`: `reverse_language` removes
83
+ alternation, materializes constant successors, reverses edges, swaps initial
84
+ and accepting states, and prunes unreachable states.
85
+ - `src/morphata/symbolic/transforms.py:108-273`: `explore_and_transform` fixes
86
+ output states and edges during exploration. An online basis can later lose
87
+ states, so use a private worklist in the RFSA construction; leave this driver
88
+ unchanged.
89
+ - `src/morphata/symbolic/bdd.py`: `partition_by_state_residual`,
90
+ `state_vars_in_support`, `state_set_to_bdd`, and `relation_from_rows` provide
91
+ symbolic successor partitions and output construction.
92
+ - `src/morphata/symbolic/automaton.py:746-787`: public transform-method pattern.
93
+ `src/morphata/symbolic/__init__.py` provides explicit free-function exports.
94
+ - `tests/symbolic/test_transforms.py` establishes transform behavior and
95
+ bounded property-test patterns; `tests/conftest.py` provides fresh manager
96
+ fixtures. Create one dedicated RFSA test file to keep this substantial test
97
+ family out of the existing large transforms test file.
98
+ - `CONTRIBUTING.md`, `justfile`, and `pytest.toml` establish verification
99
+ commands. Run direct commands; `pytest -q` includes configured source and
100
+ documentation doctests. Sphinx uses `docs/refs.bib`.
101
+
102
+ ## Construction contract and notation
103
+
104
+ For a normalized finite NFA \(A=(Q,I,F,\delta)\), all initial/successor formulas
105
+ are disjunctions of indexed states, possibly false. Interpret the false
106
+ formula as the empty subset; true successors must have been materialized by
107
+ the existing normalization. Let \(\mathcal R\) be its reachable state subsets.
108
+ Define
109
+
110
+ \[
111
+ \operatorname{covered}(S,B)
112
+ \iff S=\bigcup\{T\in B\mid T\subsetneq S\}.
113
+ \]
114
+
115
+ The union of an empty family is empty, so the empty subset is always covered.
116
+ The construction \(C(A)\) has the non-coverable members of \(\mathcal R\) as
117
+ states. With final basis \(B\), emit
118
+
119
+ \[
120
+ I_C=\{S\in B\mid S\subseteq I\},\qquad
121
+ F_C=\{S\in B\mid S\cap F\ne\varnothing\},\qquad
122
+ \delta_C(S,a)=\{T\in B\mid T\subseteq\delta(S,a)\}.
123
+ \]
124
+
125
+ The public result is \(C((C(A^R))^R)\). The first pass produces a reachable
126
+ RFSA for the reversed language; the second pass is canonical under Theorem
127
+ 5.2. Do not assert canonicality for the first pass on an arbitrary input NFA.
128
+
129
+ ### Online basis and proof obligation
130
+
131
+ This scheduling optimization is an implementation adaptation of the paper's
132
+ construction; the paper's definition alone does not prove the optimization.
133
+
134
+ 1. Begin with an empty basis and offer the initial subset. For a candidate
135
+ already in the basis, do nothing. For a covered candidate, do not store or
136
+ expand it. Otherwise admit and queue it.
137
+ 2. After an admission, remove any basis member now covered by the other
138
+ members. Skip removed members when popping pending work. A member currently
139
+ being expanded may finish expansion from its immutable subset snapshot.
140
+ 3. Expand a retained subset \(S\) by partitioning the BDD relation
141
+ \(\bigvee_{q\in S}\Delta(q)\), restricted to
142
+ `aut.alphabet.domain(manager)`. Offer the successor subset for each
143
+ satisfiable guard. Never enumerate alphabet valuations.
144
+ 4. Continue until the worklist is empty. Only then assign output state IDs and
145
+ construct the saturated initial formula and transition rows from the final
146
+ basis. Temporary successor partitions may be recomputed during emission;
147
+ do not retain a complete subset-transition graph or permanent visited table.
148
+
149
+ Required argument: every discarded subset remains a union of current basis
150
+ members. Replacing a basis member with a cover preserves that invariant, and
151
+ NFA transition images distribute over union. At termination the basis is
152
+ transition-closed up to union, so it generates all reachable subsets. Every
153
+ retained subset was itself reached from the source initial subset; combining
154
+ these facts and irredundancy identifies the final basis with the non-coverable
155
+ members of \(\mathcal R\). Strict subset covers also show that a removed
156
+ subset cannot later need admission again. Keep this reasoning near the helper.
157
+
158
+ Use `frozenset[int]` for source subsets and BDDs for guards/relations. Boolean
159
+ implication on independent state variables does not test accepted-language
160
+ inclusion. This construction requires neither language hashing nor language
161
+ inclusion queries.
162
+
163
+ ### Bounds and exceptional cases
164
+
165
+ - The private pass counts cumulative basis admissions, including members later
166
+ removed. Raise existing `MacroStateLimitError(count)` immediately after the
167
+ first admission exceeding `abort_on_max_size`. A duplicate or covered
168
+ candidate does not increment the count. Do not retain all candidates to count
169
+ them. This bounds admissions, not BDD memory or wall-clock time.
170
+ - The public operation forwards the bound independently to each existing
171
+ reversal/normalization stage and each private pass. It is not a bound on
172
+ final RFSA size. Existing normalization can exceed a zero bound even for an
173
+ empty language; do not add different special-case semantics to the wrapper.
174
+ - An empty basis emits a zero-state automaton with false initial condition and
175
+ `Finite(frozenset())`. Do not materialize an empty-language RFSA state.
176
+ - Keep a nonempty residual containing the empty word accepting. Include every
177
+ eligible initial state and transition, including non-maximal targets.
178
+ - Preserve manager and alphabet identity. Canonicality holds over the supplied
179
+ alphabet domain. Finite AFAs are normalized using existing code; reject
180
+ omega acceptance with `NotImplementedError` before construction.
181
+ - Assign final IDs contiguously from zero by sorting the integer tuple of each
182
+ source subset lexicographically. This fixes the private helper's subset-to-ID
183
+ correspondence for its tests. Predicate exploration order controls guard exploration and
184
+ must cover predicates in the relation/domain. State numbering is outside the
185
+ language-level canonicality guarantee.
186
+
187
+ ## Task 1: Implement the private cover-aware NFA construction
188
+
189
+ **Files:**
190
+
191
+ - Modify: `src/morphata/symbolic/transforms.py`, beside the existing reversal
192
+ and finite-word transforms (`reverse_language` currently at lines 582-675).
193
+ - Create: `tests/symbolic/test_rfsa.py` for RFSA fixtures and correctness tests.
194
+
195
+ **Interfaces:**
196
+
197
+ - Consumes: an already normalized, finite, non-alternating `SymbolicAutomaton`
198
+ whose constant true successors have been materialized.
199
+ - Produces the private function:
200
+
201
+ ```python
202
+ def _residualize_nfa[AP: Hashable, Symbol](
203
+ aut: SymbolicAutomaton[AP, Symbol],
204
+ *,
205
+ predicate_exploration_order: Sequence[AP] | None = None,
206
+ abort_on_max_size: int | None = None,
207
+ ) -> SymbolicAutomaton[AP, Symbol]: ...
208
+ ```
209
+
210
+ **Parallelization:** Sequential; Task 2 depends on this helper and modifies the
211
+ same implementation and test files.
212
+
213
+ - [x] **Step 1: Write the failing basis and boundary tests.** Add:
214
+ - `test_residualize_matches_exhaustive_two_state_subsets`: enumerate all
215
+ 256 two-state/two-symbol transition relations and all four initial subsets
216
+ in a test-only reference; compare the non-coverable reachable subsets with
217
+ those represented by the helper's final initial/final/transition structure.
218
+ Use the specified sorted subset-to-index correspondence and compare every
219
+ row, initial set, and final set. Fix source acceptance to `{1}`. Bound
220
+ manager lifetime per transition relation; exercise both symbols.
221
+ - `test_residualize_removes_late_cover`: initial `{0,1}`, transitions
222
+ `0 --a--> 0`, `1 --b--> 1`, all other transitions empty, both accepting.
223
+ Expect two accepting states, both initial, with separate `a`/`b` loops;
224
+ the initially admitted `{0,1}` must disappear completely.
225
+ - `test_residualize_skips_covered_candidate`: initial `{0}`,
226
+ `0 --a--> {1}`, `0 --b--> {0,1}`, `1 --a--> {1}`, `1 --b--> {0}`.
227
+ Set source acceptance to `{1}` and `predicate_exploration_order=('b','a')`,
228
+ which visits the singleton `a` valuation first under False-before-True
229
+ ordering. With cap 2, expect success, the two singleton basis states, and
230
+ both successor targets on the `b` edge from the initial state.
231
+ - `test_residualize_empty_subset`: zero-state false-initial input produces
232
+ zero states and empty acceptance.
233
+ - `test_residualize_admission_bound`: the late-cover example admits three
234
+ subsets cumulatively; cap 2 raises with `.explored == 3`, cap 3 succeeds
235
+ with two output states. The covered-candidate fixture above succeeds at
236
+ cap 2, including repeated visits to its covered union.
237
+ - `test_residualize_preserves_finite_domain`: include an unused letter `c`
238
+ in `finite_alphabet({'a','b','c'})`; partial transitions on it must reject,
239
+ and guards must respect the full alphabet domain.
240
+
241
+ - [x] **Step 2: Run `pytest tests/symbolic/test_rfsa.py -q`.** Expect failure
242
+ because `_residualize_nfa` is absent. Keep the exhaustive reference isolated
243
+ to tests; it may enumerate subsets and alphabet symbols.
244
+
245
+ - [x] **Step 3: Implement `_residualize_nfa` with the online basis and proof
246
+ above.** Keep set-cover checking local to this helper. Emit all final-basis
247
+ subsets contained in each successor image; an arbitrary irredundant cover
248
+ would omit saturated transitions. Build fresh output caches and acceptance
249
+ containing output state IDs only. Do not adapt `explore_and_transform` or
250
+ add calls to `determinize`, `minimize_dfa`, `to_min_dfa`, language equivalence,
251
+ hashing, or alphabet enumeration.
252
+
253
+ - [x] **Step 4: Run the new tests and nearby transforms tests.**
254
+ `pytest tests/symbolic/test_rfsa.py tests/symbolic/test_transforms.py -q`
255
+ must pass. Verify the set-cover argument term by term using the installed
256
+ `verifying-mathematical-notation` skill; explicitly distinguish source
257
+ subsets, output IDs, and residual languages.
258
+
259
+ - [x] **Step 5: Run focused static checks.**
260
+ `ruff check src/morphata/symbolic/transforms.py tests/symbolic/test_rfsa.py`
261
+ and `ruff format --check src/morphata/symbolic/transforms.py tests/symbolic/test_rfsa.py`
262
+ must pass. Run `zuban check` and `basedpyright`; report any pre-existing
263
+ failures separately without changing unrelated files.
264
+
265
+ - [x] **Step 6: Review the local Task 1 diff.**
266
+ `jj diff -- src/morphata/symbolic/transforms.py tests/symbolic/test_rfsa.py`.
267
+ Leave it uncommitted; inspect all changes before starting Task 2.
268
+
269
+ ## Task 2: Expose and verify the canonical two-pass operation
270
+
271
+ **Files:**
272
+
273
+ - Modify: `src/morphata/symbolic/transforms.py` for the free function.
274
+ - Modify: `src/morphata/symbolic/automaton.py:746-787` vicinity for the method.
275
+ - Modify: `src/morphata/symbolic/__init__.py` for the explicit export.
276
+ - Modify: `tests/symbolic/test_rfsa.py` for the public contract and oracle.
277
+ - Modify: `docs/concepts/symbolic.rst`, finite-word transformation pipeline.
278
+ - Modify: `docs/api/morphata.symbolic.rst`, free-function listing.
279
+ - Modify: `docs/refs.bib`, add only verified `denis2002residual` metadata.
280
+
281
+ **Interfaces:**
282
+
283
+ - Consumes: Task 1's `_residualize_nfa` and existing `reverse_language`.
284
+ - Produces the exported free function and corresponding method:
285
+
286
+ ```python
287
+ def to_min_rfsa[AP: Hashable, Symbol](
288
+ aut: SymbolicAutomaton[AP, Symbol],
289
+ *,
290
+ initial: Function[AP, int] | None = None,
291
+ predicate_exploration_order: Sequence[AP] | None = None,
292
+ abort_on_max_size: int | None = None,
293
+ ) -> SymbolicAutomaton[AP, Symbol]: ...
294
+
295
+ def to_min_rfsa(
296
+ self,
297
+ *,
298
+ initial: Function[AP, int] | None = None,
299
+ predicate_exploration_order: Sequence[AP] | None = None,
300
+ abort_on_max_size: int | None = None,
301
+ ) -> SymbolicAutomaton[AP, Symbol]: ...
302
+ ```
303
+
304
+ **Parallelization:** Sequential after Task 1. No independent agent tasks.
305
+
306
+ - [x] **Step 1: Add failing public-contract and canonicality tests.**
307
+ - `test_to_min_rfsa_finite_language`: use `finite_alphabet({'a','b','c'})`
308
+ and the language `{aa,ab,ba,cb}`. Expect four RFSA states and six minimal
309
+ DFA states in the test reference. Compare prime residual identities and all
310
+ saturated guards, not just the four-state count.
311
+ - `test_to_min_rfsa_matches_small_dfa_reference`: implement a test-only oracle
312
+ from a minimal DFA: enumerate its rooted residuals, compute strict language
313
+ containment by emptiness of differences, discard residuals covered by the
314
+ union of smaller residuals, and construct all initial/final/edge conditions
315
+ from Definition 4.5. Label languages with exact `CanonicalDfaHasher.canonical_bytes`
316
+ on their minimized rooted DFAs, using the same predicates/domain. Compare
317
+ every output state's label, initial and final label sets, and guard BDD for
318
+ each pair of labels. A hash digest alone is insufficient. Use bounded
319
+ generated NFAs with 1-3 states, two letters, 30 examples, and fresh managers.
320
+ - `test_to_min_rfsa_compressed_suffix_family`: the `n`-th symbol from the end
321
+ is `1`, for fixed `n=4,6` over `finite_alphabet({'0','1'})`. Build the NFA
322
+ with initial state 0 looping on both letters and additionally entering 1
323
+ on `1`; states 1 through `n-1` advance on either letter; only `n` accepts.
324
+ Expect RFSA sizes 5/7 and reference DFA sizes 16/64. Compare acceptance
325
+ with `len(word) >= n and word[-n] == '1'` on all words through length `n+1`.
326
+ - `test_to_min_rfsa_does_not_determinize`: after creating fixtures/oracle,
327
+ patch the free `determinize` and `minimize_dfa` and the corresponding
328
+ `SymbolicAutomaton.determinize`/`to_min_dfa` entrypoints to raise. The public
329
+ construction must still return the expected compressed RFSA. Check word
330
+ acceptance while patches are installed; run any DFA oracle outside them.
331
+ - `test_to_min_rfsa_does_not_enumerate_alphabet`: replace `alphabet.symbols`
332
+ with a raising callable and run the public operation on a powerset alphabet
333
+ with irrelevant predicates. Check same manager/alphabet identity and sample
334
+ word acceptance. Avoid a timing threshold.
335
+ - `test_to_min_rfsa_constants_and_partial_moves`: parameterize empty,
336
+ epsilon-only, universal, and nonempty partial NFAs. Expected state counts
337
+ are 0, 1, 1, and the reference count respectively. Empty/epsilon-only outputs
338
+ have no fabricated dead state; universal output has an accepting self-loop.
339
+ - `test_to_min_rfsa_selected_initial`: choose a disconnected component with
340
+ `initial=manager.q_var(q)`; also test false, true, a disjunction, and a
341
+ conjunction through existing dealternation. Compare against the selected
342
+ language, never the stored original initial condition.
343
+ - `test_to_min_rfsa_normalizes_alternation`: a small conjunction/disjunction
344
+ AFA agrees with the full-DFA oracle and returns disjunctive NFA successors.
345
+ - `test_to_min_rfsa_rejects_omega`: Buchi input raises `NotImplementedError`.
346
+ - `test_to_min_rfsa_bound_and_method`: free function and method agree, a
347
+ sufficient cap succeeds, and insufficient cap raises `MacroStateLimitError`.
348
+ Cover final canonicality under input state renaming and a second RFSA pass
349
+ through the residual-labelled oracle comparison, without requiring equal IDs.
350
+
351
+ - [x] **Step 2: Run the public tests and observe the missing interface.**
352
+ `pytest tests/symbolic/test_rfsa.py -q -k to_min_rfsa` must fail because the
353
+ public free function/method is absent.
354
+
355
+ - [x] **Step 3: Add the public free function, method, and export.**
356
+ Check finite acceptance, then call `reverse_language` with the selected
357
+ initial formula, `_residualize_nfa`, `reverse_language`, `_residualize_nfa`.
358
+ Forward order/bound parameters at each stage. Apply `initial` only at the
359
+ first reversal. Do not add a DFA fallback or silently return a noncanonical
360
+ result when a cap is reached. Keep the method a thin wrapper like existing
361
+ transforms; do not force a deterministic cache flag.
362
+
363
+ - [x] **Step 4: Document the exact contract and verified citation.**
364
+ Use NumPy-style sections/RST math in the new docstrings. Explain prime
365
+ residuals, saturated edges, the online cover invariant, partial NFA output,
366
+ canonicality up to isomorphism, and per-stage bounds. Add a small runnable
367
+ finite-language example to the concepts page and list the free function in
368
+ the API page. Obtain the entry with `zotero_ls citation export denis2002residual`
369
+ and append its verified metadata to `docs/refs.bib`; do not alter other
370
+ entries. Use the RST citation role with key `denis2002residual`. State that the full DFA is
371
+ avoided but basis/intermediate/output size can still be exponential. Do not
372
+ promise canonical serialization or unrestricted minimal-NFA size.
373
+
374
+ - [x] **Step 5: Run focused validation.**
375
+ `pytest tests/symbolic/test_rfsa.py tests/symbolic/test_transforms.py tests/symbolic/test_language_equivalence.py tests/symbolic/test_compose.py -q`
376
+ must pass, including oracle adjacency/saturation checks and both sentinels.
377
+
378
+ - [x] **Step 6: Run final validation once after the change is complete.**
379
+
380
+ ```bash
381
+ ruff format --check src/morphata/symbolic/transforms.py src/morphata/symbolic/automaton.py src/morphata/symbolic/__init__.py tests/symbolic/test_rfsa.py
382
+ ruff check src/morphata/symbolic/transforms.py src/morphata/symbolic/automaton.py src/morphata/symbolic/__init__.py tests/symbolic/test_rfsa.py
383
+ zuban check
384
+ basedpyright
385
+ pytest -q
386
+ sphinx-build -b html docs/ docs/_build/html
387
+ ```
388
+
389
+ Expected: successful formatting, lint, type checking, complete configured
390
+ tests/doctests, and Sphinx build without new diagnostics. If failures arise,
391
+ identify whether they are introduced or pre-existing; do not broaden scope
392
+ to unrelated fixes. Re-run only affected checks after a repair.
393
+
394
+ - [x] **Step 7: Perform the separate whole-change review.**
395
+ Review the final diff against the formulas, basis proof, all Review Focus
396
+ items, no-determinization/alphabet-enumeration constraints, and existing
397
+ local changes. Record actual validation results. Leave implementation and
398
+ documentation local and uncommitted for author review.
399
+
400
+ ## Planning validation and handoff
401
+
402
+ Only this plan was written during planning. A separate finite-set calculation
403
+ matched the online basis to exhaustive reachable-subset enumeration in all
404
+ 1,024 two-state/two-symbol transition/initial configurations. The same
405
+ calculation produced 5/7 states for the fixed suffix families with `n=4,6`.
406
+ This checks the proposed basis schedule on bounded examples; it is not a
407
+ production implementation or a substitute for the proof and repository tests.
408
+
409
+ Recommend direct sequential implementation after review: both tasks modify
410
+ the same algorithm/test files and Task 2 relies on Task 1. Parallel agents
411
+ offer no independent work here and are not authorized. Confirm or correct
412
+ the proposed API, bound semantics, and scope before implementation.
413
+
414
+ ## Implementation progress
415
+
416
+ Task 1: 310 tests passed; Ruff and Zuban passed. BasedPyright reported only
417
+ the expected temporary unused-private-helper warning before Task 2 integration.
418
+
419
+ Task 2: 380 focused tests passed. Ruff formatting/lint, Zuban, BasedPyright,
420
+ and Sphinx passed with no diagnostics. The separate whole-change review checked
421
+ source-subset/output-ID separation, preservation of covers under replacement,
422
+ transition closure under union, saturation, and the absence of production
423
+ determinization or alphabet enumeration.
424
+
425
+ Property testing found that an output basis can exceed the source state count;
426
+ output BDD variables are now declared before emission, with a fixed three-state
427
+ input/four-state-output regression. Exploratory tight caps in the compression
428
+ fixtures were removed: the plan specifies cumulative admissions, whose count
429
+ depends on exploration order, separately from final state count. The dedicated
430
+ late-cover admission-bound tests retain exact expected counts.
431
+
432
+ Raw `pytest -q` encountered four collection errors in stale generated
433
+ `docs/_build/html/_downloads` examples importing removed `morphata.encoders`
434
+ modules. Full source/test/documentation-doctest validation is being run with
435
+ `pytest -q --ignore=docs/_build`; no test configuration or generated files were
436
+ changed to bypass those stale examples.
437
+
438
+ ### Mathematical review
439
+
440
+ - Source of truth: the construction contract in this plan and Definition 4.5,
441
+ Lemma 5.1, and Theorem 5.2 of the verified local paper.
442
+ - Symbol ledger: source subsets are `frozenset[int]`; the basis is `basis`;
443
+ source transition images come from OR-folded source rows; guards are BDDs
444
+ restricted to `alphabet.domain`; output IDs are assigned only after closure.
445
+ - Verified correspondences: strict set covers preserve the generated union
446
+ family; source images distribute over union; accepting subsets intersect
447
+ the source accepting set; initial/target subsets use non-strict containment.
448
+ - Mismatches or ambiguities: none remain in the reviewed implementation. The
449
+ online scheduling adaptation and its exponential limits are documented.
450
+ - Recommended fix: none outstanding. All changes remain local and uncommitted.
451
+
452
+ Final validation: `pytest -q --ignore=docs/_build` completed with 770 passed,
453
+ 11 xfailed, and 16 dependency warnings in 418.58 seconds. Sphinx rebuilt
454
+ successfully; Ruff formatting/lint, Zuban, and BasedPyright all passed. The
455
+ raw-command generated-document collection issue is recorded above. Both tasks
456
+ and the whole-change review are complete; no commits or remote actions occurred.
@@ -12,15 +12,15 @@ if [[ -z "${DEVJAIL:-}" ]] && nvidia_smi="$(type -p "nvidia-smi")" && [[ -n "$nv
12
12
  if [[ -n "${cuda_major_version:-}" ]]; then
13
13
  info "Detected CUDA version: $cuda_major_version"
14
14
  export CUDA_VERSION="$cuda_major_version"
15
- PIXI_ENV="cu$CUDA_VERSION"
15
+ export CONDA_OVERRIDE_CUDA="$CUDA_VERSION"
16
+
16
17
  fi
17
18
  else
18
19
  info "Did not detect nvidia-smi, using non-GPU environment"
19
- PIXI_ENV="default"
20
20
  fi
21
- export PIXI_ENV
22
21
 
23
22
  dotenv_if_exists
24
23
 
25
- watch_file pixi.lock
26
- eval "$(pixi shell-hook -e "$PIXI_ENV")"
24
+ export PIXI_FROZEN=true
25
+
26
+ eval "$(pixi shell-hook)"
@@ -1,3 +1,5 @@
1
+ /thoughts/
2
+ /issues/
1
3
  # generated version file
2
4
  src/morphata/_version.py
3
5
 
@@ -29,6 +29,13 @@ The narrow exception: a **surgical, already-decided edit** the author has spelle
29
29
  Do exactly that and nothing more.
30
30
  When unsure whether a change qualifies, treat it as out of scope and ask.
31
31
 
32
+ ## Project Boundaries
33
+
34
+ Morphata owns automata representations, algebraic operations, semiring operators,
35
+ and execution. Automata Encoders owns symbolic input features, trainable
36
+ encoders, training/evaluation tools, and comparison baselines. Morphata must
37
+ not depend on Automata Encoders.
38
+
32
39
  ## Development Environment
33
40
 
34
41
  The development shell already has the project virtual environment activated,
@@ -38,3 +45,8 @@ etc. are on `PATH` directly.
38
45
  Just invoke the tool.
39
46
  If a tool is missing, stop and tell the user rather than trying to bootstrap
40
47
  or reinstall the environment.
48
+
49
+ ## Documentation
50
+
51
+ Use NumPy/numpydoc-style sections for Python docstrings. Use RST math markup
52
+ inside those sections and in the main docstring body.