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.
- {morphata-2.0.1 → morphata-2.0.2}/.claude/settings.local.json +9 -1
- {morphata-2.0.1 → morphata-2.0.2}/.envrc +0 -4
- {morphata-2.0.1 → morphata-2.0.2}/CHANGELOG.md +99 -30
- {morphata-2.0.1 → morphata-2.0.2}/PKG-INFO +2 -2
- morphata-2.0.2/bench/README.md +28 -0
- morphata-2.0.2/bench/conftest.py +53 -0
- morphata-2.0.2/bench/test_sere_nlm_perf.py +84 -0
- morphata-2.0.2/ci/make-release +131 -0
- {morphata-2.0.1 → morphata-2.0.2}/pixi.lock +157 -464
- {morphata-2.0.1 → morphata-2.0.2}/pixi.toml +9 -7
- {morphata-2.0.1 → morphata-2.0.2}/pyproject.toml +3 -2
- morphata-2.0.2/pytest.toml +32 -0
- morphata-2.0.2/src/morphata/logic/_common.py +136 -0
- morphata-2.0.2/src/morphata/logic/psl.py +372 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/logic/sere.py +148 -35
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/symbolic/bdd.py +2 -1
- {morphata-2.0.1 → morphata-2.0.2}/tests/conftest.py +17 -0
- morphata-2.0.2/tests/logic/__init__.py +0 -0
- morphata-2.0.2/tests/logic/test_psl.py +179 -0
- morphata-2.0.2/tests/logic/test_sere.py +489 -0
- morphata-2.0.2/tests/logic/test_sere_properties.py +132 -0
- {morphata-2.0.1 → morphata-2.0.2}/uv.lock +95 -70
- morphata-2.0.1/ci/make-release +0 -99
- morphata-2.0.1/pytest.toml +0 -14
- morphata-2.0.1/src/morphata/logic/_common.py +0 -72
- morphata-2.0.1/src/morphata/logic/psl.py +0 -495
- morphata-2.0.1/tests/logic/test_psl.py +0 -230
- morphata-2.0.1/tests/logic/test_sere.py +0 -236
- {morphata-2.0.1 → morphata-2.0.2}/.basedpyright/baseline.json +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/.dockerignore +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/.editorconfig +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/.envrc.recommended +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/.fdignore +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/.gitattributes +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/.gitignore +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/.mailmap +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/.python-version +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/.rstcheck.cfg +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/AGENTS.md +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/CLAUDE.md +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/CONTRIBUTING.md +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/LICENSE +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/README.md +0 -0
- {morphata-2.0.1/tests/logic → morphata-2.0.2/bench}/__init__.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/cliff.toml +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/_static/navigation.html +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/api/modules.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.acceptance.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.alphabet.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.automaton.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.builder.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.hoa.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.logic.ltl.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.logic.psl.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.logic.sere.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.logic.strel.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.operators.matrix.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.operators.polynomial.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.operators.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/api/morphata.symbolic.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/concepts/acceptance.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/concepts/hoa.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/concepts/index.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/concepts/operators.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/concepts/spec.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/concepts/weights.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/conf.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/index.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/quick-start.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/docs/symbolic-transforms.rst +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/justfile +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/mypy.ini +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/project.utf-8.add +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/project.utf-8.add.spl +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/pyrefly.toml +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/pyrightconfig.json +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/ruff.toml +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/__init__.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/acceptance.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/alphabet.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/automaton.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/builder.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/hoa/__init__.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/hoa/__main__.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/hoa/acc_expr.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/hoa/exporter.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/hoa/hoa.lark +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/hoa/parser.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/logic/__init__.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/logic/ltl.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/logic/strel.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/operators/__init__.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/operators/_backend.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/operators/matrix.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/operators/polynomial.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/py.typed +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/symbolic/__init__.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/symbolic/automaton.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/symbolic/equivalence.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/src/morphata/symbolic/transforms.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut1.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut11.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut2.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut3.2.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut3.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut4.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut5.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut6.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut7.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/aut8.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/finite_aut.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/gen_buchi_3.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/gen_cobuchi_2.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/rabin_2pair.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/streett_2pair.hoa +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/test_exporter_basic.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/test_exporter_roundtrip.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/hoa/test_parser.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/logic/test_ltl.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/logic/test_strel.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/operators/test_matrix_from_ir.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/operators/test_matrix_from_ltl.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/operators/test_polynomial_from_ir.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_bdd_helpers.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_from_hoaf.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_ltlf2dfa_equivalence.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_step.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_symbolic_automaton.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_symbolic_from_ir.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_transforms.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_transforms_ir.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/symbolic/test_transforms_properties.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/test_alphabet.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/test_automaton.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/test_builder.py +0 -0
- {morphata-2.0.1 → morphata-2.0.2}/tests/test_from_parsed.py +0 -0
- {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
|
}
|
|
@@ -1,47 +1,116 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 2.0.2 — 2026-05-27
|
|
4
4
|
|
|
5
5
|
### Added
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
`
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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}"
|