morphata 2.0.3__tar.gz → 2.0.4__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.3 → morphata-2.0.4}/CHANGELOG.md +9 -0
- {morphata-2.0.3 → morphata-2.0.4}/PKG-INFO +1 -1
- {morphata-2.0.3 → morphata-2.0.4}/pyproject.toml +1 -1
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/hoa/exporter.py +105 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/symbolic/automaton.py +70 -0
- morphata-2.0.4/tests/symbolic/test_to_hoa.py +142 -0
- {morphata-2.0.3 → morphata-2.0.4}/uv.lock +1 -1
- {morphata-2.0.3 → morphata-2.0.4}/.basedpyright/baseline.json +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/.claude/settings.local.json +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/.dockerignore +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/.editorconfig +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/.envrc +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/.envrc.recommended +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/.fdignore +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/.gitattributes +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/.gitignore +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/.mailmap +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/.python-version +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/.rstcheck.cfg +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/AGENTS.md +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/CLAUDE.md +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/CONTRIBUTING.md +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/LICENSE +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/README.md +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/bench/README.md +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/bench/__init__.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/bench/conftest.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/bench/test_sere_nlm_perf.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/ci/make-release +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/cliff.toml +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/_static/navigation.html +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/api/modules.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.acceptance.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.alphabet.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.automaton.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.builder.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.hoa.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.logic.ltl.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.logic.psl.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.logic.sere.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.logic.strel.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.operators.matrix.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.operators.polynomial.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.operators.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.symbolic.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/concepts/acceptance.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/concepts/hoa.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/concepts/index.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/concepts/operators.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/concepts/spec.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/concepts/weights.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/conf.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/index.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/quick-start.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/docs/symbolic-transforms.rst +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/justfile +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/mypy.ini +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/pixi.lock +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/pixi.toml +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/project.utf-8.add +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/project.utf-8.add.spl +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/pyrefly.toml +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/pyrightconfig.json +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/pytest.toml +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/ruff.toml +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/__init__.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/acceptance.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/alphabet.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/automaton.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/builder.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/hoa/__init__.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/hoa/__main__.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/hoa/acc_expr.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/hoa/hoa.lark +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/hoa/parser.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/logic/__init__.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/logic/_common.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/logic/ltl.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/logic/psl.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/logic/sere.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/logic/strel.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/operators/__init__.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/operators/_backend.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/operators/matrix.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/operators/polynomial.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/py.typed +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/symbolic/__init__.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/symbolic/bdd.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/symbolic/compose.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/symbolic/equivalence.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/src/morphata/symbolic/transforms.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/conftest.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut1.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut11.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut2.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut3.2.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut3.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut4.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut5.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut6.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut7.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut8.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/finite_aut.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/gen_buchi_3.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/gen_cobuchi_2.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/rabin_2pair.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/streett_2pair.hoa +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/test_exporter_basic.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/test_exporter_roundtrip.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/test_parser.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/logic/__init__.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/logic/test_ltl.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/logic/test_psl.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/logic/test_sere.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/logic/test_sere_properties.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/logic/test_strel.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/operators/test_matrix_from_ir.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/operators/test_matrix_from_ltl.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/operators/test_polynomial_from_ir.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_bdd_helpers.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_compose.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_from_hoaf.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_ltlf2dfa_equivalence.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_step.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_symbolic_automaton.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_symbolic_from_ir.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_transforms.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_transforms_ir.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_transforms_properties.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/test_alphabet.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/test_automaton.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/test_builder.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/tests/test_from_parsed.py +0 -0
- {morphata-2.0.3 → morphata-2.0.4}/ty.toml +0 -0
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.4 — 2026-05-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
Technically a feature release almost immediately
|
|
8
|
+
after another feature release masquerading as a patch,
|
|
9
|
+
this release adds the ability to translate from a `SymbolicAutomaton` back to an
|
|
10
|
+
`Automaton`, allowing for the `SymbolicAutomaton` to be exported to an HOA file.
|
|
11
|
+
|
|
3
12
|
## 2.0.3 — 2026-05-28
|
|
4
13
|
|
|
5
14
|
### 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.4"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
classifiers = [
|
|
8
8
|
"Intended Audience :: Science/Research",
|
|
@@ -7,6 +7,7 @@ The morphata-specific ``Final(n)`` extension is preserved.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import dataclasses
|
|
10
11
|
import io
|
|
11
12
|
import typing as ty
|
|
12
13
|
from collections.abc import Iterable, Mapping, Sequence
|
|
@@ -43,6 +44,7 @@ def automaton_to_hoa(
|
|
|
43
44
|
if isinstance(aut.delta, Step):
|
|
44
45
|
raise TypeError("HOA export requires an Outgoing delta; got Step.")
|
|
45
46
|
|
|
47
|
+
aut = _materialize_sinks(aut)
|
|
46
48
|
states = sorted(_require_int_states(aut))
|
|
47
49
|
ap_order = _ap_order(aut.predicates)
|
|
48
50
|
ap_to_index = {ap: i for i, ap in enumerate(ap_order)}
|
|
@@ -70,6 +72,109 @@ def automaton_to_hoa(
|
|
|
70
72
|
return buf.getvalue()
|
|
71
73
|
|
|
72
74
|
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Sink-state materialisation
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _materialize_sinks(
|
|
81
|
+
aut: Automaton[int, ty.Any, ty.Any],
|
|
82
|
+
) -> Automaton[int, ty.Any, ty.Any]:
|
|
83
|
+
"""Replace ``Literal(True)`` / ``Literal(False)`` successors with sink states.
|
|
84
|
+
|
|
85
|
+
HOA does not have an in-band representation of "accept here" / "reject
|
|
86
|
+
here" successors. AFAs naturally produce them: a satisfied conjunctive
|
|
87
|
+
obligation lowers to ``Literal(True)``; a contradiction lowers to
|
|
88
|
+
``Literal(False)``. We materialise explicit accepting / rejecting
|
|
89
|
+
sink states with self-loops and rewrite any ``Literal`` successors
|
|
90
|
+
(and the initial expression) to point at them.
|
|
91
|
+
|
|
92
|
+
Only :class:`~morphata.acceptance.Finite` acceptance is currently
|
|
93
|
+
supported; omega-regular acceptance conditions with literal
|
|
94
|
+
successors raise ``NotImplementedError`` because correct sink-set
|
|
95
|
+
placement depends on the condition's set semantics.
|
|
96
|
+
"""
|
|
97
|
+
from morphata.acceptance import Finite
|
|
98
|
+
from morphata.automaton import Outgoing
|
|
99
|
+
|
|
100
|
+
if not isinstance(aut.delta, Outgoing):
|
|
101
|
+
return aut
|
|
102
|
+
|
|
103
|
+
states_iter = aut.iter_states()
|
|
104
|
+
if states_iter is None:
|
|
105
|
+
return aut
|
|
106
|
+
states = sorted(states_iter)
|
|
107
|
+
if not states or not all(isinstance(s, int) for s in states):
|
|
108
|
+
return aut
|
|
109
|
+
|
|
110
|
+
rows_by_state: dict[int, list[tuple[logic.BoolExpr[ty.Any], logic.BoolExpr[int]]]] = {
|
|
111
|
+
s: list(aut.delta.fn(s)) for s in states
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
needs_true = isinstance(aut.initial, logic.Literal) and bool(aut.initial.value)
|
|
115
|
+
needs_false = isinstance(aut.initial, logic.Literal) and not aut.initial.value
|
|
116
|
+
for rows in rows_by_state.values():
|
|
117
|
+
for _, succ in rows:
|
|
118
|
+
if isinstance(succ, logic.Literal):
|
|
119
|
+
if succ.value:
|
|
120
|
+
needs_true = True
|
|
121
|
+
else:
|
|
122
|
+
needs_false = True
|
|
123
|
+
|
|
124
|
+
if not (needs_true or needs_false):
|
|
125
|
+
return aut
|
|
126
|
+
|
|
127
|
+
if not isinstance(aut.acceptance, Finite):
|
|
128
|
+
raise NotImplementedError(
|
|
129
|
+
"HOA export: Literal(True)/Literal(False) successors require "
|
|
130
|
+
"sink-state materialisation, which is only implemented for "
|
|
131
|
+
f"Finite acceptance; got {type(aut.acceptance).__name__}."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
next_id = max(states) + 1
|
|
135
|
+
true_sink: int | None = None
|
|
136
|
+
false_sink: int | None = None
|
|
137
|
+
if needs_true:
|
|
138
|
+
true_sink = next_id
|
|
139
|
+
next_id += 1
|
|
140
|
+
if needs_false:
|
|
141
|
+
false_sink = next_id
|
|
142
|
+
next_id += 1
|
|
143
|
+
|
|
144
|
+
def _swap(expr: logic.BoolExpr[int]) -> logic.BoolExpr[int]:
|
|
145
|
+
if isinstance(expr, logic.Literal):
|
|
146
|
+
if expr.value and true_sink is not None:
|
|
147
|
+
return logic.Variable(true_sink)
|
|
148
|
+
if not expr.value and false_sink is not None:
|
|
149
|
+
return logic.Variable(false_sink)
|
|
150
|
+
return expr
|
|
151
|
+
|
|
152
|
+
new_rows: dict[int, list[tuple[logic.BoolExpr[ty.Any], logic.BoolExpr[int]]]] = {
|
|
153
|
+
s: [(g, _swap(succ)) for g, succ in rs] for s, rs in rows_by_state.items()
|
|
154
|
+
}
|
|
155
|
+
if true_sink is not None:
|
|
156
|
+
new_rows[true_sink] = [(logic.Literal(True), logic.Variable(true_sink))]
|
|
157
|
+
if false_sink is not None:
|
|
158
|
+
new_rows[false_sink] = [(logic.Literal(True), logic.Variable(false_sink))]
|
|
159
|
+
|
|
160
|
+
new_initial = _swap(aut.initial)
|
|
161
|
+
new_accepting = frozenset(aut.acceptance.accepting) | ({true_sink} if true_sink is not None else frozenset[int]())
|
|
162
|
+
|
|
163
|
+
def _fn(
|
|
164
|
+
s: int,
|
|
165
|
+
_r: dict[int, list[tuple[logic.BoolExpr[ty.Any], logic.BoolExpr[int]]]] = new_rows,
|
|
166
|
+
) -> Iterable[tuple[logic.BoolExpr[ty.Any], logic.BoolExpr[int]]]:
|
|
167
|
+
return iter(_r.get(s, []))
|
|
168
|
+
|
|
169
|
+
return dataclasses.replace(
|
|
170
|
+
aut,
|
|
171
|
+
states=frozenset(range(next_id)),
|
|
172
|
+
initial=new_initial,
|
|
173
|
+
delta=Outgoing(fn=_fn),
|
|
174
|
+
acceptance=Finite(accepting=new_accepting),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
73
178
|
# ---------------------------------------------------------------------------
|
|
74
179
|
# Header
|
|
75
180
|
# ---------------------------------------------------------------------------
|
|
@@ -166,6 +166,76 @@ class SymbolicAutomaton(Generic[AP]):
|
|
|
166
166
|
"""``True`` iff the automaton accepts *word* under finite-word semantics."""
|
|
167
167
|
return self.evaluate_at_accepting(self.run(word))
|
|
168
168
|
|
|
169
|
+
def to_automaton(self) -> "Automaton[int, Any, AP]":
|
|
170
|
+
"""Round-trip the BDD-shaped automaton back to the canonical IR.
|
|
171
|
+
|
|
172
|
+
Decodes guard BDDs through ``self.ap_registry`` (BDD var name
|
|
173
|
+
-> AP) and successor BDDs through the default ``q{i} -> int``
|
|
174
|
+
decoder. Emits one IR row per cached ``(guard_bdd, succ_bdd)``
|
|
175
|
+
pair; no merging or simplification.
|
|
176
|
+
|
|
177
|
+
Raises
|
|
178
|
+
------
|
|
179
|
+
ValueError
|
|
180
|
+
If ``self`` was constructed with ``cache_transitions=False``
|
|
181
|
+
and there are no rows to serialize.
|
|
182
|
+
"""
|
|
183
|
+
from morphata.automaton import Outgoing
|
|
184
|
+
from morphata.symbolic.bdd import bdd_to_boolexpr
|
|
185
|
+
|
|
186
|
+
if self.num_states > 0 and not self.transitions:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
"to_automaton requires a populated transition cache; "
|
|
189
|
+
"reconstruct SymbolicAutomaton with cache_transitions=True."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
ap_decoder_map: dict[str, AP] = {var: ap for ap, var in self.ap_registry.items()}
|
|
193
|
+
|
|
194
|
+
def _ap_decode(name: str) -> AP:
|
|
195
|
+
try:
|
|
196
|
+
return ap_decoder_map[name]
|
|
197
|
+
except KeyError as exc:
|
|
198
|
+
raise ValueError(f"Guard BDD references unknown variable {name!r}") from exc
|
|
199
|
+
|
|
200
|
+
rows_by_state: dict[int, list[tuple[BoolExpr[AP], BoolExpr[int]]]] = {}
|
|
201
|
+
for s in range(self.num_states):
|
|
202
|
+
decoded_rows: list[tuple[BoolExpr[AP], BoolExpr[int]]] = []
|
|
203
|
+
for guard_bdd, succ_bdd in self.transitions.get(s, ()):
|
|
204
|
+
guard_expr = typing.cast(
|
|
205
|
+
"BoolExpr[AP]",
|
|
206
|
+
bdd_to_boolexpr(guard_bdd, var_decoder=_ap_decode),
|
|
207
|
+
)
|
|
208
|
+
succ_expr = typing.cast("BoolExpr[int]", bdd_to_boolexpr(succ_bdd))
|
|
209
|
+
decoded_rows.append((guard_expr, succ_expr))
|
|
210
|
+
rows_by_state[s] = decoded_rows
|
|
211
|
+
|
|
212
|
+
def _delta_fn(
|
|
213
|
+
s: int,
|
|
214
|
+
_rows: dict[int, list[tuple[BoolExpr[AP], BoolExpr[int]]]] = rows_by_state,
|
|
215
|
+
) -> Iterable[tuple[BoolExpr[AP], BoolExpr[int]]]:
|
|
216
|
+
return iter(_rows.get(s, []))
|
|
217
|
+
|
|
218
|
+
initial_expr = typing.cast("BoolExpr[int]", bdd_to_boolexpr(self.initial))
|
|
219
|
+
|
|
220
|
+
return Automaton(
|
|
221
|
+
states=frozenset(range(self.num_states)),
|
|
222
|
+
predicates=frozenset(self.ap_registry),
|
|
223
|
+
initial=initial_expr,
|
|
224
|
+
delta=Outgoing(fn=_delta_fn),
|
|
225
|
+
acceptance=self.acceptance,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def to_hoa(
|
|
229
|
+
self,
|
|
230
|
+
*,
|
|
231
|
+
name: str | None = None,
|
|
232
|
+
properties: Sequence[str] | None = None,
|
|
233
|
+
) -> str:
|
|
234
|
+
"""Serialize ``self`` to an HOA v1 string via :func:`automaton_to_hoa`."""
|
|
235
|
+
from morphata.hoa import automaton_to_hoa
|
|
236
|
+
|
|
237
|
+
return automaton_to_hoa(self.to_automaton(), name=name, properties=properties)
|
|
238
|
+
|
|
169
239
|
def to_min_dfa(self) -> "SymbolicAutomaton[AP]":
|
|
170
240
|
"""Determinize and minimize ``self`` into a minimal DFA.
|
|
171
241
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Tests for ``SymbolicAutomaton.to_automaton`` and ``SymbolicAutomaton.to_hoa``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import itertools
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import logic_asts as logic
|
|
9
|
+
import logic_asts.ltl as ltl
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
import morphata.hoa
|
|
13
|
+
from morphata.acceptance import Finite
|
|
14
|
+
from morphata.alphabet import powerset_alphabet
|
|
15
|
+
from morphata.automaton import Automaton, Outgoing
|
|
16
|
+
from morphata.builder import AutomatonBuilder
|
|
17
|
+
from morphata.symbolic import SymbolicAutomaton
|
|
18
|
+
from morphata.symbolic.bdd import Manager
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _lift(aut: Automaton[int, Any, str], mgr: Manager) -> SymbolicAutomaton[str]:
|
|
22
|
+
alphabet = powerset_alphabet(aut.predicates)
|
|
23
|
+
return SymbolicAutomaton.from_automaton(aut, alphabet=alphabet, manager=mgr)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _round_trip(sym: SymbolicAutomaton[str], mgr: Manager) -> SymbolicAutomaton[str]:
|
|
27
|
+
"""Export *sym* to HOA, re-parse, and lift back into a SymbolicAutomaton."""
|
|
28
|
+
hoa_text = sym.to_hoa()
|
|
29
|
+
parsed = morphata.hoa.parse(hoa_text)
|
|
30
|
+
aut = Automaton.from_parsed(parsed)
|
|
31
|
+
return _lift(aut, mgr)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _all_words(aps: frozenset[str], lengths: range) -> list[list[frozenset[str]]]:
|
|
35
|
+
aps_sorted = sorted(aps)
|
|
36
|
+
symbols = [frozenset(s) for r in range(len(aps_sorted) + 1) for s in itertools.combinations(aps_sorted, r)]
|
|
37
|
+
return [list(w) for n in lengths for w in itertools.product(symbols, repeat=n)]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# to_automaton: structural checks
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestToAutomatonStructure:
|
|
46
|
+
def test_preserves_predicates_and_states(self, mgr: Manager) -> None:
|
|
47
|
+
b = AutomatonBuilder[int, str]()
|
|
48
|
+
b.add_state(0, initial=True)
|
|
49
|
+
b.add_state(1, accepting=True)
|
|
50
|
+
b.add_transition(0, logic.Variable("a"), 1)
|
|
51
|
+
b.add_transition(1, logic.Variable("a"), 1)
|
|
52
|
+
aut = b.build()
|
|
53
|
+
sym = _lift(aut, mgr)
|
|
54
|
+
|
|
55
|
+
roundtrip = sym.to_automaton()
|
|
56
|
+
assert roundtrip.predicates == frozenset({"a"})
|
|
57
|
+
assert set(roundtrip.iter_states() or ()) == {0, 1}
|
|
58
|
+
assert roundtrip.acceptance is sym.acceptance
|
|
59
|
+
assert isinstance(roundtrip.delta, Outgoing)
|
|
60
|
+
|
|
61
|
+
def test_row_count_matches_transition_cache(self, mgr: Manager) -> None:
|
|
62
|
+
b = AutomatonBuilder[int, str]()
|
|
63
|
+
b.add_state(0, initial=True, accepting=True)
|
|
64
|
+
b.add_state(1, accepting=True)
|
|
65
|
+
b.add_transition(0, logic.Variable("a"), 1)
|
|
66
|
+
b.add_transition(0, logic.Variable("b"), 0)
|
|
67
|
+
b.add_transition(1, logic.Variable("a"), 0)
|
|
68
|
+
aut = b.build()
|
|
69
|
+
sym = _lift(aut, mgr)
|
|
70
|
+
|
|
71
|
+
roundtrip = sym.to_automaton()
|
|
72
|
+
assert isinstance(roundtrip.delta, Outgoing)
|
|
73
|
+
for s in range(sym.num_states):
|
|
74
|
+
assert len(list(roundtrip.delta.fn(s))) == len(sym.transitions[s])
|
|
75
|
+
|
|
76
|
+
def test_no_cache_raises(self, mgr: Manager) -> None:
|
|
77
|
+
b = AutomatonBuilder[int, str]()
|
|
78
|
+
b.add_state(0, initial=True, accepting=True)
|
|
79
|
+
b.add_transition(0, logic.Variable("a"), 0)
|
|
80
|
+
aut = b.build()
|
|
81
|
+
alphabet = powerset_alphabet(aut.predicates)
|
|
82
|
+
sym = SymbolicAutomaton.from_automaton(aut, alphabet=alphabet, manager=mgr, cache_transitions=False)
|
|
83
|
+
with pytest.raises(ValueError, match="cache_transitions=True"):
|
|
84
|
+
sym.to_automaton()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# to_hoa: semantic round-trip via HOA parse
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestToHoaRoundTrip:
|
|
93
|
+
def test_builder_dfa(self, mgr_factory: Any) -> None:
|
|
94
|
+
# Accept words with at least one 'a'.
|
|
95
|
+
mgr1 = mgr_factory()
|
|
96
|
+
b = AutomatonBuilder[int, str]()
|
|
97
|
+
b.add_state(0, initial=True)
|
|
98
|
+
b.add_state(1, accepting=True)
|
|
99
|
+
b.add_transition(0, logic.Variable("a"), 1)
|
|
100
|
+
b.add_transition(0, logic.Not(logic.Variable("a")), 0)
|
|
101
|
+
b.add_transition(1, logic.Literal(True), 1)
|
|
102
|
+
aut = b.build()
|
|
103
|
+
sym = _lift(aut, mgr1)
|
|
104
|
+
|
|
105
|
+
mgr2 = mgr_factory()
|
|
106
|
+
reparsed = _round_trip(sym, mgr2)
|
|
107
|
+
|
|
108
|
+
for w in _all_words(frozenset({"a"}), range(0, 4)):
|
|
109
|
+
assert sym.accepts(w) == reparsed.accepts(w), w
|
|
110
|
+
|
|
111
|
+
def test_alternating_ltlf(self, mgr_factory: Any) -> None:
|
|
112
|
+
# F a -- alternating AFA; produces a Literal(True) successor sink.
|
|
113
|
+
formula = ltl.Eventually(logic.Variable("a"))
|
|
114
|
+
mgr1 = mgr_factory()
|
|
115
|
+
alphabet = powerset_alphabet(frozenset({"a"}))
|
|
116
|
+
sym = SymbolicAutomaton.from_ltl(formula, alphabet=alphabet, finite=True, manager=mgr1)
|
|
117
|
+
assert isinstance(sym.acceptance, Finite)
|
|
118
|
+
|
|
119
|
+
mgr2 = mgr_factory()
|
|
120
|
+
reparsed = _round_trip(sym, mgr2)
|
|
121
|
+
|
|
122
|
+
for w in _all_words(frozenset({"a"}), range(0, 4)):
|
|
123
|
+
assert sym.accepts(w) == reparsed.accepts(w), w
|
|
124
|
+
|
|
125
|
+
def test_min_dfa_of_ltlf(self, mgr_factory: Any) -> None:
|
|
126
|
+
# F(a) & G(a -> X b)
|
|
127
|
+
formula = ltl.And(
|
|
128
|
+
(
|
|
129
|
+
ltl.Eventually(logic.Variable("a")),
|
|
130
|
+
ltl.Always(logic.Implies(logic.Variable("a"), ltl.Next(logic.Variable("b")))),
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
mgr1 = mgr_factory()
|
|
134
|
+
alphabet = powerset_alphabet(frozenset({"a", "b"}))
|
|
135
|
+
sym = SymbolicAutomaton.from_ltl(formula, alphabet=alphabet, finite=True, manager=mgr1)
|
|
136
|
+
sym_min = sym.to_min_dfa()
|
|
137
|
+
|
|
138
|
+
mgr2 = mgr_factory()
|
|
139
|
+
reparsed = _round_trip(sym_min, mgr2)
|
|
140
|
+
|
|
141
|
+
for w in _all_words(frozenset({"a", "b"}), range(0, 4)):
|
|
142
|
+
assert sym_min.accepts(w) == reparsed.accepts(w), w
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|