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.
Files changed (134) hide show
  1. {morphata-2.0.3 → morphata-2.0.4}/CHANGELOG.md +9 -0
  2. {morphata-2.0.3 → morphata-2.0.4}/PKG-INFO +1 -1
  3. {morphata-2.0.3 → morphata-2.0.4}/pyproject.toml +1 -1
  4. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/hoa/exporter.py +105 -0
  5. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/symbolic/automaton.py +70 -0
  6. morphata-2.0.4/tests/symbolic/test_to_hoa.py +142 -0
  7. {morphata-2.0.3 → morphata-2.0.4}/uv.lock +1 -1
  8. {morphata-2.0.3 → morphata-2.0.4}/.basedpyright/baseline.json +0 -0
  9. {morphata-2.0.3 → morphata-2.0.4}/.claude/settings.local.json +0 -0
  10. {morphata-2.0.3 → morphata-2.0.4}/.dockerignore +0 -0
  11. {morphata-2.0.3 → morphata-2.0.4}/.editorconfig +0 -0
  12. {morphata-2.0.3 → morphata-2.0.4}/.envrc +0 -0
  13. {morphata-2.0.3 → morphata-2.0.4}/.envrc.recommended +0 -0
  14. {morphata-2.0.3 → morphata-2.0.4}/.fdignore +0 -0
  15. {morphata-2.0.3 → morphata-2.0.4}/.gitattributes +0 -0
  16. {morphata-2.0.3 → morphata-2.0.4}/.gitignore +0 -0
  17. {morphata-2.0.3 → morphata-2.0.4}/.mailmap +0 -0
  18. {morphata-2.0.3 → morphata-2.0.4}/.python-version +0 -0
  19. {morphata-2.0.3 → morphata-2.0.4}/.rstcheck.cfg +0 -0
  20. {morphata-2.0.3 → morphata-2.0.4}/AGENTS.md +0 -0
  21. {morphata-2.0.3 → morphata-2.0.4}/CLAUDE.md +0 -0
  22. {morphata-2.0.3 → morphata-2.0.4}/CONTRIBUTING.md +0 -0
  23. {morphata-2.0.3 → morphata-2.0.4}/LICENSE +0 -0
  24. {morphata-2.0.3 → morphata-2.0.4}/README.md +0 -0
  25. {morphata-2.0.3 → morphata-2.0.4}/bench/README.md +0 -0
  26. {morphata-2.0.3 → morphata-2.0.4}/bench/__init__.py +0 -0
  27. {morphata-2.0.3 → morphata-2.0.4}/bench/conftest.py +0 -0
  28. {morphata-2.0.3 → morphata-2.0.4}/bench/test_sere_nlm_perf.py +0 -0
  29. {morphata-2.0.3 → morphata-2.0.4}/ci/make-release +0 -0
  30. {morphata-2.0.3 → morphata-2.0.4}/cliff.toml +0 -0
  31. {morphata-2.0.3 → morphata-2.0.4}/docs/_static/navigation.html +0 -0
  32. {morphata-2.0.3 → morphata-2.0.4}/docs/api/modules.rst +0 -0
  33. {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.acceptance.rst +0 -0
  34. {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.alphabet.rst +0 -0
  35. {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.automaton.rst +0 -0
  36. {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.builder.rst +0 -0
  37. {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.hoa.rst +0 -0
  38. {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.logic.ltl.rst +0 -0
  39. {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.logic.psl.rst +0 -0
  40. {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.logic.sere.rst +0 -0
  41. {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.logic.strel.rst +0 -0
  42. {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.operators.matrix.rst +0 -0
  43. {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.operators.polynomial.rst +0 -0
  44. {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.operators.rst +0 -0
  45. {morphata-2.0.3 → morphata-2.0.4}/docs/api/morphata.symbolic.rst +0 -0
  46. {morphata-2.0.3 → morphata-2.0.4}/docs/concepts/acceptance.rst +0 -0
  47. {morphata-2.0.3 → morphata-2.0.4}/docs/concepts/hoa.rst +0 -0
  48. {morphata-2.0.3 → morphata-2.0.4}/docs/concepts/index.rst +0 -0
  49. {morphata-2.0.3 → morphata-2.0.4}/docs/concepts/operators.rst +0 -0
  50. {morphata-2.0.3 → morphata-2.0.4}/docs/concepts/spec.rst +0 -0
  51. {morphata-2.0.3 → morphata-2.0.4}/docs/concepts/weights.rst +0 -0
  52. {morphata-2.0.3 → morphata-2.0.4}/docs/conf.py +0 -0
  53. {morphata-2.0.3 → morphata-2.0.4}/docs/index.rst +0 -0
  54. {morphata-2.0.3 → morphata-2.0.4}/docs/quick-start.rst +0 -0
  55. {morphata-2.0.3 → morphata-2.0.4}/docs/symbolic-transforms.rst +0 -0
  56. {morphata-2.0.3 → morphata-2.0.4}/justfile +0 -0
  57. {morphata-2.0.3 → morphata-2.0.4}/mypy.ini +0 -0
  58. {morphata-2.0.3 → morphata-2.0.4}/pixi.lock +0 -0
  59. {morphata-2.0.3 → morphata-2.0.4}/pixi.toml +0 -0
  60. {morphata-2.0.3 → morphata-2.0.4}/project.utf-8.add +0 -0
  61. {morphata-2.0.3 → morphata-2.0.4}/project.utf-8.add.spl +0 -0
  62. {morphata-2.0.3 → morphata-2.0.4}/pyrefly.toml +0 -0
  63. {morphata-2.0.3 → morphata-2.0.4}/pyrightconfig.json +0 -0
  64. {morphata-2.0.3 → morphata-2.0.4}/pytest.toml +0 -0
  65. {morphata-2.0.3 → morphata-2.0.4}/ruff.toml +0 -0
  66. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/__init__.py +0 -0
  67. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/acceptance.py +0 -0
  68. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/alphabet.py +0 -0
  69. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/automaton.py +0 -0
  70. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/builder.py +0 -0
  71. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/hoa/__init__.py +0 -0
  72. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/hoa/__main__.py +0 -0
  73. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/hoa/acc_expr.py +0 -0
  74. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/hoa/hoa.lark +0 -0
  75. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/hoa/parser.py +0 -0
  76. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/logic/__init__.py +0 -0
  77. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/logic/_common.py +0 -0
  78. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/logic/ltl.py +0 -0
  79. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/logic/psl.py +0 -0
  80. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/logic/sere.py +0 -0
  81. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/logic/strel.py +0 -0
  82. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/operators/__init__.py +0 -0
  83. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/operators/_backend.py +0 -0
  84. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/operators/matrix.py +0 -0
  85. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/operators/polynomial.py +0 -0
  86. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/py.typed +0 -0
  87. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/symbolic/__init__.py +0 -0
  88. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/symbolic/bdd.py +0 -0
  89. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/symbolic/compose.py +0 -0
  90. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/symbolic/equivalence.py +0 -0
  91. {morphata-2.0.3 → morphata-2.0.4}/src/morphata/symbolic/transforms.py +0 -0
  92. {morphata-2.0.3 → morphata-2.0.4}/tests/conftest.py +0 -0
  93. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut1.hoa +0 -0
  94. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut11.hoa +0 -0
  95. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut2.hoa +0 -0
  96. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut3.2.hoa +0 -0
  97. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut3.hoa +0 -0
  98. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut4.hoa +0 -0
  99. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut5.hoa +0 -0
  100. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut6.hoa +0 -0
  101. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut7.hoa +0 -0
  102. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/aut8.hoa +0 -0
  103. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/finite_aut.hoa +0 -0
  104. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/gen_buchi_3.hoa +0 -0
  105. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/gen_cobuchi_2.hoa +0 -0
  106. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/rabin_2pair.hoa +0 -0
  107. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/streett_2pair.hoa +0 -0
  108. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/test_exporter_basic.py +0 -0
  109. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/test_exporter_roundtrip.py +0 -0
  110. {morphata-2.0.3 → morphata-2.0.4}/tests/hoa/test_parser.py +0 -0
  111. {morphata-2.0.3 → morphata-2.0.4}/tests/logic/__init__.py +0 -0
  112. {morphata-2.0.3 → morphata-2.0.4}/tests/logic/test_ltl.py +0 -0
  113. {morphata-2.0.3 → morphata-2.0.4}/tests/logic/test_psl.py +0 -0
  114. {morphata-2.0.3 → morphata-2.0.4}/tests/logic/test_sere.py +0 -0
  115. {morphata-2.0.3 → morphata-2.0.4}/tests/logic/test_sere_properties.py +0 -0
  116. {morphata-2.0.3 → morphata-2.0.4}/tests/logic/test_strel.py +0 -0
  117. {morphata-2.0.3 → morphata-2.0.4}/tests/operators/test_matrix_from_ir.py +0 -0
  118. {morphata-2.0.3 → morphata-2.0.4}/tests/operators/test_matrix_from_ltl.py +0 -0
  119. {morphata-2.0.3 → morphata-2.0.4}/tests/operators/test_polynomial_from_ir.py +0 -0
  120. {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_bdd_helpers.py +0 -0
  121. {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_compose.py +0 -0
  122. {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_from_hoaf.py +0 -0
  123. {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_ltlf2dfa_equivalence.py +0 -0
  124. {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_step.py +0 -0
  125. {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_symbolic_automaton.py +0 -0
  126. {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_symbolic_from_ir.py +0 -0
  127. {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_transforms.py +0 -0
  128. {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_transforms_ir.py +0 -0
  129. {morphata-2.0.3 → morphata-2.0.4}/tests/symbolic/test_transforms_properties.py +0 -0
  130. {morphata-2.0.3 → morphata-2.0.4}/tests/test_alphabet.py +0 -0
  131. {morphata-2.0.3 → morphata-2.0.4}/tests/test_automaton.py +0 -0
  132. {morphata-2.0.3 → morphata-2.0.4}/tests/test_builder.py +0 -0
  133. {morphata-2.0.3 → morphata-2.0.4}/tests/test_from_parsed.py +0 -0
  134. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphata
3
- Version: 2.0.3
3
+ Version: 2.0.4
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
@@ -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.3"
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
@@ -1049,7 +1049,7 @@ wheels = [
1049
1049
 
1050
1050
  [[package]]
1051
1051
  name = "morphata"
1052
- version = "2.0.3"
1052
+ version = "2.0.4"
1053
1053
  source = { editable = "." }
1054
1054
  dependencies = [
1055
1055
  { name = "algebraic-arrays" },
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