morphata 2.0.9__tar.gz → 2.1.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 (152) hide show
  1. morphata-2.1.0/.basedpyright/baseline.json +3 -0
  2. {morphata-2.0.9 → morphata-2.1.0}/AGENTS.md +3 -3
  3. {morphata-2.0.9 → morphata-2.1.0}/CHANGELOG.md +50 -0
  4. {morphata-2.0.9 → morphata-2.1.0}/CONTRIBUTING.md +10 -48
  5. {morphata-2.0.9 → morphata-2.1.0}/PKG-INFO +3 -2
  6. {morphata-2.0.9 → morphata-2.1.0}/bench/README.md +5 -0
  7. {morphata-2.0.9 → morphata-2.1.0}/bench/conftest.py +4 -2
  8. {morphata-2.0.9 → morphata-2.1.0}/bench/test_hoa_exporter_perf.py +24 -8
  9. morphata-2.1.0/bench/test_hoa_parser_perf.py +128 -0
  10. {morphata-2.0.9 → morphata-2.1.0}/bench/test_sere_nlm_perf.py +21 -6
  11. {morphata-2.0.9 → morphata-2.1.0}/docs/conf.py +3 -1
  12. {morphata-2.0.9 → morphata-2.1.0}/docs/symbolic-transforms.rst +21 -0
  13. {morphata-2.0.9 → morphata-2.1.0}/justfile +7 -5
  14. {morphata-2.0.9 → morphata-2.1.0}/mypy.ini +10 -0
  15. {morphata-2.0.9 → morphata-2.1.0}/pixi.lock +546 -492
  16. {morphata-2.0.9 → morphata-2.1.0}/pixi.toml +17 -10
  17. {morphata-2.0.9 → morphata-2.1.0}/pyproject.toml +39 -7
  18. morphata-2.1.0/pyrightconfig.json +26 -0
  19. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/acceptance.py +180 -36
  20. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/alphabet.py +13 -13
  21. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/automaton.py +102 -52
  22. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/builder.py +25 -21
  23. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/hoa/__init__.py +7 -1
  24. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/hoa/__main__.py +3 -1
  25. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/hoa/exporter.py +106 -65
  26. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/hoa/hoa.lark +13 -7
  27. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/hoa/parser.py +21 -11
  28. morphata-2.1.0/src/morphata/logic/_common.py +325 -0
  29. morphata-2.1.0/src/morphata/logic/ltl.py +268 -0
  30. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/logic/psl.py +54 -107
  31. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/logic/sere.py +72 -152
  32. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/logic/strel.py +134 -191
  33. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/operators/_backend.py +8 -5
  34. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/operators/matrix.py +78 -46
  35. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/operators/polynomial.py +88 -81
  36. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/symbolic/__init__.py +8 -2
  37. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/symbolic/automaton.py +84 -77
  38. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/symbolic/bdd.py +290 -67
  39. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/symbolic/compose.py +88 -44
  40. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/symbolic/equivalence.py +37 -25
  41. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/symbolic/persistence.py +198 -109
  42. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/symbolic/transforms.py +333 -146
  43. morphata-2.1.0/src/morphata/utils.py +74 -0
  44. {morphata-2.0.9 → morphata-2.1.0}/tests/conftest.py +33 -17
  45. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/test_exporter_streaming.py +23 -11
  46. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/test_parser.py +5 -3
  47. {morphata-2.0.9 → morphata-2.1.0}/tests/logic/test_ltl.py +84 -32
  48. {morphata-2.0.9 → morphata-2.1.0}/tests/logic/test_psl.py +25 -8
  49. {morphata-2.0.9 → morphata-2.1.0}/tests/logic/test_sere.py +97 -39
  50. {morphata-2.0.9 → morphata-2.1.0}/tests/logic/test_sere_properties.py +26 -6
  51. {morphata-2.0.9 → morphata-2.1.0}/tests/logic/test_strel.py +77 -5
  52. {morphata-2.0.9 → morphata-2.1.0}/tests/operators/test_matrix_from_ir.py +19 -11
  53. {morphata-2.0.9 → morphata-2.1.0}/tests/operators/test_matrix_from_ltl.py +11 -5
  54. {morphata-2.0.9 → morphata-2.1.0}/tests/operators/test_polynomial_from_ir.py +20 -11
  55. {morphata-2.0.9 → morphata-2.1.0}/tests/symbolic/test_alphabet_aware_transitions.py +9 -3
  56. {morphata-2.0.9 → morphata-2.1.0}/tests/symbolic/test_bdd_helpers.py +53 -12
  57. {morphata-2.0.9 → morphata-2.1.0}/tests/symbolic/test_compose.py +168 -56
  58. {morphata-2.0.9 → morphata-2.1.0}/tests/symbolic/test_composition_map.py +76 -28
  59. {morphata-2.0.9 → morphata-2.1.0}/tests/symbolic/test_equivalence_naive_property.py +12 -4
  60. {morphata-2.0.9 → morphata-2.1.0}/tests/symbolic/test_ltlf2dfa_equivalence.py +46 -15
  61. morphata-2.1.0/tests/symbolic/test_persistence.py +308 -0
  62. morphata-2.1.0/tests/symbolic/test_prune.py +280 -0
  63. {morphata-2.0.9 → morphata-2.1.0}/tests/symbolic/test_step.py +6 -2
  64. {morphata-2.0.9 → morphata-2.1.0}/tests/symbolic/test_symbolic_automaton.py +135 -20
  65. {morphata-2.0.9 → morphata-2.1.0}/tests/symbolic/test_symbolic_from_ir.py +3 -3
  66. {morphata-2.0.9 → morphata-2.1.0}/tests/symbolic/test_to_hoa.py +28 -15
  67. {morphata-2.0.9 → morphata-2.1.0}/tests/symbolic/test_transforms.py +96 -182
  68. {morphata-2.0.9 → morphata-2.1.0}/tests/symbolic/test_transforms_ir.py +2 -2
  69. {morphata-2.0.9 → morphata-2.1.0}/tests/symbolic/test_transforms_properties.py +32 -10
  70. morphata-2.1.0/tests/test_acceptance_validation.py +45 -0
  71. {morphata-2.0.9 → morphata-2.1.0}/tests/test_alphabet.py +16 -6
  72. {morphata-2.0.9 → morphata-2.1.0}/tests/test_automaton.py +42 -8
  73. {morphata-2.0.9 → morphata-2.1.0}/tests/test_from_parsed.py +6 -2
  74. {morphata-2.0.9 → morphata-2.1.0}/uv.lock +821 -692
  75. morphata-2.0.9/.basedpyright/baseline.json +0 -854
  76. morphata-2.0.9/.claude/settings.local.json +0 -32
  77. morphata-2.0.9/pyrefly.toml +0 -2
  78. morphata-2.0.9/pyrightconfig.json +0 -14
  79. morphata-2.0.9/ruff.toml +0 -11
  80. morphata-2.0.9/src/morphata/logic/_common.py +0 -136
  81. morphata-2.0.9/src/morphata/logic/ltl.py +0 -499
  82. morphata-2.0.9/tests/symbolic/test_persistence.py +0 -175
  83. morphata-2.0.9/ty.toml +0 -7
  84. {morphata-2.0.9 → morphata-2.1.0}/.dockerignore +0 -0
  85. {morphata-2.0.9 → morphata-2.1.0}/.editorconfig +0 -0
  86. {morphata-2.0.9 → morphata-2.1.0}/.envrc +0 -0
  87. {morphata-2.0.9 → morphata-2.1.0}/.envrc.recommended +0 -0
  88. {morphata-2.0.9 → morphata-2.1.0}/.fdignore +0 -0
  89. {morphata-2.0.9 → morphata-2.1.0}/.gitattributes +0 -0
  90. {morphata-2.0.9 → morphata-2.1.0}/.gitignore +0 -0
  91. {morphata-2.0.9 → morphata-2.1.0}/.mailmap +0 -0
  92. {morphata-2.0.9 → morphata-2.1.0}/.python-version +0 -0
  93. {morphata-2.0.9 → morphata-2.1.0}/.rstcheck.cfg +0 -0
  94. {morphata-2.0.9 → morphata-2.1.0}/CLAUDE.md +0 -0
  95. {morphata-2.0.9 → morphata-2.1.0}/LICENSE +0 -0
  96. {morphata-2.0.9 → morphata-2.1.0}/README.md +0 -0
  97. {morphata-2.0.9 → morphata-2.1.0}/bench/__init__.py +0 -0
  98. {morphata-2.0.9 → morphata-2.1.0}/bench/test_equivalence_perf.py +0 -0
  99. {morphata-2.0.9 → morphata-2.1.0}/bench/test_successors_for_alphabet_perf.py +0 -0
  100. {morphata-2.0.9 → morphata-2.1.0}/ci/make-release +0 -0
  101. {morphata-2.0.9 → morphata-2.1.0}/cliff.toml +0 -0
  102. {morphata-2.0.9 → morphata-2.1.0}/docs/_static/navigation.html +0 -0
  103. {morphata-2.0.9 → morphata-2.1.0}/docs/api/modules.rst +0 -0
  104. {morphata-2.0.9 → morphata-2.1.0}/docs/api/morphata.acceptance.rst +0 -0
  105. {morphata-2.0.9 → morphata-2.1.0}/docs/api/morphata.alphabet.rst +0 -0
  106. {morphata-2.0.9 → morphata-2.1.0}/docs/api/morphata.automaton.rst +0 -0
  107. {morphata-2.0.9 → morphata-2.1.0}/docs/api/morphata.builder.rst +0 -0
  108. {morphata-2.0.9 → morphata-2.1.0}/docs/api/morphata.hoa.rst +0 -0
  109. {morphata-2.0.9 → morphata-2.1.0}/docs/api/morphata.logic.ltl.rst +0 -0
  110. {morphata-2.0.9 → morphata-2.1.0}/docs/api/morphata.logic.psl.rst +0 -0
  111. {morphata-2.0.9 → morphata-2.1.0}/docs/api/morphata.logic.sere.rst +0 -0
  112. {morphata-2.0.9 → morphata-2.1.0}/docs/api/morphata.logic.strel.rst +0 -0
  113. {morphata-2.0.9 → morphata-2.1.0}/docs/api/morphata.operators.matrix.rst +0 -0
  114. {morphata-2.0.9 → morphata-2.1.0}/docs/api/morphata.operators.polynomial.rst +0 -0
  115. {morphata-2.0.9 → morphata-2.1.0}/docs/api/morphata.operators.rst +0 -0
  116. {morphata-2.0.9 → morphata-2.1.0}/docs/api/morphata.symbolic.rst +0 -0
  117. {morphata-2.0.9 → morphata-2.1.0}/docs/concepts/acceptance.rst +0 -0
  118. {morphata-2.0.9 → morphata-2.1.0}/docs/concepts/hoa.rst +0 -0
  119. {morphata-2.0.9 → morphata-2.1.0}/docs/concepts/index.rst +0 -0
  120. {morphata-2.0.9 → morphata-2.1.0}/docs/concepts/operators.rst +0 -0
  121. {morphata-2.0.9 → morphata-2.1.0}/docs/concepts/spec.rst +0 -0
  122. {morphata-2.0.9 → morphata-2.1.0}/docs/concepts/weights.rst +0 -0
  123. {morphata-2.0.9 → morphata-2.1.0}/docs/index.rst +0 -0
  124. {morphata-2.0.9 → morphata-2.1.0}/docs/quick-start.rst +0 -0
  125. {morphata-2.0.9 → morphata-2.1.0}/project.utf-8.add +0 -0
  126. {morphata-2.0.9 → morphata-2.1.0}/project.utf-8.add.spl +0 -0
  127. {morphata-2.0.9 → morphata-2.1.0}/pytest.toml +0 -0
  128. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/__init__.py +0 -0
  129. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/hoa/acc_expr.py +0 -0
  130. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/logic/__init__.py +0 -0
  131. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/operators/__init__.py +0 -0
  132. {morphata-2.0.9 → morphata-2.1.0}/src/morphata/py.typed +0 -0
  133. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/aut1.hoa +0 -0
  134. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/aut11.hoa +0 -0
  135. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/aut2.hoa +0 -0
  136. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/aut3.2.hoa +0 -0
  137. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/aut3.hoa +0 -0
  138. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/aut4.hoa +0 -0
  139. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/aut5.hoa +0 -0
  140. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/aut6.hoa +0 -0
  141. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/aut7.hoa +0 -0
  142. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/aut8.hoa +0 -0
  143. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/finite_aut.hoa +0 -0
  144. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/gen_buchi_3.hoa +0 -0
  145. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/gen_cobuchi_2.hoa +0 -0
  146. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/rabin_2pair.hoa +0 -0
  147. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/streett_2pair.hoa +0 -0
  148. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/test_exporter_basic.py +0 -0
  149. {morphata-2.0.9 → morphata-2.1.0}/tests/hoa/test_exporter_roundtrip.py +0 -0
  150. {morphata-2.0.9 → morphata-2.1.0}/tests/logic/__init__.py +0 -0
  151. {morphata-2.0.9 → morphata-2.1.0}/tests/symbolic/test_from_hoaf.py +0 -0
  152. {morphata-2.0.9 → morphata-2.1.0}/tests/test_builder.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ "files": {}
