morphata 2.0.9__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.9 → 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.9 → morphata-2.0.10}/CHANGELOG.md +14 -0
- {morphata-2.0.9 → morphata-2.0.10}/PKG-INFO +1 -1
- {morphata-2.0.9 → morphata-2.0.10}/bench/README.md +5 -0
- morphata-2.0.10/bench/test_hoa_parser_perf.py +114 -0
- {morphata-2.0.9 → morphata-2.0.10}/mypy.ini +10 -0
- {morphata-2.0.9 → morphata-2.0.10}/pyproject.toml +1 -1
- {morphata-2.0.9 → morphata-2.0.10}/pyrightconfig.json +1 -1
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/hoa/hoa.lark +13 -7
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/hoa/parser.py +2 -1
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/logic/ltl.py +2 -138
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/logic/psl.py +2 -2
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/logic/sere.py +3 -2
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/symbolic/__init__.py +1 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/symbolic/automaton.py +8 -3
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/symbolic/bdd.py +104 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/conftest.py +1 -1
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_persistence.py +76 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_symbolic_automaton.py +36 -0
- {morphata-2.0.9 → morphata-2.0.10}/uv.lock +540 -471
- {morphata-2.0.9 → morphata-2.0.10}/.basedpyright/baseline.json +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/.dockerignore +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/.editorconfig +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/.envrc +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/.envrc.recommended +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/.fdignore +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/.gitattributes +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/.gitignore +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/.mailmap +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/.python-version +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/.rstcheck.cfg +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/AGENTS.md +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/CLAUDE.md +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/CONTRIBUTING.md +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/LICENSE +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/README.md +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/bench/__init__.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/bench/conftest.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/bench/test_equivalence_perf.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/bench/test_hoa_exporter_perf.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/bench/test_sere_nlm_perf.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/bench/test_successors_for_alphabet_perf.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/ci/make-release +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/cliff.toml +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/_static/navigation.html +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/api/modules.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/api/morphata.acceptance.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/api/morphata.alphabet.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/api/morphata.automaton.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/api/morphata.builder.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/api/morphata.hoa.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/api/morphata.logic.ltl.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/api/morphata.logic.psl.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/api/morphata.logic.sere.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/api/morphata.logic.strel.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/api/morphata.operators.matrix.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/api/morphata.operators.polynomial.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/api/morphata.operators.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/api/morphata.symbolic.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/concepts/acceptance.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/concepts/hoa.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/concepts/index.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/concepts/operators.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/concepts/spec.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/concepts/weights.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/conf.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/index.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/quick-start.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/docs/symbolic-transforms.rst +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/justfile +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/pixi.lock +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/pixi.toml +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/project.utf-8.add +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/project.utf-8.add.spl +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/pyrefly.toml +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/pytest.toml +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/ruff.toml +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/__init__.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/acceptance.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/alphabet.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/automaton.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/builder.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/hoa/__init__.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/hoa/__main__.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/hoa/acc_expr.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/hoa/exporter.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/logic/__init__.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/logic/_common.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/logic/strel.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/operators/__init__.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/operators/_backend.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/operators/matrix.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/operators/polynomial.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/py.typed +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/symbolic/compose.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/symbolic/equivalence.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/symbolic/persistence.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/src/morphata/symbolic/transforms.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/aut1.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/aut11.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/aut2.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/aut3.2.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/aut3.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/aut4.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/aut5.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/aut6.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/aut7.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/aut8.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/finite_aut.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/gen_buchi_3.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/gen_cobuchi_2.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/rabin_2pair.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/streett_2pair.hoa +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/test_exporter_basic.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/test_exporter_roundtrip.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/test_exporter_streaming.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/hoa/test_parser.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/logic/__init__.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/logic/test_ltl.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/logic/test_psl.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/logic/test_sere.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/logic/test_sere_properties.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/logic/test_strel.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/operators/test_matrix_from_ir.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/operators/test_matrix_from_ltl.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/operators/test_polynomial_from_ir.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_alphabet_aware_transitions.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_bdd_helpers.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_compose.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_composition_map.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_equivalence_naive_property.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_from_hoaf.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_ltlf2dfa_equivalence.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_step.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_symbolic_from_ir.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_to_hoa.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_transforms.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_transforms_ir.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/symbolic/test_transforms_properties.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/test_alphabet.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/test_automaton.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/test_builder.py +0 -0
- {morphata-2.0.9 → morphata-2.0.10}/tests/test_from_parsed.py +0 -0
- {morphata-2.0.9 → 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,19 @@
|
|
|
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
|
+
|
|
3
17
|
## 2.0.9 — 2026-06-04
|
|
4
18
|
|
|
5
19
|
### Fixed
|
|
@@ -20,6 +20,11 @@ To get pytest-benchmark's tabular wall-time output:
|
|
|
20
20
|
wall time across regimes that stress the per-row guard/successor
|
|
21
21
|
caching (small DFA, sparse-big-alphabet DFA, conjunction min-DFA, raw
|
|
22
22
|
AFA without minimization).
|
|
23
|
+
- `test_hoa_parser_perf.py` -- ``hoa.parser.parse`` (parse + transform)
|
|
24
|
+
across the sample corpus, parametrized over ``earley`` vs ``lalr`` from
|
|
25
|
+
the same grammar to quantify the LALR switch. Run with
|
|
26
|
+
``--benchmark-group-by=param:case`` to put the two algorithms side by
|
|
27
|
+
side per input.
|
|
23
28
|
|
|
24
29
|
## Recording results
|
|
25
30
|
|
|
@@ -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",
|
|
@@ -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: "!"
|
|
@@ -29,6 +29,7 @@ from morphata.symbolic.bdd import (
|
|
|
29
29
|
Function,
|
|
30
30
|
Manager,
|
|
31
31
|
bdd_to_boolexpr,
|
|
32
|
+
bdd_to_positive_boolexpr,
|
|
32
33
|
declare_state_vars,
|
|
33
34
|
mgr_partition_ap_by_state_residual,
|
|
34
35
|
state_var_name,
|
|
@@ -117,7 +118,7 @@ def ltl_to_automaton(
|
|
|
117
118
|
if state_residual == mgr.false:
|
|
118
119
|
# No transition fires under this AP-cube; omit the row.
|
|
119
120
|
continue
|
|
120
|
-
succ_expr =
|
|
121
|
+
succ_expr = ty.cast("BoolExpr[int]", bdd_to_positive_boolexpr(state_residual))
|
|
121
122
|
guard_expr = ty.cast("BoolExpr[AP]", bdd_to_boolexpr(ap_cube, var_decoder=ap_guard_decoder))
|
|
122
123
|
row_list.append((guard_expr, succ_expr))
|
|
123
124
|
rows[sid] = row_list
|
|
@@ -148,143 +149,6 @@ def ltl_to_automaton(
|
|
|
148
149
|
)
|
|
149
150
|
|
|
150
151
|
|
|
151
|
-
def _state_bdd_to_positive_boolexpr(node: Function, mgr: Manager) -> BoolExpr[int]:
|
|
152
|
-
"""Convert a monotone-positive BDD over state vars ``q{i}`` to a positive
|
|
153
|
-
:class:`BoolExpr[int]` (no ``Not`` over state variables).
|
|
154
|
-
|
|
155
|
-
Why a dedicated converter
|
|
156
|
-
-------------------------
|
|
157
|
-
The downstream :func:`morphata.symbolic.bdd.boolexpr_to_bdd` rejects
|
|
158
|
-
``Not`` over state variables on purpose: the AFA-to-NFA dealternation
|
|
159
|
-
pipeline (``mgr_minimal_pos_satisfying_subsets``,
|
|
160
|
-
``mgr_partition_ap_by_state_residual``, etc.) relies on every
|
|
161
|
-
successor BDD being monotone-positive in the state vars. The generic
|
|
162
|
-
:func:`bdd_to_boolexpr` emits the symmetric Shannon form
|
|
163
|
-
``(v & high) | (~v & low)`` at the "neither short-circuits" arm,
|
|
164
|
-
which would slip a ``~q_i`` into the row's successor expression and
|
|
165
|
-
fail at re-BDD time even though the underlying function is positive.
|
|
166
|
-
|
|
167
|
-
Algorithm
|
|
168
|
-
---------
|
|
169
|
-
Memoised post-order walk over the BDD nodes. At each non-terminal
|
|
170
|
-
node with variable ``v`` and children ``low = walk(n.low)``,
|
|
171
|
-
``high = walk(n.high)``::
|
|
172
|
-
|
|
173
|
-
if low == high: built = low
|
|
174
|
-
elif low_is_false and high_is_true: built = var
|
|
175
|
-
elif low_is_false: built = var & high
|
|
176
|
-
elif high_is_true: built = var | low
|
|
177
|
-
else: built = (var & high) | low
|
|
178
|
-
|
|
179
|
-
Terminals decode to ``Literal(True)`` / ``Literal(False)``; the
|
|
180
|
-
equality test ``n == mgr.true`` already accounts for the complement
|
|
181
|
-
bit on terminal edges.
|
|
182
|
-
|
|
183
|
-
Correctness of ``(v & high) | low`` (the general arm)
|
|
184
|
-
-----------------------------------------------------
|
|
185
|
-
The standard Shannon expansion is::
|
|
186
|
-
|
|
187
|
-
f = (v & high) | (~v & low)
|
|
188
|
-
|
|
189
|
-
For monotone-positive ``f`` the cofactors satisfy ``low <= high``
|
|
190
|
-
(i.e. ``low ==> high`` -- the set of satisfying assignments of
|
|
191
|
-
``low`` is contained in that of ``high``). Therefore
|
|
192
|
-
``low == low & high`` and::
|
|
193
|
-
|
|
194
|
-
(v & high) | low
|
|
195
|
-
= (v & high) | (low & high)
|
|
196
|
-
= high & (v | low)
|
|
197
|
-
= ((v | low) & high)
|
|
198
|
-
|
|
199
|
-
Checking against the standard form:
|
|
200
|
-
|
|
201
|
-
* ``v = True``: standard gives ``high``; simplified gives
|
|
202
|
-
``True & high | low = high | low = high`` (since ``low <= high``).
|
|
203
|
-
* ``v = False``: standard gives ``low``; simplified gives
|
|
204
|
-
``False & high | low = low``.
|
|
205
|
-
|
|
206
|
-
Both match, so the simplified form is equivalent to the standard
|
|
207
|
-
Shannon expansion at every node, and it never mentions ``~v``.
|
|
208
|
-
|
|
209
|
-
Where the monotone-positive invariant comes from
|
|
210
|
-
------------------------------------------------
|
|
211
|
-
The polynomial compiled into ``node`` by
|
|
212
|
-
:func:`_polynomial_to_bdd` is built from
|
|
213
|
-
:func:`ltl_symbolic_expansion`, which only ever emits state atoms
|
|
214
|
-
via ``logic.Variable(("state", obj))`` -- never with a wrapping
|
|
215
|
-
``Not``. So the polynomial is positive in state vars by
|
|
216
|
-
construction, and cofactoring on AP variables (which is what
|
|
217
|
-
:func:`mgr_partition_ap_by_state_residual` does) cannot introduce
|
|
218
|
-
a negation on a state var. Every state-residual BDD reaching this
|
|
219
|
-
function therefore satisfies the contract.
|
|
220
|
-
|
|
221
|
-
Guard rails
|
|
222
|
-
-----------
|
|
223
|
-
Two ``elif`` arms cover BDD shapes that cannot occur for a
|
|
224
|
-
monotone-positive function (``high == false`` with ``low != false``,
|
|
225
|
-
or ``low == true`` with ``high != true``) and raise instead of
|
|
226
|
-
silently producing an incorrect Shannon form. A non-terminal node
|
|
227
|
-
whose top edge is complemented (``Function.negated``) would flip the
|
|
228
|
-
function's value to its negation -- also incompatible with
|
|
229
|
-
monotone-positivity -- and likewise raises.
|
|
230
|
-
|
|
231
|
-
Raises
|
|
232
|
-
------
|
|
233
|
-
ValueError
|
|
234
|
-
If ``node`` references a non-state variable, has a complement
|
|
235
|
-
edge on a non-terminal, or otherwise fails the
|
|
236
|
-
monotone-positive contract.
|
|
237
|
-
"""
|
|
238
|
-
cache: dict[Function, BoolExpr[int]] = {}
|
|
239
|
-
|
|
240
|
-
def walk(n: Function) -> BoolExpr[int]:
|
|
241
|
-
if n in cache:
|
|
242
|
-
return cache[n]
|
|
243
|
-
if n.var is None:
|
|
244
|
-
result: BoolExpr[int] = logic.Literal(n == mgr.true)
|
|
245
|
-
cache[n] = result
|
|
246
|
-
return result
|
|
247
|
-
if not n.var.startswith("q"):
|
|
248
|
-
raise ValueError(f"expected state variable, got {n.var!r}")
|
|
249
|
-
if getattr(n, "negated", False):
|
|
250
|
-
# A complemented top-edge on a non-terminal would make this
|
|
251
|
-
# subtree non-monotone in state vars, which contradicts the
|
|
252
|
-
# caller's contract.
|
|
253
|
-
raise ValueError("state residual BDD has complement edge; not monotone-positive")
|
|
254
|
-
idx = int(n.var[1:])
|
|
255
|
-
var: BoolExpr[int] = logic.Variable(idx)
|
|
256
|
-
assert n.low is not None and n.high is not None
|
|
257
|
-
low = walk(n.low)
|
|
258
|
-
high = walk(n.high)
|
|
259
|
-
low_is_false = isinstance(low, logic.Literal) and low.value is False
|
|
260
|
-
low_is_true = isinstance(low, logic.Literal) and low.value is True
|
|
261
|
-
high_is_false = isinstance(high, logic.Literal) and high.value is False
|
|
262
|
-
high_is_true = isinstance(high, logic.Literal) and high.value is True
|
|
263
|
-
|
|
264
|
-
if low == high:
|
|
265
|
-
built: BoolExpr[int] = low
|
|
266
|
-
elif high_is_false:
|
|
267
|
-
# Monotone-positive plus high=false forces low=false (low<=high),
|
|
268
|
-
# already handled by low==high above.
|
|
269
|
-
raise ValueError("non-monotone BDD: high=false but low!=false")
|
|
270
|
-
elif low_is_true:
|
|
271
|
-
# Same: low=true forces high=true; handled by low==high.
|
|
272
|
-
raise ValueError("non-monotone BDD: low=true but high!=true")
|
|
273
|
-
elif low_is_false and high_is_true:
|
|
274
|
-
built = var
|
|
275
|
-
elif low_is_false:
|
|
276
|
-
built = logic.And((var, high))
|
|
277
|
-
elif high_is_true:
|
|
278
|
-
built = logic.Or((var, low))
|
|
279
|
-
else:
|
|
280
|
-
# General positive form: (var & high) | low, valid because low <= high.
|
|
281
|
-
built = logic.Or((logic.And((var, high)), low))
|
|
282
|
-
cache[n] = built
|
|
283
|
-
return built
|
|
284
|
-
|
|
285
|
-
return walk(node)
|
|
286
|
-
|
|
287
|
-
|
|
288
152
|
def _polynomial_to_bdd(
|
|
289
153
|
mgr: Manager,
|
|
290
154
|
poly: BoolExpr[tuple[str, Hashable]],
|
|
@@ -25,7 +25,6 @@ from morphata.logic._common import dualize_polynomial as common_dualize_polynomi
|
|
|
25
25
|
from morphata.logic._common import make_logic_context
|
|
26
26
|
from morphata.logic.ltl import (
|
|
27
27
|
_polynomial_to_bdd,
|
|
28
|
-
_state_bdd_to_positive_boolexpr,
|
|
29
28
|
ltl_empty_trace_value,
|
|
30
29
|
ltl_symbolic_expansion,
|
|
31
30
|
)
|
|
@@ -41,6 +40,7 @@ from morphata.logic.sere import (
|
|
|
41
40
|
from morphata.symbolic.bdd import (
|
|
42
41
|
Manager,
|
|
43
42
|
bdd_to_boolexpr,
|
|
43
|
+
bdd_to_positive_boolexpr,
|
|
44
44
|
declare_state_vars,
|
|
45
45
|
mgr_partition_ap_by_state_residual,
|
|
46
46
|
)
|
|
@@ -339,7 +339,7 @@ def psl_to_automaton(
|
|
|
339
339
|
for ap_cube, state_residual in mgr_partition_ap_by_state_residual(mgr, bdd_poly, ap_var_names):
|
|
340
340
|
if state_residual == mgr.false:
|
|
341
341
|
continue
|
|
342
|
-
succ_expr =
|
|
342
|
+
succ_expr = ty.cast("BoolExpr[int]", bdd_to_positive_boolexpr(state_residual))
|
|
343
343
|
guard_expr = ty.cast(
|
|
344
344
|
"BoolExpr[AP]",
|
|
345
345
|
bdd_to_boolexpr(ap_cube, var_decoder=ap_guard_decoder),
|
|
@@ -32,10 +32,11 @@ from morphata.acceptance import Finite
|
|
|
32
32
|
from morphata.automaton import Automaton, Outgoing
|
|
33
33
|
from morphata.logic._common import dualize_polynomial as _common_dualize_polynomial
|
|
34
34
|
from morphata.logic._common import make_logic_context
|
|
35
|
-
from morphata.logic.ltl import _polynomial_to_bdd
|
|
35
|
+
from morphata.logic.ltl import _polynomial_to_bdd
|
|
36
36
|
from morphata.symbolic.bdd import (
|
|
37
37
|
Manager,
|
|
38
38
|
bdd_to_boolexpr,
|
|
39
|
+
bdd_to_positive_boolexpr,
|
|
39
40
|
declare_state_vars,
|
|
40
41
|
mgr_partition_ap_by_state_residual,
|
|
41
42
|
)
|
|
@@ -485,7 +486,7 @@ def sere_to_automaton(
|
|
|
485
486
|
for ap_cube, state_residual in mgr_partition_ap_by_state_residual(mgr, bdd_poly, ap_var_names):
|
|
486
487
|
if state_residual == mgr.false:
|
|
487
488
|
continue
|
|
488
|
-
succ_expr =
|
|
489
|
+
succ_expr = ty.cast("BoolExpr[int]", bdd_to_positive_boolexpr(state_residual))
|
|
489
490
|
guard_expr = ty.cast(
|
|
490
491
|
"BoolExpr[AP]",
|
|
491
492
|
bdd_to_boolexpr(ap_cube, var_decoder=ap_guard_decoder),
|
|
@@ -31,6 +31,7 @@ from morphata.symbolic.bdd import APRegistry as APRegistry
|
|
|
31
31
|
from morphata.symbolic.bdd import Function as Function
|
|
32
32
|
from morphata.symbolic.bdd import Manager as Manager
|
|
33
33
|
from morphata.symbolic.bdd import bdd_to_boolexpr as bdd_to_boolexpr
|
|
34
|
+
from morphata.symbolic.bdd import bdd_to_positive_boolexpr as bdd_to_positive_boolexpr
|
|
34
35
|
from morphata.symbolic.bdd import boolexpr_to_bdd as boolexpr_to_bdd
|
|
35
36
|
from morphata.symbolic.bdd import declare_ap_vars as declare_ap_vars
|
|
36
37
|
from morphata.symbolic.bdd import declare_state_vars as declare_state_vars
|
|
@@ -196,7 +196,7 @@ class SymbolicAutomaton(Generic[AP]):
|
|
|
196
196
|
and there are no rows to serialize.
|
|
197
197
|
"""
|
|
198
198
|
from morphata.automaton import Outgoing
|
|
199
|
-
from morphata.symbolic.bdd import bdd_to_boolexpr
|
|
199
|
+
from morphata.symbolic.bdd import bdd_to_boolexpr, bdd_to_positive_boolexpr
|
|
200
200
|
|
|
201
201
|
if self.num_states > 0 and not self.transitions:
|
|
202
202
|
raise ValueError(
|
|
@@ -220,7 +220,10 @@ class SymbolicAutomaton(Generic[AP]):
|
|
|
220
220
|
"BoolExpr[AP]",
|
|
221
221
|
bdd_to_boolexpr(guard_bdd, var_decoder=_ap_decode),
|
|
222
222
|
)
|
|
223
|
-
|
|
223
|
+
# Successor BDDs are monotone-positive over the state vars;
|
|
224
|
+
# decode them so the result stays in positive normal form
|
|
225
|
+
# (PolynomialOperator rejects Not over state variables).
|
|
226
|
+
succ_expr = typing.cast("BoolExpr[int]", bdd_to_positive_boolexpr(succ_bdd))
|
|
224
227
|
decoded_rows.append((guard_expr, succ_expr))
|
|
225
228
|
rows_by_state[s] = decoded_rows
|
|
226
229
|
|
|
@@ -230,7 +233,9 @@ class SymbolicAutomaton(Generic[AP]):
|
|
|
230
233
|
) -> Iterable[tuple[BoolExpr[AP], BoolExpr[int]]]:
|
|
231
234
|
return iter(_rows.get(s, []))
|
|
232
235
|
|
|
233
|
-
|
|
236
|
+
# The initial-state set is monotone-positive over the state vars; keep
|
|
237
|
+
# it in positive normal form for the same reason as successors above.
|
|
238
|
+
initial_expr = typing.cast("BoolExpr[int]", bdd_to_positive_boolexpr(self.initial))
|
|
234
239
|
|
|
235
240
|
return Automaton(
|
|
236
241
|
states=frozenset(range(self.num_states)),
|
|
@@ -53,6 +53,7 @@ __all__ = [
|
|
|
53
53
|
"Manager",
|
|
54
54
|
"STATE_VAR_FORMAT",
|
|
55
55
|
"bdd_to_boolexpr",
|
|
56
|
+
"bdd_to_positive_boolexpr",
|
|
56
57
|
"boolexpr_to_bdd",
|
|
57
58
|
"declare_ap_vars",
|
|
58
59
|
"declare_state_vars",
|
|
@@ -301,6 +302,109 @@ def bdd_to_boolexpr(
|
|
|
301
302
|
return walk(node)
|
|
302
303
|
|
|
303
304
|
|
|
305
|
+
def bdd_to_positive_boolexpr(
|
|
306
|
+
node: Function,
|
|
307
|
+
*,
|
|
308
|
+
var_decoder: Callable[[str], Hashable] | None = None,
|
|
309
|
+
) -> BoolExpr[Any]:
|
|
310
|
+
"""Decode a *monotone-positive* BDD to a :class:`BoolExpr` without ``Not``.
|
|
311
|
+
|
|
312
|
+
The dual of :func:`boolexpr_to_bdd`, which rejects ``Not`` on
|
|
313
|
+
construction. Use this -- not :func:`bdd_to_boolexpr` -- whenever the
|
|
314
|
+
BDD encodes a function that is monotone positive in its variables and
|
|
315
|
+
the decoded expression must stay in positive normal form: AFA
|
|
316
|
+
successor and initial-state expressions over the ``q{i}`` state
|
|
317
|
+
namespace are the canonical case (``PolynomialOperator`` rejects
|
|
318
|
+
``Not`` over state variables).
|
|
319
|
+
|
|
320
|
+
The generic :func:`bdd_to_boolexpr` emits the symmetric Shannon form
|
|
321
|
+
``(v & high) | (~v & low)`` at the arm where neither cofactor is
|
|
322
|
+
constant, slipping a ``~v`` into the output even when the underlying
|
|
323
|
+
function is positive. For monotone-positive ``f`` the cofactors satisfy
|
|
324
|
+
``low <= high``, so ``(v & high) | (~v & low) == (v & high) | low`` --
|
|
325
|
+
the same function with no negation. This decoder emits the latter.
|
|
326
|
+
|
|
327
|
+
Parameters
|
|
328
|
+
----------
|
|
329
|
+
node :
|
|
330
|
+
Live ``dd.Function`` node encoding a monotone-positive function.
|
|
331
|
+
var_decoder :
|
|
332
|
+
Optional mapping from BDD variable name to the user-level variable
|
|
333
|
+
identifier embedded in the returned ``BoolExpr``. When ``None``
|
|
334
|
+
(the default), variable names must follow :data:`STATE_VAR_FORMAT`
|
|
335
|
+
(``q{i}``) and decode to ``int`` indices.
|
|
336
|
+
|
|
337
|
+
Returns
|
|
338
|
+
-------
|
|
339
|
+
BoolExpr
|
|
340
|
+
Positive (``Not``-free) expression equal to ``node``. With the
|
|
341
|
+
default decoder the result is ``BoolExpr[int]`` over state indices.
|
|
342
|
+
|
|
343
|
+
Raises
|
|
344
|
+
------
|
|
345
|
+
ValueError
|
|
346
|
+
If ``node`` has a complement edge on a non-terminal, or otherwise
|
|
347
|
+
fails the monotone-positive contract (``high == false`` with
|
|
348
|
+
``low != false``, or ``low == true`` with ``high != true``). These
|
|
349
|
+
shapes cannot occur for a monotone-positive function and indicate
|
|
350
|
+
the input did not satisfy the precondition.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
def _default_decoder(name: str) -> int:
|
|
354
|
+
if not name.startswith("q"):
|
|
355
|
+
raise ValueError(f"Cannot convert BDD with non-state variable {name!r} to BoolExpr[int]")
|
|
356
|
+
return int(name[1:])
|
|
357
|
+
|
|
358
|
+
decode: Callable[[str], Hashable] = var_decoder if var_decoder is not None else _default_decoder
|
|
359
|
+
|
|
360
|
+
cache: dict[Function, BoolExpr[Any]] = {}
|
|
361
|
+
|
|
362
|
+
def walk(n: Function) -> BoolExpr[Any]:
|
|
363
|
+
if n in cache:
|
|
364
|
+
return cache[n]
|
|
365
|
+
if n.var is None:
|
|
366
|
+
mgr = n.bdd
|
|
367
|
+
result: BoolExpr[Any] = logic.Literal(n == mgr.true)
|
|
368
|
+
cache[n] = result
|
|
369
|
+
return result
|
|
370
|
+
if getattr(n, "negated", False):
|
|
371
|
+
# A complemented top-edge on a non-terminal flips the function to
|
|
372
|
+
# its negation, which cannot be monotone-positive.
|
|
373
|
+
raise ValueError("BDD has a complement edge on a non-terminal; not monotone-positive")
|
|
374
|
+
var: BoolExpr[Any] = logic.Variable(decode(n.var))
|
|
375
|
+
assert n.low is not None and n.high is not None
|
|
376
|
+
low = walk(n.low)
|
|
377
|
+
high = walk(n.high)
|
|
378
|
+
low_is_false = isinstance(low, logic.Literal) and low.value is False
|
|
379
|
+
low_is_true = isinstance(low, logic.Literal) and low.value is True
|
|
380
|
+
high_is_true = isinstance(high, logic.Literal) and high.value is True
|
|
381
|
+
high_is_false = isinstance(high, logic.Literal) and high.value is False
|
|
382
|
+
|
|
383
|
+
built: BoolExpr[Any]
|
|
384
|
+
if low == high:
|
|
385
|
+
built = low
|
|
386
|
+
elif high_is_false:
|
|
387
|
+
# Monotone-positive plus high=false forces low=false (low<=high),
|
|
388
|
+
# already handled by low==high above.
|
|
389
|
+
raise ValueError("non-monotone BDD: high=false but low!=false")
|
|
390
|
+
elif low_is_true:
|
|
391
|
+
# Same: low=true forces high=true; handled by low==high.
|
|
392
|
+
raise ValueError("non-monotone BDD: low=true but high!=true")
|
|
393
|
+
elif low_is_false and high_is_true:
|
|
394
|
+
built = var
|
|
395
|
+
elif low_is_false:
|
|
396
|
+
built = logic.And((var, high))
|
|
397
|
+
elif high_is_true:
|
|
398
|
+
built = logic.Or((var, low))
|
|
399
|
+
else:
|
|
400
|
+
# General positive form: (var & high) | low, valid because low <= high.
|
|
401
|
+
built = logic.Or((logic.And((var, high)), low))
|
|
402
|
+
cache[n] = built
|
|
403
|
+
return built
|
|
404
|
+
|
|
405
|
+
return walk(node)
|
|
406
|
+
|
|
407
|
+
|
|
304
408
|
def mgr_eval_subset(
|
|
305
409
|
mgr: Manager,
|
|
306
410
|
bdd: Function,
|