morphata 2.0.0__tar.gz → 2.0.2__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 (139) hide show
  1. morphata-2.0.2/.claude/settings.local.json +29 -0
  2. {morphata-2.0.0 → morphata-2.0.2}/.envrc +5 -5
  3. morphata-2.0.2/.gitattributes +5 -0
  4. {morphata-2.0.0 → morphata-2.0.2}/.gitignore +3 -0
  5. {morphata-2.0.0 → morphata-2.0.2}/AGENTS.md +39 -0
  6. {morphata-2.0.0 → morphata-2.0.2}/CHANGELOG.md +113 -1
  7. {morphata-2.0.0 → morphata-2.0.2}/CONTRIBUTING.md +16 -10
  8. {morphata-2.0.0 → morphata-2.0.2}/PKG-INFO +2 -2
  9. morphata-2.0.2/bench/README.md +28 -0
  10. morphata-2.0.2/bench/conftest.py +53 -0
  11. morphata-2.0.2/bench/test_sere_nlm_perf.py +84 -0
  12. morphata-2.0.2/ci/make-release +131 -0
  13. {morphata-2.0.0 → morphata-2.0.2}/docs/api/modules.rst +2 -0
  14. morphata-2.0.2/docs/api/morphata.logic.psl.rst +15 -0
  15. morphata-2.0.2/docs/api/morphata.logic.sere.rst +9 -0
  16. {morphata-2.0.0 → morphata-2.0.2}/docs/conf.py +1 -1
  17. {morphata-2.0.0 → morphata-2.0.2}/docs/index.rst +9 -1
  18. morphata-2.0.2/justfile +70 -0
  19. morphata-2.0.2/pixi.lock +5616 -0
  20. morphata-2.0.2/pixi.toml +116 -0
  21. {morphata-2.0.0 → morphata-2.0.2}/pyproject.toml +4 -2
  22. morphata-2.0.2/pytest.toml +32 -0
  23. morphata-2.0.2/src/morphata/logic/_common.py +136 -0
  24. morphata-2.0.2/src/morphata/logic/ltl.py +499 -0
  25. morphata-2.0.2/src/morphata/logic/psl.py +372 -0
  26. morphata-2.0.2/src/morphata/logic/sere.py +516 -0
  27. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/operators/matrix.py +3 -1
  28. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/operators/polynomial.py +3 -1
  29. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/symbolic/automaton.py +1 -1
  30. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/symbolic/bdd.py +225 -14
  31. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/symbolic/transforms.py +77 -68
  32. {morphata-2.0.0/tests/symbolic → morphata-2.0.2/tests}/conftest.py +19 -2
  33. morphata-2.0.2/tests/logic/__init__.py +0 -0
  34. {morphata-2.0.0 → morphata-2.0.2}/tests/logic/test_ltl.py +122 -69
  35. morphata-2.0.2/tests/logic/test_psl.py +179 -0
  36. morphata-2.0.2/tests/logic/test_sere.py +489 -0
  37. morphata-2.0.2/tests/logic/test_sere_properties.py +132 -0
  38. {morphata-2.0.0 → morphata-2.0.2}/tests/operators/test_matrix_from_ltl.py +7 -6
  39. {morphata-2.0.0 → morphata-2.0.2}/tests/operators/test_polynomial_from_ir.py +5 -4
  40. morphata-2.0.2/tests/symbolic/test_bdd_helpers.py +277 -0
  41. {morphata-2.0.0 → morphata-2.0.2}/tests/symbolic/test_from_hoaf.py +2 -2
  42. {morphata-2.0.0 → morphata-2.0.2}/tests/symbolic/test_ltlf2dfa_equivalence.py +1 -1
  43. {morphata-2.0.0 → morphata-2.0.2}/tests/symbolic/test_step.py +8 -7
  44. {morphata-2.0.0 → morphata-2.0.2}/tests/symbolic/test_symbolic_automaton.py +26 -1
  45. {morphata-2.0.0 → morphata-2.0.2}/tests/symbolic/test_symbolic_from_ir.py +7 -6
  46. {morphata-2.0.0 → morphata-2.0.2}/tests/symbolic/test_transforms.py +47 -0
  47. {morphata-2.0.0 → morphata-2.0.2}/tests/symbolic/test_transforms_ir.py +5 -4
  48. {morphata-2.0.0 → morphata-2.0.2}/tests/symbolic/test_transforms_properties.py +45 -21
  49. {morphata-2.0.0 → morphata-2.0.2}/uv.lock +140 -91
  50. morphata-2.0.0/.claude/settings.local.json +0 -17
  51. morphata-2.0.0/.gitattributes +0 -3
  52. morphata-2.0.0/ci/make-release +0 -99
  53. morphata-2.0.0/docs/changelog.md +0 -4
  54. morphata-2.0.0/justfile +0 -54
  55. morphata-2.0.0/pytest.toml +0 -14
  56. morphata-2.0.0/src/morphata/logic/ltl.py +0 -448
  57. morphata-2.0.0/tests/symbolic/test_bdd_helpers.py +0 -143
  58. {morphata-2.0.0 → morphata-2.0.2}/.basedpyright/baseline.json +0 -0
  59. {morphata-2.0.0 → morphata-2.0.2}/.dockerignore +0 -0
  60. {morphata-2.0.0 → morphata-2.0.2}/.editorconfig +0 -0
  61. {morphata-2.0.0 → morphata-2.0.2}/.envrc.recommended +0 -0
  62. {morphata-2.0.0 → morphata-2.0.2}/.fdignore +0 -0
  63. {morphata-2.0.0 → morphata-2.0.2}/.mailmap +0 -0
  64. {morphata-2.0.0 → morphata-2.0.2}/.python-version +0 -0
  65. {morphata-2.0.0 → morphata-2.0.2}/.rstcheck.cfg +0 -0
  66. {morphata-2.0.0 → morphata-2.0.2}/CLAUDE.md +0 -0
  67. {morphata-2.0.0 → morphata-2.0.2}/LICENSE +0 -0
  68. {morphata-2.0.0 → morphata-2.0.2}/README.md +0 -0
  69. {morphata-2.0.0/tests/logic → morphata-2.0.2/bench}/__init__.py +0 -0
  70. {morphata-2.0.0 → morphata-2.0.2}/cliff.toml +0 -0
  71. {morphata-2.0.0 → morphata-2.0.2}/docs/_static/navigation.html +0 -0
  72. {morphata-2.0.0 → morphata-2.0.2}/docs/api/morphata.acceptance.rst +0 -0
  73. {morphata-2.0.0 → morphata-2.0.2}/docs/api/morphata.alphabet.rst +0 -0
  74. {morphata-2.0.0 → morphata-2.0.2}/docs/api/morphata.automaton.rst +0 -0
  75. {morphata-2.0.0 → morphata-2.0.2}/docs/api/morphata.builder.rst +0 -0
  76. {morphata-2.0.0 → morphata-2.0.2}/docs/api/morphata.hoa.rst +0 -0
  77. {morphata-2.0.0 → morphata-2.0.2}/docs/api/morphata.logic.ltl.rst +0 -0
  78. {morphata-2.0.0 → morphata-2.0.2}/docs/api/morphata.logic.strel.rst +0 -0
  79. {morphata-2.0.0 → morphata-2.0.2}/docs/api/morphata.operators.matrix.rst +0 -0
  80. {morphata-2.0.0 → morphata-2.0.2}/docs/api/morphata.operators.polynomial.rst +0 -0
  81. {morphata-2.0.0 → morphata-2.0.2}/docs/api/morphata.operators.rst +0 -0
  82. {morphata-2.0.0 → morphata-2.0.2}/docs/api/morphata.symbolic.rst +0 -0
  83. {morphata-2.0.0 → morphata-2.0.2}/docs/concepts/acceptance.rst +0 -0
  84. {morphata-2.0.0 → morphata-2.0.2}/docs/concepts/hoa.rst +0 -0
  85. {morphata-2.0.0 → morphata-2.0.2}/docs/concepts/index.rst +0 -0
  86. {morphata-2.0.0 → morphata-2.0.2}/docs/concepts/operators.rst +0 -0
  87. {morphata-2.0.0 → morphata-2.0.2}/docs/concepts/spec.rst +0 -0
  88. {morphata-2.0.0 → morphata-2.0.2}/docs/concepts/weights.rst +0 -0
  89. {morphata-2.0.0 → morphata-2.0.2}/docs/quick-start.rst +0 -0
  90. {morphata-2.0.0 → morphata-2.0.2}/docs/symbolic-transforms.rst +0 -0
  91. {morphata-2.0.0 → morphata-2.0.2}/mypy.ini +0 -0
  92. {morphata-2.0.0 → morphata-2.0.2}/project.utf-8.add +0 -0
  93. {morphata-2.0.0 → morphata-2.0.2}/project.utf-8.add.spl +0 -0
  94. {morphata-2.0.0 → morphata-2.0.2}/pyrefly.toml +0 -0
  95. {morphata-2.0.0 → morphata-2.0.2}/pyrightconfig.json +0 -0
  96. {morphata-2.0.0 → morphata-2.0.2}/ruff.toml +0 -0
  97. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/__init__.py +0 -0
  98. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/acceptance.py +0 -0
  99. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/alphabet.py +0 -0
  100. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/automaton.py +0 -0
  101. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/builder.py +0 -0
  102. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/hoa/__init__.py +0 -0
  103. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/hoa/__main__.py +0 -0
  104. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/hoa/acc_expr.py +0 -0
  105. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/hoa/exporter.py +0 -0
  106. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/hoa/hoa.lark +0 -0
  107. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/hoa/parser.py +0 -0
  108. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/logic/__init__.py +0 -0
  109. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/logic/strel.py +0 -0
  110. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/operators/__init__.py +0 -0
  111. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/operators/_backend.py +0 -0
  112. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/py.typed +0 -0
  113. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/symbolic/__init__.py +0 -0
  114. {morphata-2.0.0 → morphata-2.0.2}/src/morphata/symbolic/equivalence.py +0 -0
  115. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/aut1.hoa +0 -0
  116. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/aut11.hoa +0 -0
  117. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/aut2.hoa +0 -0
  118. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/aut3.2.hoa +0 -0
  119. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/aut3.hoa +0 -0
  120. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/aut4.hoa +0 -0
  121. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/aut5.hoa +0 -0
  122. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/aut6.hoa +0 -0
  123. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/aut7.hoa +0 -0
  124. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/aut8.hoa +0 -0
  125. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/finite_aut.hoa +0 -0
  126. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/gen_buchi_3.hoa +0 -0
  127. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/gen_cobuchi_2.hoa +0 -0
  128. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/rabin_2pair.hoa +0 -0
  129. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/streett_2pair.hoa +0 -0
  130. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/test_exporter_basic.py +0 -0
  131. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/test_exporter_roundtrip.py +0 -0
  132. {morphata-2.0.0 → morphata-2.0.2}/tests/hoa/test_parser.py +0 -0
  133. {morphata-2.0.0 → morphata-2.0.2}/tests/logic/test_strel.py +0 -0
  134. {morphata-2.0.0 → morphata-2.0.2}/tests/operators/test_matrix_from_ir.py +0 -0
  135. {morphata-2.0.0 → morphata-2.0.2}/tests/test_alphabet.py +0 -0
  136. {morphata-2.0.0 → morphata-2.0.2}/tests/test_automaton.py +0 -0
  137. {morphata-2.0.0 → morphata-2.0.2}/tests/test_builder.py +0 -0
  138. {morphata-2.0.0 → morphata-2.0.2}/tests/test_from_parsed.py +0 -0
  139. {morphata-2.0.0 → morphata-2.0.2}/ty.toml +0 -0
