morphata 2.0.6__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.
Files changed (138) hide show
  1. {morphata-2.0.6 → morphata-2.0.7}/CHANGELOG.md +7 -0
  2. {morphata-2.0.6 → morphata-2.0.7}/PKG-INFO +1 -1
  3. {morphata-2.0.6 → morphata-2.0.7}/pyproject.toml +1 -1
  4. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/symbolic/persistence.py +45 -10
  5. {morphata-2.0.6 → morphata-2.0.7}/tests/symbolic/test_persistence.py +50 -0
  6. {morphata-2.0.6 → morphata-2.0.7}/uv.lock +1 -1
  7. {morphata-2.0.6 → morphata-2.0.7}/.basedpyright/baseline.json +0 -0
  8. {morphata-2.0.6 → morphata-2.0.7}/.claude/settings.local.json +0 -0
  9. {morphata-2.0.6 → morphata-2.0.7}/.dockerignore +0 -0
  10. {morphata-2.0.6 → morphata-2.0.7}/.editorconfig +0 -0
  11. {morphata-2.0.6 → morphata-2.0.7}/.envrc +0 -0
  12. {morphata-2.0.6 → morphata-2.0.7}/.envrc.recommended +0 -0
  13. {morphata-2.0.6 → morphata-2.0.7}/.fdignore +0 -0
  14. {morphata-2.0.6 → morphata-2.0.7}/.gitattributes +0 -0
  15. {morphata-2.0.6 → morphata-2.0.7}/.gitignore +0 -0
  16. {morphata-2.0.6 → morphata-2.0.7}/.mailmap +0 -0
  17. {morphata-2.0.6 → morphata-2.0.7}/.python-version +0 -0
  18. {morphata-2.0.6 → morphata-2.0.7}/.rstcheck.cfg +0 -0
  19. {morphata-2.0.6 → morphata-2.0.7}/AGENTS.md +0 -0
  20. {morphata-2.0.6 → morphata-2.0.7}/CLAUDE.md +0 -0
  21. {morphata-2.0.6 → morphata-2.0.7}/CONTRIBUTING.md +0 -0
  22. {morphata-2.0.6 → morphata-2.0.7}/LICENSE +0 -0
  23. {morphata-2.0.6 → morphata-2.0.7}/README.md +0 -0
  24. {morphata-2.0.6 → morphata-2.0.7}/bench/README.md +0 -0
  25. {morphata-2.0.6 → morphata-2.0.7}/bench/__init__.py +0 -0
  26. {morphata-2.0.6 → morphata-2.0.7}/bench/conftest.py +0 -0
  27. {morphata-2.0.6 → morphata-2.0.7}/bench/test_sere_nlm_perf.py +0 -0
  28. {morphata-2.0.6 → morphata-2.0.7}/ci/make-release +0 -0
  29. {morphata-2.0.6 → morphata-2.0.7}/cliff.toml +0 -0
  30. {morphata-2.0.6 → morphata-2.0.7}/docs/_static/navigation.html +0 -0
  31. {morphata-2.0.6 → morphata-2.0.7}/docs/api/modules.rst +0 -0
  32. {morphata-2.0.6 → morphata-2.0.7}/docs/api/morphata.acceptance.rst +0 -0
  33. {morphata-2.0.6 → morphata-2.0.7}/docs/api/morphata.alphabet.rst +0 -0
  34. {morphata-2.0.6 → morphata-2.0.7}/docs/api/morphata.automaton.rst +0 -0
  35. {morphata-2.0.6 → morphata-2.0.7}/docs/api/morphata.builder.rst +0 -0
  36. {morphata-2.0.6 → morphata-2.0.7}/docs/api/morphata.hoa.rst +0 -0
  37. {morphata-2.0.6 → morphata-2.0.7}/docs/api/morphata.logic.ltl.rst +0 -0
  38. {morphata-2.0.6 → morphata-2.0.7}/docs/api/morphata.logic.psl.rst +0 -0
  39. {morphata-2.0.6 → morphata-2.0.7}/docs/api/morphata.logic.sere.rst +0 -0
  40. {morphata-2.0.6 → morphata-2.0.7}/docs/api/morphata.logic.strel.rst +0 -0
  41. {morphata-2.0.6 → morphata-2.0.7}/docs/api/morphata.operators.matrix.rst +0 -0
  42. {morphata-2.0.6 → morphata-2.0.7}/docs/api/morphata.operators.polynomial.rst +0 -0
  43. {morphata-2.0.6 → morphata-2.0.7}/docs/api/morphata.operators.rst +0 -0
  44. {morphata-2.0.6 → morphata-2.0.7}/docs/api/morphata.symbolic.rst +0 -0
  45. {morphata-2.0.6 → morphata-2.0.7}/docs/concepts/acceptance.rst +0 -0
  46. {morphata-2.0.6 → morphata-2.0.7}/docs/concepts/hoa.rst +0 -0
  47. {morphata-2.0.6 → morphata-2.0.7}/docs/concepts/index.rst +0 -0
  48. {morphata-2.0.6 → morphata-2.0.7}/docs/concepts/operators.rst +0 -0
  49. {morphata-2.0.6 → morphata-2.0.7}/docs/concepts/spec.rst +0 -0
  50. {morphata-2.0.6 → morphata-2.0.7}/docs/concepts/weights.rst +0 -0
  51. {morphata-2.0.6 → morphata-2.0.7}/docs/conf.py +0 -0
  52. {morphata-2.0.6 → morphata-2.0.7}/docs/index.rst +0 -0
  53. {morphata-2.0.6 → morphata-2.0.7}/docs/quick-start.rst +0 -0
  54. {morphata-2.0.6 → morphata-2.0.7}/docs/symbolic-transforms.rst +0 -0
  55. {morphata-2.0.6 → morphata-2.0.7}/justfile +0 -0
  56. {morphata-2.0.6 → morphata-2.0.7}/mypy.ini +0 -0
  57. {morphata-2.0.6 → morphata-2.0.7}/pixi.lock +0 -0
  58. {morphata-2.0.6 → morphata-2.0.7}/pixi.toml +0 -0
  59. {morphata-2.0.6 → morphata-2.0.7}/project.utf-8.add +0 -0
  60. {morphata-2.0.6 → morphata-2.0.7}/project.utf-8.add.spl +0 -0
  61. {morphata-2.0.6 → morphata-2.0.7}/pyrefly.toml +0 -0
  62. {morphata-2.0.6 → morphata-2.0.7}/pyrightconfig.json +0 -0
  63. {morphata-2.0.6 → morphata-2.0.7}/pytest.toml +0 -0
  64. {morphata-2.0.6 → morphata-2.0.7}/ruff.toml +0 -0
  65. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/__init__.py +0 -0
  66. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/acceptance.py +0 -0
  67. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/alphabet.py +0 -0
  68. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/automaton.py +0 -0
  69. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/builder.py +0 -0
  70. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/hoa/__init__.py +0 -0
  71. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/hoa/__main__.py +0 -0
  72. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/hoa/acc_expr.py +0 -0
  73. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/hoa/exporter.py +0 -0
  74. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/hoa/hoa.lark +0 -0
  75. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/hoa/parser.py +0 -0
  76. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/logic/__init__.py +0 -0
  77. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/logic/_common.py +0 -0
  78. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/logic/ltl.py +0 -0
  79. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/logic/psl.py +0 -0
  80. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/logic/sere.py +0 -0
  81. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/logic/strel.py +0 -0
  82. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/operators/__init__.py +0 -0
  83. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/operators/_backend.py +0 -0
  84. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/operators/matrix.py +0 -0
  85. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/operators/polynomial.py +0 -0
  86. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/py.typed +0 -0
  87. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/symbolic/__init__.py +0 -0
  88. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/symbolic/automaton.py +0 -0
  89. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/symbolic/bdd.py +0 -0
  90. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/symbolic/compose.py +0 -0
  91. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/symbolic/equivalence.py +0 -0
  92. {morphata-2.0.6 → morphata-2.0.7}/src/morphata/symbolic/transforms.py +0 -0
  93. {morphata-2.0.6 → morphata-2.0.7}/tests/conftest.py +0 -0
  94. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/aut1.hoa +0 -0
  95. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/aut11.hoa +0 -0
  96. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/aut2.hoa +0 -0
  97. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/aut3.2.hoa +0 -0
  98. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/aut3.hoa +0 -0
  99. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/aut4.hoa +0 -0
  100. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/aut5.hoa +0 -0
  101. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/aut6.hoa +0 -0
  102. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/aut7.hoa +0 -0
  103. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/aut8.hoa +0 -0
  104. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/finite_aut.hoa +0 -0
  105. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/gen_buchi_3.hoa +0 -0
  106. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/gen_cobuchi_2.hoa +0 -0
  107. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/rabin_2pair.hoa +0 -0
  108. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/streett_2pair.hoa +0 -0
  109. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/test_exporter_basic.py +0 -0
  110. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/test_exporter_roundtrip.py +0 -0
  111. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/test_exporter_streaming.py +0 -0
  112. {morphata-2.0.6 → morphata-2.0.7}/tests/hoa/test_parser.py +0 -0
  113. {morphata-2.0.6 → morphata-2.0.7}/tests/logic/__init__.py +0 -0
  114. {morphata-2.0.6 → morphata-2.0.7}/tests/logic/test_ltl.py +0 -0
  115. {morphata-2.0.6 → morphata-2.0.7}/tests/logic/test_psl.py +0 -0
  116. {morphata-2.0.6 → morphata-2.0.7}/tests/logic/test_sere.py +0 -0
  117. {morphata-2.0.6 → morphata-2.0.7}/tests/logic/test_sere_properties.py +0 -0
  118. {morphata-2.0.6 → morphata-2.0.7}/tests/logic/test_strel.py +0 -0
  119. {morphata-2.0.6 → morphata-2.0.7}/tests/operators/test_matrix_from_ir.py +0 -0
  120. {morphata-2.0.6 → morphata-2.0.7}/tests/operators/test_matrix_from_ltl.py +0 -0
  121. {morphata-2.0.6 → morphata-2.0.7}/tests/operators/test_polynomial_from_ir.py +0 -0
  122. {morphata-2.0.6 → morphata-2.0.7}/tests/symbolic/test_alphabet_aware_transitions.py +0 -0
  123. {morphata-2.0.6 → morphata-2.0.7}/tests/symbolic/test_bdd_helpers.py +0 -0
  124. {morphata-2.0.6 → morphata-2.0.7}/tests/symbolic/test_compose.py +0 -0
  125. {morphata-2.0.6 → morphata-2.0.7}/tests/symbolic/test_from_hoaf.py +0 -0
  126. {morphata-2.0.6 → morphata-2.0.7}/tests/symbolic/test_ltlf2dfa_equivalence.py +0 -0
  127. {morphata-2.0.6 → morphata-2.0.7}/tests/symbolic/test_step.py +0 -0
  128. {morphata-2.0.6 → morphata-2.0.7}/tests/symbolic/test_symbolic_automaton.py +0 -0
  129. {morphata-2.0.6 → morphata-2.0.7}/tests/symbolic/test_symbolic_from_ir.py +0 -0
  130. {morphata-2.0.6 → morphata-2.0.7}/tests/symbolic/test_to_hoa.py +0 -0
  131. {morphata-2.0.6 → morphata-2.0.7}/tests/symbolic/test_transforms.py +0 -0
  132. {morphata-2.0.6 → morphata-2.0.7}/tests/symbolic/test_transforms_ir.py +0 -0
  133. {morphata-2.0.6 → morphata-2.0.7}/tests/symbolic/test_transforms_properties.py +0 -0
  134. {morphata-2.0.6 → morphata-2.0.7}/tests/test_alphabet.py +0 -0
  135. {morphata-2.0.6 → morphata-2.0.7}/tests/test_automaton.py +0 -0
  136. {morphata-2.0.6 → morphata-2.0.7}/tests/test_builder.py +0 -0
  137. {morphata-2.0.6 → morphata-2.0.7}/tests/test_from_parsed.py +0 -0
  138. {morphata-2.0.6 → morphata-2.0.7}/ty.toml +0 -0
