morphata 2.0.4__tar.gz → 2.0.5__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.4 → morphata-2.0.5}/CHANGELOG.md +13 -0
- {morphata-2.0.4 → morphata-2.0.5}/PKG-INFO +2 -2
- {morphata-2.0.4 → morphata-2.0.5}/ci/make-release +3 -3
- {morphata-2.0.4 → morphata-2.0.5}/pixi.lock +1 -1
- {morphata-2.0.4 → morphata-2.0.5}/pixi.toml +1 -1
- {morphata-2.0.4 → morphata-2.0.5}/pyproject.toml +2 -2
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/hoa/__init__.py +2 -1
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/hoa/exporter.py +269 -9
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/symbolic/automaton.py +13 -4
- morphata-2.0.5/tests/hoa/test_exporter_streaming.py +250 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/symbolic/test_to_hoa.py +21 -0
- {morphata-2.0.4 → morphata-2.0.5}/uv.lock +2 -2
- {morphata-2.0.4 → morphata-2.0.5}/.basedpyright/baseline.json +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/.claude/settings.local.json +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/.dockerignore +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/.editorconfig +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/.envrc +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/.envrc.recommended +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/.fdignore +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/.gitattributes +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/.gitignore +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/.mailmap +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/.python-version +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/.rstcheck.cfg +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/AGENTS.md +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/CLAUDE.md +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/CONTRIBUTING.md +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/LICENSE +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/README.md +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/bench/README.md +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/bench/__init__.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/bench/conftest.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/bench/test_sere_nlm_perf.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/cliff.toml +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/_static/navigation.html +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/api/modules.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/api/morphata.acceptance.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/api/morphata.alphabet.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/api/morphata.automaton.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/api/morphata.builder.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/api/morphata.hoa.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/api/morphata.logic.ltl.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/api/morphata.logic.psl.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/api/morphata.logic.sere.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/api/morphata.logic.strel.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/api/morphata.operators.matrix.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/api/morphata.operators.polynomial.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/api/morphata.operators.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/api/morphata.symbolic.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/concepts/acceptance.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/concepts/hoa.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/concepts/index.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/concepts/operators.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/concepts/spec.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/concepts/weights.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/conf.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/index.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/quick-start.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/docs/symbolic-transforms.rst +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/justfile +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/mypy.ini +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/project.utf-8.add +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/project.utf-8.add.spl +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/pyrefly.toml +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/pyrightconfig.json +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/pytest.toml +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/ruff.toml +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/__init__.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/acceptance.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/alphabet.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/automaton.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/builder.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/hoa/__main__.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/hoa/acc_expr.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/hoa/hoa.lark +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/hoa/parser.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/logic/__init__.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/logic/_common.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/logic/ltl.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/logic/psl.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/logic/sere.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/logic/strel.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/operators/__init__.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/operators/_backend.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/operators/matrix.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/operators/polynomial.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/py.typed +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/symbolic/__init__.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/symbolic/bdd.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/symbolic/compose.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/symbolic/equivalence.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/src/morphata/symbolic/transforms.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/conftest.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/aut1.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/aut11.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/aut2.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/aut3.2.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/aut3.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/aut4.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/aut5.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/aut6.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/aut7.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/aut8.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/finite_aut.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/gen_buchi_3.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/gen_cobuchi_2.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/rabin_2pair.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/streett_2pair.hoa +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/test_exporter_basic.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/test_exporter_roundtrip.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/hoa/test_parser.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/logic/__init__.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/logic/test_ltl.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/logic/test_psl.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/logic/test_sere.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/logic/test_sere_properties.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/logic/test_strel.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/operators/test_matrix_from_ir.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/operators/test_matrix_from_ltl.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/operators/test_polynomial_from_ir.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/symbolic/test_bdd_helpers.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/symbolic/test_compose.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/symbolic/test_from_hoaf.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/symbolic/test_ltlf2dfa_equivalence.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/symbolic/test_step.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/symbolic/test_symbolic_automaton.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/symbolic/test_symbolic_from_ir.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/symbolic/test_transforms.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/symbolic/test_transforms_ir.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/symbolic/test_transforms_properties.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/test_alphabet.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/test_automaton.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/test_builder.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/tests/test_from_parsed.py +0 -0
- {morphata-2.0.4 → morphata-2.0.5}/ty.toml +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.5 — 2026-06-01
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
`SymbolicAutomaton` and `Automaton` now support a streaming writer
|
|
8
|
+
for HOA export via `morphata.hoa.symbolic_automaton_to_hoa`
|
|
9
|
+
and `morphata.hoa.automaton_to_hoa`, and is also present in `SymbolicAutomaton.to_hoa`.
|
|
10
|
+
|
|
11
|
+
This is mostly a performance thing:
|
|
12
|
+
providing a buffered writer to the exporter allows one to stream the HOA format into a
|
|
13
|
+
file, without having to manifest the entire string representation of the automaton,
|
|
14
|
+
which can be cumbersome.
|
|
15
|
+
|
|
3
16
|
## 2.0.4 — 2026-05-28
|
|
4
17
|
|
|
5
18
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: morphata
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.5
|
|
4
4
|
Summary: Representation theory for automata, made practical.
|
|
5
5
|
Project-URL: Documentation, https://docs.anandb.dev/morphata
|
|
6
6
|
Project-URL: Repository, https://git.anandb.dev/morphata.git
|
|
@@ -20,7 +20,7 @@ Requires-Dist: logic-asts>=1.6.2
|
|
|
20
20
|
Requires-Dist: networkx
|
|
21
21
|
Requires-Dist: numpy
|
|
22
22
|
Requires-Dist: types-networkx
|
|
23
|
-
Requires-Dist: typing-extensions
|
|
23
|
+
Requires-Dist: typing-extensions>=4.14
|
|
24
24
|
Provides-Extra: dot
|
|
25
25
|
Requires-Dist: pydot>=4.0.1; extra == 'dot'
|
|
26
26
|
Provides-Extra: jax
|
|
@@ -88,7 +88,7 @@ echo "==> Promoting CHANGELOG Unreleased -> ${NEW_VERSION} (${RELEASE_DATE})"
|
|
|
88
88
|
sed -i "s/^## Unreleased$/## ${NEW_VERSION} \xe2\x80\x94 ${RELEASE_DATE}/" CHANGELOG.md
|
|
89
89
|
|
|
90
90
|
# Commit
|
|
91
|
-
COMMIT_MSG="chore(release): $
|
|
91
|
+
COMMIT_MSG="chore(release): $(uv version)"
|
|
92
92
|
echo "==> Committing: $COMMIT_MSG"
|
|
93
93
|
|
|
94
94
|
if is_jj; then
|
|
@@ -120,8 +120,8 @@ echo "==> Tagging $TAG"
|
|
|
120
120
|
if is_jj; then
|
|
121
121
|
jj git import 2>/dev/null || true
|
|
122
122
|
fi
|
|
123
|
-
printf '%s\n\n%s\n' "$TAG_TITLE" "$TAG_BODY"
|
|
124
|
-
|
|
123
|
+
printf '%s\n\n%s\n' "$TAG_TITLE" "$TAG_BODY" |
|
|
124
|
+
git tag -a "$TAG" --cleanup=verbatim -F -
|
|
125
125
|
if is_jj; then
|
|
126
126
|
jj git import 2>/dev/null || true
|
|
127
127
|
fi
|
|
@@ -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.5"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
classifiers = [
|
|
8
8
|
"Intended Audience :: Science/Research",
|
|
@@ -21,7 +21,7 @@ dependencies = [
|
|
|
21
21
|
"networkx",
|
|
22
22
|
"numpy",
|
|
23
23
|
"types-networkx",
|
|
24
|
-
"typing-extensions",
|
|
24
|
+
"typing-extensions>=4.14",
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
[project.optional-dependencies]
|
|
@@ -37,7 +37,8 @@ Example extended HOA with finite acceptance::
|
|
|
37
37
|
|
|
38
38
|
import morphata.hoa.parser as parser
|
|
39
39
|
from morphata.hoa.exporter import automaton_to_hoa as automaton_to_hoa
|
|
40
|
+
from morphata.hoa.exporter import symbolic_automaton_to_hoa as symbolic_automaton_to_hoa
|
|
40
41
|
from morphata.hoa.parser import ParsedAutomaton as ParsedAutomaton
|
|
41
42
|
from morphata.hoa.parser import parse as parse
|
|
42
43
|
|
|
43
|
-
__all__ = ["parse", "parser", "ParsedAutomaton", "automaton_to_hoa"]
|
|
44
|
+
__all__ = ["parse", "parser", "ParsedAutomaton", "automaton_to_hoa", "symbolic_automaton_to_hoa"]
|
|
@@ -12,23 +12,53 @@ import io
|
|
|
12
12
|
import typing as ty
|
|
13
13
|
from collections.abc import Iterable, Mapping, Sequence
|
|
14
14
|
from collections.abc import Set as AbstractSet
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
15
16
|
|
|
16
17
|
import logic_asts.base as logic
|
|
18
|
+
from typing_extensions import Writer, overload
|
|
17
19
|
|
|
18
20
|
import morphata.hoa.acc_expr as acc_expr
|
|
19
21
|
|
|
20
|
-
if
|
|
22
|
+
if TYPE_CHECKING:
|
|
21
23
|
from morphata.acceptance import AcceptanceCondition
|
|
22
24
|
from morphata.automaton import Automaton
|
|
25
|
+
from morphata.symbolic.automaton import SymbolicAutomaton
|
|
26
|
+
from morphata.symbolic.bdd import Function, Manager
|
|
23
27
|
|
|
24
28
|
|
|
29
|
+
@overload
|
|
25
30
|
def automaton_to_hoa(
|
|
26
|
-
aut: Automaton[int, ty.Any, ty.Any],
|
|
31
|
+
aut: "Automaton[int, ty.Any, ty.Any]",
|
|
32
|
+
*,
|
|
33
|
+
name: str | None = ...,
|
|
34
|
+
properties: "Sequence[str] | None" = ...,
|
|
35
|
+
out: None = ...,
|
|
36
|
+
) -> str: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@overload
|
|
40
|
+
def automaton_to_hoa(
|
|
41
|
+
aut: "Automaton[int, ty.Any, ty.Any]",
|
|
42
|
+
*,
|
|
43
|
+
name: str | None = ...,
|
|
44
|
+
properties: "Sequence[str] | None" = ...,
|
|
45
|
+
out: "Writer[str]",
|
|
46
|
+
) -> None: ...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def automaton_to_hoa(
|
|
50
|
+
aut: "Automaton[int, ty.Any, ty.Any]",
|
|
27
51
|
*,
|
|
28
52
|
name: str | None = None,
|
|
29
|
-
properties: Sequence[str] | None = None,
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
properties: "Sequence[str] | None" = None,
|
|
54
|
+
out: "Writer[str] | None" = None,
|
|
55
|
+
) -> "str | None":
|
|
56
|
+
"""Serialize *aut* to HOA v1.
|
|
57
|
+
|
|
58
|
+
If ``out`` is ``None`` (the default), build the HOA in an internal
|
|
59
|
+
``io.StringIO`` and return the resulting string. If ``out`` is
|
|
60
|
+
provided, stream the HOA directly into it and return ``None``; the
|
|
61
|
+
caller owns flushing / closing.
|
|
32
62
|
|
|
33
63
|
Requirements on *aut*:
|
|
34
64
|
|
|
@@ -50,7 +80,8 @@ def automaton_to_hoa(
|
|
|
50
80
|
ap_to_index = {ap: i for i, ap in enumerate(ap_order)}
|
|
51
81
|
acc_name, acc_expr_text, num_sets, acc_state_sets = _acceptance_to_hoa(aut.acceptance)
|
|
52
82
|
|
|
53
|
-
|
|
83
|
+
owns_buffer = out is None
|
|
84
|
+
buf: Writer[str] = io.StringIO() if out is None else out
|
|
54
85
|
_write_header(
|
|
55
86
|
buf,
|
|
56
87
|
aut=aut,
|
|
@@ -69,7 +100,173 @@ def automaton_to_hoa(
|
|
|
69
100
|
ap_to_index=ap_to_index,
|
|
70
101
|
acc_state_sets=acc_state_sets,
|
|
71
102
|
)
|
|
72
|
-
|
|
103
|
+
if owns_buffer:
|
|
104
|
+
assert isinstance(buf, io.StringIO)
|
|
105
|
+
return buf.getvalue()
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@overload
|
|
110
|
+
def symbolic_automaton_to_hoa(
|
|
111
|
+
sym: "SymbolicAutomaton[ty.Any]",
|
|
112
|
+
*,
|
|
113
|
+
name: str | None = ...,
|
|
114
|
+
properties: Sequence[str] | None = ...,
|
|
115
|
+
out: None = ...,
|
|
116
|
+
) -> str: ...
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@overload
|
|
120
|
+
def symbolic_automaton_to_hoa(
|
|
121
|
+
sym: "SymbolicAutomaton[ty.Any]",
|
|
122
|
+
*,
|
|
123
|
+
name: str | None = ...,
|
|
124
|
+
properties: Sequence[str] | None = ...,
|
|
125
|
+
out: Writer[str],
|
|
126
|
+
) -> None: ...
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def symbolic_automaton_to_hoa(
|
|
130
|
+
sym: "SymbolicAutomaton[ty.Any]",
|
|
131
|
+
*,
|
|
132
|
+
name: str | None = None,
|
|
133
|
+
properties: Sequence[str] | None = None,
|
|
134
|
+
out: Writer[str] | None = None,
|
|
135
|
+
) -> str | None:
|
|
136
|
+
"""Serialize a :class:`SymbolicAutomaton` to HOA v1 directly from its BDDs.
|
|
137
|
+
|
|
138
|
+
Walks ``sym.transitions`` row-by-row. For each cached
|
|
139
|
+
``(guard_bdd, succ_bdd)`` pair, enumerates AP-cubes of ``guard_bdd``
|
|
140
|
+
via ``BDD.pick_iter`` (over the BDD's support, no ``care_vars``) and
|
|
141
|
+
successor-state cubes of ``succ_bdd`` the same way, and emits one
|
|
142
|
+
HOA ``[label] dst`` edge per (guard cube x successor cube). No
|
|
143
|
+
``logic_asts.BoolExpr`` tree is constructed.
|
|
144
|
+
|
|
145
|
+
``out`` semantics match :func:`automaton_to_hoa`: when ``None``, an
|
|
146
|
+
internal ``StringIO`` accumulates the output and is returned;
|
|
147
|
+
otherwise the output streams into ``out`` and the function returns
|
|
148
|
+
``None``.
|
|
149
|
+
|
|
150
|
+
Literal-true / literal-false successor BDDs (``mgr.true`` /
|
|
151
|
+
``mgr.false``) are handled by emitting synthetic sink states with
|
|
152
|
+
self-loops, mirroring :func:`_materialize_sinks` on the IR side. The
|
|
153
|
+
BDD manager is not mutated. As on the IR side, sink materialisation
|
|
154
|
+
is only implemented for :class:`~morphata.acceptance.Finite`
|
|
155
|
+
acceptance; omega-regular conditions with literal successors raise
|
|
156
|
+
``NotImplementedError``.
|
|
157
|
+
|
|
158
|
+
Raises
|
|
159
|
+
------
|
|
160
|
+
ValueError
|
|
161
|
+
If the cached transition table is empty but ``sym.num_states > 0``.
|
|
162
|
+
NotImplementedError
|
|
163
|
+
If literal-true / literal-false successors are present and the
|
|
164
|
+
acceptance condition is not :class:`~morphata.acceptance.Finite`.
|
|
165
|
+
"""
|
|
166
|
+
from morphata.acceptance import Finite
|
|
167
|
+
|
|
168
|
+
if sym.num_states > 0 and not sym.transitions:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
"symbolic_automaton_to_hoa requires a populated transition cache; "
|
|
171
|
+
"reconstruct SymbolicAutomaton with cache_transitions=True."
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
mgr = sym.manager
|
|
175
|
+
|
|
176
|
+
needs_true = sym.initial == mgr.true
|
|
177
|
+
needs_false = sym.initial == mgr.false
|
|
178
|
+
for s in range(sym.num_states):
|
|
179
|
+
for _, succ_bdd in sym.transitions.get(s, ()):
|
|
180
|
+
if succ_bdd == mgr.true:
|
|
181
|
+
needs_true = True
|
|
182
|
+
elif succ_bdd == mgr.false:
|
|
183
|
+
needs_false = True
|
|
184
|
+
|
|
185
|
+
if (needs_true or needs_false) and not isinstance(sym.acceptance, Finite):
|
|
186
|
+
raise NotImplementedError(
|
|
187
|
+
"HOA export: literal-true / literal-false successor BDDs require "
|
|
188
|
+
"sink-state materialisation, which is only implemented for "
|
|
189
|
+
f"Finite acceptance; got {type(sym.acceptance).__name__}."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
next_idx = sym.num_states
|
|
193
|
+
true_sink: int | None = None
|
|
194
|
+
false_sink: int | None = None
|
|
195
|
+
if needs_true:
|
|
196
|
+
true_sink = next_idx
|
|
197
|
+
next_idx += 1
|
|
198
|
+
if needs_false:
|
|
199
|
+
false_sink = next_idx
|
|
200
|
+
next_idx += 1
|
|
201
|
+
total_states = next_idx
|
|
202
|
+
|
|
203
|
+
ap_order = sorted(sym.ap_registry, key=repr)
|
|
204
|
+
bdd_var_to_ap_index: dict[str, int] = {sym.ap_registry[ap]: i for i, ap in enumerate(ap_order)}
|
|
205
|
+
acc_name, acc_expr_text, num_sets, acc_state_sets = _acceptance_to_hoa(sym.acceptance)
|
|
206
|
+
if true_sink is not None:
|
|
207
|
+
# Finite acceptance: extend set 0 (the accepting set) with the
|
|
208
|
+
# true-sink. ``_acceptance_to_hoa(Finite)`` always returns
|
|
209
|
+
# ``{0: ...}``.
|
|
210
|
+
augmented_sets: dict[int, AbstractSet[int]] = dict(acc_state_sets)
|
|
211
|
+
augmented_sets[0] = frozenset(augmented_sets.get(0, frozenset())) | {true_sink}
|
|
212
|
+
acc_state_sets = augmented_sets
|
|
213
|
+
states = list(range(sym.num_states))
|
|
214
|
+
|
|
215
|
+
owns_buffer = out is None
|
|
216
|
+
buf: Writer[str] = io.StringIO() if owns_buffer else out # type: ignore[assignment]
|
|
217
|
+
|
|
218
|
+
buf.write("HOA: v1\n")
|
|
219
|
+
if name is not None:
|
|
220
|
+
buf.write(f'name: "{name}"\n')
|
|
221
|
+
buf.write(f"States: {total_states}\n")
|
|
222
|
+
if sym.initial == mgr.true:
|
|
223
|
+
assert true_sink is not None
|
|
224
|
+
buf.write(f"Start: {true_sink}\n")
|
|
225
|
+
elif sym.initial == mgr.false:
|
|
226
|
+
assert false_sink is not None
|
|
227
|
+
buf.write(f"Start: {false_sink}\n")
|
|
228
|
+
else:
|
|
229
|
+
for start_line in _bdd_state_cubes(sym.initial, mgr=mgr):
|
|
230
|
+
buf.write(f"Start: {start_line}\n")
|
|
231
|
+
ap_words = " ".join(f'"{ap}"' for ap in ap_order)
|
|
232
|
+
buf.write(f"AP: {len(ap_order)}{(' ' + ap_words) if ap_order else ''}\n")
|
|
233
|
+
if acc_name is not None:
|
|
234
|
+
buf.write(f"acc-name: {acc_name}\n")
|
|
235
|
+
buf.write(f"Acceptance: {num_sets} {acc_expr_text}\n")
|
|
236
|
+
if properties:
|
|
237
|
+
buf.write("properties: " + " ".join(properties) + "\n")
|
|
238
|
+
|
|
239
|
+
buf.write("--BODY--\n")
|
|
240
|
+
for s in states:
|
|
241
|
+
sets_for_s = sorted(idx for idx, members in acc_state_sets.items() if s in members)
|
|
242
|
+
marker = (" {" + " ".join(str(i) for i in sets_for_s) + "}") if sets_for_s else ""
|
|
243
|
+
buf.write(f"State: {s}{marker}\n")
|
|
244
|
+
for guard_bdd, succ_bdd in sym.transitions.get(s, ()):
|
|
245
|
+
if succ_bdd == mgr.true:
|
|
246
|
+
assert true_sink is not None
|
|
247
|
+
dsts = [str(true_sink)]
|
|
248
|
+
elif succ_bdd == mgr.false:
|
|
249
|
+
assert false_sink is not None
|
|
250
|
+
dsts = [str(false_sink)]
|
|
251
|
+
else:
|
|
252
|
+
dsts = _bdd_state_cubes(succ_bdd, mgr=mgr)
|
|
253
|
+
for cube in mgr.pick_iter(guard_bdd):
|
|
254
|
+
label = _cube_to_hoa_label(cube, bdd_var_to_ap_index)
|
|
255
|
+
for dst in dsts:
|
|
256
|
+
buf.write(f"[{label}] {dst}\n")
|
|
257
|
+
for sink in (true_sink, false_sink):
|
|
258
|
+
if sink is None:
|
|
259
|
+
continue
|
|
260
|
+
sets_for_sink = sorted(idx for idx, members in acc_state_sets.items() if sink in members)
|
|
261
|
+
marker = (" {" + " ".join(str(i) for i in sets_for_sink) + "}") if sets_for_sink else ""
|
|
262
|
+
buf.write(f"State: {sink}{marker}\n")
|
|
263
|
+
buf.write(f"[t] {sink}\n")
|
|
264
|
+
buf.write("--END--\n")
|
|
265
|
+
|
|
266
|
+
if owns_buffer:
|
|
267
|
+
assert isinstance(buf, io.StringIO)
|
|
268
|
+
return buf.getvalue()
|
|
269
|
+
return None
|
|
73
270
|
|
|
74
271
|
|
|
75
272
|
# ---------------------------------------------------------------------------
|
|
@@ -181,7 +378,7 @@ def _materialize_sinks(
|
|
|
181
378
|
|
|
182
379
|
|
|
183
380
|
def _write_header(
|
|
184
|
-
buf:
|
|
381
|
+
buf: Writer[str],
|
|
185
382
|
*,
|
|
186
383
|
aut: Automaton[int, ty.Any, ty.Any],
|
|
187
384
|
states: list[int],
|
|
@@ -255,7 +452,7 @@ def _flatten_or(expr: logic.BoolExpr[ty.Any]) -> list[logic.BoolExpr[ty.Any]]:
|
|
|
255
452
|
|
|
256
453
|
|
|
257
454
|
def _write_body(
|
|
258
|
-
buf:
|
|
455
|
+
buf: Writer[str],
|
|
259
456
|
*,
|
|
260
457
|
aut: Automaton[int, ty.Any, ty.Any],
|
|
261
458
|
states: list[int],
|
|
@@ -351,6 +548,69 @@ def _conj_to_dst(expr: logic.BoolExpr[int], *, context: str) -> str:
|
|
|
351
548
|
raise ValueError(f"HOA {context} expression must be a disjunction of conjunctions of state variables; got {expr!r}.")
|
|
352
549
|
|
|
353
550
|
|
|
551
|
+
# ---------------------------------------------------------------------------
|
|
552
|
+
# BDD-direct helpers (symbolic_automaton_to_hoa)
|
|
553
|
+
# ---------------------------------------------------------------------------
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _cube_to_hoa_label(
|
|
557
|
+
cube: Mapping[str, bool],
|
|
558
|
+
bdd_var_to_ap_index: Mapping[str, int],
|
|
559
|
+
) -> str:
|
|
560
|
+
"""Render a single BDD cube over AP variables as an HOA label.
|
|
561
|
+
|
|
562
|
+
*cube* is a ``{bdd_var_name: bool}`` dict from ``dd``'s
|
|
563
|
+
``BDD.pick_iter``; variables absent from the BDD's support are
|
|
564
|
+
don't-cares and do not appear in the output.
|
|
565
|
+
*bdd_var_to_ap_index* maps the BDD variable name back to the AP's
|
|
566
|
+
HOA index. Literals in the output are sorted by AP index for
|
|
567
|
+
deterministic output.
|
|
568
|
+
"""
|
|
569
|
+
if not cube:
|
|
570
|
+
return "t"
|
|
571
|
+
literals: list[tuple[int, bool]] = []
|
|
572
|
+
for var_name, polarity in cube.items():
|
|
573
|
+
try:
|
|
574
|
+
idx = bdd_var_to_ap_index[var_name]
|
|
575
|
+
except KeyError as exc:
|
|
576
|
+
raise ValueError(f"BDD cube references variable {var_name!r} not in the AP registry.") from exc
|
|
577
|
+
literals.append((idx, polarity))
|
|
578
|
+
literals.sort(key=lambda iv: iv[0])
|
|
579
|
+
parts = [(str(idx) if polarity else f"!{idx}") for idx, polarity in literals]
|
|
580
|
+
if len(parts) == 1:
|
|
581
|
+
return parts[0]
|
|
582
|
+
return "(" + " & ".join(parts) + ")"
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _bdd_state_cubes(node: "Function", *, mgr: "Manager") -> list[str]:
|
|
586
|
+
"""Enumerate satisfying cubes of *node* over state variables as HOA dst strings.
|
|
587
|
+
|
|
588
|
+
*node* is a ``dd.Function`` over the state variables ``q0..q{n-1}``;
|
|
589
|
+
*mgr* is the owning ``dd.BDD`` manager (``pick_iter`` lives on it,
|
|
590
|
+
not on Function objects). We enumerate minterms over the BDD's
|
|
591
|
+
*support* (i.e. no ``care_vars`` argument), so a point-successor
|
|
592
|
+
function naturally yields one cube.
|
|
593
|
+
|
|
594
|
+
For each cube:
|
|
595
|
+
|
|
596
|
+
- Positive state variables -> the cube's HOA destination
|
|
597
|
+
``"d1&d2&..."`` (a single integer when there is one positive).
|
|
598
|
+
- Negative state variables in the cube are dropped from the output
|
|
599
|
+
(HOA `dst` expressions list only the chosen states).
|
|
600
|
+
|
|
601
|
+
A cube with zero positive state variables is treated as a bug and
|
|
602
|
+
raises ``ValueError``; the caller is responsible for rejecting
|
|
603
|
+
literal-false successor BDDs before reaching here.
|
|
604
|
+
"""
|
|
605
|
+
cubes: list[str] = []
|
|
606
|
+
for cube in mgr.pick_iter(node):
|
|
607
|
+
positives = sorted(int(name[1:]) for name, val in cube.items() if val)
|
|
608
|
+
if not positives:
|
|
609
|
+
raise ValueError("Successor BDD cube has no positive state variable; cannot render an HOA destination.")
|
|
610
|
+
cubes.append("&".join(str(i) for i in positives))
|
|
611
|
+
return cubes
|
|
612
|
+
|
|
613
|
+
|
|
354
614
|
# ---------------------------------------------------------------------------
|
|
355
615
|
# Acceptance
|
|
356
616
|
# ---------------------------------------------------------------------------
|
|
@@ -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 typing_extensions import Writer
|
|
48
|
+
|
|
47
49
|
from morphata.alphabet import BooleanAlphabet
|
|
48
50
|
from morphata.symbolic.bdd import (
|
|
49
51
|
APRegistry,
|
|
@@ -230,11 +232,18 @@ class SymbolicAutomaton(Generic[AP]):
|
|
|
230
232
|
*,
|
|
231
233
|
name: str | None = None,
|
|
232
234
|
properties: Sequence[str] | None = None,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
235
|
+
out: "Writer[str] | None" = None,
|
|
236
|
+
) -> str | None:
|
|
237
|
+
"""Serialize ``self`` to HOA v1 via :func:`symbolic_automaton_to_hoa`.
|
|
238
|
+
|
|
239
|
+
Walks the cached transition BDDs directly -- no
|
|
240
|
+
``logic_asts.BoolExpr`` intermediates. When ``out`` is given,
|
|
241
|
+
streams output into it and returns ``None``; otherwise returns
|
|
242
|
+
the full HOA string.
|
|
243
|
+
"""
|
|
244
|
+
from morphata.hoa import symbolic_automaton_to_hoa
|
|
236
245
|
|
|
237
|
-
return
|
|
246
|
+
return symbolic_automaton_to_hoa(self, name=name, properties=properties, out=out)
|
|
238
247
|
|
|
239
248
|
def to_min_dfa(self) -> "SymbolicAutomaton[AP]":
|
|
240
249
|
"""Determinize and minimize ``self`` into a minimal DFA.
|