3
+ }
@@ -7,7 +7,7 @@ It packages structural automaton representations together with semiring-valued
7
7
  ## Development Environment
8
8
 
9
9
  The development shell already has the project virtual environment activated,
10
- so `pytest`, `mypy`, `ruff`, `sphinx-build`, etc. are on `PATH` directly.
10
+ so `pytest`, `zuban`, `basedpyright`, `ruff`, `sphinx-build`, etc. are on `PATH` directly.
11
11
  **Do not** prefix commands with `uv run`, `pixi run`, `poetry run`, or similar.
12
12
  Just invoke the tool.
13
13
  If a tool is missing, stop and tell the user rather than trying to bootstrap
@@ -242,7 +242,7 @@ for DFA-shaped symbolic automata -- ``language_equivalent``, ``canonical_form``,
242
242
  ``backend=``; do not bake backend-only assumptions into operator
243
243
  code.
244
244
  6. **No Deprecation** : delete old code instead of wrapping it.
245
- 7. **Type Safety** : all new code must pass ``mypy --strict``.
245
+ 7. **Type Safety** : all new code must pass ``zuban`` and ``basedpyright``.
246
246
  8. **Prefer Refactoring** : update existing patterns rather than
247
247
  special-casing.
248
248
 
@@ -282,7 +282,7 @@ for DFA-shaped symbolic automata -- ``language_equivalent``, ``canonical_form``,
282
282
  ## Testing