@@ -1,5 +1,12 @@
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
+
3
10
  ## 2.0.6 — 2026-06-02
4
11
 
5
12
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphata
3
- Version: 2.0.6
3
+ Version: 2.0.7
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.6"
5
+ version = "2.0.7"
6
6
  requires-python = ">=3.12"
7
7
  classifiers = [
8
8
  "Intended Audience :: Science/Research",
@@ -189,6 +189,23 @@ def to_bdd_json(sym: "SymbolicAutomaton[Any]", path: str | Path) -> None:
189
189
  rows_for_s.append([g_name, s_name])
190
190
  transitions_layout[str(s)] = rows_for_s
191
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
+
192
209
  ap_registry_json: dict[str, str] = {}
193
210
  for ap, var in sym.ap_registry.items():
194
211
  if not isinstance(ap, str):
@@ -207,13 +224,22 @@ def to_bdd_json(sym: "SymbolicAutomaton[Any]", path: str | Path) -> None:
207
224
  "ap_var_format": AP_VAR_FORMAT,
208
225
  "initial_root_name": "initial",
209
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,
210
232
  }
211
233
 
212
- mgr.dump(str(bdd_partial), roots=roots, filetype="json")
234
+ if bdd_roots:
235
+ mgr.dump(str(bdd_partial), roots=bdd_roots, filetype="json")
213
236
  with open(meta_partial, "w") as f:
