morphata 2.0.8__tar.gz → 2.0.10__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 (149) hide show
  1. {morphata-2.0.8 → morphata-2.0.10}/.claude/settings.local.json +4 -1
  2. morphata-2.0.10/.codegraph/.gitignore +16 -0
  3. morphata-2.0.10/.codegraph/codegraph.db +0 -0
  4. morphata-2.0.10/.codegraph/codegraph.db-shm +0 -0
  5. morphata-2.0.10/.codegraph/codegraph.db-wal +0 -0
  6. morphata-2.0.10/.codegraph/daemon.pid +6 -0
  7. {morphata-2.0.8 → morphata-2.0.10}/CHANGELOG.md +36 -0
  8. {morphata-2.0.8 → morphata-2.0.10}/PKG-INFO +1 -1
  9. {morphata-2.0.8 → morphata-2.0.10}/bench/README.md +9 -0
  10. morphata-2.0.10/bench/test_hoa_exporter_perf.py +124 -0
  11. morphata-2.0.10/bench/test_hoa_parser_perf.py +114 -0
  12. {morphata-2.0.8 → morphata-2.0.10}/mypy.ini +10 -0
  13. {morphata-2.0.8 → morphata-2.0.10}/pyproject.toml +1 -1
  14. {morphata-2.0.8 → morphata-2.0.10}/pyrightconfig.json +1 -1
  15. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/hoa/exporter.py +74 -20
  16. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/hoa/hoa.lark +13 -7
  17. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/hoa/parser.py +2 -1
  18. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/logic/ltl.py +2 -138
  19. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/logic/psl.py +2 -2
  20. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/logic/sere.py +3 -2
  21. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/symbolic/__init__.py +1 -0
  22. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/symbolic/automaton.py +8 -3
  23. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/symbolic/bdd.py +104 -0
  24. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/symbolic/persistence.py +38 -3
  25. {morphata-2.0.8 → morphata-2.0.10}/tests/conftest.py +1 -1
  26. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_persistence.py +76 -0
  27. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_symbolic_automaton.py +36 -0
  28. {morphata-2.0.8 → morphata-2.0.10}/uv.lock +540 -471
  29. {morphata-2.0.8 → morphata-2.0.10}/.basedpyright/baseline.json +0 -0
  30. {morphata-2.0.8 → morphata-2.0.10}/.dockerignore +0 -0
  31. {morphata-2.0.8 → morphata-2.0.10}/.editorconfig +0 -0
  32. {morphata-2.0.8 → morphata-2.0.10}/.envrc +0 -0
  33. {morphata-2.0.8 → morphata-2.0.10}/.envrc.recommended +0 -0
  34. {morphata-2.0.8 → morphata-2.0.10}/.fdignore +0 -0
  35. {morphata-2.0.8 → morphata-2.0.10}/.gitattributes +0 -0
  36. {morphata-2.0.8 → morphata-2.0.10}/.gitignore +0 -0
  37. {morphata-2.0.8 → morphata-2.0.10}/.mailmap +0 -0
  38. {morphata-2.0.8 → morphata-2.0.10}/.python-version +0 -0
  39. {morphata-2.0.8 → morphata-2.0.10}/.rstcheck.cfg +0 -0
  40. {morphata-2.0.8 → morphata-2.0.10}/AGENTS.md +0 -0
  41. {morphata-2.0.8 → morphata-2.0.10}/CLAUDE.md +0 -0
  42. {morphata-2.0.8 → morphata-2.0.10}/CONTRIBUTING.md +0 -0
  43. {morphata-2.0.8 → morphata-2.0.10}/LICENSE +0 -0
  44. {morphata-2.0.8 → morphata-2.0.10}/README.md +0 -0
  45. {morphata-2.0.8 → morphata-2.0.10}/bench/__init__.py +0 -0
  46. {morphata-2.0.8 → morphata-2.0.10}/bench/conftest.py +0 -0
  47. {morphata-2.0.8 → morphata-2.0.10}/bench/test_equivalence_perf.py +0 -0
  48. {morphata-2.0.8 → morphata-2.0.10}/bench/test_sere_nlm_perf.py +0 -0
  49. {morphata-2.0.8 → morphata-2.0.10}/bench/test_successors_for_alphabet_perf.py +0 -0
  50. {morphata-2.0.8 → morphata-2.0.10}/ci/make-release +0 -0
  51. {morphata-2.0.8 → morphata-2.0.10}/cliff.toml +0 -0
  52. {morphata-2.0.8 → morphata-2.0.10}/docs/_static/navigation.html +0 -0
  53. {morphata-2.0.8 → morphata-2.0.10}/docs/api/modules.rst +0 -0
  54. {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.acceptance.rst +0 -0
  55. {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.alphabet.rst +0 -0
  56. {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.automaton.rst +0 -0
  57. {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.builder.rst +0 -0
  58. {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.hoa.rst +0 -0
  59. {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.logic.ltl.rst +0 -0
  60. {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.logic.psl.rst +0 -0
  61. {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.logic.sere.rst +0 -0
  62. {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.logic.strel.rst +0 -0
  63. {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.operators.matrix.rst +0 -0
  64. {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.operators.polynomial.rst +0 -0
  65. {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.operators.rst +0 -0
  66. {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.symbolic.rst +0 -0
  67. {morphata-2.0.8 → morphata-2.0.10}/docs/concepts/acceptance.rst +0 -0
  68. {morphata-2.0.8 → morphata-2.0.10}/docs/concepts/hoa.rst +0 -0
  69. {morphata-2.0.8 → morphata-2.0.10}/docs/concepts/index.rst +0 -0
  70. {morphata-2.0.8 → morphata-2.0.10}/docs/concepts/operators.rst +0 -0
  71. {morphata-2.0.8 → morphata-2.0.10}/docs/concepts/spec.rst +0 -0
  72. {morphata-2.0.8 → morphata-2.0.10}/docs/concepts/weights.rst +0 -0
  73. {morphata-2.0.8 → morphata-2.0.10}/docs/conf.py +0 -0
  74. {morphata-2.0.8 → morphata-2.0.10}/docs/index.rst +0 -0
  75. {morphata-2.0.8 → morphata-2.0.10}/docs/quick-start.rst +0 -0
  76. {morphata-2.0.8 → morphata-2.0.10}/docs/symbolic-transforms.rst +0 -0
  77. {morphata-2.0.8 → morphata-2.0.10}/justfile +0 -0
  78. {morphata-2.0.8 → morphata-2.0.10}/pixi.lock +0 -0
  79. {morphata-2.0.8 → morphata-2.0.10}/pixi.toml +0 -0
  80. {morphata-2.0.8 → morphata-2.0.10}/project.utf-8.add +0 -0
  81. {morphata-2.0.8 → morphata-2.0.10}/project.utf-8.add.spl +0 -0
  82. {morphata-2.0.8 → morphata-2.0.10}/pyrefly.toml +0 -0
  83. {morphata-2.0.8 → morphata-2.0.10}/pytest.toml +0 -0
  84. {morphata-2.0.8 → morphata-2.0.10}/ruff.toml +0 -0
  85. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/__init__.py +0 -0
  86. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/acceptance.py +0 -0
  87. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/alphabet.py +0 -0
  88. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/automaton.py +0 -0
  89. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/builder.py +0 -0
  90. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/hoa/__init__.py +0 -0
  91. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/hoa/__main__.py +0 -0
  92. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/hoa/acc_expr.py +0 -0
  93. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/logic/__init__.py +0 -0
  94. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/logic/_common.py +0 -0
  95. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/logic/strel.py +0 -0
  96. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/operators/__init__.py +0 -0
  97. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/operators/_backend.py +0 -0
  98. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/operators/matrix.py +0 -0
  99. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/operators/polynomial.py +0 -0
  100. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/py.typed +0 -0
  101. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/symbolic/compose.py +0 -0
  102. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/symbolic/equivalence.py +0 -0
  103. {morphata-2.0.8 → morphata-2.0.10}/src/morphata/symbolic/transforms.py +0 -0
  104. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut1.hoa +0 -0
  105. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut11.hoa +0 -0
  106. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut2.hoa +0 -0
  107. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut3.2.hoa +0 -0
  108. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut3.hoa +0 -0
  109. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut4.hoa +0 -0
  110. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut5.hoa +0 -0
  111. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut6.hoa +0 -0
  112. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut7.hoa +0 -0
  113. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut8.hoa +0 -0
  114. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/finite_aut.hoa +0 -0
  115. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/gen_buchi_3.hoa +0 -0
  116. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/gen_cobuchi_2.hoa +0 -0
  117. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/rabin_2pair.hoa +0 -0
  118. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/streett_2pair.hoa +0 -0
  119. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/test_exporter_basic.py +0 -0
  120. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/test_exporter_roundtrip.py +0 -0
  121. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/test_exporter_streaming.py +0 -0
  122. {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/test_parser.py +0 -0
  123. {morphata-2.0.8 → morphata-2.0.10}/tests/logic/__init__.py +0 -0
  124. {morphata-2.0.8 → morphata-2.0.10}/tests/logic/test_ltl.py +0 -0
  125. {morphata-2.0.8 → morphata-2.0.10}/tests/logic/test_psl.py +0 -0
  126. {morphata-2.0.8 → morphata-2.0.10}/tests/logic/test_sere.py +0 -0
  127. {morphata-2.0.8 → morphata-2.0.10}/tests/logic/test_sere_properties.py +0 -0
  128. {morphata-2.0.8 → morphata-2.0.10}/tests/logic/test_strel.py +0 -0
  129. {morphata-2.0.8 → morphata-2.0.10}/tests/operators/test_matrix_from_ir.py +0 -0
  130. {morphata-2.0.8 → morphata-2.0.10}/tests/operators/test_matrix_from_ltl.py +0 -0
  131. {morphata-2.0.8 → morphata-2.0.10}/tests/operators/test_polynomial_from_ir.py +0 -0
  132. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_alphabet_aware_transitions.py +0 -0
  133. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_bdd_helpers.py +0 -0
  134. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_compose.py +0 -0
  135. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_composition_map.py +0 -0
  136. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_equivalence_naive_property.py +0 -0
  137. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_from_hoaf.py +0 -0
  138. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_ltlf2dfa_equivalence.py +0 -0
  139. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_step.py +0 -0
  140. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_symbolic_from_ir.py +0 -0
  141. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_to_hoa.py +0 -0
  142. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_transforms.py +0 -0
  143. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_transforms_ir.py +0 -0
  144. {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_transforms_properties.py +0 -0
  145. {morphata-2.0.8 → morphata-2.0.10}/tests/test_alphabet.py +0 -0
  146. {morphata-2.0.8 → morphata-2.0.10}/tests/test_automaton.py +0 -0
  147. {morphata-2.0.8 → morphata-2.0.10}/tests/test_builder.py +0 -0
  148. {morphata-2.0.8 → morphata-2.0.10}/tests/test_from_parsed.py +0 -0
  149. {morphata-2.0.8 → morphata-2.0.10}/ty.toml +0 -0
@@ -26,7 +26,10 @@
26
26
  "Bash(echo \"exit: $?\")",
27
27
  "Skill(superpowers:executing-plans)",
28
28
  "Bash(ruff check *)",
29
- "Bash(ruff format *)"
29
+ "Bash(ruff format *)",
30
+ "Bash(python -c \"import algebraic.polynomials as p; print\\(p.__file__\\)\")",
31
+ "Bash(python -c \"import algebraic; print\\(algebraic.__file__\\)\")",
32
+ "Bash(python -c \"import algebraic; print\\(algebraic.__path__\\)\")"
30
33
  ]
31
34
  }
32
35
  }
@@ -0,0 +1,16 @@
1
+ # CodeGraph data files
2
+ # These are local to each machine and should not be committed
3
+
4
+ # Database
5
+ *.db
6
+ *.db-wal
7
+ *.db-shm
8
+
9
+ # Cache
10
+ cache/
11
+
12
+ # Logs
13
+ *.log
14
+
15
+ # Hook markers
16
+ .dirty
Binary file
@@ -0,0 +1,6 @@
1
+ {
2
+ "pid": 92,
3
+ "version": "0.9.9",
4
+ "socketPath": "/home/anand/workspace/automata-stuff/morphata/.codegraph/daemon.sock",
5
+ "startedAt": 1780894603056
6
+ }
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.10 — 2026-06-08
4
+
5
+ ### Fixed
6
+
7
+ - Using `SymbolicAutomaton.to_bdd_json` and `SymbolicAutomaton.from_bdd_json` did not
8
+ preserve the positive normal form of the BDDs because we were using incorrect Shannon
9
+ cofactor expansion.
10
+
11
+ ### Performance
12
+
13
+ - The HOA grammar is now LALR compatible! This means we get a huge performance boost
14
+ even for pretty large automata.
15
+ - Similar thing was done in `logic-asts` and thus, the version will be bumped.
16
+
17
+ ## 2.0.9 — 2026-06-04
18
+
19
+ ### Fixed
20
+
21
+ - `dd._copy` was using `shelve` to create a hard-coded `__shelve__` directory in the
22
+ current working directory.
23
+ This caused a clash whenever we ran multiple data generation processes
24
+ (either different runs or multiprocessing), and a fatal error.
25
+ This release fixes that by, unfortunately,
26
+ duplicating the code that is used to serialize/deserialize BDDs in `dd`,
27
+ but using a `tempfile.mkdtemp` block to ensure the `shelve` is in a namespaced
28
+ directory.
29
+
30
+ ### Performance
31
+
32
+ - This is a major performance thing.
33
+ In addition to all the previous things we have done,
34
+ we now ensure that `symbolic_automaton_to_hoa`
35
+ (and in-effect, `SymbolicAutomaton.to_hoa`) **does not materialize any `BoolExpr`**.
36
+ Instead, we opt to use a (locally cached) BDD to label/guard expression function,
37
+ which increases throughput like CRAZY!!!
38
+
3
39
  ## 2.0.8 — 2026-06-03
4
40
 
5
41
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphata
3
- Version: 2.0.8
3
+ Version: 2.0.10
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
@@ -16,6 +16,15 @@ To get pytest-benchmark's tabular wall-time output:
16
16
  - `test_sere_nlm_perf.py` -- NLM-intersection state-space blowup.
17
17
  Cases: `NLMInter(a0..a_{n-1})` for n in 2..6, `NLMInter(a_i;b_i)`
18
18
  for n in 2..4, plus length-matching `Inter` baselines.
19
+ - `test_hoa_exporter_perf.py` -- ``symbolic_automaton_to_hoa`` end-to-end
20
+ wall time across regimes that stress the per-row guard/successor
21
+ caching (small DFA, sparse-big-alphabet DFA, conjunction min-DFA, raw
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.
19
28
 
20
29
  ## Recording results
21
30
 
@@ -0,0 +1,124 @@
1
+ """Micro-benchmarks for ``symbolic_automaton_to_hoa``.
2
+
3
+ Targets the per-row ``bdd_to_boolexpr`` work in
4
+ ``morphata.hoa.exporter.symbolic_automaton_to_hoa``. The export walks
5
+ every cached ``(guard_bdd, succ_bdd)`` row, decodes both BDDs into
6
+ ``BoolExpr`` trees, and renders text. In real workloads the same
7
+ ``dd.Function`` recurs heavily across rows -- canonical guards shared by
8
+ many transitions, and successor BDDs shared by many guards in DFAs --
9
+ so caching the per-Function rendering is expected to dominate raw
10
+ ``bdd_to_boolexpr`` cost.
11
+
12
+ These cases hit the three regimes that matter:
13
+
14
+ - ``small_alphabet_dfa``: small AP universe, dense DFA. Baseline; cache
15
+ effect should be small (few distinct guards anyway).
16
+ - ``sparse_big_alphabet_dfa``: large AP universe but sparse guards
17
+ (mirrors ``test_equivalence_perf``). Many rows, few distinct
18
+ ``guard_bdd`` Functions -> label cache pays.
19
+ - ``conjunction_min_dfa``: ``Eventually(a) & ... & Eventually(d)`` min-DFA
20
+ with extra padding APs. Both caches pay: same guards across many
21
+ states, and many guards point to the same successor partition.
22
+ - ``afa_no_minimize``: raw LTL -> AFA without minimization. Exercises
23
+ the un-determinized regime where successor BDDs are richer (one per
24
+ conjunctive obligation).
25
+
26
+ Run with: ``pytest bench/test_hoa_exporter_perf.py -v``
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import logic_asts.ltl as ltl
32
+ from pytest_benchmark.fixture import BenchmarkFixture as Benchmark
33
+
34
+ from morphata.alphabet import powerset_alphabet
35
+ from morphata.hoa.exporter import symbolic_automaton_to_hoa
36
+ from morphata.symbolic.automaton import SymbolicAutomaton
37
+ from morphata.symbolic.bdd import Manager
38
+
39
+
40
+ def _small_alphabet_dfa(mgr: Manager) -> SymbolicAutomaton[str]:
41
+ aps = frozenset({"a", "b"})
42
+ formula = ltl.Eventually(ltl.And((ltl.Variable("a"), ltl.Variable("b"))))
43
+ alphabet = powerset_alphabet(aps)
44
+ return SymbolicAutomaton.from_ltl(formula, alphabet=alphabet, manager=mgr, finite=True).to_min_dfa()
45
+
46
+
47
+ def _sparse_big_alphabet_dfa(mgr: Manager) -> SymbolicAutomaton[str]:
48
+ aps = frozenset({f"p{i}" for i in range(15)} | {"a", "b", "c", "d"})
49
+ formula = ltl.And(
50
+ (
51
+ ltl.Eventually(ltl.Variable("a")),
52
+ ltl.Eventually(ltl.Variable("b")),
53
+ ltl.Eventually(ltl.Variable("c")),
54
+ ltl.Eventually(ltl.Variable("d")),
55
+ )
56
+ )
57
+ alphabet = powerset_alphabet(aps)
58
+ return SymbolicAutomaton.from_ltl(formula, alphabet=alphabet, manager=mgr, finite=True).to_min_dfa()
59
+
60
+
61
+ def _conjunction_min_dfa(mgr: Manager) -> SymbolicAutomaton[str]:
62
+ aps = frozenset({f"q{i}" for i in range(8)} | {"a", "b", "c", "d", "e"})
63
+ formula = ltl.And(
64
+ (
65
+ ltl.Eventually(ltl.Variable("a")),
66
+ ltl.Eventually(ltl.Variable("b")),
67
+ ltl.Eventually(ltl.Variable("c")),
68
+ ltl.Eventually(ltl.Variable("d")),
69
+ ltl.Eventually(ltl.Variable("e")),
70
+ )
71
+ )
72
+ alphabet = powerset_alphabet(aps)
73
+ return SymbolicAutomaton.from_ltl(formula, alphabet=alphabet, manager=mgr, finite=True).to_min_dfa()
74
+
75
+
76
+ def _afa_no_minimize(mgr: Manager) -> SymbolicAutomaton[str]:
77
+ aps = frozenset({"a", "b", "c", "d"})
78
+ formula = ltl.Until(
79
+ ltl.Variable("a"),
80
+ ltl.And(
81
+ (
82
+ ltl.Variable("b"),
83
+ ltl.Eventually(ltl.And((ltl.Variable("c"), ltl.Eventually(ltl.Variable("d"))))),
84
+ )
85
+ ),
86
+ )
87
+ alphabet = powerset_alphabet(aps)
88
+ return SymbolicAutomaton.from_ltl(formula, alphabet=alphabet, manager=mgr, finite=True)
89
+
90
+
91
+ def test_hoa_export_small_alphabet_dfa(benchmark: Benchmark, bench_mgr: Manager) -> None:
92
+ sym = _small_alphabet_dfa(bench_mgr)
93
+
94
+ def _run() -> int:
95
+ return len(symbolic_automaton_to_hoa(sym))
96
+
97
+ assert benchmark(_run) > 0
98
+
99
+
100
+ def test_hoa_export_sparse_big_alphabet_dfa(benchmark: Benchmark, bench_mgr: Manager) -> None:
101
+ sym = _sparse_big_alphabet_dfa(bench_mgr)
102
+
103
+ def _run() -> int:
104
+ return len(symbolic_automaton_to_hoa(sym))
105
+
106
+ assert benchmark(_run) > 0
107
+
108
+
109
+ def test_hoa_export_conjunction_min_dfa(benchmark: Benchmark, bench_mgr: Manager) -> None:
110
+ sym = _conjunction_min_dfa(bench_mgr)
111
+
112
+ def _run() -> int:
113
+ return len(symbolic_automaton_to_hoa(sym))
114
+
115
+ assert benchmark(_run) > 0
116
+
117
+
118
+ def test_hoa_export_afa_no_minimize(benchmark: Benchmark, bench_mgr: Manager) -> None:
119
+ sym = _afa_no_minimize(bench_mgr)
120
+
121
+ def _run() -> int:
122
+ return len(symbolic_automaton_to_hoa(sym))
123
+
124
+ assert benchmark(_run) > 0
@@ -0,0 +1,114 @@
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 HOA_GRAMMAR_FILE, ParsedAutomaton, _AstTransformer
41
+ from morphata.symbolic.bdd import make_manager
42
+ from morphata.symbolic.compose import min_dfa_of_conjunction
43
+
44
+ _GRAMMAR = HOA_GRAMMAR_FILE.read_text()
45
+ _HOA_DIR = Path(__file__).resolve().parent.parent / "tests" / "hoa"
46
+
47
+ # One Lark instance per algorithm, built once and reused across rounds.
48
+ _PARSERS: dict[str, Lark] = {
49
+ "earley": Lark(_GRAMMAR, start="automaton", parser="earley", maybe_placeholders=True),
50
+ "lalr": Lark(_GRAMMAR, start="automaton", parser="lalr", maybe_placeholders=True),
51
+ }
52
+
53
+ # Representative slice of the sample corpus: smallest, a couple of mid-size
54
+ # omega conditions, the two largest acceptance formulas, and a synthetic
55
+ # stress input (built lazily, see ``_craftax_hoa``).
56
+ _CASES = ["aut1", "gen_buchi_3", "aut8", "rabin_2pair", "streett_2pair", "craftax"]
57
+
58
+
59
+ @functools.cache
60
+ def _craftax_hoa() -> str:
61
+ """Export the Craftax ENCHANT_SWORD-shaped min-DFA (~7 conjuncts, |AP|=25).
62
+
63
+ Mirrors ``test_min_dfa_of_conjunction_craftax_shaped_stress``. Cached so the
64
+ min-DFA is built once regardless of how many benchmark rounds run.
65
+ """
66
+ aps = {f"p{i}" for i in range(25)}
67
+ alphabet = powerset_alphabet(aps)
68
+ p = sorted(aps)
69
+
70
+ def F(name: str) -> logic.LTLExpr[str]: # noqa: N802
71
+ return ltl.Eventually(logic.Variable(name))
72
+
73
+ def U(lhs: logic.LTLExpr[str], rhs: logic.LTLExpr[str]) -> logic.LTLExpr[str]: # noqa: N802
74
+ return ltl.Until(lhs, rhs)
75
+
76
+ formulas: list[logic.LTLExpr[str]] = [
77
+ F(p[0]),
78
+ U(logic.Not(logic.Variable(p[0])), logic.Or((logic.Variable(p[1]), logic.Variable(p[2])))),
79
+ U(
80
+ logic.Not(logic.Variable(p[0])),
81
+ logic.Or((logic.Variable(p[3]), logic.Variable(p[4]), logic.Variable(p[5]))),
82
+ ),
83
+ U(logic.Not(logic.Variable(p[0])), logic.Or((logic.Variable(p[6]), logic.Variable(p[7])))),
84
+ F(p[1]),
85
+ F(p[4]),
86
+ F(p[7]),
87
+ ]
88
+ sym, _ = min_dfa_of_conjunction(formulas, alphabet=alphabet, manager=make_manager())
89
+ return symbolic_automaton_to_hoa(sym)
90
+
91
+
92
+ def _hoa_text(case: str) -> str:
93
+ if case == "craftax":
94
+ return _craftax_hoa()
95
+ return (_HOA_DIR / f"{case}.hoa").read_text()
96
+
97
+
98
+ def _parse_with(lark: Lark, text: str) -> ParsedAutomaton:
99
+ tree = lark.parse(text)
100
+ ret = _AstTransformer().transform(tree)
101
+ assert isinstance(ret, ParsedAutomaton)
102
+ return ret
103
+
104
+
105
+ @pytest.mark.parametrize("kind", ["earley", "lalr"])
106
+ @pytest.mark.parametrize("case", _CASES)
107
+ def test_hoa_parse(benchmark: Benchmark, case: str, kind: str) -> None:
108
+ text = _hoa_text(case)
109
+ lark = _PARSERS[kind]
110
+
111
+ def _run() -> int:
112
+ return len(_parse_with(lark, text).body)
113
+
114
+ assert benchmark(_run) >= 0
@@ -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
@@ -2,7 +2,7 @@
2
2
  name = "morphata"
3
3
  authors = [{ name = "Anand Balakrishnan", email = "anandbala1597@gmail.com" }]
4
4
  description = "Representation theory for automata, made practical."
5
- version = "2.0.8"
5
+ version = "2.0.10"
6
6
  requires-python = ">=3.12"
7
7
  classifiers = [
8
8
  "Intended Audience :: Science/Research",
@@ -2,7 +2,7 @@
2
2
  "include": [
3
3
  "src",
4
4
  "tests",
5
- "packages"
5
+ "bench"
6
6
  ],
7
7
  "pythonVersion": "3.12",
8
8
  "pythonPlatform": "All",
@@ -205,13 +205,7 @@ def symbolic_automaton_to_hoa(
205
205
 
206
206
  ap_order = sorted(sym.ap_registry, key=repr)
207
207
  ap_to_index: dict[ty.Any, int] = {ap: i for i, ap in enumerate(ap_order)}
208
- ap_decoder_map: dict[str, ty.Any] = {var: ap for ap, var in sym.ap_registry.items()}
209
-
210
- def _ap_decode(name: str) -> ty.Any: # noqa: ANN401
211
- try:
212
- return ap_decoder_map[name]
213
- except KeyError as exc:
214
- raise ValueError(f"Guard BDD references variable {name!r} not in the AP registry.") from exc
208
+ bdd_var_to_ap_index: dict[str, int] = {var: ap_to_index[ap] for ap, var in sym.ap_registry.items()}
215
209
 
216
210
  acc_name, acc_expr_text, num_sets, acc_state_sets = _acceptance_to_hoa(sym.acceptance)
217
211
  if true_sink is not None:
@@ -248,11 +242,22 @@ def symbolic_automaton_to_hoa(
248
242
  if properties:
249
243
  buf.write("properties: " + " ".join(properties) + "\n")
250
244
 
245
+ # Per-call caches keyed by dd.Function identity. Guards and successor
246
+ # BDDs recur heavily across rows (shared canonical guards; many guards
247
+ # leading to the same target set in DFAs). With the pick_iter-based
248
+ # renderers below the per-Function rendering cost is much smaller
249
+ # than it was with the bdd_to_boolexpr + _label_to_hoa pipeline, but
250
+ # the caches still pay back on shared Functions. Kept as two separate
251
+ # dicts since the pipelines produce different shapes (str vs list[str]);
252
+ # freed explicitly before return to limit Function references held
253
+ # against the CUDD backend.
254
+ label_cache: dict[ty.Any, str] = {}
255
+ dsts_cache: dict[ty.Any, list[str]] = {}
256
+ marker_by_state = _markers_by_state(acc_state_sets)
257
+
251
258
  buf.write("--BODY--\n")
252
259
  for s in states:
253
- sets_for_s = sorted(idx for idx, members in acc_state_sets.items() if s in members)
254
- marker = (" {" + " ".join(str(i) for i in sets_for_s) + "}") if sets_for_s else ""
255
- buf.write(f"State: {s}{marker}\n")
260
+ buf.write(f"State: {s}{marker_by_state.get(s, '')}\n")
256
261
  for guard_bdd, succ_bdd in sym.transitions.get(s, ()):
257
262
  if succ_bdd == mgr.true:
258
263
  assert true_sink is not None
@@ -260,22 +265,27 @@ def symbolic_automaton_to_hoa(
260
265
  elif succ_bdd == mgr.false:
261
266
  assert false_sink is not None
262
267
  dsts = [str(false_sink)]
268
+ elif succ_bdd in dsts_cache:
269
+ dsts = dsts_cache[succ_bdd]
270
+ else:
271
+ dsts = _bdd_state_cubes(succ_bdd, mgr=mgr)
272
+ dsts_cache[succ_bdd] = dsts
273
+ if guard_bdd in label_cache:
274
+ label = label_cache[guard_bdd]
263
275
  else:
264
- succ_expr = ty.cast("logic.BoolExpr[int]", bdd_to_boolexpr(succ_bdd))
265
- dsts = _state_expr_to_edges(succ_expr)
266
- guard_expr = bdd_to_boolexpr(guard_bdd, var_decoder=_ap_decode)
267
- label = _label_to_hoa(guard_expr, ap_to_index)
276
+ label = _bdd_to_hoa_label(guard_bdd, mgr=mgr, bdd_var_to_ap_index=bdd_var_to_ap_index)
277
+ label_cache[guard_bdd] = label
268
278
  for dst in dsts:
269
279
  buf.write(f"[{label}] {dst}\n")
270
280
  for sink in (true_sink, false_sink):
271
281
  if sink is None:
272
282
  continue
273
- sets_for_sink = sorted(idx for idx, members in acc_state_sets.items() if sink in members)
274
- marker = (" {" + " ".join(str(i) for i in sets_for_sink) + "}") if sets_for_sink else ""
275
- buf.write(f"State: {sink}{marker}\n")
283
+ buf.write(f"State: {sink}{marker_by_state.get(sink, '')}\n")
276
284
  buf.write(f"[t] {sink}\n")
277
285
  buf.write("--END--\n")
278
286
 
287
+ del label_cache, dsts_cache
288
+
279
289
  if owns_buffer:
280
290
  assert isinstance(buf, io.StringIO)
281
291
  return buf.getvalue()
@@ -475,11 +485,10 @@ def _write_body(
475
485
  from morphata.automaton import Outgoing
476
486
 
477
487
  assert isinstance(aut.delta, Outgoing)
488
+ marker_by_state = _markers_by_state(acc_state_sets)
478
489
  buf.write("--BODY--\n")
479
490
  for s in states:
480
- sets_for_s = sorted(idx for idx, members in acc_state_sets.items() if s in members)
481
- marker = (" {" + " ".join(str(i) for i in sets_for_s) + "}") if sets_for_s else ""
482
- buf.write(f"State: {s}{marker}\n")
491
+ buf.write(f"State: {s}{marker_by_state.get(s, '')}\n")
483
492
  for guard, succ in aut.delta.fn(s):
484
493
  label_text = _label_to_hoa(guard, ap_to_index)
485
494
  for dst_text in _state_expr_to_edges(succ):
@@ -595,6 +604,34 @@ def _cube_to_hoa_label(
595
604
  return "(" + " & ".join(parts) + ")"
596
605
 
597
606
 
607
+ def _bdd_to_hoa_label(
608
+ node: "Function",
609
+ *,
610
+ mgr: "Manager",
611
+ bdd_var_to_ap_index: Mapping[str, int],
612
+ ) -> str:
613
+ """Render a guard BDD as a single HOA label string.
614
+
615
+ Bypasses :func:`~morphata.symbolic.bdd.bdd_to_boolexpr` -- which
616
+ walks the BDD via Shannon expansion, builds a ``logic_asts``
617
+ ``BoolExpr`` tree, and runs NNF normalisation -- by enumerating the
618
+ BDD's cubes directly with ``mgr.pick_iter`` and joining their HOA
619
+ renderings with ``|``. Output shape matches the prior
620
+ ``bdd_to_boolexpr`` + :func:`_label_to_hoa` pipeline for HOA
621
+ consumers: one label per row, possibly disjunctive.
622
+ """
623
+ if node == mgr.true:
624
+ return "t"
625
+ if node == mgr.false:
626
+ return "f"
627
+ cube_labels = [_cube_to_hoa_label(cube, bdd_var_to_ap_index) for cube in mgr.pick_iter(node)]
628
+ if not cube_labels:
629
+ return "f"
630
+ if len(cube_labels) == 1:
631
+ return cube_labels[0]
632
+ return "(" + " | ".join(cube_labels) + ")"
633
+
634
+
598
635
  def _bdd_state_cubes(node: "Function", *, mgr: "Manager") -> list[str]:
599
636
  """Enumerate satisfying cubes of *node* over state variables as HOA dst strings.
600
637
 
@@ -629,6 +666,23 @@ def _bdd_state_cubes(node: "Function", *, mgr: "Manager") -> list[str]:
629
666
  # ---------------------------------------------------------------------------
630
667
 
631
668
 
669
+ def _markers_by_state(
670
+ acc_state_sets: Mapping[int, AbstractSet[int]],
671
+ ) -> dict[int, str]:
672
+ """Invert ``acc_state_sets`` into per-state ``" {i j ...}"`` markers.
673
+
674
+ Replaces the per-state ``sorted(idx for ... if s in members)`` scan
675
+ in the body loop, which is O(|states| * |acc_sets|). Inverting once
676
+ is O(sum of set sizes). Only states with at least one membership get
677
+ an entry; callers default to ``""`` via ``dict.get(s, "")``.
678
+ """
679
+ by_state: dict[int, list[int]] = {}
680
+ for idx, members in acc_state_sets.items():
681
+ for s in members:
682
+ by_state.setdefault(s, []).append(idx)
683
+ return {s: " {" + " ".join(str(i) for i in sorted(indices)) + "}" for s, indices in by_state.items()}
684
+
685
+
632
686
  def _acceptance_to_hoa(
633
687
  cond: AcceptanceCondition[int],
634
688
  ) -> tuple[str | None, str, int, Mapping[int, AbstractSet[int]]]:
@@ -38,13 +38,19 @@ state_conj: INT ("&" INT)*
38
38
  | "(" label_expr ")"
39
39
 
40
40
 
41
- ?acceptance_cond: BOOLEAN -> acc_bool
42
- | "Fin" "(" acc_set ")" -> acc_fin
43
- | "Inf" "(" acc_set ")" -> acc_inf
44
- | "Final" "(" acc_set ")" -> acc_final
45
- | "(" acceptance_cond ")"
46
- | acceptance_cond "&" acceptance_cond -> acc_and
47
- | acceptance_cond "|" acceptance_cond -> acc_or
41
+ ?acceptance_cond: acc_or
42
+
43
+ ?acc_or : acc_and
44
+ | acc_or "|" acc_and -> acc_or
45
+
46
+ ?acc_and : acc_atom
47
+ | acc_and "&" acc_atom -> acc_and
48
+
49
+ ?acc_atom: BOOLEAN -> acc_bool
50
+ | "Fin" "(" acc_set ")" -> acc_fin
51
+ | "Inf" "(" acc_set ")" -> acc_inf
52
+ | "Final" "(" acc_set ")" -> acc_final
53
+ | "(" acceptance_cond ")"
48
54
 
49
55
  acc_set: [INVERT] INT
50
56
  INVERT: "!"
@@ -365,7 +365,8 @@ with open(HOA_GRAMMAR_FILE, "r") as grammar:
365
365
  HOA_GRAMMAR = Lark(
366
366
  grammar,
367
367
  start="automaton",
368
- strict=True,
368
+ parser="lalr",
369
+ cache=True,
369
370
  maybe_placeholders=True,
370
371
  )
371
372