283
283
 
284
284
  - **Framework** : pytest with jaxtyping.
285
- - **Type Safety** : 100% `mypy --strict`.
285
+ - **Type Safety** : `zuban` and `basedpyright` (both must pass; preferred over mypy for spec conformance and performance).
286
286
  - **Layout** : tests live under `tests/`:
287
287
  - flat suites at the top: ``test_alphabet.py``, ``test_automaton.py``,
288
288
  ``test_builder.py``, ``test_from_parsed.py``;
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.0 — 2026-06-11
4
+
5
+ ### Added
6
+
7
+ - Add `morphata.symbolic.prune` that does a cheap, language-preserving structural
8
+ pruning of an AFA. The idea here is to try and reduce the number of states in the AFA
9
+ (no guarantees) by performing a forward reachability pass, dead-state pruning, and
10
+ a parameterized number of passes for one-step acceptance-partition deduplication.
11
+
12
+ ### Changed
13
+
14
+ - `MatrixOperator` and `PolynomialOperator` are generic now — `MatrixOperator[Symbol, AP]`
15
+ and `PolynomialOperator[Symbol, AP]`, just like `SymbolicAutomaton`. When I first banged
16
+ these out I left them un-parameterized (read: `Any` smeared everywhere), so now the
17
+ symbol and AP types actually flow through and `MatrixOperator[frozenset[str], str]`
18
+ means something. Nothing changes at runtime; the one thing you might notice is your
19
+ type-checker nudging you to fill in the params if you were annotating a bare
20
+ `MatrixOperator` or `PolynomialOperator`.
21
+ - The BDD-JSON sidecar cache jumped to schema **v2**. Acceptance conditions now
22
+ (de)serialize through `cattrs` instead of the hand-rolled `meta.get(...)` dance, and
23
+ Rabin/Streett pairs are written as objects (`{"rejecting": ..., "accepting": ...}`)
24
+ rather than nested lists. Heads up: old `v1` `.bdd.json` caches will politely refuse
25
+ to load (clean error, not a silent miscompile) --- just regenerate them and you're good.
26
+ Remember, this is a *cache*, not a portable format, so this shouldn't bite anyone hard.
27
+ - STREL conversion shed ~120 lines. All the timed-interval unrolling (`G[a,b]`, `F[a,b]`,
28
+ `U[t1,t2]`, `X[n]`) and the spatial `Somewhere`/`Everywhere` desugaring used to be
29
+ hand-rolled in `strel.py`, but `logic-asts` already does all of it in `.expand()` — so
30
+ I ripped them out and just lean on the upstream rules. This needs the newer `logic-asts`
31
+ (the one with the spatial `expand()` rules), so that dep gets bumped along with it.
32
+
33
+ ### Miscellaneous
34
+
35
+ - Housekeeping, but worth saying: the whole package now type-checks clean under *both*
36
+ `zuban` and `basedpyright` (0 errors, 0 warnings), so contributing should be a bit less
37
+ whack-a-mole.
38
+
39
+ ## 2.0.10 — 2026-06-08
40
+
41
+ ### Fixed
42
+
43
+ - Using `SymbolicAutomaton.to_bdd_json` and `SymbolicAutomaton.from_bdd_json` did not
44
+ preserve the positive normal form of the BDDs because we were using incorrect Shannon
45
+ cofactor expansion.
46
+
47
+ ### Performance
48
+
49
+ - The HOA grammar is now LALR compatible! This means we get a huge performance boost
50
+ even for pretty large automata.
51
+ - Similar thing was done in `logic-asts` and thus, the version will be bumped.
52
+
3
53
  ## 2.0.9 — 2026-06-04
