morphata 2.0.5__tar.gz → 2.0.7__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.5 → morphata-2.0.7}/CHANGELOG.md +20 -0
- {morphata-2.0.5 → morphata-2.0.7}/PKG-INFO +1 -1
- {morphata-2.0.5 → morphata-2.0.7}/pyproject.toml +1 -1
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/hoa/exporter.py +25 -12
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/symbolic/__init__.py +5 -1
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/symbolic/automaton.py +105 -1
- morphata-2.0.7/src/morphata/symbolic/persistence.py +346 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/test_exporter_streaming.py +48 -0
- morphata-2.0.7/tests/symbolic/test_alphabet_aware_transitions.py +140 -0
- morphata-2.0.7/tests/symbolic/test_persistence.py +175 -0
- {morphata-2.0.5 → morphata-2.0.7}/uv.lock +1 -1
- {morphata-2.0.5 → morphata-2.0.7}/.basedpyright/baseline.json +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/.claude/settings.local.json +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/.dockerignore +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/.editorconfig +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/.envrc +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/.envrc.recommended +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/.fdignore +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/.gitattributes +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/.gitignore +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/.mailmap +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/.python-version +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/.rstcheck.cfg +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/AGENTS.md +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/CLAUDE.md +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/CONTRIBUTING.md +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/LICENSE +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/README.md +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/bench/README.md +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/bench/__init__.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/bench/conftest.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/bench/test_sere_nlm_perf.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/ci/make-release +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/cliff.toml +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/_static/navigation.html +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/api/modules.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/api/morphata.acceptance.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/api/morphata.alphabet.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/api/morphata.automaton.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/api/morphata.builder.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/api/morphata.hoa.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/api/morphata.logic.ltl.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/api/morphata.logic.psl.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/api/morphata.logic.sere.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/api/morphata.logic.strel.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/api/morphata.operators.matrix.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/api/morphata.operators.polynomial.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/api/morphata.operators.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/api/morphata.symbolic.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/concepts/acceptance.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/concepts/hoa.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/concepts/index.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/concepts/operators.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/concepts/spec.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/concepts/weights.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/conf.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/index.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/quick-start.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/docs/symbolic-transforms.rst +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/justfile +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/mypy.ini +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/pixi.lock +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/pixi.toml +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/project.utf-8.add +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/project.utf-8.add.spl +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/pyrefly.toml +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/pyrightconfig.json +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/pytest.toml +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/ruff.toml +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/__init__.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/acceptance.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/alphabet.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/automaton.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/builder.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/hoa/__init__.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/hoa/__main__.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/hoa/acc_expr.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/hoa/hoa.lark +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/hoa/parser.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/logic/__init__.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/logic/_common.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/logic/ltl.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/logic/psl.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/logic/sere.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/logic/strel.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/operators/__init__.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/operators/_backend.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/operators/matrix.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/operators/polynomial.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/py.typed +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/symbolic/bdd.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/symbolic/compose.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/symbolic/equivalence.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/src/morphata/symbolic/transforms.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/conftest.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/aut1.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/aut11.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/aut2.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/aut3.2.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/aut3.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/aut4.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/aut5.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/aut6.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/aut7.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/aut8.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/finite_aut.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/gen_buchi_3.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/gen_cobuchi_2.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/rabin_2pair.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/streett_2pair.hoa +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/test_exporter_basic.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/test_exporter_roundtrip.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/hoa/test_parser.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/logic/__init__.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/logic/test_ltl.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/logic/test_psl.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/logic/test_sere.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/logic/test_sere_properties.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/logic/test_strel.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/operators/test_matrix_from_ir.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/operators/test_matrix_from_ltl.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/operators/test_polynomial_from_ir.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/symbolic/test_bdd_helpers.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/symbolic/test_compose.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/symbolic/test_from_hoaf.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/symbolic/test_ltlf2dfa_equivalence.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/symbolic/test_step.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/symbolic/test_symbolic_automaton.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/symbolic/test_symbolic_from_ir.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/symbolic/test_to_hoa.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/symbolic/test_transforms.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/symbolic/test_transforms_ir.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/symbolic/test_transforms_properties.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/test_alphabet.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/test_automaton.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/test_builder.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/tests/test_from_parsed.py +0 -0
- {morphata-2.0.5 → morphata-2.0.7}/ty.toml +0 -0
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.7 — 2026-06-02
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- The BDD JSON exporter in `dd.cudd` needed to be worked around because of a bug where
|
|
8
|
+
`mgr.true` and `mrg.false` do not survive a round trip serialization.
|
|
9
|
+
|
|
10
|
+
## 2.0.6 — 2026-06-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- A major regression where the streaming `to_hoa` stuff changed the label enumeration
|
|
15
|
+
semantics to be _minterms_ instead of _cubes_.
|
|
16
|
+
This is now fixed.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Added a free-floating function that helps enumerate the successor BDDs from a symbolic
|
|
21
|
+
automaton state, given a specific alphabet and set of symbols.
|
|
22
|
+
|
|
3
23
|
## 2.0.5 — 2026-06-01
|
|
4
24
|
|
|
5
25
|
### Added
|
|
@@ -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.7"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
classifiers = [
|
|
8
8
|
"Intended Audience :: Science/Research",
|
|
@@ -136,11 +136,13 @@ def symbolic_automaton_to_hoa(
|
|
|
136
136
|
"""Serialize a :class:`SymbolicAutomaton` to HOA v1 directly from its BDDs.
|
|
137
137
|
|
|
138
138
|
Walks ``sym.transitions`` row-by-row. For each cached
|
|
139
|
-
``(guard_bdd, succ_bdd)`` pair,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
``(guard_bdd, succ_bdd)`` pair, decodes both BDDs into compact
|
|
140
|
+
``logic_asts.BoolExpr`` trees via Shannon expansion
|
|
141
|
+
(:func:`~morphata.symbolic.bdd.bdd_to_boolexpr`) and emits one HOA
|
|
142
|
+
edge per disjunct of the successor expression, with a single label
|
|
143
|
+
expression per row. Cube structure of the guard is preserved -- variables
|
|
144
|
+
absent from a given path through the BDD are projected out -- so the
|
|
145
|
+
output size is proportional to the BDD shape, not to ``2^|support|``.
|
|
144
146
|
|
|
145
147
|
``out`` semantics match :func:`automaton_to_hoa`: when ``None``, an
|
|
146
148
|
internal ``StringIO`` accumulates the output and is returned;
|
|
@@ -164,6 +166,7 @@ def symbolic_automaton_to_hoa(
|
|
|
164
166
|
acceptance condition is not :class:`~morphata.acceptance.Finite`.
|
|
165
167
|
"""
|
|
166
168
|
from morphata.acceptance import Finite
|
|
169
|
+
from morphata.symbolic.bdd import bdd_to_boolexpr
|
|
167
170
|
|
|
168
171
|
if sym.num_states > 0 and not sym.transitions:
|
|
169
172
|
raise ValueError(
|
|
@@ -201,7 +204,15 @@ def symbolic_automaton_to_hoa(
|
|
|
201
204
|
total_states = next_idx
|
|
202
205
|
|
|
203
206
|
ap_order = sorted(sym.ap_registry, key=repr)
|
|
204
|
-
|
|
207
|
+
ap_to_index: dict[ty.Any, int] = {ap: i for i, ap in enumerate(ap_order)}
|
|
208
|
+
ap_decoder_map: dict[str, ty.Any] = {var: ap for ap, var in sym.ap_registry.items()}
|
|
209
|
+
|
|
210
|
+
def _ap_decode(name: str) -> ty.Any: # noqa: ANN401
|
|
211
|
+
try:
|
|
212
|
+
return ap_decoder_map[name]
|
|
213
|
+
except KeyError as exc:
|
|
214
|
+
raise ValueError(f"Guard BDD references variable {name!r} not in the AP registry.") from exc
|
|
215
|
+
|
|
205
216
|
acc_name, acc_expr_text, num_sets, acc_state_sets = _acceptance_to_hoa(sym.acceptance)
|
|
206
217
|
if true_sink is not None:
|
|
207
218
|
# Finite acceptance: extend set 0 (the accepting set) with the
|
|
@@ -226,7 +237,8 @@ def symbolic_automaton_to_hoa(
|
|
|
226
237
|
assert false_sink is not None
|
|
227
238
|
buf.write(f"Start: {false_sink}\n")
|
|
228
239
|
else:
|
|
229
|
-
|
|
240
|
+
initial_expr = ty.cast("logic.BoolExpr[int]", bdd_to_boolexpr(sym.initial))
|
|
241
|
+
for start_line in _initial_start_lines(initial_expr):
|
|
230
242
|
buf.write(f"Start: {start_line}\n")
|
|
231
243
|
ap_words = " ".join(f'"{ap}"' for ap in ap_order)
|
|
232
244
|
buf.write(f"AP: {len(ap_order)}{(' ' + ap_words) if ap_order else ''}\n")
|
|
@@ -249,11 +261,12 @@ def symbolic_automaton_to_hoa(
|
|
|
249
261
|
assert false_sink is not None
|
|
250
262
|
dsts = [str(false_sink)]
|
|
251
263
|
else:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
264
|
+
succ_expr = ty.cast("logic.BoolExpr[int]", bdd_to_boolexpr(succ_bdd))
|
|
265
|
+
dsts = _state_expr_to_edges(succ_expr)
|
|
266
|
+
guard_expr = bdd_to_boolexpr(guard_bdd, var_decoder=_ap_decode)
|
|
267
|
+
label = _label_to_hoa(guard_expr, ap_to_index)
|
|
268
|
+
for dst in dsts:
|
|
269
|
+
buf.write(f"[{label}] {dst}\n")
|
|
257
270
|
for sink in (true_sink, false_sink):
|
|
258
271
|
if sink is None:
|
|
259
272
|
continue
|
|
@@ -10,7 +10,10 @@ This subpackage provides:
|
|
|
10
10
|
- :mod:`morphata.symbolic.automaton`: :class:`SymbolicAutomaton`, a
|
|
11
11
|
representation that wraps an :class:`~morphata.automaton.Automaton`
|
|
12
12
|
so the initial state set and every cached transition are live BDD
|
|
13
|
-
nodes in a single ``dd.BDD`` manager.
|
|
13
|
+
nodes in a single ``dd.BDD`` manager. Also exposes
|
|
14
|
+
:func:`successors_for_alphabet`, a freestanding enumerator that
|
|
15
|
+
yields ``(symbol, succ_bdd)`` per declared symbol without
|
|
16
|
+
materialising a per-state transition relation.
|
|
14
17
|
- :mod:`morphata.symbolic.transforms`: BDD-level structural transforms
|
|
15
18
|
-- :func:`~morphata.symbolic.transforms.nfa_from_afa` (dealternation),
|
|
16
19
|
:func:`~morphata.symbolic.transforms.determinize` (subset construction
|
|
@@ -21,6 +24,7 @@ This subpackage provides:
|
|
|
21
24
|
"""
|
|
22
25
|
|
|
23
26
|
from morphata.symbolic.automaton import SymbolicAutomaton as SymbolicAutomaton
|
|
27
|
+
from morphata.symbolic.automaton import successors_for_alphabet as successors_for_alphabet
|
|
24
28
|
from morphata.symbolic.bdd import AP_VAR_FORMAT as AP_VAR_FORMAT
|
|
25
29
|
from morphata.symbolic.bdd import STATE_VAR_FORMAT as STATE_VAR_FORMAT
|
|
26
30
|
from morphata.symbolic.bdd import APRegistry as APRegistry
|
|
@@ -32,7 +32,7 @@ see :mod:`morphata.symbolic.transforms`.
|
|
|
32
32
|
from __future__ import annotations
|
|
33
33
|
|
|
34
34
|
import typing
|
|
35
|
-
from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence
|
|
35
|
+
from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence
|
|
36
36
|
from collections.abc import Set as AbstractSet
|
|
37
37
|
from dataclasses import dataclass, field
|
|
38
38
|
from typing import TYPE_CHECKING, Any, Generic
|
|
@@ -44,6 +44,8 @@ from morphata.automaton import Automaton, BoolExpr
|
|
|
44
44
|
from morphata.hoa import ParsedAutomaton
|
|
45
45
|
|
|
46
46
|
if TYPE_CHECKING:
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
|
|
47
49
|
from typing_extensions import Writer
|
|
48
50
|
|
|
49
51
|
from morphata.alphabet import BooleanAlphabet
|
|
@@ -245,6 +247,31 @@ class SymbolicAutomaton(Generic[AP]):
|
|
|
245
247
|
|
|
246
248
|
return symbolic_automaton_to_hoa(self, name=name, properties=properties, out=out)
|
|
247
249
|
|
|
250
|
+
def to_bdd_json(self, path: "str | Path") -> None:
|
|
251
|
+
"""Persist ``self`` to a BDD JSON cache via :func:`to_bdd_json`.
|
|
252
|
+
|
|
253
|
+
Writes ``path`` (the BDD nodes, via :mod:`dd`'s multi-root JSON
|
|
254
|
+
dump) and a sibling ``path + ".meta.json"`` (acceptance, AP
|
|
255
|
+
registry, transition root layout). Intended as an internal
|
|
256
|
+
fast-path cache; HOA remains the right format for sharing
|
|
257
|
+
automata with other tools.
|
|
258
|
+
"""
|
|
259
|
+
from morphata.symbolic.persistence import to_bdd_json
|
|
260
|
+
|
|
261
|
+
to_bdd_json(self, path)
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def from_bdd_json(
|
|
265
|
+
path: "str | Path",
|
|
266
|
+
*,
|
|
267
|
+
alphabet: "BooleanAlphabet[Any, str]",
|
|
268
|
+
manager: Manager | None = None,
|
|
269
|
+
) -> "SymbolicAutomaton[str]":
|
|
270
|
+
"""Reload a :class:`SymbolicAutomaton` written by :meth:`to_bdd_json`."""
|
|
271
|
+
from morphata.symbolic.persistence import from_bdd_json
|
|
272
|
+
|
|
273
|
+
return from_bdd_json(path, alphabet=alphabet, manager=manager)
|
|
274
|
+
|
|
248
275
|
def to_min_dfa(self) -> "SymbolicAutomaton[AP]":
|
|
249
276
|
"""Determinize and minimize ``self`` into a minimal DFA.
|
|
250
277
|
|
|
@@ -358,6 +385,83 @@ class SymbolicAutomaton(Generic[AP]):
|
|
|
358
385
|
)
|
|
359
386
|
|
|
360
387
|
|
|
388
|
+
# ---------------------------------------------------------------------------
|
|
389
|
+
# Alphabet-aware enumeration
|
|
390
|
+
# ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def successors_for_alphabet(
|
|
394
|
+
sym_aut: "SymbolicAutomaton[AP]",
|
|
395
|
+
source: int,
|
|
396
|
+
alphabet: "BooleanAlphabet[Any, AP]",
|
|
397
|
+
symbols: Iterable[Any],
|
|
398
|
+
) -> Iterator[tuple[Any, Function]]:
|
|
399
|
+
"""Yield ``(symbol, succ_bdd)`` for each ``symbol in symbols`` whose
|
|
400
|
+
cofactor of the source's outgoing transitions is not ``mgr.false``.
|
|
401
|
+
|
|
402
|
+
Each yielded ``succ_bdd`` is a BDD over state variables encoding the
|
|
403
|
+
set of successors of ``source`` on ``symbol``. For DFA-shaped
|
|
404
|
+
automata it is a single state variable; for non-deterministic
|
|
405
|
+
shapes it is the OR of active successors.
|
|
406
|
+
|
|
407
|
+
Cost is bounded by ``|symbols| * len(sym_aut.transitions[source])``
|
|
408
|
+
``mgr.let`` calls on per-row guards (each typically very small).
|
|
409
|
+
No per-state transition relation is materialised.
|
|
410
|
+
|
|
411
|
+
Yield order is a stable contract: pairs are yielded in the same
|
|
412
|
+
order ``symbols`` is iterated, with ``mgr.false`` symbols omitted.
|
|
413
|
+
Callers may walk the input alongside the yielded pairs with a two-
|
|
414
|
+
pointer pattern instead of materialising an intermediate dict.
|
|
415
|
+
|
|
416
|
+
Equivalence to ``step``: for any ``symbol``, the yielded
|
|
417
|
+
``succ_bdd`` equals ``sym_aut.step(mgr.var(state_var_name(source)),
|
|
418
|
+
symbol)``; symbols whose ``step`` result is ``mgr.false`` are not
|
|
419
|
+
yielded.
|
|
420
|
+
|
|
421
|
+
Parameters
|
|
422
|
+
----------
|
|
423
|
+
sym_aut
|
|
424
|
+
SymbolicAutomaton constructed with ``cache_transitions=True``.
|
|
425
|
+
source
|
|
426
|
+
Source state index in ``range(sym_aut.num_states)``.
|
|
427
|
+
alphabet
|
|
428
|
+
Boolean labeling alphabet over the same AP space as
|
|
429
|
+
``sym_aut.ap_registry``.
|
|
430
|
+
symbols
|
|
431
|
+
Symbols to enumerate. Iterated exactly once.
|
|
432
|
+
|
|
433
|
+
Raises
|
|
434
|
+
------
|
|
435
|
+
ValueError
|
|
436
|
+
If ``sym_aut`` was constructed with ``cache_transitions=False``
|
|
437
|
+
(no per-row pairs to cofactor), or if ``alphabet.predicates``
|
|
438
|
+
contains an AP not present in ``sym_aut.ap_registry``.
|
|
439
|
+
"""
|
|
440
|
+
if sym_aut.num_states > 0 and not sym_aut.transitions:
|
|
441
|
+
raise ValueError(
|
|
442
|
+
"successors_for_alphabet requires a populated transition cache; "
|
|
443
|
+
"reconstruct SymbolicAutomaton with cache_transitions=True."
|
|
444
|
+
)
|
|
445
|
+
unknown = set(alphabet.predicates) - set(sym_aut.ap_registry)
|
|
446
|
+
if unknown:
|
|
447
|
+
raise ValueError(f"alphabet has unknown predicates not in ap_registry: {sorted(map(repr, unknown))}")
|
|
448
|
+
|
|
449
|
+
mgr = sym_aut.manager
|
|
450
|
+
rows = sym_aut.transitions[source]
|
|
451
|
+
ap_registry = sym_aut.ap_registry
|
|
452
|
+
|
|
453
|
+
for symbol in symbols:
|
|
454
|
+
truths = alphabet.labeling(symbol)
|
|
455
|
+
ap_point: dict[str, Function] = {ap_registry[ap]: (mgr.true if ap in truths else mgr.false) for ap in ap_registry}
|
|
456
|
+
succ_acc: Function = mgr.false
|
|
457
|
+
for guard_bdd, succ_bdd in rows:
|
|
458
|
+
restricted = mgr.let(ap_point, guard_bdd)
|
|
459
|
+
if restricted == mgr.true:
|
|
460
|
+
succ_acc = mgr.apply("or", succ_acc, succ_bdd)
|
|
461
|
+
if succ_acc != mgr.false:
|
|
462
|
+
yield symbol, succ_acc
|
|
463
|
+
|
|
464
|
+
|
|
361
465
|
# ---------------------------------------------------------------------------
|
|
362
466
|
# Private helpers
|
|
363
467
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Binary-ish BDD-level persistence for :class:`SymbolicAutomaton`.
|
|
2
|
+
|
|
3
|
+
Persists a :class:`SymbolicAutomaton` to two files via :mod:`dd`'s native
|
|
4
|
+
JSON dump:
|
|
5
|
+
|
|
6
|
+
- ``<path>``: the BDD nodes themselves, written by ``Manager.dump(...,
|
|
7
|
+
filetype='json')`` as a multi-root JSON file. Cross-root structural
|
|
8
|
+
sharing in the manager is preserved at the file-format level (each
|
|
9
|
+
unique BDD node appears once; roots reference node ids).
|
|
10
|
+
- ``<path>.meta.json``: a sidecar JSON file holding the surrounding
|
|
11
|
+
``SymbolicAutomaton`` metadata that is not carried by the BDDs
|
|
12
|
+
themselves (state count, accepting states, acceptance condition, AP
|
|
13
|
+
registry, transition root-name layout).
|
|
14
|
+
|
|
15
|
+
Motivation
|
|
16
|
+
----------
|
|
17
|
+
HOA is the right on-disk format for sharing automata with downstream
|
|
18
|
+
tooling, but for an internal cache it has two costs:
|
|
19
|
+
|
|
20
|
+
1. The HOA body writes each row's label as fresh text, so the file size
|
|
21
|
+
grows with (rows x label bytes) rather than with the manager's
|
|
22
|
+
unique node count.
|
|
23
|
+
2. Parsing HOA back into a :class:`SymbolicAutomaton` goes through a
|
|
24
|
+
lark parser that allocates per-token Python AST nodes; on
|
|
25
|
+
million-line HOA files this dominates load time and memory.
|
|
26
|
+
|
|
27
|
+
The BDD JSON format here avoids both: dd's loader is C-level (no lark),
|
|
28
|
+
and node deduplication keeps the unique-node table proportional to the
|
|
29
|
+
manager's reachable node count rather than the per-row text footprint.
|
|
30
|
+
|
|
31
|
+
The format is intended as a cache, not a portable artifact: it bakes in
|
|
32
|
+
the AP and state variable naming conventions used by morphata, so the
|
|
33
|
+
loader can reconstruct the manager into the same variable namespace.
|
|
34
|
+
|
|
35
|
+
Atomic write
|
|
36
|
+
------------
|
|
37
|
+
Each file is written via a ``.partial`` sibling and then ``Path.replace``-d
|
|
38
|
+
into place. The two replaces are sequential, so a crash between them can
|
|
39
|
+
leave the BDD file in place without the sidecar; both files always land
|
|
40
|
+
*together* only modulo that window. Callers that need both-or-neither
|
|
41
|
+
semantics should write to a scratch directory and rename the directory.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import json
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
from typing import TYPE_CHECKING, Any
|
|
49
|
+
|
|
50
|
+
from morphata.acceptance import (
|
|
51
|
+
AcceptanceCondition,
|
|
52
|
+
AccPair,
|
|
53
|
+
Buchi,
|
|
54
|
+
CoBuchi,
|
|
55
|
+
Finite,
|
|
56
|
+
GeneralizedBuchi,
|
|
57
|
+
GeneralizedCoBuchi,
|
|
58
|
+
Rabin,
|
|
59
|
+
Streett,
|
|
60
|
+
)
|
|
61
|
+
from morphata.symbolic.bdd import (
|
|
62
|
+
AP_VAR_FORMAT,
|
|
63
|
+
STATE_VAR_FORMAT,
|
|
64
|
+
Function,
|
|
65
|
+
Manager,
|
|
66
|
+
declare_state_vars,
|
|
67
|
+
make_manager,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if TYPE_CHECKING:
|
|
71
|
+
from morphata.alphabet import BooleanAlphabet
|
|
72
|
+
from morphata.symbolic.automaton import SymbolicAutomaton
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
SCHEMA_VERSION = "1"
|
|
76
|
+
"""Sidecar schema version. Bump on any breaking change to the JSON layout."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Acceptance condition <-> JSON
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _acceptance_to_json(cond: AcceptanceCondition[int]) -> dict[str, Any]:
|
|
85
|
+
if isinstance(cond, Finite):
|
|
86
|
+
return {"kind": "Finite", "accepting": sorted(cond.accepting)}
|
|
87
|
+
if isinstance(cond, Buchi):
|
|
88
|
+
return {"kind": "Buchi", "accepting": sorted(cond.accepting)}
|
|
89
|
+
if isinstance(cond, CoBuchi):
|
|
90
|
+
return {"kind": "CoBuchi", "rejecting": sorted(cond.rejecting)}
|
|
91
|
+
if isinstance(cond, GeneralizedBuchi):
|
|
92
|
+
return {
|
|
93
|
+
"kind": "GeneralizedBuchi",
|
|
94
|
+
"accepting": [sorted(s) for s in cond.accepting],
|
|
95
|
+
}
|
|
96
|
+
if isinstance(cond, GeneralizedCoBuchi):
|
|
97
|
+
return {
|
|
98
|
+
"kind": "GeneralizedCoBuchi",
|
|
99
|
+
"rejecting": [sorted(s) for s in cond.rejecting],
|
|
100
|
+
}
|
|
101
|
+
if isinstance(cond, Rabin):
|
|
102
|
+
return {
|
|
103
|
+
"kind": "Rabin",
|
|
104
|
+
"pairs": [[sorted(p.rejecting), sorted(p.accepting)] for p in cond.pairs],
|
|
105
|
+
}
|
|
106
|
+
if isinstance(cond, Streett):
|
|
107
|
+
return {
|
|
108
|
+
"kind": "Streett",
|
|
109
|
+
"pairs": [[sorted(p.rejecting), sorted(p.accepting)] for p in cond.pairs],
|
|
110
|
+
}
|
|
111
|
+
raise NotImplementedError(
|
|
112
|
+
f"BDD JSON cache: cannot serialize acceptance condition of type "
|
|
113
|
+
f"{type(cond).__name__}; supported: Finite, Buchi, CoBuchi, "
|
|
114
|
+
"GeneralizedBuchi, GeneralizedCoBuchi, Rabin, Streett."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _acceptance_from_json(data: dict[str, Any]) -> AcceptanceCondition[int]:
|
|
119
|
+
kind = data["kind"]
|
|
120
|
+
if kind == "Finite":
|
|
121
|
+
return Finite(frozenset(data["accepting"]))
|
|
122
|
+
if kind == "Buchi":
|
|
123
|
+
return Buchi(frozenset(data["accepting"]))
|
|
124
|
+
if kind == "CoBuchi":
|
|
125
|
+
return CoBuchi(frozenset(data["rejecting"]))
|
|
126
|
+
if kind == "GeneralizedBuchi":
|
|
127
|
+
return GeneralizedBuchi(tuple(frozenset(s) for s in data["accepting"]))
|
|
128
|
+
if kind == "GeneralizedCoBuchi":
|
|
129
|
+
return GeneralizedCoBuchi(tuple(frozenset(s) for s in data["rejecting"]))
|
|
130
|
+
if kind == "Rabin":
|
|
131
|
+
return Rabin(tuple(AccPair(frozenset(r), frozenset(a)) for r, a in data["pairs"]))
|
|
132
|
+
if kind == "Streett":
|
|
133
|
+
return Streett(tuple(AccPair(frozenset(r), frozenset(a)) for r, a in data["pairs"]))
|
|
134
|
+
raise ValueError(f"BDD JSON cache: unknown acceptance kind {kind!r}")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# Top-level dump / load
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _meta_path_for(bdd_path: Path) -> Path:
|
|
143
|
+
return Path(str(bdd_path) + ".meta.json")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def to_bdd_json(sym: "SymbolicAutomaton[Any]", path: str | Path) -> None:
|
|
147
|
+
"""Persist *sym* to ``path`` (BDD JSON) plus a ``<path>.meta.json`` sidecar.
|
|
148
|
+
|
|
149
|
+
Roots written to the BDD file:
|
|
150
|
+
|
|
151
|
+
- ``initial``: the initial-state BDD.
|
|
152
|
+
- ``g_{s}_{k}``, ``s_{s}_{k}``: the guard and successor BDDs of the
|
|
153
|
+
k-th cached transition out of state ``s``.
|
|
154
|
+
|
|
155
|
+
The sidecar JSON holds everything else needed to rehydrate the
|
|
156
|
+
automaton (state count, accepting states, acceptance condition, AP
|
|
157
|
+
registry mapping AP value -> manager var name, and the per-state
|
|
158
|
+
list of ``(guard_root_name, succ_root_name)`` pairs).
|
|
159
|
+
|
|
160
|
+
Raises
|
|
161
|
+
------
|
|
162
|
+
ValueError
|
|
163
|
+
If ``sym.num_states > 0`` and the transition cache is empty.
|
|
164
|
+
NotImplementedError
|
|
165
|
+
If the AP registry contains a non-string AP value, or the
|
|
166
|
+
acceptance condition is not one of the named conditions handled
|
|
167
|
+
by :func:`_acceptance_to_json`.
|
|
168
|
+
"""
|
|
169
|
+
if sym.num_states > 0 and not sym.transitions:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
"to_bdd_json requires a populated transition cache; reconstruct SymbolicAutomaton with cache_transitions=True."
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
bdd_path = Path(path)
|
|
175
|
+
meta_path = _meta_path_for(bdd_path)
|
|
176
|
+
bdd_partial = Path(str(bdd_path) + ".partial")
|
|
177
|
+
meta_partial = Path(str(meta_path) + ".partial")
|
|
178
|
+
|
|
179
|
+
mgr = sym.manager
|
|
180
|
+
roots: dict[str, Function] = {"initial": sym.initial}
|
|
181
|
+
transitions_layout: dict[str, list[list[str]]] = {}
|
|
182
|
+
for s in range(sym.num_states):
|
|
183
|
+
rows_for_s: list[list[str]] = []
|
|
184
|
+
for k, (g, n) in enumerate(sym.transitions.get(s, ())):
|
|
185
|
+
g_name = f"g_{s}_{k}"
|
|
186
|
+
s_name = f"s_{s}_{k}"
|
|
187
|
+
roots[g_name] = g
|
|
188
|
+
roots[s_name] = n
|
|
189
|
+
rows_for_s.append([g_name, s_name])
|
|
190
|
+
transitions_layout[str(s)] = rows_for_s
|
|
191
|
+
|
|
192
|
+
# dd's JSON dump/load round-trip is buggy for terminal roots on the
|
|
193
|
+
# CUDD backend: ``_dump_bdd_info`` emits a numeric id for True/False
|
|
194
|
+
# roots while ``_dump_bdd`` short-circuits terminals as ``"T"`` /
|
|
195
|
+
# ``"F"`` and never writes a body for those ids, so ``manager.load``
|
|
196
|
+
# raises KeyError. Sidestep by filtering terminal roots out of the
|
|
197
|
+
# dict handed to dd.dump and recording their names in the sidecar.
|
|
198
|
+
true_root_names: list[str] = []
|
|
199
|
+
false_root_names: list[str] = []
|
|
200
|
+
bdd_roots: dict[str, Function] = {}
|
|
201
|
+
for name, node in roots.items():
|
|
202
|
+
if node == mgr.true:
|
|
203
|
+
true_root_names.append(name)
|
|
204
|
+
elif node == mgr.false:
|
|
205
|
+
false_root_names.append(name)
|
|
206
|
+
else:
|
|
207
|
+
bdd_roots[name] = node
|
|
208
|
+
|
|
209
|
+
ap_registry_json: dict[str, str] = {}
|
|
210
|
+
for ap, var in sym.ap_registry.items():
|
|
211
|
+
if not isinstance(ap, str):
|
|
212
|
+
raise NotImplementedError(
|
|
213
|
+
f"BDD JSON cache currently supports only str-valued APs; got AP value {ap!r} of type {type(ap).__name__}."
|
|
214
|
+
)
|
|
215
|
+
ap_registry_json[ap] = var
|
|
216
|
+
|
|
217
|
+
meta: dict[str, Any] = {
|
|
218
|
+
"schema_version": SCHEMA_VERSION,
|
|
219
|
+
"num_states": sym.num_states,
|
|
220
|
+
"accepting_states": sorted(sym.accepting_states),
|
|
221
|
+
"acceptance": _acceptance_to_json(sym.acceptance),
|
|
222
|
+
"ap_registry": ap_registry_json,
|
|
223
|
+
"state_var_format": STATE_VAR_FORMAT,
|
|
224
|
+
"ap_var_format": AP_VAR_FORMAT,
|
|
225
|
+
"initial_root_name": "initial",
|
|
226
|
+
"transitions": transitions_layout,
|
|
227
|
+
"true_root_names": sorted(true_root_names),
|
|
228
|
+
"false_root_names": sorted(false_root_names),
|
|
229
|
+
# dd's dump raises if roots={}, so flag the all-terminal case and
|
|
230
|
+
# skip the BDD file altogether; the sidecar carries everything.
|
|
231
|
+
"bdd_file_empty": not bdd_roots,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if bdd_roots:
|
|
235
|
+
mgr.dump(str(bdd_partial), roots=bdd_roots, filetype="json")
|
|
236
|
+
with open(meta_partial, "w") as f:
|
|
237
|
+
json.dump(meta, f, indent=2, sort_keys=True)
|
|
238
|
+
|
|
239
|
+
if bdd_roots:
|
|
240
|
+
bdd_partial.replace(bdd_path)
|
|
241
|
+
elif bdd_path.exists():
|
|
242
|
+
bdd_path.unlink()
|
|
243
|
+
meta_partial.replace(meta_path)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def from_bdd_json(
|
|
247
|
+
path: str | Path,
|
|
248
|
+
*,
|
|
249
|
+
alphabet: "BooleanAlphabet[Any, str]",
|
|
250
|
+
manager: Manager | None = None,
|
|
251
|
+
) -> "SymbolicAutomaton[str]":
|
|
252
|
+
"""Reload a :class:`SymbolicAutomaton` from :func:`to_bdd_json`'s output.
|
|
253
|
+
|
|
254
|
+
Declares state and AP variables in *manager* (a fresh one if not
|
|
255
|
+
given) matching the writer's namespace, loads the BDD JSON into the
|
|
256
|
+
manager via ``Manager.load``, and reassembles
|
|
257
|
+
:attr:`SymbolicAutomaton.transitions` from the sidecar layout.
|
|
258
|
+
|
|
259
|
+
AP values are returned as ``str`` (the only AP value type supported
|
|
260
|
+
by :func:`to_bdd_json`).
|
|
261
|
+
|
|
262
|
+
Raises
|
|
263
|
+
------
|
|
264
|
+
ValueError
|
|
265
|
+
If the sidecar schema version is unknown, if its
|
|
266
|
+
``state_var_format`` does not match morphata's, or if a root
|
|
267
|
+
named by the sidecar is missing from the BDD file.
|
|
268
|
+
"""
|
|
269
|
+
from morphata.symbolic.automaton import SymbolicAutomaton
|
|
270
|
+
|
|
271
|
+
bdd_path = Path(path)
|
|
272
|
+
meta_path = _meta_path_for(bdd_path)
|
|
273
|
+
|
|
274
|
+
with open(meta_path) as f:
|
|
275
|
+
meta = json.load(f)
|
|
276
|
+
|
|
277
|
+
schema = meta.get("schema_version")
|
|
278
|
+
if schema != SCHEMA_VERSION:
|
|
279
|
+
raise ValueError(f"BDD JSON cache schema mismatch: file has {schema!r}, loader expected {SCHEMA_VERSION!r}.")
|
|
280
|
+
if meta.get("state_var_format") != STATE_VAR_FORMAT:
|
|
281
|
+
raise ValueError(
|
|
282
|
+
f"state_var_format mismatch: file uses {meta.get('state_var_format')!r}, loader expects {STATE_VAR_FORMAT!r}."
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
num_states = int(meta["num_states"])
|
|
286
|
+
ap_registry_meta: dict[str, str] = dict(meta["ap_registry"])
|
|
287
|
+
|
|
288
|
+
if manager is None:
|
|
289
|
+
manager = make_manager(reordering=True)
|
|
290
|
+
declare_state_vars(manager, num_states)
|
|
291
|
+
existing = set(manager.vars)
|
|
292
|
+
to_add = [name for name in ap_registry_meta.values() if name not in existing]
|
|
293
|
+
if to_add:
|
|
294
|
+
manager.declare(*to_add)
|
|
295
|
+
|
|
296
|
+
if meta.get("bdd_file_empty", False):
|
|
297
|
+
roots: dict[str, Function] = {}
|
|
298
|
+
else:
|
|
299
|
+
loaded = manager.load(str(bdd_path))
|
|
300
|
+
if not isinstance(loaded, dict):
|
|
301
|
+
raise ValueError(
|
|
302
|
+
f"BDD JSON load returned {type(loaded).__name__}; expected a "
|
|
303
|
+
"dict of named roots. The file may have been written by a "
|
|
304
|
+
"different tool."
|
|
305
|
+
)
|
|
306
|
+
roots = loaded
|
|
307
|
+
|
|
308
|
+
# Restore terminal roots that were filtered out at dump time.
|
|
309
|
+
for name in meta.get("true_root_names", []):
|
|
310
|
+
roots[name] = manager.true
|
|
311
|
+
for name in meta.get("false_root_names", []):
|
|
312
|
+
roots[name] = manager.false
|
|
313
|
+
|
|
314
|
+
initial_name = meta["initial_root_name"]
|
|
315
|
+
try:
|
|
316
|
+
initial = roots[initial_name]
|
|
317
|
+
except KeyError as exc:
|
|
318
|
+
raise ValueError(f"BDD JSON file is missing initial root {initial_name!r}") from exc
|
|
319
|
+
|
|
320
|
+
transitions: dict[int, list[tuple[Function, Function]]] = {}
|
|
321
|
+
for s_str, rows in meta["transitions"].items():
|
|
322
|
+
s = int(s_str)
|
|
323
|
+
pairs: list[tuple[Function, Function]] = []
|
|
324
|
+
for entry in rows:
|
|
325
|
+
g_name, n_name = entry
|
|
326
|
+
try:
|
|
327
|
+
pairs.append((roots[g_name], roots[n_name]))
|
|
328
|
+
except KeyError as exc:
|
|
329
|
+
raise ValueError(
|
|
330
|
+
f"BDD JSON file is missing root {exc.args[0]!r} referenced by sidecar state {s} row layout."
|
|
331
|
+
) from exc
|
|
332
|
+
transitions[s] = pairs
|
|
333
|
+
|
|
334
|
+
accepting_states = frozenset(int(s) for s in meta["accepting_states"])
|
|
335
|
+
acceptance = _acceptance_from_json(meta["acceptance"])
|
|
336
|
+
|
|
337
|
+
return SymbolicAutomaton(
|
|
338
|
+
manager=manager,
|
|
339
|
+
initial=initial,
|
|
340
|
+
transitions=transitions,
|
|
341
|
+
ap_registry=ap_registry_meta,
|
|
342
|
+
accepting_states=accepting_states,
|
|
343
|
+
num_states=num_states,
|
|
344
|
+
acceptance=acceptance,
|
|
345
|
+
alphabet=alphabet,
|
|
346
|
+
)
|
|
@@ -220,6 +220,54 @@ class TestSymbolicSinkMaterialisation:
|
|
|
220
220
|
assert "State: 2 {0}\n[t] 2\n" in hoa_text
|
|
221
221
|
|
|
222
222
|
|
|
223
|
+
class TestSymbolicCubeNotMinterm:
|
|
224
|
+
"""Regression for the 2.0.5 bug where the streaming exporter emitted one
|
|
225
|
+
HOA edge per BDD minterm instead of per cube.
|
|
226
|
+
|
|
227
|
+
``dd.cudd.BDD.pick_iter(u)`` defaults ``care_vars`` to ``support(u)`` and
|
|
228
|
+
then enumerates *minterms* (fully-assigned models) over that support, so
|
|
229
|
+
a guard with non-trivial support like ``a | b`` would produce three
|
|
230
|
+
edges (``a&b``, ``a&!b``, ``!a&b``) instead of the single cube ``(a | b)``.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
def _build_disjunctive_guard_dfa(self) -> SymbolicAutomaton[str]:
|
|
234
|
+
# Two-state DFA over {a, b}. State 0 transitions to state 1 under
|
|
235
|
+
# the guard ``a | b`` (BDD support = {a, b}; pre-fix expands to 3
|
|
236
|
+
# minterms). Otherwise loops in state 0.
|
|
237
|
+
b = AutomatonBuilder[int, str]()
|
|
238
|
+
b.add_state(0, initial=True)
|
|
239
|
+
b.add_state(1, accepting=True)
|
|
240
|
+
b.add_transition(0, logic.Or((logic.Variable("a"), logic.Variable("b"))), 1)
|
|
241
|
+
b.add_transition(
|
|
242
|
+
0,
|
|
243
|
+
logic.And((logic.Not(logic.Variable("a")), logic.Not(logic.Variable("b")))),
|
|
244
|
+
0,
|
|
245
|
+
)
|
|
246
|
+
b.add_transition(1, logic.Variable("a"), 1)
|
|
247
|
+
b.add_transition(1, logic.Not(logic.Variable("a")), 1)
|
|
248
|
+
aut = b.build()
|
|
249
|
+
return SymbolicAutomaton.from_automaton(aut, alphabet=powerset_alphabet(aut.predicates))
|
|
250
|
+
|
|
251
|
+
def test_disjunctive_guard_emits_single_cube_edge(self) -> None:
|
|
252
|
+
sym = self._build_disjunctive_guard_dfa()
|
|
253
|
+
hoa_text = symbolic_automaton_to_hoa(sym)
|
|
254
|
+
assert hoa_text is not None
|
|
255
|
+
# Find the body block for state 0.
|
|
256
|
+
body = hoa_text.split("--BODY--\n", 1)[1]
|
|
257
|
+
state0_block = body.split("State: 0", 1)[1].split("State:", 1)[0]
|
|
258
|
+
edges_to_1 = [line for line in state0_block.splitlines() if line.endswith(" 1")]
|
|
259
|
+
# Post-fix: a single edge labelled with the disjunction goes to 1.
|
|
260
|
+
# Pre-fix (the bug): three minterm edges go to 1.
|
|
261
|
+
assert len(edges_to_1) == 1, f"expected one cube edge to state 1, got {len(edges_to_1)}: {edges_to_1!r}"
|
|
262
|
+
|
|
263
|
+
def test_writer_vs_string_byte_identical_on_disjunctive_guard(self) -> None:
|
|
264
|
+
sym = self._build_disjunctive_guard_dfa()
|
|
265
|
+
string_form = symbolic_automaton_to_hoa(sym)
|
|
266
|
+
buf = io.StringIO()
|
|
267
|
+
symbolic_automaton_to_hoa(sym, out=buf)
|
|
268
|
+
assert buf.getvalue() == string_form
|
|
269
|
+
|
|
270
|
+
|
|
223
271
|
class TestSymbolicErrorPaths:
|
|
224
272
|
def test_literal_successor_with_non_finite_acceptance_raises(self) -> None:
|
|
225
273
|
from morphata.acceptance import Buchi
|