morphata 2.2.2__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.
Files changed (140) hide show
  1. {morphata-2.2.2 → morphata-2.2.3}/.envrc +3 -5
  2. {morphata-2.2.2 → morphata-2.2.3}/.gitignore +2 -0
  3. {morphata-2.2.2 → morphata-2.2.3}/CHANGELOG.md +154 -0
  4. {morphata-2.2.2 → morphata-2.2.3}/PKG-INFO +3 -2
  5. {morphata-2.2.2 → morphata-2.2.3}/bench/README.md +11 -0
  6. morphata-2.2.3/bench/test_distance_perf.py +218 -0
  7. {morphata-2.2.2 → morphata-2.2.3}/bench/test_equivalence_perf.py +1 -2
  8. morphata-2.2.3/bench/test_matrix_operator_perf.py +412 -0
  9. {morphata-2.2.2 → morphata-2.2.3}/bench/test_transforms_perf.py +1 -1
  10. {morphata-2.2.2 → morphata-2.2.3}/docs/api/modules.rst +1 -0
  11. {morphata-2.2.2 → morphata-2.2.3}/docs/api/morphata.operators.rst +5 -0
  12. morphata-2.2.3/docs/api/morphata.storage.rst +36 -0
  13. {morphata-2.2.2 → morphata-2.2.3}/docs/api/morphata.symbolic.rst +3 -1
  14. {morphata-2.2.2 → morphata-2.2.3}/docs/concepts/operators.rst +38 -15
  15. {morphata-2.2.2 → morphata-2.2.3}/docs/concepts/weights.rst +13 -4
  16. {morphata-2.2.2 → morphata-2.2.3}/docs/conf.py +13 -13
  17. {morphata-2.2.2 → morphata-2.2.3}/docs/index.rst +9 -1
  18. {morphata-2.2.2 → morphata-2.2.3}/docs/quick-start.rst +22 -8
  19. {morphata-2.2.2 → morphata-2.2.3}/mypy.ini +2 -1
  20. morphata-2.2.3/pixi.lock +5107 -0
  21. {morphata-2.2.2 → morphata-2.2.3}/pyproject.toml +49 -44
  22. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/__init__.py +2 -1
  23. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/_version.py +2 -2
  24. morphata-2.2.3/src/morphata/alphabet.py +258 -0
  25. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/builder.py +9 -27
  26. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/logic/_common.py +15 -7
  27. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/logic/psl.py +2 -2
  28. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/operators/__init__.py +4 -1
  29. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/operators/matrix.py +150 -79
  30. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/operators/polynomial.py +13 -19
  31. morphata-2.2.3/src/morphata/storage/__init__.py +6 -0
  32. morphata-2.2.3/src/morphata/storage/serialize.py +260 -0
  33. morphata-2.2.3/src/morphata/storage/signature.py +207 -0
  34. morphata-2.2.3/src/morphata/symbolic/__init__.py +19 -0
  35. morphata-2.2.3/src/morphata/symbolic/automaton.py +1307 -0
  36. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/symbolic/bdd.py +242 -31
  37. morphata-2.2.3/src/morphata/symbolic/distance.py +361 -0
  38. morphata-2.2.3/src/morphata/symbolic/transforms.py +1040 -0
  39. morphata-2.2.3/src/morphata/utils/__init__.py +9 -0
  40. morphata-2.2.2/src/morphata/utils.py → morphata-2.2.3/src/morphata/utils/expressions.py +2 -78
  41. morphata-2.2.3/src/morphata/utils/memory.py +75 -0
  42. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/test_exporter.py +27 -21
  43. {morphata-2.2.2 → morphata-2.2.3}/tests/logic/test_psl.py +2 -1
  44. {morphata-2.2.2 → morphata-2.2.3}/tests/operators/test_matrix.py +210 -36
  45. {morphata-2.2.2 → morphata-2.2.3}/tests/operators/test_polynomial.py +4 -3
  46. morphata-2.2.3/tests/storage/test_serialize.py +322 -0
  47. morphata-2.2.3/tests/storage/test_signature.py +470 -0
  48. {morphata-2.2.2 → morphata-2.2.3}/tests/symbolic/test_bdd_helpers.py +184 -0
  49. {morphata-2.2.2 → morphata-2.2.3}/tests/symbolic/test_compose.py +21 -20
  50. morphata-2.2.3/tests/symbolic/test_distance.py +150 -0
  51. {morphata-2.2.2 → morphata-2.2.3}/tests/symbolic/test_from_hoaf.py +5 -3
  52. {morphata-2.2.2 → morphata-2.2.3}/tests/symbolic/test_language_equivalence.py +14 -15
  53. {morphata-2.2.2 → morphata-2.2.3}/tests/symbolic/test_ltlf2dfa_equivalence.py +3 -4
  54. {morphata-2.2.2 → morphata-2.2.3}/tests/symbolic/test_symbolic_automaton.py +141 -42
  55. {morphata-2.2.2 → morphata-2.2.3}/tests/symbolic/test_to_hoa.py +5 -5
  56. {morphata-2.2.2 → morphata-2.2.3}/tests/symbolic/test_transforms.py +480 -26
  57. {morphata-2.2.2 → morphata-2.2.3}/tests/test_alphabet.py +10 -2
  58. {morphata-2.2.2 → morphata-2.2.3}/tests/test_builder.py +24 -30
  59. morphata-2.2.3/uv.lock +3285 -0
  60. morphata-2.2.2/pixi.lock +0 -7844
  61. morphata-2.2.2/pixi.toml +0 -127
  62. morphata-2.2.2/src/morphata/alphabet.py +0 -92
  63. morphata-2.2.2/src/morphata/symbolic/__init__.py +0 -14
  64. morphata-2.2.2/src/morphata/symbolic/automaton.py +0 -847
  65. morphata-2.2.2/src/morphata/symbolic/utils.py +0 -723
  66. morphata-2.2.2/uv.lock +0 -3335
  67. {morphata-2.2.2 → morphata-2.2.3}/.basedpyright/baseline.json +0 -0
  68. {morphata-2.2.2 → morphata-2.2.3}/.dockerignore +0 -0
  69. {morphata-2.2.2 → morphata-2.2.3}/.editorconfig +0 -0
  70. {morphata-2.2.2 → morphata-2.2.3}/.envrc.recommended +0 -0
  71. {morphata-2.2.2 → morphata-2.2.3}/.fdignore +0 -0
  72. {morphata-2.2.2 → morphata-2.2.3}/.gitattributes +0 -0
  73. {morphata-2.2.2 → morphata-2.2.3}/.mailmap +0 -0
  74. {morphata-2.2.2 → morphata-2.2.3}/.python-version +0 -0
  75. {morphata-2.2.2 → morphata-2.2.3}/.rstcheck.cfg +0 -0
  76. {morphata-2.2.2 → morphata-2.2.3}/AGENTS.md +0 -0
  77. {morphata-2.2.2 → morphata-2.2.3}/CLAUDE.md +0 -0
  78. {morphata-2.2.2 → morphata-2.2.3}/CONTRIBUTING.md +0 -0
  79. {morphata-2.2.2 → morphata-2.2.3}/LICENSE +0 -0
  80. {morphata-2.2.2 → morphata-2.2.3}/README.rst +0 -0
  81. {morphata-2.2.2 → morphata-2.2.3}/bench/__init__.py +0 -0
  82. {morphata-2.2.2 → morphata-2.2.3}/bench/conftest.py +0 -0
  83. {morphata-2.2.2 → morphata-2.2.3}/bench/test_hoa_exporter_perf.py +0 -0
  84. {morphata-2.2.2 → morphata-2.2.3}/bench/test_hoa_parser_perf.py +0 -0
  85. {morphata-2.2.2 → morphata-2.2.3}/bench/test_sere_nlm_perf.py +0 -0
  86. {morphata-2.2.2 → morphata-2.2.3}/ci/make-release +0 -0
  87. {morphata-2.2.2 → morphata-2.2.3}/docs/Makefile +0 -0
  88. {morphata-2.2.2 → morphata-2.2.3}/docs/_static/navigation.html +0 -0
  89. {morphata-2.2.2 → morphata-2.2.3}/docs/api/morphata.acc_expr.rst +0 -0
  90. {morphata-2.2.2 → morphata-2.2.3}/docs/api/morphata.acceptance.rst +0 -0
  91. {morphata-2.2.2 → morphata-2.2.3}/docs/api/morphata.alphabet.rst +0 -0
  92. {morphata-2.2.2 → morphata-2.2.3}/docs/api/morphata.builder.rst +0 -0
  93. {morphata-2.2.2 → morphata-2.2.3}/docs/api/morphata.hoa.rst +0 -0
  94. {morphata-2.2.2 → morphata-2.2.3}/docs/api/morphata.logic.rst +0 -0
  95. {morphata-2.2.2 → morphata-2.2.3}/docs/changelog.md +0 -0
  96. {morphata-2.2.2 → morphata-2.2.3}/docs/concepts/acceptance.rst +0 -0
  97. {morphata-2.2.2 → morphata-2.2.3}/docs/concepts/hoa.rst +0 -0
  98. {morphata-2.2.2 → morphata-2.2.3}/docs/concepts/index.rst +0 -0
  99. {morphata-2.2.2 → morphata-2.2.3}/docs/concepts/symbolic.rst +0 -0
  100. {morphata-2.2.2 → morphata-2.2.3}/justfile +0 -0
  101. {morphata-2.2.2 → morphata-2.2.3}/project.utf-8.add +0 -0
  102. {morphata-2.2.2 → morphata-2.2.3}/project.utf-8.add.spl +0 -0
  103. {morphata-2.2.2 → morphata-2.2.3}/pyrightconfig.json +0 -0
  104. {morphata-2.2.2 → morphata-2.2.3}/pytest.toml +0 -0
  105. {morphata-2.2.2 → morphata-2.2.3}/rules/no-derivative-multiarg.yml +0 -0
  106. {morphata-2.2.2 → morphata-2.2.3}/rules/no-typing-as-ty.yml +0 -0
  107. {morphata-2.2.2 → morphata-2.2.3}/sgconfig.yml +0 -0
  108. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/acc_expr.py +0 -0
  109. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/acceptance.py +0 -0
  110. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/hoa/__init__.py +0 -0
  111. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/hoa/__main__.py +0 -0
  112. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/hoa/hoa.lark +0 -0
  113. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/hoa/parser.py +0 -0
  114. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/logic/__init__.py +0 -0
  115. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/logic/__init__.pyi +0 -0
  116. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/logic/ltl.py +0 -0
  117. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/logic/sere.py +0 -0
  118. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/operators/_backend.py +0 -0
  119. {morphata-2.2.2 → morphata-2.2.3}/src/morphata/py.typed +0 -0
  120. {morphata-2.2.2 → morphata-2.2.3}/tests/conftest.py +0 -0
  121. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/aut1.hoa +0 -0
  122. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/aut11.hoa +0 -0
  123. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/aut2.hoa +0 -0
  124. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/aut3.2.hoa +0 -0
  125. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/aut3.hoa +0 -0
  126. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/aut4.hoa +0 -0
  127. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/aut5.hoa +0 -0
  128. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/aut6.hoa +0 -0
  129. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/aut7.hoa +0 -0
  130. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/aut8.hoa +0 -0
  131. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/finite_aut.hoa +0 -0
  132. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/gen_buchi_3.hoa +0 -0
  133. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/gen_cobuchi_2.hoa +0 -0
  134. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/rabin_2pair.hoa +0 -0
  135. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/streett_2pair.hoa +0 -0
  136. {morphata-2.2.2 → morphata-2.2.3}/tests/hoa/test_parser.py +0 -0
  137. {morphata-2.2.2 → morphata-2.2.3}/tests/logic/__init__.py +0 -0
  138. {morphata-2.2.2 → morphata-2.2.3}/tests/logic/test_ltl.py +0 -0
  139. {morphata-2.2.2 → morphata-2.2.3}/tests/logic/test_sere.py +0 -0
  140. {morphata-2.2.2 → 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
- PIXI_ENV="cu$CUDA_VERSION"
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
- watch_file pixi.lock
26
- eval "$(pixi shell-hook -e "$PIXI_ENV")"
24
+ eval "$(pixi shell-hook)"
@@ -1,3 +1,5 @@
1
+ /thoughts/
2
+ /issues/
1
3
  # generated version file
2
4
  src/morphata/_version.py
3
5
 
@@ -1,5 +1,159 @@
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
+
3
157
  ## 2.2.2 — 2026-08-23
4
158
 
5
159
  ### Fixed
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: morphata
3
- Version: 2.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, dfa)
46
+ return dfa.is_language_equivalent(dfa)
48
47
 
49
48
  assert benchmark(_run)