4
54
 
5
55
  ### Fixed
@@ -39,20 +39,21 @@ just fmt
39
39
  # OR
40
40
  ruff format
41
41
  ruff check --output-format concise --fix --exit-non-zero-on-fix
42
-
43
- # Format + type check
44
- just lint
45
42
  ```
46
43
 
47
44
  **Type Checking** :
48
45
  ```bash
49
46
  # Run all type checkers
50
47
  just type-check
51
-
48
+ # OR
52
49
  # Run specific type checkers
53
- mypy --strict
54
- ty check --output-format concise
55
- pyrefly check --output-format min-text
50
+ zuban check
51
+ basedpyright
52
+ ```
53
+
54
+ **Formatting + Type Checking together**:
55
+ ```bash
56
+ just lint
56
57
  ```
57
58
 
58
59
  **Development Setup** :
@@ -60,7 +61,7 @@ pyrefly check --output-format min-text
60
61
  # Set up dev environment (sync dependencies)
61
62
  just dev
62
63
  # OR
63
- uv sync --frozen --inexact --dev
64
+ pixi install
64
65
 
65
66
  # For CUDA support (set CUDA_VERSION=12 or 13)
66
67
  CUDA_VERSION=12 just dev
@@ -74,7 +75,7 @@ One can also copy the included `.envrc.recommended` file to `.envrc` and use it
74
75
 
75
76
  ### Type Annotations
76
77
 
77
- - All code must pass `mypy --strict` . Use explicit type annotations.
78
+ - All code must pass `zuban` and `basedpyright`. Use explicit type annotations.
78
79
  - Use Python 3.12+ syntax: `type` aliases, `|` for unions, generic classes and functions
79
80
  with `[T]`
80
81
  - Always specify return types, including `-> None`
@@ -89,42 +90,3 @@ One can also copy the included `.envrc.recommended` file to `.envrc` and use it
89
90
  - Format: NumPy-style docstrings with sections: Parameters, Returns,
90
91
  Raises, Notes, Examples.
91
92
  - Inline comments should say _why_ something is done, not _what_.
92
-
93
- ## Source Layout
94
-
95
- ```
96
- morphata/
97
- ├── src/morphata/
98
- │ ├── automaton.py # Automaton IR + Outgoing/Step deltas
99
- │ ├── alphabet.py # Input alphabet + characteristic function
100
- │ ├── builder.py # AutomatonBuilder (mutable IR builder)
101
- │ ├── acceptance.py # Finite + omega-regular acceptance
102
- │ ├── weights.py # Guard-based weight predicates
103
- │ ├── logic/
104
- │ │ ├── ltl.py # LTL/LTLf -> AFA
105
- │ │ └── strel.py # STREL automaton (Step delta)
106
- │ ├── hoa/ # Extended HOA v1 parser
107
- │ │ ├── parser.py
108
- │ │ ├── exporter.py # placeholder; not yet implemented
109
- │ │ └── acc_expr.py
110
- │ ├── operators/ # Semiring-valued morphisms
111
- │ │ ├── matrix.py # MatrixOperator (weighted NFAs)
112
- │ │ ├── polynomial.py # PolynomialOperator (AFAs)
113
- │ │ └── _backend.py # backend resolution + JAX pytree aux
114
- │ └── symbolic/ # BDD-backed structural layer
115
- │ ├── automaton.py # SymbolicAutomaton
116
- │ ├── bdd.py # BoolExpr <-> dd.Function bridge
117
- │ ├── transforms.py # determinize, minimize, nfa_from_afa
118
- │ └── equivalence.py # language_equivalent, isomorphic, canonical_form
119
- │
120
- └── tests/ # Mirrors the package layout
121
- ├── test_alphabet.py
122
- ├── test_automaton.py
123
- ├── test_builder.py
124
- ├── test_from_parsed.py
125
- ├── test_weights.py
126
- ├── hoa/
127
- ├── logic/
128
- ├── operators/
129
- └── symbolic/
130
- ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphata
3
- Version: 2.0.9
3
+ Version: 2.1.0
4
4
  Summary: Representation theory for automata, made practical.
