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.
- {morphata-2.0.8 → morphata-2.0.10}/.claude/settings.local.json +4 -1
- morphata-2.0.10/.codegraph/.gitignore +16 -0
- morphata-2.0.10/.codegraph/codegraph.db +0 -0
- morphata-2.0.10/.codegraph/codegraph.db-shm +0 -0
- morphata-2.0.10/.codegraph/codegraph.db-wal +0 -0
- morphata-2.0.10/.codegraph/daemon.pid +6 -0
- {morphata-2.0.8 → morphata-2.0.10}/CHANGELOG.md +36 -0
- {morphata-2.0.8 → morphata-2.0.10}/PKG-INFO +1 -1
- {morphata-2.0.8 → morphata-2.0.10}/bench/README.md +9 -0
- morphata-2.0.10/bench/test_hoa_exporter_perf.py +124 -0
- morphata-2.0.10/bench/test_hoa_parser_perf.py +114 -0
- {morphata-2.0.8 → morphata-2.0.10}/mypy.ini +10 -0
- {morphata-2.0.8 → morphata-2.0.10}/pyproject.toml +1 -1
- {morphata-2.0.8 → morphata-2.0.10}/pyrightconfig.json +1 -1
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/hoa/exporter.py +74 -20
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/hoa/hoa.lark +13 -7
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/hoa/parser.py +2 -1
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/logic/ltl.py +2 -138
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/logic/psl.py +2 -2
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/logic/sere.py +3 -2
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/symbolic/__init__.py +1 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/symbolic/automaton.py +8 -3
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/symbolic/bdd.py +104 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/symbolic/persistence.py +38 -3
- {morphata-2.0.8 → morphata-2.0.10}/tests/conftest.py +1 -1
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_persistence.py +76 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_symbolic_automaton.py +36 -0
- {morphata-2.0.8 → morphata-2.0.10}/uv.lock +540 -471
- {morphata-2.0.8 → morphata-2.0.10}/.basedpyright/baseline.json +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/.dockerignore +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/.editorconfig +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/.envrc +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/.envrc.recommended +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/.fdignore +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/.gitattributes +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/.gitignore +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/.mailmap +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/.python-version +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/.rstcheck.cfg +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/AGENTS.md +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/CLAUDE.md +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/CONTRIBUTING.md +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/LICENSE +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/README.md +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/bench/__init__.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/bench/conftest.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/bench/test_equivalence_perf.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/bench/test_sere_nlm_perf.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/bench/test_successors_for_alphabet_perf.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/ci/make-release +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/cliff.toml +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/_static/navigation.html +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/api/modules.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.acceptance.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.alphabet.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.automaton.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.builder.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.hoa.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.logic.ltl.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.logic.psl.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.logic.sere.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.logic.strel.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.operators.matrix.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.operators.polynomial.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.operators.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/api/morphata.symbolic.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/concepts/acceptance.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/concepts/hoa.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/concepts/index.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/concepts/operators.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/concepts/spec.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/concepts/weights.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/conf.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/index.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/quick-start.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/docs/symbolic-transforms.rst +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/justfile +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/pixi.lock +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/pixi.toml +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/project.utf-8.add +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/project.utf-8.add.spl +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/pyrefly.toml +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/pytest.toml +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/ruff.toml +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/__init__.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/acceptance.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/alphabet.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/automaton.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/builder.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/hoa/__init__.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/hoa/__main__.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/hoa/acc_expr.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/logic/__init__.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/logic/_common.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/logic/strel.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/operators/__init__.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/operators/_backend.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/operators/matrix.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/operators/polynomial.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/py.typed +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/symbolic/compose.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/symbolic/equivalence.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/src/morphata/symbolic/transforms.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut1.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut11.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut2.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut3.2.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut3.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut4.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut5.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut6.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut7.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/aut8.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/finite_aut.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/gen_buchi_3.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/gen_cobuchi_2.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/rabin_2pair.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/streett_2pair.hoa +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/test_exporter_basic.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/test_exporter_roundtrip.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/test_exporter_streaming.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/hoa/test_parser.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/logic/__init__.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/logic/test_ltl.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/logic/test_psl.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/logic/test_sere.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/logic/test_sere_properties.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/logic/test_strel.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/operators/test_matrix_from_ir.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/operators/test_matrix_from_ltl.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/operators/test_polynomial_from_ir.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_alphabet_aware_transitions.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_bdd_helpers.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_compose.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_composition_map.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_equivalence_naive_property.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_from_hoaf.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_ltlf2dfa_equivalence.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_step.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_symbolic_from_ir.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_to_hoa.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_transforms.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_transforms_ir.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/symbolic/test_transforms_properties.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/test_alphabet.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/test_automaton.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/test_builder.py +0 -0
- {morphata-2.0.8 → morphata-2.0.10}/tests/test_from_parsed.py +0 -0
- {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
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
@@ -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.
|
|
5
|
+
version = "2.0.10"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
classifiers = [
|
|
8
8
|
"Intended Audience :: Science/Research",
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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: "!"
|