@@ -0,0 +1,29 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(mypy *)",
5
+ "Bash(python -m mypy *)",
6
+ "Bash(python3 -m mypy *)",
7
+ "Bash(pytest *)",
8
+ "Bash(python -m pytest *)",
9
+ "Bash(python3 -m pytest *)",
10
+ "Bash(just lint *)",
11
+ "Bash(just test *)",
12
+ "Bash(rg *)",
13
+ "Bash(uv run pytest *)",
14
+ "Bash(uv run mypy *)",
15
+ "Bash(.venv/bin/python -m pytest *)",
16
+ "Skill(superpowers:writing-plans)",
17
+ "Skill(superpowers:subagent-driven-development)",
18
+ "Bash(grep -v \"AssertionError\\\\|Traceback\\\\|File \\\\|^\\\\s*$\")",
19
+ "Skill(superpowers:brainstorming)",
20
+ "Bash(jj log *)",
21
+ "Bash(jj st *)",
22
+ "Bash(jj diff *)",
23
+ "Skill(superpowers:requesting-code-review)",
24
+ "Bash(timeout 15 python -c ' *)",
25
+ "Read(//tmp/**)",
26
+ "Bash(echo \"exit: $?\")"
27
+ ]
28
+ }
29
+ }
@@ -12,15 +12,15 @@ if [[ -z "${DEVJAIL:-}" ]] && nvidia_smi="$(type -p "nvidia-smi")" && [[ -n "$nv
12
12
  if [[ -n "${cuda_major_version:-}" ]]; then
13
13
  info "Detected CUDA version: $cuda_major_version"
14
14
  export CUDA_VERSION="$cuda_major_version"
15
+ PIXI_ENV="cu$CUDA_VERSION"
15
16
  fi
16
17
  else
17
18
  info "Did not detect nvidia-smi, using non-GPU environment"
19
+ PIXI_ENV="default"
18
20
  fi
21
+ export PIXI_ENV
19
22
 
20
23
  dotenv_if_exists
21
24
 
22
- if [[ -z "${DEVJAIL:-}" ]] || [[ ! -e ".venv/bin/activate" ]]; then
23
- uvx --from rust-just just dev
24
- fi
25
- source ./.venv/bin/activate
26
- watch_file pyproject.toml
25
+ watch_file pixi.lock
26
+ eval "$(pixi shell-hook -e "$PIXI_ENV")"
@@ -0,0 +1,5 @@
1
+ # GitHub syntax highlighting
2
+ pixi.lock linguist-language=YAML
3
+
4
+ # SCM syntax highlighting & preventing 3-way merges
5
+ pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff
@@ -257,3 +257,6 @@ $RECYCLE.BIN/
257
257
  *.lnk
258
258
 
259
259
  # End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux
260
+ # pixi environments
261
+ .pixi/*
262
+ !.pixi/config.toml
@@ -4,6 +4,14 @@
4
4
  It packages structural automaton representations together with semiring-valued
5
5
  *operators* that turn an automaton into a computable interpretation.
6
6
 
7
+ ## Development Environment
8
+
9
+ The development shell already has the project virtual environment activated,
10
+ so `pytest`, `mypy`, `ruff`, `sphinx-build`, etc. are on `PATH` directly.
11
+ **Do not** prefix commands with `uv run`, `pixi run`, `poetry run`, or similar.
12
+ Just invoke the tool. If a tool is missing, stop and tell the user rather than
13
+ trying to bootstrap or reinstall the environment.
14
+
7
15
  ## Core Architecture
8
16
 
9
17
  ### IR layer (`morphata.automaton`, `morphata.alphabet`, `morphata.acceptance`)
@@ -85,6 +93,21 @@ delta - the transition consumes the labelled graph eagerly, so STREL
85
93
  automata are not symbolically liftable. Operators: Reach, Escape,
86
94
  Somewhere, Everywhere. Boolean-only.
87
95
 
96
+ **`morphata.logic.sere.sere_to_automaton(expr)`** : SERE -> alternating
97
+ finite automaton. Supports ``Concat``, ``Fusion``, ``Repeat`` (bounded
98
+ and unbounded), ``Inter``, ``Alt``, and Boolean leaves over AP.
99
+ Acceptance is always ``Finite``; the accepting-state set is exactly
100
+ the *nullable* states (those that match the empty word).
101
+
102
+ **`morphata.logic.psl.psl_to_automaton(phi, *, finite=False)`** : PSL ->
103
+ alternating automaton. Wraps pure-LTL subformulas in an internal
104
+ ``_LTL`` tag and dispatches to the LTL expansion rule; closure
105
+ operators (``{r}!``, ``{r}``, ``!{r}``, ``{r}<>-> f``, ``{r}[]-> f``)
106
+ become their own tagged states with SERE-step expansion. Suffix
107
+ implications follow Spot's overlapping semantics: the last symbol of
108
+ ``r`` and the first position of ``f`` coincide. ``finite=True`` ->
109
+ ``Finite`` acceptance (PSLf); ``finite=False`` -> ``Buchi``.
110
+
88
111
  Hand-written automata are built with ``AutomatonBuilder``; the package
89
112
  has no separate ``constructions`` namespace.
90
113
 
@@ -211,6 +234,18 @@ is backend-agnostic via `backend=`.
211
234
  sets of transitions. The HOA parser ingests ``{n}`` markers from
212
235
  ``State:`` declarations; per-edge ``{n}`` markers are ignored. Convert
213
236
  transition-based HOA files to state-based form before loading them.
237
+ - ``psl_to_automaton`` requires the continuation of any
238
+ ``SuffixImpliesExist`` / ``SuffixImpliesUniv`` to be a pure-LTL
239
+ formula in v1. Nested PSL closures inside a suffix-implication
240
+ continuation raise ``NotImplementedError``. Likewise, LTL temporal
241
+ operators wrapping a PSL closure at the top level
242
+ (e.g. ``Always(StrongClosure(r))``) are not yet decomposed; lift the
243
+ PSL fragment out of the LTL temporal operator before calling.
244
+ - The PSL Buchi acceptance classification is the natural per-tag rule
245
+ (strong-closure non-accepting, weak-closure accepting, etc.). It has
246
+ not been cross-checked against a published reference for every
247
+ combination of operators; consult ``docs/superpowers/specs/`` before
248
+ relying on omega-language semantics in production.
214
249
 
215
250
  ## Testing
216
251
 
@@ -334,6 +369,10 @@ matrix_product = algebraic.matmul(arr1, arr2)
334
369
  - `src/morphata/builder.py` : ``AutomatonBuilder``.
335
370
  - `src/morphata/acceptance.py` : expression-based acceptance conditions.
336
371
  - `src/morphata/logic/ltl.py` : LTL/LTLf -> AFA conversion.
372
+ - `src/morphata/logic/sere.py` : SERE -> AFA conversion.
373
+ - `src/morphata/logic/psl.py` : PSL -> AFA conversion.
374
+ - `src/morphata/logic/_common.py` : shared BDD scratch-space setup used
375
+ by all three logic converters.
337
376
  - `src/morphata/logic/strel.py` : STREL automaton (``Step`` delta).
338
377
  - `src/morphata/hoa/` : HOA v1 parser, exporter, grammar.
339
378
  - `src/morphata/operators/matrix.py` : ``MatrixOperator``.
@@ -1,6 +1,118 @@
1
1
  # Changelog
2
2
 
3
- ## 2.0.0[Unreleased]
3
+ ## 2.0.22026-05-27
4
+
5
+ ### Added
6
+
7
+ - The rest of the SERE surface that needed real work, rather than a desugaring rule:
8
+ non-length-matching intersection
9
+ (`NLMInter`, the `&` operator), `Complement` (`~`), and `FirstMatch`.
10
+ NLM intersection is handled by a local rewrite in the expansion polynomial;
11
+ complement dualises the polynomial and gets a small BFS-loop special case
12
+ so the state space stays compact.
13
+ `GotoRepeat`, `EqualRepeat`,
14
+ and bounded `FusionRepeat` desugar through `logic_asts.spec.Expr.expand`.
15
+ Unbounded `FusionRepeat[:*i..]` deliberately raises `NotImplementedError` —
16
+ I don't have a clean expansion for it yet.
17
+ - Benchmarking script for SERE NLM intersection, so I can see when I
18
+ break the perf curve.
19
+
20
+ ### Changed
21
+
22
+ - `morphata.logic.psl` dropped a layer of in-tree wrappers
23
+ and leans on `logic_asts` types directly now
24
+ that the upstream package carries the extended SERE surface I needed.
25
+ Less code, fewer places for the two sides to disagree.
26
+
27
+ ### Miscellaneous
28
+
29
+ - The README is linked from `pyproject.toml`, so it actually shows up
30
+ on PyPI.
31
+
32
+ ## 2.0.1 — 2026-05-24
33
+
34
+ The point release that delivers the SERE
35
+ and PSL frontends originally promised alongside 2.0,
36
+ plus the perf work that made the symbolic pipeline actually finish on formulas of
37
+ interest.
38
+
39
+ ### Added
40
+
41
+ - `morphata.logic.sere.sere_to_automaton(formula)` —
42
+ converts a SERE expression to an alternating finite automaton.
43
+ Supports `Concat`, `Fusion`, `Alt`, `Inter` (length-matching), `Repeat`
44
+ (bounded and unbounded),
45
+ and Boolean leaves (`Literal`, `Variable`, `Not`, `And`, `Or`).
46
+ - `morphata.logic.psl.psl_to_automaton(phi, *, finite=False)` —
47
+ converts a PSL expression to an alternating automaton,
48
+ with `Finite` acceptance for PSLf (`finite=True`) and `Büchi` acceptance
49
+ for full PSL (`finite=False`).
50
+ Supports the strong / weak / negated-strong closure operators
51
+ and existential / universal suffix implication.
52
+ Suffix implications follow Spot's overlapping semantics:
53
+ the last symbol matched by the SERE prefix is shared with the first position at
54
+ which the continuation is evaluated.
55
+
56
+ ### Changed
57
+
58
+ - `morphata.logic.ltl` factored its per-call BDD scratch-space construction into a
59
+ shared helper `morphata.logic._common.make_logic_context`, reused by the SERE and PSL
60
+ converters.
61
+ Two LTL helpers (`_aut_symbolic_expansion`, `_empty_trace_value`) were renamed to
62
+ `ltl_symbolic_expansion` / `ltl_empty_trace_value` so PSL can delegate to them for
63
+ pure-LTL subformulas.
64
+ - Moved off `uv` and onto Pixi. `uv`'s handling of CUDA packages and
65
+ the way it churns the lockfile weren't working for me; Pixi does.
66
+
67
+ ### Fixed
68
+
69
+ - New BDD constructions in `morphata.logic` take an explicit manager parameter instead
70
+ of silently spinning up their own.
71
+ - `bdd_to_boolexpr` in `morphata.symbolic.bdd` was silently folding `~x`
72
+ and `x` together because the CUDD complement-edge flag on the current node wasn't
73
+ being respected on the recursive walk.
74
+ Caught while rewriting `ltl_to_automaton`; fixed there.
75
+
76
+ ### Performance
77
+
78
+ - `ltl_to_automaton` no longer iterates `2^|AP|` input symbols per AFA state.
79
+ The closure BFS keeps APs symbolic, builds an expansion polynomial with tagged atoms
80
+ (one tag for guard atoms, one for successor states),
81
+ compiles it to a per-call BDD,
82
+ and partitions AP-space by distinct state cofactors via the new
83
+ `mgr_partition_ap_by_state_residual` helper.
84
+ Per-AFA-state cost drops from `O(2^|AP|)` to `O(distinct successor classes)`,
85
+ and AP variables that don't affect a given cofactor get pruned.
86
+ A depth-15 sequential conjunction `F(a_0) & X(F(a_1) & X(...))`
87
+ that used to take ~330s and OOM at depth 20 now builds in ~0.02s.
88
+ The remaining wall-clock on those formulas lives in `nfa_from_afa`,
89
+ which is intrinsic `2^N` in macro state count.
90
+ - `SymbolicAutomaton.to_min_dfa()` is tractable on formulas with non-trivial `|AP|`
91
+ (`~5+` for monotone conjunctions, `~10+` for sequential), where it used to hang.
92
+ Three pieces: prime-implicant DFS (`mgr_minimal_pos_satisfying_subsets`) replaces the
93
+ old `2^(2^|AP|)` BDD-evaluation pass in `_nfa_macro_step` after dealternation;
94
+ symbolic AP-space partition helpers (`mgr_partition_ap_by_classifiers` and
95
+ `mgr_partition_ap_by_state_residual`) replace explicit `2^|AP|` minterm enumeration in
96
+ `determinize` and `nfa_from_afa`; and `_extract_dfa_target` parses the single
97
+ state-variable name directly instead of linear-scanning every state in the manager,
98
+ removing an `O(num_states^2)` per-refinement-round cost inside `minimize()`.
99
+
100
+ Sadly (but not really sadly),
101
+ these performance fixes make the BDD manager a viral argument in `morphata.logic`,
102
+ but it isn't too bad as it is optional,
103
+ and the `morphata.logic.*_to_automaton` functions will automatically create a
104
+ short-lived manager for these things.
105
+
106
+ ### Known Limitations
107
+
108
+ - `psl_to_automaton` does not yet decompose LTL temporal operators
109
+ that wrap PSL closures at the top level (e.g. `Always(StrongClosure(r))`).
110
+ Lift the PSL fragment out of the surrounding LTL temporal operator before calling.
111
+ - The continuation of a `SuffixImpliesExist` / `SuffixImpliesUniv`
112
+ must be a pure-LTL formula; nested PSL closures inside a
113
+ continuation raise `NotImplementedError`.
114
+
115
+ ## 2.0.0 — 2026-05-23
4
116
 
5
117
  The major version bump folds the old `automatix` package into `morphata`
6
118
  and replaces the constellation automata classes with a single typed intermediate
@@ -6,24 +6,30 @@ The `justfile` defines a set of build, lint, and test commands, which can direct
6
6
  run as below:
7
7
 
8
8
 
9
+ The examples below assume the project virtual environment is activated (either
10
+ via `direnv` and `.envrc`, or by sourcing `.venv/bin/activate` manually). With
11
+ the venv active the tools are on `PATH` directly; no `uv run` prefix is needed.
12
+ If you'd rather not activate, prefix each command with `uv run --dev --frozen`
13
+ (or use the `just` recipes, which handle this for you).
14
+
9
15
  **Testing** :
10
16
  ```bash
11
17
  # Run all tests (uses --lf for last-failed)
12
18
  just test
13
19
  # OR
14
- uv run --dev --frozen pytest --lf
20
+ pytest --lf
15
21
 
16
22
  # Run all tests verbosely without --lf
17
- uv run --dev --frozen pytest -v
23
+ pytest -v
18
24
 
19
25
  # Run a single test file
20
- uv run --dev --frozen pytest tests/test_weights.py -v
26
+ pytest tests/test_alphabet.py -v
21
27
 
22
28
  # Run a single test function
23
- uv run --dev --frozen pytest tests/test_weights.py::TestWeightFunctionBasics::test_constant_weight_function -v
29
+ pytest tests/test_alphabet.py::TestBooleanAlphabet::test_powerset_alphabet -v
24
30
 
25
31
  # Run with coverage
26
- uv run --dev --frozen pytest --cov=morphata --cov-report=term
32
+ pytest --cov=morphata --cov-report=term
27
33
  ```
28
34
 
29
35
  **Linting and Formatting** :
@@ -31,8 +37,8 @@ uv run --dev --frozen pytest --cov=morphata --cov-report=term
31
37
  # Format and lint (fixes issues automatically)
32
38
  just fmt
33
39
  # OR
34
- uv run --frozen ruff format
35
- uv run --frozen ruff check --output-format concise --fix --exit-non-zero-on-fix
40
+ ruff format
41
+ ruff check --output-format concise --fix --exit-non-zero-on-fix
36
42
 
37
43
  # Format + type check
38
44
  just lint
@@ -44,9 +50,9 @@ just lint
44
50
  just type-check
45
51
 
46
52
  # Run specific type checkers
47
- uv run --frozen mypy --strict
48
- uv run --frozen ty check --output-format concise
49
- uv run --frozen pyrefly check --output-format min-text
53
+ mypy --strict
54
+ ty check --output-format concise
55
+ pyrefly check --output-format min-text
50
56
  ```
51
57
 
52
58
  **Development Setup** :
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphata
3
- Version: 2.0.0
3
+ Version: 2.0.2
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
@@ -16,7 +16,7 @@ Requires-Dist: attrs>=25.4.0
16
16
  Requires-Dist: dd>=0.6.0
17
17
  Requires-Dist: jaxtyping>=0.3
18
18
  Requires-Dist: lark>=1.3.1
19
- Requires-Dist: logic-asts>=1.4.1
19
+ Requires-Dist: logic-asts>=1.6.2
20
20
  Requires-Dist: networkx
21
21
  Requires-Dist: numpy
22
22
  Requires-Dist: types-networkx
@@ -0,0 +1,28 @@
1
+ # morphata bench
2
+
3
+ Opt-in benchmark suite. Not collected by the default `pytest`
4
+ invocation.
5
+
6
+ ## Running
7
+
8
+ pytest bench/ -v
9
+
10
+ To get pytest-benchmark's tabular wall-time output:
11
+
12
+ pytest bench/ -v --benchmark-columns=mean,median,min,max,rounds
13
+
14
+ ## Suites
15
+
16
+ - `test_sere_nlm_perf.py` -- NLM-intersection state-space blowup.
17
+ Cases: `NLMInter(a0..a_{n-1})` for n in 2..6, `NLMInter(a_i;b_i)`
18
+ for n in 2..4, plus length-matching `Inter` baselines.
19
+
20
+ ## Recording results
21
+
22
+ After running, paste the resulting tables (state count + wall time)
23
+ into the spec doc at:
24
+ `docs/superpowers/specs/2026-05-26-psl-refactor-and-nlm-perf-design.md`
25
+
26
+ The Phase 2 fix (memoization / pre-pass canonicalization / first-class
27
+ `_NLMInter` state) is chosen based on the numbers; the choice is
28
+ recorded as a follow-up plan.
@@ -0,0 +1,53 @@
1
+ """pytest fixtures for the morphata bench suite.
2
+
3
+ The bench/ tree is opt-in: invoke with `pytest bench/` (or
4
+ `pytest bench/test_sere_nlm_perf.py`). It is not collected by the
5
+ default `pytest` invocation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import gc
11
+ from collections.abc import Iterator
12
+
13
+ import pytest
14
+
15
+ from morphata.logic.sere import _expansion_call_counter
16
+ from morphata.symbolic.bdd import Manager, make_manager
17
+
18
+
19
+ @pytest.fixture
20
+ def bench_mgr() -> Iterator[Manager]:
21
+ """Shared BDD manager scoped to one bench test (= many benchmark rounds).
22
+
23
+ Bench tests pass this manager to ``sere_to_automaton(..., manager=mgr)``
24
+ so the per-call ``make_manager()`` path is skipped. That path leaks
25
+ ~12 MB of CUDD-internal state per call, which compounds across the
26
+ ~100-1000 rounds pytest-benchmark runs per test and exhausts host
27
+ RAM. Sharing the manager keeps RSS flat.
28
+ """
29
+ manager = make_manager()
30
+ try:
31
+ yield manager
32
+ finally:
33
+ gc.collect()
34
+
35
+
36
+ @pytest.fixture
37
+ def expansion_calls() -> Iterator[list[int]]:
38
+ """Activate the expansion-call counter for the duration of one test.
39
+
40
+ Yields a single-element list whose entry is filled in by the test
41
+ with the final call count snapshot (`current_expansion_call_count()`).
42
+ """
43
+ token = _expansion_call_counter.set(0)
44
+ try:
45
+ yield [0]
46
+ finally:
47
+ _expansion_call_counter.reset(token)
48
+
49
+
50
+ def current_expansion_call_count() -> int:
51
+ """Read the current expansion-call counter value (0 if inactive)."""
52
+ val = _expansion_call_counter.get()
53
+ return val if val is not None else 0
@@ -0,0 +1,84 @@
1
+ """Benchmark suite for SERE NLMInter state-space behaviour.
2
+
3
+ Run with: ``pytest bench/test_sere_nlm_perf.py -v``
4
+
5
+ Records state count, expansion-call count, and wall time for
6
+ NLM-intersection of n branches, plus length-matching Inter as a
7
+ baseline.
8
+
9
+ History note: the original spec hypothesised an exponential blowup
10
+ in branch count for ``NLMInter``. Direct measurement (see the spec's
11
+ "Phase 1 Benchmark Results" section) showed no such blowup -- state
12
+ count stays at 3 for arbitrarily many AP-literal branches and grows
13
+ linearly (2n+3) for length-2-concat branches. The bench is retained
14
+ as a regression guard.
15
+
16
+ A separate finding from this investigation: ``make_manager()`` leaks
17
+ ~12 MB of CUDD-internal state per call, so the bench passes a single
18
+ ``bench_mgr`` fixture into ``sere_to_automaton(..., manager=mgr)``
19
+ to keep RSS flat across the ~100-1000 rounds pytest-benchmark runs
20
+ per test. Without that, repeated calls compound to GiB-scale RSS and
21
+ hang the host.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logic_asts as logic
27
+ import logic_asts.sere as sere
28
+ import pytest
29
+ from pytest_benchmark.fixture import BenchmarkFixture as Benchmark
30
+
31
+ from morphata.logic.sere import sere_to_automaton
32
+ from morphata.symbolic.bdd import Manager
33
+
34
+ from .conftest import current_expansion_call_count
35
+
36
+
37
+ def _nlm_of_aps(n: int) -> logic.Expr:
38
+ return sere.NLMInter(tuple(sere.Variable(f"a{i}") for i in range(n)))
39
+
40
+
41
+ def _lm_of_aps(n: int) -> logic.Expr:
42
+ return sere.Inter(tuple(sere.Variable(f"a{i}") for i in range(n)))
43
+
44
+
45
+ def _nlm_of_concats(n: int) -> logic.Expr:
46
+ return sere.NLMInter(tuple(sere.Concat((sere.Variable(f"a{i}"), sere.Variable(f"b{i}"))) for i in range(n)))
47
+
48
+
49
+ def _run_and_measure(formula: logic.Expr, mgr: Manager) -> tuple[int, int]:
50
+ """Return ``(state_count, expansion_call_count)``."""
51
+ aut = sere_to_automaton(formula, manager=mgr)
52
+ states = aut.states
53
+ n_states = len(states) if states is not None and not callable(states) else 0
54
+ return (n_states, current_expansion_call_count())
55
+
56
+
57
+ @pytest.mark.timeout(10)
58
+ @pytest.mark.parametrize("n", list(range(2, 10)))
59
+ def test_bench_nlm_of_aps(benchmark: Benchmark, expansion_calls: list[int], bench_mgr: Manager, n: int) -> None:
60
+ formula = _nlm_of_aps(n)
61
+ result = benchmark(_run_and_measure, formula, bench_mgr)
62
+ state_count, call_count = result
63
+ expansion_calls[0] = call_count
64
+ print(f"\nNLMInter(a0..a{n - 1}): states={state_count} expansion_calls={call_count}")
65
+
66
+
67
+ @pytest.mark.timeout(10)
68
+ @pytest.mark.parametrize("n", list(range(2, 10)))
69
+ def test_bench_lm_of_aps_baseline(benchmark: Benchmark, expansion_calls: list[int], bench_mgr: Manager, n: int) -> None:
70
+ formula = _lm_of_aps(n)
71
+ result = benchmark(_run_and_measure, formula, bench_mgr)
72
+ state_count, call_count = result
73
+ expansion_calls[0] = call_count
74
+ print(f"\nInter(a0..a{n - 1}): states={state_count} expansion_calls={call_count}")
75
+
76
+
77
+ @pytest.mark.timeout(10)
78
+ @pytest.mark.parametrize("n", list(range(2, 10)))
79
+ def test_bench_nlm_of_concats(benchmark: Benchmark, expansion_calls: list[int], bench_mgr: Manager, n: int) -> None:
80
+ formula = _nlm_of_concats(n)
81
+ result = benchmark(_run_and_measure, formula, bench_mgr)
82
+ state_count, call_count = result
83
+ expansion_calls[0] = call_count
84
+ print(f"\nNLMInter(a_i;b_i) n={n}: states={state_count} expansion_calls={call_count}")
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Usage ------------------------------------------------------------------
5
+ usage() {
6
+ cat <<EOF
7
+ Usage: $(basename "$0") <bump>
8
+
9
+ bump Version bump kind passed to 'uv version --bump'
10
+ (patch, minor, major, etc.)
11
+
12
+ Examples:
13
+ $(basename "$0") patch
14
+ $(basename "$0") minor
15
+ EOF
16
+ exit 1
17
+ }
18
+
19
+ if [[ $# -ne 1 ]]; then
20
+ usage
21
+ fi
22
+
23
+ PROJECT_DIR="$(cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")/.." && pwd)"
24
+ cd "$PROJECT_DIR"
25
+
26
+ BUMP="$1"
27
+
28
+ # Helpers ----------------------------------------------------------------
29
+ is_jj() { [[ -d "$PROJECT_DIR/.jj" ]]; }
30
+
31
+ check_clean() {
32
+ if is_jj; then
33
+ if [[ -n "$(jj diff --summary 2>/dev/null)" ]]; then
34
+ echo "error: working copy is not clean (jj diff shows changes)" >&2
35
+ exit 1
36
+ fi
37
+ else
38
+ if [[ -n "$(git status --porcelain)" ]]; then
39
+ echo "error: working copy is not clean (git status shows changes)" >&2
40
+ exit 1
41
+ fi
42
+ fi
43
+ }
44
+
45
+ # Lint & type-check
46
+ echo "==> Running linters and type checkers..."
47
+ just lint
48
+
49
+ # Ensure working copy is clean
50
+ echo -n "==> Checking working copy is clean... "
51
+ check_clean
52
+ echo " OK"
53
+
54
+ # CHANGELOG.md is hand-authored under an `## Unreleased` heading; this
55
+ # script promotes that heading to the new version and dates it.
56
+ # Make sure that the Unreleased section is correctly there
57
+
58
+ UNRELEASED_COUNT="$(grep -c '^## Unreleased$' CHANGELOG.md || true)"
59
+ if [[ "$UNRELEASED_COUNT" -eq 0 ]]; then
60
+ echo "error: CHANGELOG.md has no '## Unreleased' section to promote" >&2
61
+ exit 1
62
+ elif [[ "$UNRELEASED_COUNT" -gt 1 ]]; then
63
+ echo "error: CHANGELOG.md has ${UNRELEASED_COUNT} '## Unreleased' sections; expected exactly one" >&2
64
+ exit 1
65
+ fi
66
+
67
+ # Bump version and check tag
68
+ OLD_VERSION="$(uv version --short)"
69
+ NEW_VERSION="$(uv version --bump "$BUMP" --dry-run --short)"
70
+ TAG="v${NEW_VERSION}"
71
+ RELEASE_DATE="$(date +%Y-%m-%d)"
72
+
73
+ echo "==> Bumping $BUMP version..."
74
+ echo " Current version: $OLD_VERSION"
75
+ echo " New version: $NEW_VERSION"
76
+ echo " Tag: $TAG"
77
+ echo " Date: $RELEASE_DATE"
78
+
79
+ if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null 2>&1; then
80
+ echo "error: tag '$TAG' already exists" >&2
81
+ echo "error: refusing to bump $BUMP version" >&2
82
+ exit 1
83
+ fi
84
+ uv version --bump "$BUMP"
85
+
86
+ # Promote CHANGELOG `Unreleased` to the new version
87
+ echo "==> Promoting CHANGELOG Unreleased -> ${NEW_VERSION} (${RELEASE_DATE})"
88
+ sed -i "s/^## Unreleased$/## ${NEW_VERSION} \xe2\x80\x94 ${RELEASE_DATE}/" CHANGELOG.md
89
+
90
+ # Commit
91
+ COMMIT_MSG="chore(release): ${NEW_VERSION}"
92
+ echo "==> Committing: $COMMIT_MSG"
93
+
94
+ if is_jj; then
95
+ jj commit -m "$COMMIT_MSG"
96
+ else
97
+ git add pyproject.toml CHANGELOG.md
98
+ git commit -m "$COMMIT_MSG"
99
+ fi
100
+
101
+ # Build the tag annotation from the just-promoted CHANGELOG section.
102
+ # Title is "release: $(uv version)"; body is the section content (the
103
+ # lines between the new `## <ver> ...` heading and the next `## ` heading).
104
+ TAG_TITLE="release: $(uv version)"
105
+ TAG_BODY="$(awk -v ver="$NEW_VERSION" -v prev="$OLD_VERSION" '
106
+ $0 ~ "^## " ver "( |$)" { capture = 1; next }
107
+ capture && /^## / {
108
+ if ($0 !~ "^## " prev "( |$)") {
109
+ printf "error: expected next changelog heading to be %s, got: %s\n", \
110
+ "## " prev, $0 > "/dev/stderr"
111
+ exit 2
112
+ }
113
+ exit
114
+ }
115
+ capture { print }
116
+ ' CHANGELOG.md)"
117
+
118
+ # Tag
119
+ echo "==> Tagging $TAG"
120
+ if is_jj; then
121
+ jj git import 2>/dev/null || true
122
+ fi
123
+ printf '%s\n\n%s\n' "$TAG_TITLE" "$TAG_BODY" \
124
+ | git tag -a "$TAG" --cleanup=verbatim -F -
125
+ if is_jj; then
126
+ jj git import 2>/dev/null || true
127
+ fi
128
+
129
+ echo ""
130
+ echo "Done! Created tag: ${TAG}"
131
+ echo "To publish, push the tag: git push origin ${TAG}"
@@ -13,6 +13,8 @@
13
13
  morphata.acceptance
14
14
  morphata.hoa
15
15
  morphata.logic.ltl
16
+ morphata.logic.sere
17
+ morphata.logic.psl
16
18
  morphata.logic.strel
17
19
  morphata.operators
18
20
  morphata.operators.matrix
@@ -0,0 +1,15 @@
1
+ PSL
2
+ ===
3
+
4
+ Property Specification Language to Alternating Automaton conversion.
5
+
6
+ Finite-trace (PSLf) and omega-trace (Büchi) acceptance are both
7
+ supported via the ``finite`` keyword argument to
8
+ :func:`~morphata.logic.psl.psl_to_automaton`. Suffix implications
9
+ follow Spot's overlapping semantics: the last symbol matched by the
10
+ SERE prefix is shared with the first position at which the
11
+ continuation is evaluated.
12
+
13
+ .. automodule:: morphata.logic.psl
14
+ :members:
15
+ :show-inheritance:
@@ -0,0 +1,9 @@
1
+ SERE
2
+ ====
3
+
4
+ Sequential Extended Regular Expression to Alternating Finite Automaton
5
+ conversion.
6
+
7
+ .. automodule:: morphata.logic.sere
8
+ :members:
9
+ :show-inheritance:
@@ -27,7 +27,7 @@ extensions = [
27
27
  ]
28
28
 
29
29
  templates_path = ["_templates"]
30
- exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
30
+ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "superpowers"]
31
31
 
32
32
  autodoc_default_options = {
33
33
  "members": True,