5
5
  Project-URL: Documentation, https://docs.anandb.dev/morphata
6
6
  Project-URL: Repository, https://git.anandb.dev/morphata.git
@@ -13,10 +13,11 @@ Classifier: Topic :: Scientific/Engineering :: Mathematics
13
13
  Requires-Python: >=3.12
14
14
  Requires-Dist: algebraic-arrays>=1.3.0
15
15
  Requires-Dist: attrs>=25.4.0
16
+ Requires-Dist: cattrs>=26.1.0
16
17
  Requires-Dist: dd>=0.6.0
17
18
  Requires-Dist: jaxtyping>=0.3
18
19
  Requires-Dist: lark>=1.3.1
19
- Requires-Dist: logic-asts>=1.6.2
20
+ Requires-Dist: logic-asts>=1.7
20
21
  Requires-Dist: networkx
21
22
  Requires-Dist: numpy
22
23
  Requires-Dist: types-networkx
@@ -20,6 +20,11 @@ To get pytest-benchmark's tabular wall-time output:
20
20
  wall time across regimes that stress the per-row guard/successor
21
21
  caching (small DFA, sparse-big-alphabet DFA, conjunction min-DFA, raw
22
22
  AFA without minimization).
23
+ - `test_hoa_parser_perf.py` -- ``hoa.parser.parse`` (parse + transform)
24
+ across the sample corpus, parametrized over ``earley`` vs ``lalr`` from
25
+ the same grammar to quantify the LALR switch. Run with
26
+ ``--benchmark-group-by=param:case`` to put the two algorithms side by
27
+ side per input.
23
28
 
24
29
  ## Recording results
25
30
 
@@ -12,7 +12,9 @@ from collections.abc import Iterator
12
12
 
13
13
  import pytest
14
14
 
15
- from morphata.logic.sere import _expansion_call_counter
15
+ from morphata.logic.sere import (
16
+ _expansion_call_counter, # pyright: ignore[reportPrivateUsage]
17
+ )
16
18
  from morphata.symbolic.bdd import Manager, make_manager
17
19
 
18
20
 
@@ -30,7 +32,7 @@ def bench_mgr() -> Iterator[Manager]:
30
32
  try:
31
33
  yield manager
32
34
  finally:
33
- gc.collect()
35
+ _ = gc.collect()
34
36
 
35
37
 
36
38
  @pytest.fixture
@@ -41,7 +41,9 @@ def _small_alphabet_dfa(mgr: Manager) -> SymbolicAutomaton[str]:
41
41
  aps = frozenset({"a", "b"})
42
42
  formula = ltl.Eventually(ltl.And((ltl.Variable("a"), ltl.Variable("b"))))
43
43
  alphabet = powerset_alphabet(aps)
44
- return SymbolicAutomaton.from_ltl(formula, alphabet=alphabet, manager=mgr, finite=True).to_min_dfa()
44
+ return SymbolicAutomaton.from_ltl(
45
+ formula, alphabet=alphabet, manager=mgr, finite=True
46
+ ).to_min_dfa()
45
47
 
46
48
 
47
49
  def _sparse_big_alphabet_dfa(mgr: Manager) -> SymbolicAutomaton[str]:
@@ -55,7 +57,9 @@ def _sparse_big_alphabet_dfa(mgr: Manager) -> SymbolicAutomaton[str]:
55
57
  )
56
58
  )
57
59
  alphabet = powerset_alphabet(aps)
58
- return SymbolicAutomaton.from_ltl(formula, alphabet=alphabet, manager=mgr, finite=True).to_min_dfa()
60
+ return SymbolicAutomaton.from_ltl(
61
+ formula, alphabet=alphabet, manager=mgr, finite=True
62
+ ).to_min_dfa()
59
63
 
60
64
 
61
65
  def _conjunction_min_dfa(mgr: Manager) -> SymbolicAutomaton[str]:
@@ -70,7 +74,9 @@ def _conjunction_min_dfa(mgr: Manager) -> SymbolicAutomaton[str]:
70
74
  )
71
75
  )
72
76
  alphabet = powerset_alphabet(aps)
73
- return SymbolicAutomaton.from_ltl(formula, alphabet=alphabet, manager=mgr, finite=True).to_min_dfa()
77
+ return SymbolicAutomaton.from_ltl(
78
+ formula, alphabet=alphabet, manager=mgr, finite=True
79
+ ).to_min_dfa()
74
80
 
