morphata 2.2.1__tar.gz → 2.2.3__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.2.1 → morphata-2.2.3}/.envrc +3 -5
- {morphata-2.2.1 → morphata-2.2.3}/.gitignore +2 -0
- {morphata-2.2.1 → morphata-2.2.3}/CHANGELOG.md +177 -0
- {morphata-2.2.1 → morphata-2.2.3}/PKG-INFO +3 -2
- {morphata-2.2.1 → morphata-2.2.3}/bench/README.md +11 -0
- morphata-2.2.3/bench/test_distance_perf.py +218 -0
- {morphata-2.2.1 → morphata-2.2.3}/bench/test_equivalence_perf.py +1 -2
- morphata-2.2.3/bench/test_matrix_operator_perf.py +412 -0
- {morphata-2.2.1 → morphata-2.2.3}/bench/test_transforms_perf.py +1 -1
- {morphata-2.2.1 → morphata-2.2.3}/docs/api/modules.rst +1 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/api/morphata.operators.rst +5 -0
- morphata-2.2.3/docs/api/morphata.storage.rst +36 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/api/morphata.symbolic.rst +3 -1
- {morphata-2.2.1 → morphata-2.2.3}/docs/concepts/operators.rst +38 -15
- {morphata-2.2.1 → morphata-2.2.3}/docs/concepts/weights.rst +13 -4
- {morphata-2.2.1 → morphata-2.2.3}/docs/conf.py +13 -13
- {morphata-2.2.1 → morphata-2.2.3}/docs/index.rst +9 -1
- {morphata-2.2.1 → morphata-2.2.3}/docs/quick-start.rst +22 -8
- {morphata-2.2.1 → morphata-2.2.3}/mypy.ini +2 -1
- morphata-2.2.3/pixi.lock +5107 -0
- {morphata-2.2.1 → morphata-2.2.3}/pyproject.toml +49 -44
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/__init__.py +2 -1
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/_version.py +2 -2
- morphata-2.2.3/src/morphata/alphabet.py +258 -0
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/builder.py +9 -27
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/logic/_common.py +15 -7
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/logic/psl.py +2 -2
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/operators/__init__.py +4 -1
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/operators/matrix.py +150 -79
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/operators/polynomial.py +13 -19
- morphata-2.2.3/src/morphata/storage/__init__.py +6 -0
- morphata-2.2.3/src/morphata/storage/serialize.py +260 -0
- morphata-2.2.3/src/morphata/storage/signature.py +207 -0
- morphata-2.2.3/src/morphata/symbolic/__init__.py +19 -0
- morphata-2.2.3/src/morphata/symbolic/automaton.py +1307 -0
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/symbolic/bdd.py +242 -31
- morphata-2.2.3/src/morphata/symbolic/distance.py +361 -0
- morphata-2.2.3/src/morphata/symbolic/transforms.py +1040 -0
- morphata-2.2.3/src/morphata/utils/__init__.py +9 -0
- morphata-2.2.1/src/morphata/utils.py → morphata-2.2.3/src/morphata/utils/expressions.py +2 -78
- morphata-2.2.3/src/morphata/utils/memory.py +75 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/test_exporter.py +27 -21
- {morphata-2.2.1 → morphata-2.2.3}/tests/logic/test_psl.py +2 -1
- {morphata-2.2.1 → morphata-2.2.3}/tests/operators/test_matrix.py +210 -36
- {morphata-2.2.1 → morphata-2.2.3}/tests/operators/test_polynomial.py +4 -3
- morphata-2.2.3/tests/storage/test_serialize.py +322 -0
- morphata-2.2.3/tests/storage/test_signature.py +470 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/symbolic/test_bdd_helpers.py +184 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/symbolic/test_compose.py +106 -10
- morphata-2.2.3/tests/symbolic/test_distance.py +150 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/symbolic/test_from_hoaf.py +5 -3
- {morphata-2.2.1 → morphata-2.2.3}/tests/symbolic/test_language_equivalence.py +62 -13
- {morphata-2.2.1 → morphata-2.2.3}/tests/symbolic/test_ltlf2dfa_equivalence.py +3 -4
- {morphata-2.2.1 → morphata-2.2.3}/tests/symbolic/test_symbolic_automaton.py +280 -33
- {morphata-2.2.1 → morphata-2.2.3}/tests/symbolic/test_to_hoa.py +5 -5
- {morphata-2.2.1 → morphata-2.2.3}/tests/symbolic/test_transforms.py +480 -26
- {morphata-2.2.1 → morphata-2.2.3}/tests/test_alphabet.py +10 -2
- {morphata-2.2.1 → morphata-2.2.3}/tests/test_builder.py +24 -30
- morphata-2.2.3/uv.lock +3285 -0
- morphata-2.2.1/pixi.lock +0 -7844
- morphata-2.2.1/pixi.toml +0 -127
- morphata-2.2.1/src/morphata/alphabet.py +0 -92
- morphata-2.2.1/src/morphata/symbolic/__init__.py +0 -14
- morphata-2.2.1/src/morphata/symbolic/automaton.py +0 -820
- morphata-2.2.1/src/morphata/symbolic/utils.py +0 -708
- morphata-2.2.1/uv.lock +0 -3335
- {morphata-2.2.1 → morphata-2.2.3}/.basedpyright/baseline.json +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/.dockerignore +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/.editorconfig +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/.envrc.recommended +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/.fdignore +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/.gitattributes +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/.mailmap +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/.python-version +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/.rstcheck.cfg +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/AGENTS.md +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/CLAUDE.md +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/CONTRIBUTING.md +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/LICENSE +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/README.rst +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/bench/__init__.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/bench/conftest.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/bench/test_hoa_exporter_perf.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/bench/test_hoa_parser_perf.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/bench/test_sere_nlm_perf.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/ci/make-release +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/Makefile +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/_static/navigation.html +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/api/morphata.acc_expr.rst +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/api/morphata.acceptance.rst +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/api/morphata.alphabet.rst +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/api/morphata.builder.rst +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/api/morphata.hoa.rst +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/api/morphata.logic.rst +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/changelog.md +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/concepts/acceptance.rst +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/concepts/hoa.rst +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/concepts/index.rst +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/docs/concepts/symbolic.rst +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/justfile +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/project.utf-8.add +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/project.utf-8.add.spl +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/pyrightconfig.json +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/pytest.toml +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/rules/no-derivative-multiarg.yml +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/rules/no-typing-as-ty.yml +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/sgconfig.yml +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/acc_expr.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/acceptance.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/hoa/__init__.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/hoa/__main__.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/hoa/hoa.lark +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/hoa/parser.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/logic/__init__.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/logic/__init__.pyi +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/logic/ltl.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/logic/sere.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/operators/_backend.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/src/morphata/py.typed +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/conftest.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/aut1.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/aut11.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/aut2.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/aut3.2.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/aut3.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/aut4.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/aut5.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/aut6.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/aut7.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/aut8.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/finite_aut.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/gen_buchi_3.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/gen_cobuchi_2.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/rabin_2pair.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/streett_2pair.hoa +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/hoa/test_parser.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/logic/__init__.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/logic/test_ltl.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/logic/test_sere.py +0 -0
- {morphata-2.2.1 → morphata-2.2.3}/tests/test_acceptance_validation.py +0 -0
|
@@ -12,15 +12,13 @@ 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
|
-
|
|
15
|
+
export CONDA_OVERRIDE_CUDA="$CUDA_VERSION"
|
|
16
|
+
|
|
16
17
|
fi
|
|
17
18
|
else
|
|
18
19
|
info "Did not detect nvidia-smi, using non-GPU environment"
|
|
19
|
-
PIXI_ENV="default"
|
|
20
20
|
fi
|
|
21
|
-
export PIXI_ENV
|
|
22
21
|
|
|
23
22
|
dotenv_if_exists
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
eval "$(pixi shell-hook -e "$PIXI_ENV")"
|
|
24
|
+
eval "$(pixi shell-hook)"
|
|
@@ -1,5 +1,182 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.3 — 2026-09-22
|
|
4
|
+
|
|
5
|
+
This is a hefty release, but all really cool things that I am super proud of
|
|
6
|
+
because now `morphata` is developed by directly dogfooding my projects.
|
|
7
|
+
I don't think there was any major change to the public API of this package,
|
|
8
|
+
so I will just be making this a patch release
|
|
9
|
+
(which is funny to imagine the huge patch on something physical).
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Alphabet domains are now respected by transformations, persistent storage,
|
|
14
|
+
and canonical DFA signatures.
|
|
15
|
+
Valuations outside the alphabet are treated
|
|
16
|
+
as impossible instead of quietly inflating transition partitions, serialized guards,
|
|
17
|
+
or language hashes.
|
|
18
|
+
No more ghost alphabet sneaking into the bookkeeping.
|
|
19
|
+
|
|
20
|
+
- This is a big change (in the context of the changeset),
|
|
21
|
+
but shouldn't be a big one for the public facing API for consumers:
|
|
22
|
+
`SymbolicAutomaton` now relies only on the `transition_relation` field
|
|
23
|
+
as a first class field, and the outgoing transitions in `transitions` are computed
|
|
24
|
+
on-the-fly (with a cache that is possibly precomputed and passed to the initializer).
|
|
25
|
+
Effectively, this flips what we were doing earlier,
|
|
26
|
+
where we computed the transition relation from the list of outgoing transitions.
|
|
27
|
+
|
|
28
|
+
- One thing that changed in the shadows of the previous one is that the API
|
|
29
|
+
for `SymbolicAutomaton` is more consistent!
|
|
30
|
+
`SymbolicAutomaton.materialize_sinks` was the only method
|
|
31
|
+
that mutated the automaton in-place, which feels wrong and, hence, was rectified:
|
|
32
|
+
`materialize_sinks` returns a new automaton *always*.
|
|
33
|
+
|
|
34
|
+
- The documentation now uses the Furo theme.
|
|
35
|
+
It is a small change to the library, and as much as I love Alabaster,
|
|
36
|
+
I feel like Furo does a better job with typography and minimizing clutter.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- Meet `SparseMatrixOperator`, the explicitly sparse sibling of `MatrixOperator`.
|
|
41
|
+
Both expose the same construction and evaluation API,
|
|
42
|
+
but the choice of execution strategy is now visible in the type:
|
|
43
|
+
`MatrixOperator` builds dense transition matrices,
|
|
44
|
+
while `SparseMatrixOperator.step` works directly over the automaton's edges.
|
|
45
|
+
The sparse operator still exposes `delta` for those moments
|
|
46
|
+
when a dense view really is what you want.
|
|
47
|
+
|
|
48
|
+
- Matrix operators now support leading batch dimensions natively.
|
|
49
|
+
The state axis is always last, so `step` accepts arrays shaped `(..., q)`,
|
|
50
|
+
`delta` returns `(..., q, q)`, and `accepts` preserves the leading batch shape.
|
|
51
|
+
This works with JAX transformations too, including `jit`, `vmap`, and gradients.
|
|
52
|
+
|
|
53
|
+
- `BDDManager.guard_prime_cover` can now compute a prime cover relative to a `domain`,
|
|
54
|
+
with an optional deterministic `predicate_order`.
|
|
55
|
+
`partition_by_state_residual` has the same domain-aware vocabulary.
|
|
56
|
+
Outside the domain is a genuine don't-care region,
|
|
57
|
+
which keeps finite-alphabet guards compact without changing their meaning.
|
|
58
|
+
|
|
59
|
+
- Symbolic automaton transformations accept an optional initial state formula,
|
|
60
|
+
allowing a transformation to start from a formula other than the automaton's
|
|
61
|
+
stored initial condition without modifying the source automaton.
|
|
62
|
+
|
|
63
|
+
- `prune_unreachable`, which removes the states unreachable from an automaton's initial
|
|
64
|
+
states and, with `remove_sink_states=True`, the materialized sink states too (an
|
|
65
|
+
accepting sink becomes `manager.true` and a rejecting one `manager.false` in every
|
|
66
|
+
formula referring to it).
|
|
67
|
+
The surviving states keep their indices, so the result may have gaps.
|
|
68
|
+
|
|
69
|
+
- `reindex_reachable`, which walks the reachable states breadth-first
|
|
70
|
+
and reindexes them contiguously,
|
|
71
|
+
optionally recording the correspondence in a caller-supplied `StateVarManager`,
|
|
72
|
+
materializing constant successors as sink states,
|
|
73
|
+
and breaking BFS ties with `predicate_exploration_order`.
|
|
74
|
+
|
|
75
|
+
Both are also available as `SymbolicAutomaton` methods.
|
|
76
|
+
|
|
77
|
+
- A `SymbolicAutomaton.sorted_transitions` method
|
|
78
|
+
that returns a state's `(guard, successors)` pairs
|
|
79
|
+
(or every state's, when no state is given) in a canonical order.
|
|
80
|
+
The AP valuations are enumerated lexicographically in `predicate_order`,
|
|
81
|
+
with `False` before `True`; without one,
|
|
82
|
+
the predicates are ordered by their string forms,
|
|
83
|
+
since the manager's variable levels are not stable under reordering.
|
|
84
|
+
The partition is recomputed on each call.
|
|
85
|
+
|
|
86
|
+
- A `make_sink_states` keyword on `remove_alternation`, `determinize`, and
|
|
87
|
+
`minimize_dfa`, controlling whether constant `True` and `False` successors are
|
|
88
|
+
materialized as indexed sink states.
|
|
89
|
+
|
|
90
|
+
- A `reverse_language` utility that produces an automaton
|
|
91
|
+
that accepts the reversed language.
|
|
92
|
+
|
|
93
|
+
- An `is_cyclic` utility to check if an automaton's state graph has a cycle
|
|
94
|
+
|
|
95
|
+
- An optional ``abort_on_max_size`` bound on macro-state exploration in
|
|
96
|
+
``explore_and_transform``, determinization, dealternation, and DFA minimization.
|
|
97
|
+
Exceeding the bound raises ``MacroStateLimitError`` with the number of states reached.
|
|
98
|
+
|
|
99
|
+
- An option for predictable ordering of visited/yielded states
|
|
100
|
+
when performing BFS-based exploration/partitioning of a `SymbolicAutomaton`.
|
|
101
|
+
|
|
102
|
+
The keyword argument `predicate_exploration_order` added to `explore_and_transform`
|
|
103
|
+
and `partition_by_state_residual`
|
|
104
|
+
(and, virally, to its dependents)
|
|
105
|
+
allows users to provide a lexicographic order in
|
|
106
|
+
which atomic predicate valuations will be visited, with `False` before `True`.
|
|
107
|
+
Doing so allows for the BFS ties to be broken through this lexicographic order, but,
|
|
108
|
+
since we do some processing of the ordering, it may lead to some tiny performance drop
|
|
109
|
+
(not been profiled, just a caveat).
|
|
110
|
+
|
|
111
|
+
- Add a signed version of the Levenshtein distance between two nondeterministic finite
|
|
112
|
+
automata.
|
|
113
|
+
|
|
114
|
+
- `SemiringAlphabet` now has two optional fields that allows one to enumerate all
|
|
115
|
+
symbols in the alphabet (not just predicates), and test if some symbol is contained in
|
|
116
|
+
the alphabet.
|
|
117
|
+
|
|
118
|
+
- In addition, `BooleanAlphabet` now have a mandatory `domain` keyword argument which
|
|
119
|
+
constrains the universe of allowed predicate valuations as a BDD.
|
|
120
|
+
|
|
121
|
+
- `morphata.storage` has means to store canonical representations (morphata-specific) of
|
|
122
|
+
`SymbolicAutomaton` to disk.
|
|
123
|
+
Additionally it exposes `CanonicalDfaHasher` that helps generate hashes
|
|
124
|
+
for the equivalence languages (residual and global language) of a DFA.
|
|
125
|
+
|
|
126
|
+
### Performance
|
|
127
|
+
|
|
128
|
+
- Matrix operators evaluate each distinct transition guard once per symbol
|
|
129
|
+
and then gather the result for its edges.
|
|
130
|
+
Besides removing a splendid amount of repeated work,
|
|
131
|
+
this keeps JAX's compiled weight construction tied to the number of distinct guards.
|
|
132
|
+
Repeated edges are handled by a gather, with no extra `weight_fn` calls.
|
|
133
|
+
|
|
134
|
+
- Sparse stepping now gathers source-state values, multiplies only the real edges,
|
|
135
|
+
and reduces them with `segment_sum`.
|
|
136
|
+
Dense stepping remains the happy path for genuinely dense automata;
|
|
137
|
+
sparse stepping stops a low-degree automaton from paying the full quadratic bill.
|
|
138
|
+
That is why these are separate classes rather than a clever-but-invisible flag.
|
|
139
|
+
|
|
140
|
+
### Developer Experience
|
|
141
|
+
|
|
142
|
+
- The Pixi environments now describe CPU, CUDA 12, and CUDA 13 targets with virtual
|
|
143
|
+
packages, so dependency resolution does not have to guess which machine it is
|
|
144
|
+
eventually going to meet. `sphinx-autobuild` also joins the development toolbox
|
|
145
|
+
for faster documentation tinkering.
|
|
146
|
+
|
|
147
|
+
### Fixed
|
|
148
|
+
|
|
149
|
+
- `reverse_language` now keeps an initial reversed state even when
|
|
150
|
+
that state has no outgoing transitions.
|
|
151
|
+
A perfectly valid dead end should still exist, after all.
|
|
152
|
+
|
|
153
|
+
- `min_dfa_of_conjunction` formerly said that it can only take list of `LTLExpr`,
|
|
154
|
+
but that isn't true.
|
|
155
|
+
Now, we correctly type it as `logic.Expr`.
|
|
156
|
+
|
|
157
|
+
## 2.2.2 — 2026-08-23
|
|
158
|
+
|
|
159
|
+
### Fixed
|
|
160
|
+
|
|
161
|
+
- Fixes a bug with complement and composition of nondeterministic automata
|
|
162
|
+
|
|
163
|
+
`__invert__` dualized each
|
|
164
|
+
(guard, successor)
|
|
165
|
+
pair on its own, leaving the implicit disjunction across a state's pairs undualized.
|
|
166
|
+
Read each state's relation back through `partition_by_state_residual` first,
|
|
167
|
+
so the disjunction sits inside one residual per disjoint guard class;
|
|
168
|
+
missing moves come back as a `false` residual, whose dual is the correct `true`.
|
|
169
|
+
|
|
170
|
+
`compose_automata` remapped only the reachable states,
|
|
171
|
+
so a fresh index could land on an unreachable state that kept its own index
|
|
172
|
+
and silently overwrite a transition row.
|
|
173
|
+
Remap every state and prune the unreachable rows afterwards.
|
|
174
|
+
`reindex_states` now rejects an assignment whose image collides with a state it does
|
|
175
|
+
not remap, so the same mistake raises instead of corrupting.
|
|
176
|
+
|
|
177
|
+
Together these made `is_language_equivalent` report False
|
|
178
|
+
for automata with identical languages.
|
|
179
|
+
|
|
3
180
|
## 2.2.1 — 2026-08-20
|
|
4
181
|
|
|
5
182
|
This version bump is mostly a cleanup after the 2.2.0 rewrite,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: morphata
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.3
|
|
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
|
|
@@ -13,6 +13,7 @@ Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
|
13
13
|
Requires-Python: >=3.12
|
|
14
14
|
Requires-Dist: algebraic-arrays>=1.3.0
|
|
15
15
|
Requires-Dist: attrs>=25.4.0
|
|
16
|
+
Requires-Dist: bitarray>=3.10.0
|
|
16
17
|
Requires-Dist: cattrs>=26.1.0
|
|
17
18
|
Requires-Dist: jaxtyping>=0.3
|
|
18
19
|
Requires-Dist: lark>=1.3.1
|
|
@@ -13,6 +13,13 @@ To get pytest-benchmark's tabular wall-time output:
|
|
|
13
13
|
|
|
14
14
|
## Suites
|
|
15
15
|
|
|
16
|
+
- `test_matrix_operator_perf.py` -- dense `MatrixOperator.step` versus sparse
|
|
17
|
+
`SparseMatrixOperator.step`, across NumPy eager, JAX `jit`, and PyTorch
|
|
18
|
+
`compile`. It reports public operator timings, prepared-topology kernel timings
|
|
19
|
+
over a density sweep, and a representative component breakdown.
|
|
20
|
+
JAX-only scale cases are opt-in via
|
|
21
|
+
`MORPHATA_RUN_SCALE_BENCHMARKS=1`; run each scale case in a fresh process under
|
|
22
|
+
a hard memory limit.
|
|
16
23
|
- `test_sere_nlm_perf.py` -- NLM-intersection state-space blowup.
|
|
17
24
|
Cases: `NLMInter(a0..a_{n-1})` for n in 2..6, `NLMInter(a_i;b_i)`
|
|
18
25
|
for n in 2..4, plus length-matching `Inter` baselines.
|
|
@@ -25,6 +32,10 @@ To get pytest-benchmark's tabular wall-time output:
|
|
|
25
32
|
the same grammar to quantify the LALR switch. Run with
|
|
26
33
|
``--benchmark-group-by=param:case`` to put the two algorithms side by
|
|
27
34
|
side per input.
|
|
35
|
+
- `test_distance_perf.py` -- signed Levenshtein distance, split into
|
|
36
|
+
end-to-end budget growth, ADD edit-model construction, layered neighborhoods,
|
|
37
|
+
and nondeterministic threshold inclusion. Each case prints intermediate state,
|
|
38
|
+
transition, ADD-node, and CUDD-node counts to locate the practical cliff.
|
|
28
39
|
|
|
29
40
|
## Recording results
|
|
30
41
|
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Benchmarks for signed symbolic Levenshtein distance.
|
|
2
|
+
|
|
3
|
+
The suite separates the main sources of growth instead of treating the input
|
|
4
|
+
state count as a sufficient predictor:
|
|
5
|
+
|
|
6
|
+
* end-to-end distance as the finite edit budget grows;
|
|
7
|
+
* ADD edit-model construction as the target NFA grows;
|
|
8
|
+
* compilation of an ADD model into a budget-layered neighborhood;
|
|
9
|
+
* language inclusion against a nondeterministic neighborhood.
|
|
10
|
+
|
|
11
|
+
The printed diagnostics identify which intermediate representation reaches the
|
|
12
|
+
practical limit first. There are deliberately no timing assertions: this is an
|
|
13
|
+
opt-in exploratory suite whose timeout or memory limit records the cliff.
|
|
14
|
+
|
|
15
|
+
Run with::
|
|
16
|
+
|
|
17
|
+
pytest bench/test_distance_perf.py -v \
|
|
18
|
+
--benchmark-columns=mean,median,min,max,rounds
|
|
19
|
+
|
|
20
|
+
For a one-pass structural sweep with every diagnostic visible::
|
|
21
|
+
|
|
22
|
+
pytest bench/test_distance_perf.py -q -s --benchmark-disable
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from collections.abc import Set as AbstractSet
|
|
28
|
+
|
|
29
|
+
import pytest
|
|
30
|
+
from pytest_benchmark.fixture import BenchmarkFixture as Benchmark
|
|
31
|
+
|
|
32
|
+
from morphata.acceptance import Finite
|
|
33
|
+
from morphata.alphabet import BooleanAlphabet, powerset_alphabet
|
|
34
|
+
from morphata.symbolic import (
|
|
35
|
+
BDDManager,
|
|
36
|
+
Function,
|
|
37
|
+
SymbolicAutomaton,
|
|
38
|
+
levenshtein_distance,
|
|
39
|
+
)
|
|
40
|
+
from morphata.symbolic.distance import (
|
|
41
|
+
_edit_model, # pyright: ignore[reportPrivateUsage]
|
|
42
|
+
_is_subset, # pyright: ignore[reportPrivateUsage]
|
|
43
|
+
_neighborhood, # pyright: ignore[reportPrivateUsage]
|
|
44
|
+
_prepare, # pyright: ignore[reportPrivateUsage]
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
type _Symbol = AbstractSet[str]
|
|
48
|
+
type _Aut = SymbolicAutomaton[str, _Symbol]
|
|
49
|
+
|
|
50
|
+
_BUDGETS = [1, 2, 4, 8, 12, 16]
|
|
51
|
+
_TARGET_SIZES = [4, 8, 16, 32, 64]
|
|
52
|
+
_LAYER_SIZES = [2, 4, 8, 12, 16, 24]
|
|
53
|
+
_INCLUSION_SIZES = [2, 4, 6, 8, 10, 12]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _alphabet() -> BooleanAlphabet[str, _Symbol]:
|
|
57
|
+
"""Return the two-symbol AP-valuation alphabet used by every family."""
|
|
58
|
+
return powerset_alphabet({"a"})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _epsilon(
|
|
62
|
+
manager: BDDManager[str, int], alphabet: BooleanAlphabet[str, _Symbol]
|
|
63
|
+
) -> _Aut:
|
|
64
|
+
"""Return the singleton language containing only the empty word."""
|
|
65
|
+
manager.declare_state_vars(0)
|
|
66
|
+
transitions: dict[int, list[tuple[Function[str, int], Function[str, int]]]] = {
|
|
67
|
+
0: []
|
|
68
|
+
}
|
|
69
|
+
return SymbolicAutomaton(
|
|
70
|
+
manager=manager,
|
|
71
|
+
initial=manager.q_var(0),
|
|
72
|
+
transition_relation={
|
|
73
|
+
q: manager.relation_from_rows(r) for q, r in transitions.items()
|
|
74
|
+
},
|
|
75
|
+
acceptance=Finite(frozenset({0})),
|
|
76
|
+
alphabet=alphabet,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _length_at_most(
|
|
81
|
+
manager: BDDManager[str, int],
|
|
82
|
+
alphabet: BooleanAlphabet[str, _Symbol],
|
|
83
|
+
length: int,
|
|
84
|
+
) -> _Aut:
|
|
85
|
+
"""Return an NFA for all AP words of length at most ``length``."""
|
|
86
|
+
manager.declare_state_vars(*range(length + 1))
|
|
87
|
+
transitions = {
|
|
88
|
+
state: [(manager.true, manager.q_var(state + 1))] for state in range(length)
|
|
89
|
+
}
|
|
90
|
+
transitions[length] = []
|
|
91
|
+
return SymbolicAutomaton(
|
|
92
|
+
manager=manager,
|
|
93
|
+
initial=manager.q_var(0),
|
|
94
|
+
transition_relation={
|
|
95
|
+
q: manager.relation_from_rows(r) for q, r in transitions.items()
|
|
96
|
+
},
|
|
97
|
+
acceptance=Finite(frozenset(range(length + 1))),
|
|
98
|
+
alphabet=alphabet,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _branching_nfa(
|
|
103
|
+
manager: BDDManager[str, int],
|
|
104
|
+
alphabet: BooleanAlphabet[str, _Symbol],
|
|
105
|
+
num_states: int,
|
|
106
|
+
) -> _Aut:
|
|
107
|
+
"""Return a reachable, coaccessible NFA with overlapping successors."""
|
|
108
|
+
manager.declare_ap_vars("a")
|
|
109
|
+
manager.declare_state_vars(*range(num_states))
|
|
110
|
+
a = manager.ap_var("a")
|
|
111
|
+
transitions = {
|
|
112
|
+
state: [
|
|
113
|
+
(a, manager.q_var(state) | manager.q_var(state + 1)),
|
|
114
|
+
(~a, manager.q_var(state + 1)),
|
|
115
|
+
]
|
|
116
|
+
for state in range(num_states - 1)
|
|
117
|
+
}
|
|
118
|
+
transitions[num_states - 1] = [(manager.true, manager.q_var(num_states - 1))]
|
|
119
|
+
return SymbolicAutomaton(
|
|
120
|
+
manager=manager,
|
|
121
|
+
initial=manager.q_var(0),
|
|
122
|
+
transition_relation={
|
|
123
|
+
q: manager.relation_from_rows(r) for q, r in transitions.items()
|
|
124
|
+
},
|
|
125
|
+
acceptance=Finite(frozenset({num_states - 1})),
|
|
126
|
+
alphabet=alphabet,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _manager_stats(manager: BDDManager[str, int]) -> str:
|
|
131
|
+
"""Return current and peak CUDD node counts for benchmark diagnostics."""
|
|
132
|
+
raw_manager = manager.true.fn.manager
|
|
133
|
+
return f"cudd_nodes={raw_manager.node_count}, peak_cudd_nodes={raw_manager.peak_node_count}"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.mark.limit_memory("2 GB")
|
|
137
|
+
@pytest.mark.timeout(10 * 60)
|
|
138
|
+
@pytest.mark.parametrize("budget", _BUDGETS)
|
|
139
|
+
def test_distance_budget_growth(
|
|
140
|
+
benchmark: Benchmark, bench_mgr: BDDManager[str, int], budget: int
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Measure the complete finite-positive branch as its answer grows."""
|
|
143
|
+
alphabet = _alphabet()
|
|
144
|
+
source = _length_at_most(bench_mgr, alphabet, budget)
|
|
145
|
+
target = _epsilon(bench_mgr, alphabet)
|
|
146
|
+
|
|
147
|
+
result = benchmark(levenshtein_distance, source, target)
|
|
148
|
+
|
|
149
|
+
assert result == budget
|
|
150
|
+
bound = (1 + source.num_states) * target.num_states
|
|
151
|
+
message = f"\ndistance budget={budget}: |A|={source.num_states}, |B|={target.num_states}, "
|
|
152
|
+
message += f"K={bound}, {_manager_stats(bench_mgr)}"
|
|
153
|
+
print(message)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@pytest.mark.limit_memory("2 GB")
|
|
157
|
+
@pytest.mark.timeout(10 * 60)
|
|
158
|
+
@pytest.mark.parametrize("num_states", _TARGET_SIZES)
|
|
159
|
+
def test_edit_model_target_growth(
|
|
160
|
+
benchmark: Benchmark, bench_mgr: BDDManager[str, int], num_states: int
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Measure deletion closure and ADD construction for growing targets."""
|
|
163
|
+
target = _prepare(_branching_nfa(bench_mgr, _alphabet(), num_states))
|
|
164
|
+
|
|
165
|
+
model = benchmark(_edit_model, target)
|
|
166
|
+
|
|
167
|
+
add_nodes = [weight.node_count() for weight in model.transition_cost.values()]
|
|
168
|
+
message = f"\nedit model |B|={num_states}: entries={len(add_nodes)}, "
|
|
169
|
+
message += (
|
|
170
|
+
f"add_nodes_total={sum(add_nodes)}, add_nodes_max={max(add_nodes, default=0)}, "
|
|
171
|
+
)
|
|
172
|
+
message += _manager_stats(bench_mgr)
|
|
173
|
+
print(message)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@pytest.mark.limit_memory("2 GB")
|
|
177
|
+
@pytest.mark.timeout(10 * 60)
|
|
178
|
+
@pytest.mark.parametrize("num_states", _LAYER_SIZES)
|
|
179
|
+
def test_neighborhood_layer_growth(
|
|
180
|
+
benchmark: Benchmark, bench_mgr: BDDManager[str, int], num_states: int
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Measure reachable layered-state growth at ``k = |Q_B|``."""
|
|
183
|
+
target = _prepare(_branching_nfa(bench_mgr, _alphabet(), num_states))
|
|
184
|
+
model = _edit_model(target)
|
|
185
|
+
k = num_states
|
|
186
|
+
|
|
187
|
+
neighborhood = benchmark(_neighborhood, model, k)
|
|
188
|
+
|
|
189
|
+
num_transitions = sum(len(row) for row in neighborhood.transitions().values())
|
|
190
|
+
message = (
|
|
191
|
+
f"\nneighborhood |B|={num_states}, k={k}: states={neighborhood.num_states}, "
|
|
192
|
+
)
|
|
193
|
+
message += f"transitions={num_transitions}, {_manager_stats(bench_mgr)}"
|
|
194
|
+
print(message)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@pytest.mark.limit_memory("2 GB")
|
|
198
|
+
@pytest.mark.timeout(10 * 60)
|
|
199
|
+
@pytest.mark.parametrize("num_states", _INCLUSION_SIZES)
|
|
200
|
+
def test_threshold_inclusion_nondeterminism(
|
|
201
|
+
benchmark: Benchmark, bench_mgr: BDDManager[str, int], num_states: int
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Measure the inclusion check after constructing a nondeterministic neighborhood."""
|
|
204
|
+
source = _prepare(_branching_nfa(bench_mgr, _alphabet(), num_states))
|
|
205
|
+
model = _edit_model(source)
|
|
206
|
+
k = max(1, num_states // 2)
|
|
207
|
+
neighborhood = _neighborhood(model, k)
|
|
208
|
+
|
|
209
|
+
result = benchmark(_is_subset, source, neighborhood)
|
|
210
|
+
|
|
211
|
+
assert result
|
|
212
|
+
num_transitions = sum(len(row) for row in neighborhood.transitions().values())
|
|
213
|
+
message = f"\ninclusion |A|=|B|={num_states}, k={k}: "
|
|
214
|
+
message += f"neighborhood_states={neighborhood.num_states}, "
|
|
215
|
+
message += (
|
|
216
|
+
f"neighborhood_transitions={num_transitions}, {_manager_stats(bench_mgr)}"
|
|
217
|
+
)
|
|
218
|
+
print(message)
|
|
@@ -21,7 +21,6 @@ from pytest_benchmark.fixture import BenchmarkFixture as Benchmark
|
|
|
21
21
|
|
|
22
22
|
from morphata.alphabet import powerset_alphabet
|
|
23
23
|
from morphata.symbolic.automaton import SymbolicAutomaton
|
|
24
|
-
from morphata.symbolic.utils import is_language_equivalent
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
def _sparse_big_alphabet_dfa() -> SymbolicAutomaton[str, AbstractSet[str]]:
|
|
@@ -44,6 +43,6 @@ def test_language_equivalent_sparse_big_alphabet(benchmark: Benchmark) -> None:
|
|
|
44
43
|
dfa = _sparse_big_alphabet_dfa()
|
|
45
44
|
|
|
46
45
|
def _run() -> bool:
|
|
47
|
-
return is_language_equivalent(dfa
|
|
46
|
+
return dfa.is_language_equivalent(dfa)
|
|
48
47
|
|
|
49
48
|
assert benchmark(_run)
|