214
237
  json.dump(meta, f, indent=2, sort_keys=True)
215
238
 
216
- bdd_partial.replace(bdd_path)
239
+ if bdd_roots:
240
+ bdd_partial.replace(bdd_path)
241
+ elif bdd_path.exists():
242
+ bdd_path.unlink()
217
243
  meta_partial.replace(meta_path)
218
244
 
219
245
 
@@ -267,14 +293,23 @@ def from_bdd_json(
267
293
  if to_add:
268
294
  manager.declare(*to_add)
269
295
 
270
- loaded = manager.load(str(bdd_path))
271
- if not isinstance(loaded, dict):
272
- raise ValueError(
273
- f"BDD JSON load returned {type(loaded).__name__}; expected a "
274
- "dict of named roots. The file may have been written by a "
275
- "different tool."
276
- )
277
- roots: dict[str, Function] = loaded
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
278
313
 
279
314
  initial_name = meta["initial_root_name"]
280
315
  try:
@@ -112,6 +112,56 @@ class TestAcceptanceRoundTrip:
112
112
  assert _acceptance_from_json(_acceptance_to_json(cond)) == cond
113
113
 
114
114
 
115
+ class TestTerminalRoots:
116
+ """Regression tests for the dd CUDD-backend terminal-root dump bug.
117
+
118
+ Why: ``mgr.dump(..., filetype='json')`` emits a numeric node id for
119
+ True/False roots without a body, and ``mgr.load`` then KeyError's
120
+ when looking up that id. ``to_bdd_json`` works around it by lifting
121
+ terminal roots into the sidecar.
122
+ """
123
+
124
+ def test_a_u_b_round_trip(self, tmp_path: Path) -> None:
125
+ # Min-DFA for `a U b` has a True-guarded accepting sink; the
126
+ # row pointing at it produces a terminal-True succ root.
127
+ a = logic.Variable("a")
128
+ b = logic.Variable("b")
129
+ builder = AutomatonBuilder[int, str]()
130
+ builder.add_state(0, initial=True)
131
+ builder.add_state(1, accepting=True)
132
+ builder.add_state(2)
133
+ builder.add_transition(0, b, 1)
134
+ builder.add_transition(0, logic.And((a, logic.Not(b))), 0)
135
+ builder.add_transition(0, logic.And((logic.Not(a), logic.Not(b))), 2)
136
+ builder.add_transition(1, logic.Literal(True), 1)
137
+ builder.add_transition(2, logic.Literal(True), 2)
138
+ aut = builder.build()
139
+ sym = SymbolicAutomaton.from_automaton(aut, alphabet=powerset_alphabet(aut.predicates))
140
+
141
+ cache = tmp_path / "aub.bdd.json"
142
+ sym.to_bdd_json(cache)
143
+ loaded = SymbolicAutomaton.from_bdd_json(cache, alphabet=powerset_alphabet(frozenset(sym.ap_registry)))
144
+ assert loaded.num_states == sym.num_states
145
+ assert loaded.accepting_states == sym.accepting_states
146
+ assert language_equivalent(sym, loaded)
147
+
148
+ def test_all_terminal_roots(self, tmp_path: Path) -> None:
149
+ # 1-state automaton, only edge is a True-guarded self-loop.
150
+ # Every root (initial + guard + succ) is terminal.
151
+ builder = AutomatonBuilder[int, str]()
152
+ builder.add_state(0, initial=True, accepting=True)
153
+ builder.add_transition(0, logic.Literal(True), 0)
154
+ aut = builder.build()
155
+ sym = SymbolicAutomaton.from_automaton(aut, alphabet=powerset_alphabet(aut.predicates))
156
+
157
+ cache = tmp_path / "one.bdd.json"
158
+ sym.to_bdd_json(cache)
159
+ loaded = SymbolicAutomaton.from_bdd_json(cache, alphabet=powerset_alphabet(frozenset(sym.ap_registry)))
160
+ assert loaded.num_states == 1
161
+ assert loaded.accepting_states == frozenset({0})
162
+ assert language_equivalent(sym, loaded)
163
+
164
+
115
165
  class TestSchemaGuard:
116
166
  def test_mismatched_schema_version_raises(self, tmp_path: Path) -> None:
117
167
  sym = _two_state_dfa()
@@ -1049,7 +1049,7 @@ wheels = [
1049
1049
 
1050
1050
  [[package]]
1051
1051
  name = "morphata"
1052
- version = "2.0.6"
1052
+ version = "2.0.7"
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