75
81
 
76
82
  def _afa_no_minimize(mgr: Manager) -> SymbolicAutomaton[str]:
@@ -80,15 +86,21 @@ def _afa_no_minimize(mgr: Manager) -> SymbolicAutomaton[str]:
80
86
  ltl.And(
81
87
  (
82
88
  ltl.Variable("b"),
83
- ltl.Eventually(ltl.And((ltl.Variable("c"), ltl.Eventually(ltl.Variable("d"))))),
89
+ ltl.Eventually(
90
+ ltl.And((ltl.Variable("c"), ltl.Eventually(ltl.Variable("d"))))
91
+ ),
84
92
  )
85
93
  ),
86
94
  )
87
95
  alphabet = powerset_alphabet(aps)
88
- return SymbolicAutomaton.from_ltl(formula, alphabet=alphabet, manager=mgr, finite=True)
96
+ return SymbolicAutomaton.from_ltl(
97
+ formula, alphabet=alphabet, manager=mgr, finite=True
98
+ )
89
99
 
90
100
 
91
- def test_hoa_export_small_alphabet_dfa(benchmark: Benchmark, bench_mgr: Manager) -> None:
101
+ def test_hoa_export_small_alphabet_dfa(
102
+ benchmark: Benchmark, bench_mgr: Manager
103
+ ) -> None:
92
104
  sym = _small_alphabet_dfa(bench_mgr)
93
105
 
94
106
  def _run() -> int:
@@ -97,7 +109,9 @@ def test_hoa_export_small_alphabet_dfa(benchmark: Benchmark, bench_mgr: Manager)
97
109
  assert benchmark(_run) > 0
98
110
 
99
111
 
100
- def test_hoa_export_sparse_big_alphabet_dfa(benchmark: Benchmark, bench_mgr: Manager) -> None:
112
+ def test_hoa_export_sparse_big_alphabet_dfa(
113
+ benchmark: Benchmark, bench_mgr: Manager
114
+ ) -> None:
101
115
  sym = _sparse_big_alphabet_dfa(bench_mgr)
102
116
 
103
117
  def _run() -> int:
@@ -106,7 +120,9 @@ def test_hoa_export_sparse_big_alphabet_dfa(benchmark: Benchmark, bench_mgr: Man
106
120
  assert benchmark(_run) > 0
107
121
 
108
122
 
109
- def test_hoa_export_conjunction_min_dfa(benchmark: Benchmark, bench_mgr: Manager) -> None:
123
+ def test_hoa_export_conjunction_min_dfa(
124
+ benchmark: Benchmark, bench_mgr: Manager
125
+ ) -> None:
110
126
  sym = _conjunction_min_dfa(bench_mgr)
111
127
 
112
128
  def _run() -> int:
@@ -0,0 +1,128 @@
1
+ """Micro-benchmarks for ``morphata.hoa.parser.parse``.
2
+
3
+ Quantifies the Earley -> LALR switch in the HOA parser. Both variants are
4
+ built from the *same* committed grammar (`hoa.lark`); the only difference is
5
+ ``parser="earley"`` (the old default) versus ``parser="lalr"`` (current). The
6
+ grammar's ``acceptance_cond`` is stratified, so it is unambiguous and parses
7
+ under both algorithms -- the comparison is purely the parsing algorithm plus
8
+ the shared ``_AstTransformer`` pass, i.e. the full ``parse()`` pipeline.
9
+
10
+ Cases span the sample corpus from small (``aut1``) to the largest hand-written
11
+ files (``rabin_2pair``, ``streett_2pair``), plus a synthetic ``craftax``
12
+ stress input: a ~36-state min-DFA over 25 APs (the
13
+ ``test_min_dfa_of_conjunction_craftax_shaped_stress`` conjunction), exported to
14
+ HOA. At ~18 KB with dense per-edge guards it is ~35x the largest sample file
15
+ and pushes Earley into its superlinear regime, where the LALR win is starkest.
16
+
17
+ Run with::
18
+
19
+ pytest bench/test_hoa_parser_perf.py -v \\
20
+ --benchmark-columns=mean,median,min,max,rounds \\
21
+ --benchmark-group-by=param:case
22
+
23
+ Grouping by ``case`` puts the earley/lalr rows for the same input side by side
24
+ so the speedup is read directly off the table.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import functools
30
+ from pathlib import Path
31
+
32
+ import logic_asts as logic
33
+ import logic_asts.ltl as ltl
34
+ import pytest
35
+ from lark import Lark
36
+ from pytest_benchmark.fixture import BenchmarkFixture as Benchmark
37
+
38
+ from morphata.alphabet import powerset_alphabet
39
+ from morphata.hoa.exporter import symbolic_automaton_to_hoa
40
+ from morphata.hoa.parser import (
41
+ HOA_GRAMMAR_FILE,
42
+ ParsedAutomaton,
43
+ _AstTransformer, # pyright: ignore[reportPrivateUsage]
44
+ )
45
+ from morphata.symbolic.bdd import make_manager
46
+ from morphata.symbolic.compose import min_dfa_of_conjunction
47
+
48
+ _GRAMMAR = HOA_GRAMMAR_FILE.read_text()
49
+ _HOA_DIR = Path(__file__).resolve().parent.parent / "tests" / "hoa"
50
+
51
+ # One Lark instance per algorithm, built once and reused across rounds.
52
+ _PARSERS: dict[str, Lark] = {
53
+ "earley": Lark(
54
+ _GRAMMAR, start="automaton", parser="earley", maybe_placeholders=True
55
+ ),
56
+ "lalr": Lark(_GRAMMAR, start="automaton", parser="lalr", maybe_placeholders=True),
57
+ }
58
+
59
+ # Representative slice of the sample corpus: smallest, a couple of mid-size
60
+ # omega conditions, the two largest acceptance formulas, and a synthetic
61
+ # stress input (built lazily, see ``_craftax_hoa``).
62
+ _CASES = ["aut1", "gen_buchi_3", "aut8", "rabin_2pair", "streett_2pair", "craftax"]
63
+
64
+
65
+ @functools.cache
66
+ def _craftax_hoa() -> str:
67
+ """Export the Craftax ENCHANT_SWORD-shaped min-DFA (~7 conjuncts, |AP|=25).
68
+
69
+ Mirrors ``test_min_dfa_of_conjunction_craftax_shaped_stress``. Cached so the
70
+ min-DFA is built once regardless of how many benchmark rounds run.
71
+ """
72
+ aps = {f"p{i}" for i in range(25)}
73
+ alphabet = powerset_alphabet(aps)
74
+ p = sorted(aps)
75
+
76
+ def F(name: str) -> logic.LTLExpr[str]: # noqa: N802
77
+ return ltl.Eventually(logic.Variable(name))
78
+
79
+ def U(lhs: logic.LTLExpr[str], rhs: logic.LTLExpr[str]) -> logic.LTLExpr[str]: # noqa: N802
80
+ return ltl.Until(lhs, rhs)
81
+
82
+ formulas: list[logic.LTLExpr[str]] = [
83
+ F(p[0]),
84
+ U(
85
+ logic.Not(logic.Variable(p[0])),
86
+ logic.Or((logic.Variable(p[1]), logic.Variable(p[2]))),
87
+ ),
88
+ U(
89
+ logic.Not(logic.Variable(p[0])),
90
+ logic.Or(
91
+ (logic.Variable(p[3]), logic.Variable(p[4]), logic.Variable(p[5]))
92
+ ),
93
+ ),
94
+ U(
95
+ logic.Not(logic.Variable(p[0])),
96
+ logic.Or((logic.Variable(p[6]), logic.Variable(p[7]))),
97
+ ),
98
+ F(p[1]),
99
+ F(p[4]),
100
+ F(p[7]),
101
+ ]
102
+ sym, _ = min_dfa_of_conjunction(formulas, alphabet=alphabet, manager=make_manager())
103
+ return symbolic_automaton_to_hoa(sym)
104
+
105
+
106
+ def _hoa_text(case: str) -> str:
107
+ if case == "craftax":
108
+ return _craftax_hoa()
109
+ return (_HOA_DIR / f"{case}.hoa").read_text()
110
+
111
+
112
+ def _parse_with(lark: Lark, text: str) -> ParsedAutomaton:
113
+ tree = lark.parse(text)
114
+ ret = _AstTransformer().transform(tree)
115
+ assert isinstance(ret, ParsedAutomaton)
116
+ return ret
117
+
118
+
119
+ @pytest.mark.parametrize("kind", ["earley", "lalr"])
120
+ @pytest.mark.parametrize("case", _CASES)
121
+ def test_hoa_parse(benchmark: Benchmark, case: str, kind: str) -> None:
122
+ text = _hoa_text(case)
123
+ lark = _PARSERS[kind]
124
+
125
+ def _run() -> int:
126
+ return len(_parse_with(lark, text).body)
127
+
128
+ assert benchmark(_run) >= 0
@@ -43,7 +43,12 @@ def _lm_of_aps(n: int) -> logic.Expr:
43
43
 
44
44
 
45
45
  def _nlm_of_concats(n: int) -> logic.Expr:
46
- return sere.NLMInter(tuple(sere.Concat((sere.Variable(f"a{i}"), sere.Variable(f"b{i}"))) for i in range(n)))
46
+ return sere.NLMInter(
47
+ tuple(
48
+ sere.Concat((sere.Variable(f"a{i}"), sere.Variable(f"b{i}")))
49
+ for i in range(n)
50
+ )
51
+ )
47
52
 
48
53
 
49
54
  def _run_and_measure(formula: logic.Expr, mgr: Manager) -> tuple[int, int]:
@@ -56,17 +61,23 @@ def _run_and_measure(formula: logic.Expr, mgr: Manager) -> tuple[int, int]:
56
61
 
57
62
  @pytest.mark.timeout(10)
58
63
  @pytest.mark.parametrize("n", list(range(2, 10)))
59
- def test_bench_nlm_of_aps(benchmark: Benchmark, expansion_calls: list[int], bench_mgr: Manager, n: int) -> None:
64
+ def test_bench_nlm_of_aps(
65
+ benchmark: Benchmark, expansion_calls: list[int], bench_mgr: Manager, n: int
66
+ ) -> None:
60
67
  formula = _nlm_of_aps(n)
61
68
  result = benchmark(_run_and_measure, formula, bench_mgr)
62
69
  state_count, call_count = result
63
70
  expansion_calls[0] = call_count
64
- print(f"\nNLMInter(a0..a{n - 1}): states={state_count} expansion_calls={call_count}")
71
+ print(
72
+ f"\nNLMInter(a0..a{n - 1}): states={state_count} expansion_calls={call_count}"
73
+ )
65
74
 
66
75
 
67
76
  @pytest.mark.timeout(10)
68
77
  @pytest.mark.parametrize("n", list(range(2, 10)))
69
- def test_bench_lm_of_aps_baseline(benchmark: Benchmark, expansion_calls: list[int], bench_mgr: Manager, n: int) -> None:
78
+ def test_bench_lm_of_aps_baseline(
79
+ benchmark: Benchmark, expansion_calls: list[int], bench_mgr: Manager, n: int
80
+ ) -> None:
70
81
  formula = _lm_of_aps(n)
71
82
  result = benchmark(_run_and_measure, formula, bench_mgr)
72
83
  state_count, call_count = result
@@ -76,9 +87,13 @@ def test_bench_lm_of_aps_baseline(benchmark: Benchmark, expansion_calls: list[in
76
87
 
77
88
  @pytest.mark.timeout(10)
78
89
  @pytest.mark.parametrize("n", list(range(2, 10)))
79
- def test_bench_nlm_of_concats(benchmark: Benchmark, expansion_calls: list[int], bench_mgr: Manager, n: int) -> None:
90
+ def test_bench_nlm_of_concats(
91
+ benchmark: Benchmark, expansion_calls: list[int], bench_mgr: Manager, n: int
92
+ ) -> None:
80
93
  formula = _nlm_of_concats(n)
81
94
  result = benchmark(_run_and_measure, formula, bench_mgr)
82
95
  state_count, call_count = result
83
96
  expansion_calls[0] = call_count
84
- print(f"\nNLMInter(a_i;b_i) n={n}: states={state_count} expansion_calls={call_count}")
97
+ print(
98
+ f"\nNLMInter(a_i;b_i) n={n}: states={state_count} expansion_calls={call_count}"
99
+ )
@@ -36,7 +36,9 @@ autodoc_default_options = {
36
36
  autodoc_typehints = "description"
37
37
  autosummary_generate = True
38
38
 
39
- intersphinx_mapping: dict[str, tuple[str, None | str | tuple[str, tuple[str | None, ...]]]] = {
39
+ intersphinx_mapping: dict[
40
+ str, tuple[str, None | str | tuple[str, tuple[str | None, ...]]]
41
+ ] = {
40
42
  "equinox": ("https://docs.kidger.site/equinox", None),
41
43
  "algebraic": ("https://docs.anandb.dev/algebraic", None),
42
44
  }
@@ -19,6 +19,27 @@ For the common case of running the whole pipeline, call
19
19
  <morphata.symbolic.automaton.SymbolicAutomaton.to_min_dfa>`, which
20
20
  composes the three transforms in order.
21
21
 
22
+ Best-effort pruning
23
+ -------------------
24
+
25
+ :func:`morphata.symbolic.transforms.prune` shrinks an alternating
26
+ automaton in place *without* determinising it. Unlike
27
+ :meth:`~morphata.symbolic.automaton.SymbolicAutomaton.to_min_dfa`, it
28
+ preserves the alternating structure (successor BDDs keep their
29
+ conjunctive shape) and is *best-effort*, not minimal. It applies, in
30
+ order: forward reachability, an AFA-aware dead-state fixpoint, and one or
31
+ more rounds (``passes``, default 1) of one-step acceptance-partition
32
+ bisimulation dedup. Requires :class:`~morphata.acceptance.Finite`
33
+ acceptance. ``num_states`` never increases and the accepted language is
34
+ preserved.
35
+
36
+ .. code-block:: python
37
+
38
+ from morphata.symbolic.transforms import prune
39
+
40
+ sym_aut = SymbolicAutomaton.from_ltl(formula, alphabet=ba, finite=True)
41
+ sym_aut = prune(sym_aut)
42
+
22
43
  BDD-shape invariants
23
44
  --------------------
24
45
 
@@ -42,16 +42,18 @@ pyrefly-check:
42
42
  mypy-check:
43
43
  mypy --strict
44
44
 
45
+ [no-cd]
46
+ [private]
47
+ zuban-check:
48
+ zuban check
49
+
45
50
  [no-cd]
46
51
  [private]
47
52
  pyright-check:
48
53
  basedpyright
49
54
 
50
- # [parallel]
51
- type-check: mypy-check
52
-
53
- # pyright-check
54
- # ty-check pyrefly-check
55
+ [parallel]
56
+ type-check: zuban-check pyright-check
55
57
 
56
58
  # Run both formatting and type checking
57
59
  [no-cd]
@@ -8,6 +8,8 @@ files = $MYPY_CONFIG_FILE_DIR/src/**/*.py,
8
8
 
9
9
  cache_dir = $MYPY_CONFIG_FILE_DIR/.mypy_cache
10
10
 
11
+ mypy_path = $MYPY_CONFIG_FILE_DIR/typings
12
+
11
13
  exclude_gitignore = True
12
14
 
13
15
 
@@ -43,3 +45,11 @@ ignore_missing_imports = True
43
45
 
44
46
  [mypy-wadler_lindig]
45
47
  ignore_missing_imports = True
48
+
49
+ [mypy-spot]
50
+ ignore_missing_imports = True
51
+ disallow_untyped_defs = False
52
+
53
+ [mypy-spot.*]
54
+ ignore_missing_imports = True
55
+ disallow_untyped_defs = False