morphata 2.0.1__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 (137) hide show
  1. {morphata-2.0.1 → morphata-2.0.2}/.claude/settings.local.json +9 -1
  2. {morphata-2.0.1 → morphata-2.0.2}/.envrc +0 -4
  3. {morphata-2.0.1 → morphata-2.0.2}/CHANGELOG.md +99 -30
  4. {morphata-2.0.1 → morphata-2.0.2}/PKG-INFO +2 -2
  5. morphata-2.0.2/bench/README.md +28 -0
  6. morphata-2.0.2/bench/conftest.py +53 -0
  7. morphata-2.0.2/bench/test_sere_nlm_perf.py +84 -0
  8. morphata-2.0.2/ci/make-release +131 -0
  9. {morphata-2.0.1 → morphata-2.0.2}/pixi.lock +157 -464
  10. {morphata-2.0.1 → morphata-2.0.2}/pixi.toml +9 -7
  11. {morphata-2.0.1 → morphata-2.0.2}/pyproject.toml +3 -2
  12. morphata-2.0.2/pytest.toml +32 -0
  13. morphata-2.0.2/src/morphata/logic/_common.py +136 -0
  14. morphata-2.0.2/src/morphata/logic/psl.py +372 -0
  15. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/logic/sere.py +148 -35
  16. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/symbolic/bdd.py +2 -1
  17. {morphata-2.0.1 → morphata-2.0.2}/tests/conftest.py +17 -0
  18. morphata-2.0.2/tests/logic/__init__.py +0 -0
  19. morphata-2.0.2/tests/logic/test_psl.py +179 -0
  20. morphata-2.0.2/tests/logic/test_sere.py +489 -0
  21. morphata-2.0.2/tests/logic/test_sere_properties.py +132 -0
  22. {morphata-2.0.1 → morphata-2.0.2}/uv.lock +95 -70
  23. morphata-2.0.1/ci/make-release +0 -99
  24. morphata-2.0.1/pytest.toml +0 -14
  25. morphata-2.0.1/src/morphata/logic/_common.py +0 -72
  26. morphata-2.0.1/src/morphata/logic/psl.py +0 -495
  27. morphata-2.0.1/tests/logic/test_psl.py +0 -230
  28. morphata-2.0.1/tests/logic/test_sere.py +0 -236
  29. {morphata-2.0.1 → morphata-2.0.2}/.basedpyright/baseline.json +0 -0
  30. {morphata-2.0.1 → morphata-2.0.2}/.dockerignore +0 -0
  31. {morphata-2.0.1 → morphata-2.0.2}/.editorconfig +0 -0
  32. {morphata-2.0.1 → morphata-2.0.2}/.envrc.recommended +0 -0
  33. {morphata-2.0.1 → morphata-2.0.2}/.fdignore +0 -0
  34. {morphata-2.0.1 → morphata-2.0.2}/.gitattributes +0 -0
  35. {morphata-2.0.1 → morphata-2.0.2}/.gitignore +0 -0
  36. {morphata-2.0.1 → morphata-2.0.2}/.mailmap +0 -0
  37. {morphata-2.0.1 → morphata-2.0.2}/.python-version +0 -0
  38. {morphata-2.0.1 → morphata-2.0.2}/.rstcheck.cfg +0 -0
  39. {morphata-2.0.1 → morphata-2.0.2}/AGENTS.md +0 -0
  40. {morphata-2.0.1 → morphata-2.0.2}/CLAUDE.md +0 -0
  41. {morphata-2.0.1 → morphata-2.0.2}/CONTRIBUTING.md +0 -0
  42. {morphata-2.0.1 → morphata-2.0.2}/LICENSE +0 -0
  43. {morphata-2.0.1 → morphata-2.0.2}/README.md +0 -0
  44. {morphata-2.0.1/tests/logic → morphata-2.0.2/bench}/__init__.py +0 -0
  45. {morphata-2.0.1 → morphata-2.0.2}/cliff.toml +0 -0
  46. {morphata-2.0.1 → morphata-2.0.2}/docs/_static/navigation.html +0 -0
  47. {morphata-2.0.1 → morphata-2.0.2}/docs/api/modules.rst +0 -0
  48. {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.acceptance.rst +0 -0
  49. {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.alphabet.rst +0 -0
  50. {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.automaton.rst +0 -0
  51. {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.builder.rst +0 -0
  52. {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.hoa.rst +0 -0
  53. {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.logic.ltl.rst +0 -0
  54. {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.logic.psl.rst +0 -0
  55. {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.logic.sere.rst +0 -0
  56. {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.logic.strel.rst +0 -0
  57. {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.operators.matrix.rst +0 -0
  58. {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.operators.polynomial.rst +0 -0
  59. {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.operators.rst +0 -0
  60. {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.symbolic.rst +0 -0
  61. {morphata-2.0.1 → morphata-2.0.2}/docs/concepts/acceptance.rst +0 -0
  62. {morphata-2.0.1 → morphata-2.0.2}/docs/concepts/hoa.rst +0 -0
  63. {morphata-2.0.1 → morphata-2.0.2}/docs/concepts/index.rst +0 -0
  64. {morphata-2.0.1 → morphata-2.0.2}/docs/concepts/operators.rst +0 -0
  65. {morphata-2.0.1 → morphata-2.0.2}/docs/concepts/spec.rst +0 -0
  66. {morphata-2.0.1 → morphata-2.0.2}/docs/concepts/weights.rst +0 -0
  67. {morphata-2.0.1 → morphata-2.0.2}/docs/conf.py +0 -0
  68. {morphata-2.0.1 → morphata-2.0.2}/docs/index.rst +0 -0
  69. {morphata-2.0.1 → morphata-2.0.2}/docs/quick-start.rst +0 -0
  70. {morphata-2.0.1 → morphata-2.0.2}/docs/symbolic-transforms.rst +0 -0
  71. {morphata-2.0.1 → morphata-2.0.2}/justfile +0 -0
  72. {morphata-2.0.1 → morphata-2.0.2}/mypy.ini +0 -0
  73. {morphata-2.0.1 → morphata-2.0.2}/project.utf-8.add +0 -0
  74. {morphata-2.0.1 → morphata-2.0.2}/project.utf-8.add.spl +0 -0
  75. {morphata-2.0.1 → morphata-2.0.2}/pyrefly.toml +0 -0
  76. {morphata-2.0.1 → morphata-2.0.2}/pyrightconfig.json +0 -0
  77. {morphata-2.0.1 → morphata-2.0.2}/ruff.toml +0 -0
  78. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/__init__.py +0 -0
  79. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/acceptance.py +0 -0
  80. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/alphabet.py +0 -0
  81. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/automaton.py +0 -0
  82. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/builder.py +0 -0
  83. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/hoa/__init__.py +0 -0
  84. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/hoa/__main__.py +0 -0
  85. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/hoa/acc_expr.py +0 -0
  86. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/hoa/exporter.py +0 -0
  87. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/hoa/hoa.lark +0 -0
  88. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/hoa/parser.py +0 -0
  89. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/logic/__init__.py +0 -0
  90. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/logic/ltl.py +0 -0
  91. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/logic/strel.py +0 -0
  92. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/operators/__init__.py +0 -0
  93. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/operators/_backend.py +0 -0
  94. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/operators/matrix.py +0 -0
  95. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/operators/polynomial.py +0 -0
  96. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/py.typed +0 -0
  97. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/symbolic/__init__.py +0 -0
  98. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/symbolic/automaton.py +0 -0
  99. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/symbolic/equivalence.py +0 -0
  100. {morphata-2.0.1 → morphata-2.0.2}/src/morphata/symbolic/transforms.py +0 -0
  101. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut1.hoa +0 -0
  102. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut11.hoa +0 -0
  103. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut2.hoa +0 -0
  104. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut3.2.hoa +0 -0
  105. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut3.hoa +0 -0
  106. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut4.hoa +0 -0
  107. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut5.hoa +0 -0
  108. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut6.hoa +0 -0
  109. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut7.hoa +0 -0
  110. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut8.hoa +0 -0
  111. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/finite_aut.hoa +0 -0
  112. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/gen_buchi_3.hoa +0 -0
  113. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/gen_cobuchi_2.hoa +0 -0
  114. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/rabin_2pair.hoa +0 -0
  115. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/streett_2pair.hoa +0 -0
  116. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/test_exporter_basic.py +0 -0
  117. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/test_exporter_roundtrip.py +0 -0
  118. {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/test_parser.py +0 -0
  119. {morphata-2.0.1 → morphata-2.0.2}/tests/logic/test_ltl.py +0 -0
  120. {morphata-2.0.1 → morphata-2.0.2}/tests/logic/test_strel.py +0 -0
  121. {morphata-2.0.1 → morphata-2.0.2}/tests/operators/test_matrix_from_ir.py +0 -0
  122. {morphata-2.0.1 → morphata-2.0.2}/tests/operators/test_matrix_from_ltl.py +0 -0
  123. {morphata-2.0.1 → morphata-2.0.2}/tests/operators/test_polynomial_from_ir.py +0 -0
  124. {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_bdd_helpers.py +0 -0
  125. {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_from_hoaf.py +0 -0
  126. {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_ltlf2dfa_equivalence.py +0 -0
  127. {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_step.py +0 -0
  128. {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_symbolic_automaton.py +0 -0
  129. {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_symbolic_from_ir.py +0 -0
  130. {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_transforms.py +0 -0
  131. {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_transforms_ir.py +0 -0
  132. {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_transforms_properties.py +0 -0
  133. {morphata-2.0.1 → morphata-2.0.2}/tests/test_alphabet.py +0 -0
  134. {morphata-2.0.1 → morphata-2.0.2}/tests/test_automaton.py +0 -0
  135. {morphata-2.0.1 → morphata-2.0.2}/tests/test_builder.py +0 -0
  136. {morphata-2.0.1 → morphata-2.0.2}/tests/test_from_parsed.py +0 -0
  137. {morphata-2.0.1 → morphata-2.0.2}/ty.toml +0 -0
@@ -15,7 +15,15 @@
15
15
  "Bash(.venv/bin/python -m pytest *)",
16
16
  "Skill(superpowers:writing-plans)",
17
17
  "Skill(superpowers:subagent-driven-development)",
18
- "Bash(grep -v \"AssertionError\\\\|Traceback\\\\|File \\\\|^\\\\s*$\")"
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: $?\")"
19
27
  ]
20
28
  }
21
29
  }
@@ -24,7 +24,3 @@ dotenv_if_exists
24
24
 
25
25
  watch_file pixi.lock
26
26
  eval "$(pixi shell-hook -e "$PIXI_ENV")"
27
-
28
- if [[ -d "$CONDA_PREFIX" ]]; then
29
- export VIRTUAL_ENV="$CONDA_PREFIX"
30
- fi
@@ -1,47 +1,116 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 2.0.2 — 2026-05-27
4
4
 
5
5
  ### Added
6
6
 
7
- - `morphata.logic.sere.sere_to_automaton(formula)` converts a
8
- SERE expression to an alternating finite automaton. Supports
9
- `Concat`, `Fusion`, `Alt`, `Inter` (length-matching), `Repeat`
10
- (bounded and unbounded), and Boolean leaves (`Literal`, `Variable`,
11
- `Not`, `And`, `Or`).
12
- - `morphata.logic.psl.psl_to_automaton(phi, *, finite=False)`
13
- converts a PSL expression to an alternating automaton, with
14
- `Finite` acceptance for PSLf (`finite=True`) and `Büchi` acceptance
15
- for full PSL (`finite=False`). Supports the strong / weak /
16
- negated-strong closure operators and existential / universal
17
- suffix implication. Suffix implications follow Spot's overlapping
18
- semantics: the last symbol matched by the SERE prefix is shared
19
- with the first position at which the continuation is evaluated.
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.
20
19
 
21
20
  ### Changed
22
21
 
23
- - `morphata.logic.ltl` factored its per-call BDD scratch-space
24
- construction into a shared helper
25
- `morphata.logic._common.make_logic_context`, reused by the SERE
26
- and PSL converters. Two LTL helpers (`_aut_symbolic_expansion`,
27
- `_empty_trace_value`) were renamed to `ltl_symbolic_expansion` /
28
- `ltl_empty_trace_value` so PSL can delegate to them for pure-LTL
29
- subformulas.
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
30
57
 
31
- ### Known limitations
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
32
107
 
33
108
  - `psl_to_automaton` does not yet decompose LTL temporal operators
34
- that wrap PSL closures at the top level
35
- (e.g. `Always(StrongClosure(r))`). Lift the PSL fragment out of
36
- the surrounding LTL temporal operator before calling.
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.
37
111
  - The continuation of a `SuffixImpliesExist` / `SuffixImpliesUniv`
38
112
  must be a pure-LTL formula; nested PSL closures inside a
39
113
  continuation raise `NotImplementedError`.
40
- - The Büchi acceptance classification for PSL has not been
41
- cross-checked against a published reference for every operator
42
- combination; finite-trace (PSLf) acceptance is exercised end-to-end
43
- via `SymbolicAutomaton.accepts` and is high-confidence, but Büchi
44
- is currently only smoke-tested for construction.
45
114
 
46
115
  ## 2.0.0 — 2026-05-23
47
116
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphata
3
- Version: 2.0.1
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.6.0
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}"