morphata 2.0.7__tar.gz → 2.0.8__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.7 → morphata-2.0.8}/CHANGELOG.md +39 -0
- {morphata-2.0.7 → morphata-2.0.8}/PKG-INFO +1 -1
- morphata-2.0.8/bench/test_equivalence_perf.py +57 -0
- morphata-2.0.8/bench/test_successors_for_alphabet_perf.py +124 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/api/morphata.symbolic.rst +7 -0
- {morphata-2.0.7 → morphata-2.0.8}/pyproject.toml +1 -1
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/symbolic/__init__.py +2 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/symbolic/automaton.py +68 -12
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/symbolic/compose.py +205 -106
- morphata-2.0.8/src/morphata/symbolic/equivalence.py +187 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/symbolic/transforms.py +22 -2
- {morphata-2.0.7 → morphata-2.0.8}/tests/symbolic/test_alphabet_aware_transitions.py +28 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/symbolic/test_compose.py +20 -18
- morphata-2.0.8/tests/symbolic/test_composition_map.py +264 -0
- morphata-2.0.8/tests/symbolic/test_equivalence_naive_property.py +130 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/symbolic/test_ltlf2dfa_equivalence.py +39 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/symbolic/test_symbolic_automaton.py +21 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/symbolic/test_transforms.py +38 -0
- {morphata-2.0.7 → morphata-2.0.8}/uv.lock +1 -1
- morphata-2.0.7/src/morphata/symbolic/equivalence.py +0 -114
- {morphata-2.0.7 → morphata-2.0.8}/.basedpyright/baseline.json +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/.claude/settings.local.json +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/.dockerignore +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/.editorconfig +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/.envrc +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/.envrc.recommended +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/.fdignore +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/.gitattributes +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/.gitignore +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/.mailmap +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/.python-version +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/.rstcheck.cfg +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/AGENTS.md +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/CLAUDE.md +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/CONTRIBUTING.md +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/LICENSE +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/README.md +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/bench/README.md +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/bench/__init__.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/bench/conftest.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/bench/test_sere_nlm_perf.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/ci/make-release +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/cliff.toml +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/_static/navigation.html +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/api/modules.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/api/morphata.acceptance.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/api/morphata.alphabet.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/api/morphata.automaton.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/api/morphata.builder.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/api/morphata.hoa.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/api/morphata.logic.ltl.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/api/morphata.logic.psl.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/api/morphata.logic.sere.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/api/morphata.logic.strel.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/api/morphata.operators.matrix.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/api/morphata.operators.polynomial.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/api/morphata.operators.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/concepts/acceptance.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/concepts/hoa.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/concepts/index.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/concepts/operators.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/concepts/spec.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/concepts/weights.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/conf.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/index.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/quick-start.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/docs/symbolic-transforms.rst +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/justfile +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/mypy.ini +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/pixi.lock +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/pixi.toml +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/project.utf-8.add +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/project.utf-8.add.spl +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/pyrefly.toml +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/pyrightconfig.json +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/pytest.toml +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/ruff.toml +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/__init__.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/acceptance.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/alphabet.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/automaton.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/builder.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/hoa/__init__.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/hoa/__main__.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/hoa/acc_expr.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/hoa/exporter.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/hoa/hoa.lark +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/hoa/parser.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/logic/__init__.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/logic/_common.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/logic/ltl.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/logic/psl.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/logic/sere.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/logic/strel.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/operators/__init__.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/operators/_backend.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/operators/matrix.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/operators/polynomial.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/py.typed +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/symbolic/bdd.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/src/morphata/symbolic/persistence.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/conftest.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/aut1.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/aut11.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/aut2.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/aut3.2.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/aut3.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/aut4.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/aut5.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/aut6.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/aut7.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/aut8.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/finite_aut.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/gen_buchi_3.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/gen_cobuchi_2.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/rabin_2pair.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/streett_2pair.hoa +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/test_exporter_basic.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/test_exporter_roundtrip.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/test_exporter_streaming.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/hoa/test_parser.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/logic/__init__.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/logic/test_ltl.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/logic/test_psl.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/logic/test_sere.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/logic/test_sere_properties.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/logic/test_strel.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/operators/test_matrix_from_ir.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/operators/test_matrix_from_ltl.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/operators/test_polynomial_from_ir.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/symbolic/test_bdd_helpers.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/symbolic/test_from_hoaf.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/symbolic/test_persistence.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/symbolic/test_step.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/symbolic/test_symbolic_from_ir.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/symbolic/test_to_hoa.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/symbolic/test_transforms_ir.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/symbolic/test_transforms_properties.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/test_alphabet.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/test_automaton.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/test_builder.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/tests/test_from_parsed.py +0 -0
- {morphata-2.0.7 → morphata-2.0.8}/ty.toml +0 -0
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.8 — 2026-06-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `compose_min_dfa` and `min_dfa_of_conjunction` are variadic now
|
|
8
|
+
and always return a `CompositionMap`: the per-factor automata,
|
|
9
|
+
the representative-to-tuple map,
|
|
10
|
+
and the full surjection from reachable per-factor tuples to composed states.
|
|
11
|
+
I initially wanted the section-only view,
|
|
12
|
+
but minimization can merge classes such
|
|
13
|
+
that a representative's per-factor successor lands on a non-representative tuple,
|
|
14
|
+
so the full surjection has to be there.
|
|
15
|
+
- `iter_composed_transitions` enumerates `(state, symbol, successor)`
|
|
16
|
+
of a composed DFA without building the product BDD, walking factor
|
|
17
|
+
deltas directly and hitting `tuple_to_class` once per
|
|
18
|
+
`(state, symbol)`.
|
|
19
|
+
|
|
20
|
+
### Performance
|
|
21
|
+
|
|
22
|
+
- `successors_for_alphabet` drops the `|Sigma| * |rows|` cofactor sweep
|
|
23
|
+
for a per-row support-projected symbol partition with a constant-guard fast path.
|
|
24
|
+
Fixes the small-row regression from 2.0.6 while keeping the wide-state win.
|
|
25
|
+
Same treatment for `language_equivalent` and `canonical_form`,
|
|
26
|
+
so sparse guards over a big AP universe stop iterating the full powerset.
|
|
27
|
+
- Bucketing rows by guard support amortizes the partition
|
|
28
|
+
and per-class point dicts across same-support rows.
|
|
29
|
+
Conjunction-fold DFAs get 1.3-1.6x; `F a & F b & F c & F d` q1 went ~343us -> ~175us.
|
|
30
|
+
- `isomorphic` routes through a compact support-reduced fingerprint
|
|
31
|
+
and skips the broadcast expansion. `|AP|=19` sparse case:
|
|
32
|
+
~333ms -> ~83ms.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- `SymbolicAutomaton` carries a derived `ap_inverse: Mapping[str, AP]`
|
|
37
|
+
populated in `__post_init__`; `to_automaton` and
|
|
38
|
+
`compose._merge_managers` route through it.
|
|
39
|
+
- New symbolic benches are pytest-benchmark cases, so timings get
|
|
40
|
+
recorded rather than gated on a hard-coded threshold.
|
|
41
|
+
|
|
3
42
|
## 2.0.7 — 2026-06-02
|
|
4
43
|
|
|
5
44
|
### Fixed
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Micro-benchmarks for the rewritten equivalence functions.
|
|
2
|
+
|
|
3
|
+
The case: an LTLf min-DFA whose AP universe is large (|aps| ~ 19) but
|
|
4
|
+
whose guards mention only 4 APs. Pre-rewrite this regime explodes;
|
|
5
|
+
post-rewrite the per-pair (language_equivalent) and per-automaton
|
|
6
|
+
(isomorphic) support reductions keep work bounded.
|
|
7
|
+
|
|
8
|
+
pytest-benchmark records the timing distribution; no hard-coded
|
|
9
|
+
thresholds, the dataset is the regression signal.
|
|
10
|
+
|
|
11
|
+
Run with: ``pytest bench/test_equivalence_perf.py -v``
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logic_asts.ltl as ltl
|
|
17
|
+
from pytest_benchmark.fixture import BenchmarkFixture as Benchmark
|
|
18
|
+
|
|
19
|
+
from morphata.alphabet import powerset_alphabet
|
|
20
|
+
from morphata.automaton import Automaton
|
|
21
|
+
from morphata.logic.ltl import Input, ltl_to_automaton
|
|
22
|
+
from morphata.symbolic import isomorphic
|
|
23
|
+
from morphata.symbolic.automaton import SymbolicAutomaton
|
|
24
|
+
from morphata.symbolic.equivalence import language_equivalent
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _sparse_big_alphabet_dfa() -> SymbolicAutomaton[str]:
|
|
28
|
+
aps = frozenset({f"p{i}" for i in range(15)} | {"a", "b", "c", "d"})
|
|
29
|
+
formula = ltl.And(
|
|
30
|
+
(
|
|
31
|
+
ltl.Eventually(ltl.Variable("a")),
|
|
32
|
+
ltl.Eventually(ltl.Variable("b")),
|
|
33
|
+
ltl.Eventually(ltl.Variable("c")),
|
|
34
|
+
ltl.Eventually(ltl.Variable("d")),
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
alphabet = powerset_alphabet(aps)
|
|
38
|
+
aut: Automaton[int, Input[str], str] = ltl_to_automaton(formula, finite=True)
|
|
39
|
+
return SymbolicAutomaton.from_automaton(aut, alphabet=alphabet).to_min_dfa()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_language_equivalent_sparse_big_alphabet(benchmark: Benchmark) -> None:
|
|
43
|
+
dfa = _sparse_big_alphabet_dfa()
|
|
44
|
+
|
|
45
|
+
def _run() -> bool:
|
|
46
|
+
return language_equivalent(dfa, dfa)
|
|
47
|
+
|
|
48
|
+
assert benchmark(_run)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_isomorphic_sparse_big_alphabet(benchmark: Benchmark) -> None:
|
|
52
|
+
dfa = _sparse_big_alphabet_dfa()
|
|
53
|
+
|
|
54
|
+
def _run() -> bool:
|
|
55
|
+
return isomorphic(dfa, dfa)
|
|
56
|
+
|
|
57
|
+
assert benchmark(_run)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Micro-benchmarks for :func:`morphata.symbolic.successors_for_alphabet`.
|
|
2
|
+
|
|
3
|
+
Two cases:
|
|
4
|
+
|
|
5
|
+
- Wide-cofactor: source = initial state of the min-DFA for
|
|
6
|
+
``F a & F b & F c & F d``; many outgoing rows, ~1-4 APs in support
|
|
7
|
+
per row. Exercises the support-projection win.
|
|
8
|
+
- Narrow-cofactor: source = a deep-BFS state of the same DFA after
|
|
9
|
+
most eventualities discharged; few rows, narrow support. Exercises
|
|
10
|
+
non-regression vs. the pre-rewrite ``T_src`` shape.
|
|
11
|
+
|
|
12
|
+
Run with: ``pytest bench/test_successors_for_alphabet_perf.py -v``
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
import logic_asts.ltl as ltl
|
|
20
|
+
from pytest_benchmark.fixture import BenchmarkFixture as Benchmark
|
|
21
|
+
|
|
22
|
+
from morphata.alphabet import BooleanAlphabet, finite_alphabet
|
|
23
|
+
from morphata.symbolic import SymbolicAutomaton, successors_for_alphabet
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _build_min_dfa() -> SymbolicAutomaton[str]:
|
|
27
|
+
formula = ltl.And(
|
|
28
|
+
(
|
|
29
|
+
ltl.Eventually(ltl.Variable("a")),
|
|
30
|
+
ltl.Eventually(ltl.Variable("b")),
|
|
31
|
+
ltl.Eventually(ltl.Variable("c")),
|
|
32
|
+
ltl.Eventually(ltl.Variable("d")),
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
alphabet = finite_alphabet(frozenset({"a", "b", "c", "d"}))
|
|
36
|
+
sym = SymbolicAutomaton.from_ltl(formula, alphabet=alphabet, finite=True)
|
|
37
|
+
return sym.to_min_dfa()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _wide_alphabet_with_overlap() -> BooleanAlphabet[str, str]:
|
|
41
|
+
"""26-symbol alphabet over AP universe {a, b, c, d}.
|
|
42
|
+
|
|
43
|
+
sym_0..sym_3 carry the AP truths for a/b/c/d (one each); sym_4..sym_25
|
|
44
|
+
carry no AP truth. Forces ``successors_for_alphabet`` to iterate over
|
|
45
|
+
a moderate-size symbol list with non-trivial classes per row.
|
|
46
|
+
"""
|
|
47
|
+
aps = frozenset({"a", "b", "c", "d"})
|
|
48
|
+
labels: dict[str, frozenset[str]] = {f"sym_{i}": frozenset() for i in range(26)}
|
|
49
|
+
labels["sym_0"] = frozenset({"a"})
|
|
50
|
+
labels["sym_1"] = frozenset({"b"})
|
|
51
|
+
labels["sym_2"] = frozenset({"c"})
|
|
52
|
+
labels["sym_3"] = frozenset({"d"})
|
|
53
|
+
return BooleanAlphabet(predicates=aps, labeling=lambda s: labels[s])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _initial_index(sym: SymbolicAutomaton[str]) -> int:
|
|
57
|
+
for k in range(sym.num_states):
|
|
58
|
+
if sym.initial == sym.manager.var(f"q{k}"):
|
|
59
|
+
return k
|
|
60
|
+
raise AssertionError("min-DFA initial must be a single state variable")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_wide_cofactor_state_runs_quickly() -> None:
|
|
64
|
+
"""Initial state of F a & F b & F c & F d: many outgoing rows, wide support."""
|
|
65
|
+
sym = _build_min_dfa()
|
|
66
|
+
alphabet = _wide_alphabet_with_overlap()
|
|
67
|
+
symbols = [f"sym_{i}" for i in range(26)]
|
|
68
|
+
initial = _initial_index(sym)
|
|
69
|
+
|
|
70
|
+
t0 = time.perf_counter()
|
|
71
|
+
pairs = list(successors_for_alphabet(sym, initial, alphabet, symbols))
|
|
72
|
+
elapsed = time.perf_counter() - t0
|
|
73
|
+
|
|
74
|
+
assert pairs, "expected at least one successor for the initial state"
|
|
75
|
+
# Generous bound; rewrite runs in well under 1s on this case.
|
|
76
|
+
assert elapsed < 1.0, f"successors_for_alphabet on initial took {elapsed:.3f}s"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_narrow_cofactor_state_runs_quickly() -> None:
|
|
80
|
+
"""A deep-BFS state with few rows must not regress vs. the old T_src path."""
|
|
81
|
+
sym = _build_min_dfa()
|
|
82
|
+
alphabet = _wide_alphabet_with_overlap()
|
|
83
|
+
symbols = [f"sym_{i}" for i in range(26)]
|
|
84
|
+
|
|
85
|
+
# Walk a few steps from the initial state to discharge a/b/c.
|
|
86
|
+
cur = sym.initial
|
|
87
|
+
for s in ("sym_0", "sym_1", "sym_2"):
|
|
88
|
+
cur = sym.step(cur, s)
|
|
89
|
+
deep_index = -1
|
|
90
|
+
for k in range(sym.num_states):
|
|
91
|
+
if cur == sym.manager.var(f"q{k}"):
|
|
92
|
+
deep_index = k
|
|
93
|
+
break
|
|
94
|
+
assert deep_index != -1, "expected to land on a single state after three steps"
|
|
95
|
+
|
|
96
|
+
t0 = time.perf_counter()
|
|
97
|
+
pairs = list(successors_for_alphabet(sym, deep_index, alphabet, symbols))
|
|
98
|
+
elapsed = time.perf_counter() - t0
|
|
99
|
+
|
|
100
|
+
assert pairs, "expected at least one successor"
|
|
101
|
+
assert elapsed < 0.5, f"deep-state enumeration took {elapsed:.3f}s"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_q1_post_discharge_state_per_call_cost(benchmark: Benchmark) -> None:
|
|
105
|
+
"""Per-call cost of successors_for_alphabet at the 16-row / sup-4 state.
|
|
106
|
+
|
|
107
|
+
q1 has 16 outgoing rows that all share support {a, b, c, d}. With
|
|
108
|
+
per-bucket amortization the symbol partition and per-class point
|
|
109
|
+
dicts are built once for the bucket and shared across the 16
|
|
110
|
+
rows; the per-row cost reduces to |classes| cofactor calls.
|
|
111
|
+
pytest-benchmark records the timing distribution -- no hard-coded
|
|
112
|
+
threshold here, the dataset is the regression signal.
|
|
113
|
+
"""
|
|
114
|
+
sym = _build_min_dfa()
|
|
115
|
+
alphabet = _wide_alphabet_with_overlap()
|
|
116
|
+
symbols = [f"sym_{i}" for i in range(26)]
|
|
117
|
+
|
|
118
|
+
q1 = next(k for k in range(sym.num_states) if len(sym.transitions[k]) == 16)
|
|
119
|
+
|
|
120
|
+
def _run() -> int:
|
|
121
|
+
return sum(1 for _ in successors_for_alphabet(sym, q1, alphabet, symbols))
|
|
122
|
+
|
|
123
|
+
n_yielded = benchmark(_run)
|
|
124
|
+
assert n_yielded > 0
|
|
@@ -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.8"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
classifiers = [
|
|
8
8
|
"Intended Audience :: Science/Research",
|
|
@@ -38,7 +38,9 @@ from morphata.symbolic.bdd import guard_to_bdd as guard_to_bdd
|
|
|
38
38
|
from morphata.symbolic.bdd import make_ap_registry as make_ap_registry
|
|
39
39
|
from morphata.symbolic.bdd import make_manager as make_manager
|
|
40
40
|
from morphata.symbolic.bdd import state_var_name as state_var_name
|
|
41
|
+
from morphata.symbolic.compose import CompositionMap as CompositionMap
|
|
41
42
|
from morphata.symbolic.compose import compose_min_dfa as compose_min_dfa
|
|
43
|
+
from morphata.symbolic.compose import iter_composed_transitions as iter_composed_transitions
|
|
42
44
|
from morphata.symbolic.compose import min_dfa_of_conjunction as min_dfa_of_conjunction
|
|
43
45
|
from morphata.symbolic.equivalence import canonical_form as canonical_form
|
|
44
46
|
from morphata.symbolic.equivalence import isomorphic as isomorphic
|
|
@@ -32,6 +32,7 @@ see :mod:`morphata.symbolic.transforms`.
|
|
|
32
32
|
from __future__ import annotations
|
|
33
33
|
|
|
34
34
|
import typing
|
|
35
|
+
from collections import defaultdict
|
|
35
36
|
from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence
|
|
36
37
|
from collections.abc import Set as AbstractSet
|
|
37
38
|
from dataclasses import dataclass, field
|
|
@@ -88,6 +89,10 @@ class SymbolicAutomaton(Generic[AP]):
|
|
|
88
89
|
variables. Both live in :attr:`manager`.
|
|
89
90
|
ap_registry : APRegistry[AP]
|
|
90
91
|
Maps each AP value to its BDD variable name in :attr:`manager`.
|
|
92
|
+
ap_inverse : Mapping[str, AP]
|
|
93
|
+
Derived inverse of :attr:`ap_registry`. Maps each manager
|
|
94
|
+
variable name back to its AP. Excludes state variables.
|
|
95
|
+
Populated in ``__post_init__``; not serialised.
|
|
91
96
|
accepting_states : frozenset[int]
|
|
92
97
|
Indices of accepting states. Populated when the acceptance condition
|
|
93
98
|
is :class:`~morphata.acceptance.Finite`; empty otherwise.
|
|
@@ -111,6 +116,12 @@ class SymbolicAutomaton(Generic[AP]):
|
|
|
111
116
|
num_states: int
|
|
112
117
|
acceptance: AcceptanceCondition[int] = field()
|
|
113
118
|
alphabet: "BooleanAlphabet[Any, AP]" = field(kw_only=True)
|
|
119
|
+
ap_inverse: Mapping[str, AP] = field(init=False)
|
|
120
|
+
|
|
121
|
+
def __post_init__(self) -> None:
|
|
122
|
+
# Derived inverse of ``ap_registry``. Manager-var-name -> AP.
|
|
123
|
+
# Excludes state vars (they are not in ``ap_registry``).
|
|
124
|
+
object.__setattr__(self, "ap_inverse", {var: ap for ap, var in self.ap_registry.items()})
|
|
114
125
|
|
|
115
126
|
# ------------------------------------------------------------------
|
|
116
127
|
# Symbolic primitives
|
|
@@ -193,7 +204,7 @@ class SymbolicAutomaton(Generic[AP]):
|
|
|
193
204
|
"reconstruct SymbolicAutomaton with cache_transitions=True."
|
|
194
205
|
)
|
|
195
206
|
|
|
196
|
-
ap_decoder_map:
|
|
207
|
+
ap_decoder_map: Mapping[str, AP] = self.ap_inverse
|
|
197
208
|
|
|
198
209
|
def _ap_decode(name: str) -> AP:
|
|
199
210
|
try:
|
|
@@ -449,17 +460,62 @@ def successors_for_alphabet(
|
|
|
449
460
|
mgr = sym_aut.manager
|
|
450
461
|
rows = sym_aut.transitions[source]
|
|
451
462
|
ap_registry = sym_aut.ap_registry
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
+
ap_inverse = sym_aut.ap_inverse
|
|
464
|
+
|
|
465
|
+
# Materialise the symbol list and precompute AP-truth sets once.
|
|
466
|
+
symbols_list = list(symbols)
|
|
467
|
+
n = len(symbols_list)
|
|
468
|
+
truths_list: list[AbstractSet[AP]] = [alphabet.labeling(s) for s in symbols_list]
|
|
469
|
+
accumulators: list[Function] = [mgr.false] * n
|
|
470
|
+
|
|
471
|
+
# Bucket rows by their support shape. Rows in the same bucket
|
|
472
|
+
# share the partition over input symbols and the per-class
|
|
473
|
+
# ``point`` dict; only the cofactor target ``guard_bdd`` varies.
|
|
474
|
+
buckets: dict[frozenset[str], list[tuple[Function, Function]]] = defaultdict(list)
|
|
475
|
+
for guard_bdd, succ_bdd in rows:
|
|
476
|
+
sup_key = frozenset(guard_bdd.support)
|
|
477
|
+
buckets[sup_key].append((guard_bdd, succ_bdd))
|
|
478
|
+
|
|
479
|
+
for sup, group_rows in buckets.items():
|
|
480
|
+
if not sup:
|
|
481
|
+
# Constant-guard bucket: no cofactor needed.
|
|
482
|
+
for guard_bdd, succ_bdd in group_rows:
|
|
483
|
+
if guard_bdd == mgr.true:
|
|
484
|
+
for i in range(n):
|
|
485
|
+
accumulators[i] = mgr.apply("or", accumulators[i], succ_bdd)
|
|
486
|
+
# ``mgr.false`` rows contribute nothing.
|
|
487
|
+
continue
|
|
488
|
+
|
|
489
|
+
# Translate manager-var-names in ``sup`` back to AP labels.
|
|
490
|
+
sup_aps: list[AP] = [ap_inverse[name] for name in sup]
|
|
491
|
+
|
|
492
|
+
# Partition symbol indices by their projection onto ``sup_aps``.
|
|
493
|
+
classes: dict[frozenset[AP], list[int]] = defaultdict(list)
|
|
494
|
+
for i, truths in enumerate(truths_list):
|
|
495
|
+
proj = frozenset(ap for ap in sup_aps if ap in truths)
|
|
496
|
+
classes[proj].append(i)
|
|
497
|
+
|
|
498
|
+
# Per-class ``point`` dicts are determined by ``sup_aps`` and
|
|
499
|
+
# the projection; hoist out of the row loop. Materialise the
|
|
500
|
+
# parallel ``(point, indices)`` list once so the per-row inner
|
|
501
|
+
# loop is a tight zip with no dict lookups.
|
|
502
|
+
class_points_indices: list[tuple[dict[str, Function], list[int]]] = [
|
|
503
|
+
(
|
|
504
|
+
{ap_registry[ap]: (mgr.true if ap in proj else mgr.false) for ap in sup_aps},
|
|
505
|
+
indices,
|
|
506
|
+
)
|
|
507
|
+
for proj, indices in classes.items()
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
for guard_bdd, succ_bdd in group_rows:
|
|
511
|
+
for point, indices in class_points_indices:
|
|
512
|
+
if mgr.let(point, guard_bdd) == mgr.true:
|
|
513
|
+
for i in indices:
|
|
514
|
+
accumulators[i] = mgr.apply("or", accumulators[i], succ_bdd)
|
|
515
|
+
|
|
516
|
+
for i in range(n):
|
|
517
|
+
if accumulators[i] != mgr.false:
|
|
518
|
+
yield symbols_list[i], accumulators[i]
|
|
463
519
|
|
|
464
520
|
|
|
465
521
|
# ---------------------------------------------------------------------------
|