Spaces:
Running
Running
Use public vo-regular-bp dependency
Browse files- requirements.txt +1 -1
- vendor/vo_regular_bp/LICENSE +0 -21
- vendor/vo_regular_bp/README.md +0 -570
- vendor/vo_regular_bp/pyproject.toml +0 -51
- vendor/vo_regular_bp/vo_regular_bp/__init__.py +0 -256
- vendor/vo_regular_bp/vo_regular_bp/acceptors.py +0 -660
- vendor/vo_regular_bp/vo_regular_bp/adapters.py +0 -270
- vendor/vo_regular_bp/vo_regular_bp/augmentation.py +0 -505
- vendor/vo_regular_bp/vo_regular_bp/backend.py +0 -1066
- vendor/vo_regular_bp/vo_regular_bp/brute_force.py +0 -116
- vendor/vo_regular_bp/vo_regular_bp/constraint_builders.py +0 -238
- vendor/vo_regular_bp/vo_regular_bp/constraints.py +0 -173
- vendor/vo_regular_bp/vo_regular_bp/context.py +0 -397
- vendor/vo_regular_bp/vo_regular_bp/continuator.py +0 -238
- vendor/vo_regular_bp/vo_regular_bp/experimental.py +0 -986
- vendor/vo_regular_bp/vo_regular_bp/metrics.py +0 -24
- vendor/vo_regular_bp/vo_regular_bp/minimization.py +0 -326
- vendor/vo_regular_bp/vo_regular_bp/orbit_diagnostics.py +0 -574
- vendor/vo_regular_bp/vo_regular_bp/order_stack_bp.py +0 -0
- vendor/vo_regular_bp/vo_regular_bp/padding.py +0 -35
- vendor/vo_regular_bp/vo_regular_bp/positional_bp.py +0 -393
- vendor/vo_regular_bp/vo_regular_bp/product_bp.py +0 -304
requirements.txt
CHANGED
|
@@ -6,4 +6,4 @@ numpy>=2.2
|
|
| 6 |
torch>=2.3
|
| 7 |
verovio>=6.2
|
| 8 |
./vendor/muses
|
| 9 |
-
./
|
|
|
|
| 6 |
torch>=2.3
|
| 7 |
verovio>=6.2
|
| 8 |
./vendor/muses
|
| 9 |
+
git+https://github.com/fpachet/vo-regular-bp.git
|
vendor/vo_regular_bp/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
MIT License
|
| 2 |
-
|
| 3 |
-
Copyright (c) 2026 Francois Pachet
|
| 4 |
-
|
| 5 |
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
-
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
-
in the Software without restriction, including without limitation the rights
|
| 8 |
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
-
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
-
furnished to do so, subject to the following conditions:
|
| 11 |
-
|
| 12 |
-
The above copyright notice and this permission notice shall be included in all
|
| 13 |
-
copies or substantial portions of the Software.
|
| 14 |
-
|
| 15 |
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
-
SOFTWARE.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/README.md
DELETED
|
@@ -1,570 +0,0 @@
|
|
| 1 |
-
# vo-regular-bp
|
| 2 |
-
|
| 3 |
-
`vo-regular-bp` is a Python library for exact constrained generation from
|
| 4 |
-
sparse variable-order context models, with support for positional masks, meter
|
| 5 |
-
constraints, forbidden-substring constraints, and reusable order-stack
|
| 6 |
-
backends.
|
| 7 |
-
|
| 8 |
-
The core algorithm runs backward dynamic programming on the reachable product
|
| 9 |
-
of a context graph and a deterministic acceptor. Sampling then chooses each next
|
| 10 |
-
symbol proportionally to its model probability times the downstream beta value,
|
| 11 |
-
so generated sequences are exact samples from the source model conditioned on
|
| 12 |
-
the regular language whenever the constrained mass is nonzero.
|
| 13 |
-
|
| 14 |
-
The repository also contains specialized engines and experiments for positional
|
| 15 |
-
constraints and Continuator-style order-stack backoff policies.
|
| 16 |
-
|
| 17 |
-
## Install
|
| 18 |
-
|
| 19 |
-
The package has no declared runtime dependencies and requires Python 3.10 or
|
| 20 |
-
newer.
|
| 21 |
-
|
| 22 |
-
For use from another application, install directly from GitHub:
|
| 23 |
-
|
| 24 |
-
```bash
|
| 25 |
-
python -m pip install "vo-regular-bp @ git+https://github.com/fpachet/vo-regular-bp.git"
|
| 26 |
-
```
|
| 27 |
-
|
| 28 |
-
To pin an application to a stable revision, append a branch, tag, or commit:
|
| 29 |
-
|
| 30 |
-
```bash
|
| 31 |
-
python -m pip install "vo-regular-bp @ git+https://github.com/fpachet/vo-regular-bp.git@main"
|
| 32 |
-
```
|
| 33 |
-
|
| 34 |
-
For local development:
|
| 35 |
-
|
| 36 |
-
```bash
|
| 37 |
-
python -m pip install -e ".[test]"
|
| 38 |
-
python -m pytest -q
|
| 39 |
-
```
|
| 40 |
-
|
| 41 |
-
Library-oriented integration notes are in
|
| 42 |
-
[`docs/library_usage.md`](docs/library_usage.md). The paper evaluation scripts
|
| 43 |
-
are kept separately under [`paper/variable_order_regular_bp/`](paper/variable_order_regular_bp/).
|
| 44 |
-
Optimization status and future performance ideas are tracked in
|
| 45 |
-
[`docs/optimization_roadmap.md`](docs/optimization_roadmap.md).
|
| 46 |
-
Virtual transformed-corpus augmentation is described in
|
| 47 |
-
[`docs/virtual_data_augmentation.md`](docs/virtual_data_augmentation.md).
|
| 48 |
-
|
| 49 |
-
## License
|
| 50 |
-
|
| 51 |
-
This project is released under the MIT License. See [`LICENSE`](LICENSE).
|
| 52 |
-
|
| 53 |
-
## Optimization Status
|
| 54 |
-
|
| 55 |
-
The library includes several exactness-preserving optimizations: positional
|
| 56 |
-
constraints are kept as time masks instead of DFA product state, MAXORDER /
|
| 57 |
-
forbidden-substring constraints use a dense DFA when possible, order-stack
|
| 58 |
-
sampling caches feasible candidate sets, and non-trace sampling avoids trace
|
| 59 |
-
object allocation. See [`docs/optimization_roadmap.md`](docs/optimization_roadmap.md)
|
| 60 |
-
for measured results, discarded experiments, and future optimization options.
|
| 61 |
-
|
| 62 |
-
## Quick Start
|
| 63 |
-
|
| 64 |
-
```python
|
| 65 |
-
from vo_regular_bp import ContextGraph, positional_acceptor, run_bp
|
| 66 |
-
|
| 67 |
-
graph = ContextGraph.from_counts({(): {"a": 10, "b": 1}}, max_order=0)
|
| 68 |
-
acceptor = positional_acceptor(length=1, alphabet=graph.alphabet)
|
| 69 |
-
|
| 70 |
-
bp = run_bp(graph, acceptor, length=1)
|
| 71 |
-
|
| 72 |
-
print(bp.partition_function) # 1.0
|
| 73 |
-
print(bp.conditional_probability(("a",))) # 10 / 11
|
| 74 |
-
print(bp.sample(rng=0)) # exact constrained sample
|
| 75 |
-
```
|
| 76 |
-
|
| 77 |
-
Regular constraints can be intersected. This example samples three iid symbols,
|
| 78 |
-
forces the final symbol to be `"a"`, and rejects the substring `"bb"`.
|
| 79 |
-
|
| 80 |
-
```python
|
| 81 |
-
from vo_regular_bp import (
|
| 82 |
-
ContextGraph,
|
| 83 |
-
all_of,
|
| 84 |
-
forbidden_substring_acceptor,
|
| 85 |
-
positional_acceptor,
|
| 86 |
-
run_bp,
|
| 87 |
-
)
|
| 88 |
-
|
| 89 |
-
graph = ContextGraph.from_counts({(): {"a": 1, "b": 1}}, max_order=0)
|
| 90 |
-
acceptor = all_of(
|
| 91 |
-
positional_acceptor(3, {2: {"a"}}, alphabet=graph.alphabet),
|
| 92 |
-
forbidden_substring_acceptor([("b", "b")], alphabet=graph.alphabet),
|
| 93 |
-
)
|
| 94 |
-
|
| 95 |
-
bp = run_bp(graph, acceptor, length=3)
|
| 96 |
-
samples = bp.sample_many(5, rng=123)
|
| 97 |
-
|
| 98 |
-
assert all(sample[-1] == "a" for sample in samples)
|
| 99 |
-
assert all(("b", "b") not in zip(sample, sample[1:]) for sample in samples)
|
| 100 |
-
```
|
| 101 |
-
|
| 102 |
-
## Main Concepts
|
| 103 |
-
|
| 104 |
-
`ContextGraph` stores a sparse probabilistic context model. It can be built from
|
| 105 |
-
explicit counts/probabilities, unweighted or weighted sequences, or an explicit
|
| 106 |
-
backoff mixture:
|
| 107 |
-
|
| 108 |
-
- `ContextGraph.from_counts(...)`
|
| 109 |
-
- `ContextGraph.from_probabilities(...)`
|
| 110 |
-
- `ContextGraph.from_sequences(...)`
|
| 111 |
-
- `ContextGraph.from_weighted_sequences(...)`
|
| 112 |
-
- `ContextGraph.from_backoff_sequences(...)`
|
| 113 |
-
|
| 114 |
-
`DFA` is the deterministic acceptor interface used by product BP. A transition
|
| 115 |
-
returning `None` rejects that symbol from the current acceptor state. Acceptors
|
| 116 |
-
may also expose `transition_weight(state, symbol) -> float` for soft regular
|
| 117 |
-
constraints: BP multiplies the VOMM transition probability by this weight before
|
| 118 |
-
normalizing. The default weight is `1.0`; a weight of `0.0` acts as a hard ban.
|
| 119 |
-
|
| 120 |
-
Available acceptor helpers include:
|
| 121 |
-
|
| 122 |
-
- `WeightedDFA`: convenience subclass for weighted regular constraints.
|
| 123 |
-
- `positional_acceptor`: zero-based per-position allowed symbols.
|
| 124 |
-
- `meter_acceptor`: finite meter/class patterns.
|
| 125 |
-
- `cumulative_meter_acceptor`: cumulative-cost meter predicates.
|
| 126 |
-
- `forbidden_substring_acceptor`: reject any forbidden substring.
|
| 127 |
-
- `dense_forbidden_substring_acceptor`: finite-alphabet optimized variant.
|
| 128 |
-
- `max_order_acceptor`: reject reference substrings of length `max_order + 1`.
|
| 129 |
-
- `all_of`: intersect acceptors.
|
| 130 |
-
- `true_acceptor`: accept every finite sequence.
|
| 131 |
-
|
| 132 |
-
## Engines
|
| 133 |
-
|
| 134 |
-
### Product BP
|
| 135 |
-
|
| 136 |
-
`run_bp(graph, acceptor, length=...)` is the general exact engine. It performs
|
| 137 |
-
backward DP over reachable `(context_state, acceptor_state)` product states and
|
| 138 |
-
returns a `ProductBPResult` with:
|
| 139 |
-
|
| 140 |
-
- `partition_function`: constrained probability mass.
|
| 141 |
-
- `sample(...)` and `sample_many(...)`: exact conditional samples.
|
| 142 |
-
- `conditional_probability(sequence)`: probability under the constrained
|
| 143 |
-
distribution.
|
| 144 |
-
- product-state and edge-count diagnostics for scalability studies.
|
| 145 |
-
|
| 146 |
-
`sample_exact(...)` is a convenience wrapper that runs BP and draws one sample.
|
| 147 |
-
|
| 148 |
-
### Source Graph Minimization And Experimental Merging
|
| 149 |
-
|
| 150 |
-
Exact source minimization is available as an optional compiled view. It is
|
| 151 |
-
semantics-preserving and never enabled by default:
|
| 152 |
-
|
| 153 |
-
```python
|
| 154 |
-
from vo_regular_bp import exact_context_graph_quotient_stats, minimize_context_graph
|
| 155 |
-
|
| 156 |
-
stats = exact_context_graph_quotient_stats(graph)
|
| 157 |
-
minimized = minimize_context_graph(graph)
|
| 158 |
-
```
|
| 159 |
-
|
| 160 |
-
Order-stack preparation also accepts `minimize_source_graphs=True`, which
|
| 161 |
-
minimizes the source graphs before running the ordinary exact BP backend. The
|
| 162 |
-
training/count model remains uncompressed, and minimized fixed-order graphs are
|
| 163 |
-
cached on the model for reuse.
|
| 164 |
-
|
| 165 |
-
Approximate source merging is explicitly experimental and changes the source
|
| 166 |
-
model. It lives outside the safe top-level API:
|
| 167 |
-
|
| 168 |
-
```python
|
| 169 |
-
from vo_regular_bp.experimental import alergia_merge
|
| 170 |
-
|
| 171 |
-
merged = alergia_merge(
|
| 172 |
-
graph,
|
| 173 |
-
alpha=0.01,
|
| 174 |
-
min_support=10,
|
| 175 |
-
recursive=True,
|
| 176 |
-
transition_projection=lambda state, symbol, edge: (
|
| 177 |
-
symbol - state[-1]
|
| 178 |
-
if state and isinstance(state[-1], int) and isinstance(symbol, int)
|
| 179 |
-
else symbol
|
| 180 |
-
),
|
| 181 |
-
)
|
| 182 |
-
```
|
| 183 |
-
|
| 184 |
-
The returned object is still a `ContextGraph`, so `run_bp(...)` remains exact
|
| 185 |
-
with respect to the merged source model. This is not constrained-product
|
| 186 |
-
minimization and is not applied automatically. The optional
|
| 187 |
-
`symbol_projection` and `transition_projection` arguments are where a client
|
| 188 |
-
supplies domain semantics for similarity; `transition_projection(state, symbol,
|
| 189 |
-
edge)` handles context-relative abstractions and takes precedence when both are
|
| 190 |
-
provided. The default compares raw emitted symbols.
|
| 191 |
-
|
| 192 |
-
Order-stack models can opt into the same experiment by merging each fixed-order
|
| 193 |
-
source graph independently, then using the ordinary backend:
|
| 194 |
-
|
| 195 |
-
```python
|
| 196 |
-
from vo_regular_bp.experimental import alergia_merge_order_stack_model
|
| 197 |
-
|
| 198 |
-
abstract_model = alergia_merge_order_stack_model(
|
| 199 |
-
model,
|
| 200 |
-
alpha=0.01,
|
| 201 |
-
min_support=10,
|
| 202 |
-
recursive=True,
|
| 203 |
-
transition_projection=lambda state, symbol, edge: (
|
| 204 |
-
symbol - state[-1]
|
| 205 |
-
if state and isinstance(state[-1], int) and isinstance(symbol, int)
|
| 206 |
-
else symbol
|
| 207 |
-
),
|
| 208 |
-
)
|
| 209 |
-
|
| 210 |
-
backend = prepare_constrained_order_stack(
|
| 211 |
-
abstract_model,
|
| 212 |
-
constraints,
|
| 213 |
-
length=8,
|
| 214 |
-
prefix=prefix,
|
| 215 |
-
)
|
| 216 |
-
```
|
| 217 |
-
|
| 218 |
-
This preserves the public order-stack sampling path and trace shape. It changes
|
| 219 |
-
the source model explicitly: BP and order selection remain exact for the merged
|
| 220 |
-
fixed-order graphs that are passed in.
|
| 221 |
-
|
| 222 |
-
The ALERGIA implementation is optimized for projected comparisons by caching
|
| 223 |
-
projected continuation counts, projected successor labels, and recursive
|
| 224 |
-
pair-compatibility decisions within one merge call. It also tracks active
|
| 225 |
-
merge classes incrementally instead of rebuilding class members during every
|
| 226 |
-
candidate comparison. These are internal optimizations only: they do not change
|
| 227 |
-
the explicit API or make approximate merging automatic.
|
| 228 |
-
|
| 229 |
-
### Positional BP
|
| 230 |
-
|
| 231 |
-
`run_positional_bp(...)` is a no-DFA specialization for fixed-horizon
|
| 232 |
-
time-indexed symbol masks. It is useful when the only constraints are
|
| 233 |
-
positional, because the recursion ranges over context states rather than full
|
| 234 |
-
context-acceptor products.
|
| 235 |
-
|
| 236 |
-
`LazyBackoffContextModel` matches `ContextGraph.from_backoff_sequences(...)`
|
| 237 |
-
semantically, but materializes outgoing edges only when reached by BP.
|
| 238 |
-
|
| 239 |
-
### Order-Stack BP
|
| 240 |
-
|
| 241 |
-
`OrderStackModel` and the `run_order_stack_*` functions implement
|
| 242 |
-
Continuator-style constrained policy backoff over a stack of fixed-order
|
| 243 |
-
models:
|
| 244 |
-
|
| 245 |
-
- `run_order_stack_bp`: positional constraints only.
|
| 246 |
-
- `run_order_stack_dfa_bp`: regular DFA constraints, including weighted transitions.
|
| 247 |
-
- `run_order_stack_masked_dfa_bp`: regular DFA plus positional masks.
|
| 248 |
-
|
| 249 |
-
This is a generation policy, not exact conditioning of one fixed stochastic
|
| 250 |
-
source. At each step, the engine computes feasible future mass for each order,
|
| 251 |
-
then an order policy such as `LongestFeasiblePolicy` or
|
| 252 |
-
`SingletonAvoidingBackoffPolicy` chooses among feasible candidate orders.
|
| 253 |
-
|
| 254 |
-
The main result object reports `success_mass`, order-specific start masses,
|
| 255 |
-
samples, and optional order traces.
|
| 256 |
-
|
| 257 |
-
### Public Constraint Backend
|
| 258 |
-
|
| 259 |
-
`prepare_constrained_order_stack(...)` is the recommended library-facing entry
|
| 260 |
-
point for order-stack generation. It accepts a model and a `ConstraintSet`,
|
| 261 |
-
compiles positional constraints as time masks, compiles regular constraints as
|
| 262 |
-
acceptors, runs the backend once, and returns a reusable sampler.
|
| 263 |
-
|
| 264 |
-
```python
|
| 265 |
-
from vo_regular_bp import (
|
| 266 |
-
ConstraintSet,
|
| 267 |
-
LongestFeasiblePolicy,
|
| 268 |
-
OrderStackModel,
|
| 269 |
-
prepare_constrained_order_stack,
|
| 270 |
-
prepare_constrained_order_stack_plan,
|
| 271 |
-
)
|
| 272 |
-
|
| 273 |
-
sequence = (60, 64, 67, 72, 76, 67, 71, 72)
|
| 274 |
-
model = OrderStackModel.from_sequences([sequence], max_order=2)
|
| 275 |
-
final_c = {pitch for pitch in sequence if pitch % 12 == 0}
|
| 276 |
-
|
| 277 |
-
backend = prepare_constrained_order_stack(
|
| 278 |
-
model,
|
| 279 |
-
ConstraintSet(positional={3: final_c}),
|
| 280 |
-
length=4,
|
| 281 |
-
prefix=sequence[:2],
|
| 282 |
-
policy=LongestFeasiblePolicy(),
|
| 283 |
-
)
|
| 284 |
-
|
| 285 |
-
generated = backend.sample_with_orders(rng=0)
|
| 286 |
-
sample = generated.sequence
|
| 287 |
-
orders = generated.orders
|
| 288 |
-
assert sample[-1] % 12 == 0
|
| 289 |
-
```
|
| 290 |
-
|
| 291 |
-
For convenience, `prepare_constrained_order_stack_from_sequences(...)` builds
|
| 292 |
-
the `OrderStackModel` and prepares the backend in one call. The returned
|
| 293 |
-
`ConstrainedOrderStackBackend` exposes `sample(...)`, `sample_many(...)`,
|
| 294 |
-
`sample_with_orders(...)`, `sample_many_with_orders(...)`, `sample_with_trace(...)`,
|
| 295 |
-
and a stable `diagnostics` object with context/product sizes and success mass
|
| 296 |
-
when a regular backend is used.
|
| 297 |
-
|
| 298 |
-
`run_constrained_order_stack(...)` remains available when callers need the raw
|
| 299 |
-
internal BP result object.
|
| 300 |
-
|
| 301 |
-
When the model, horizon, and constraints are fixed but many prefixes will be
|
| 302 |
-
sampled, prepare a prefix-independent plan once and bind prefixes later:
|
| 303 |
-
|
| 304 |
-
```python
|
| 305 |
-
plan = prepare_constrained_order_stack_plan(
|
| 306 |
-
model,
|
| 307 |
-
ConstraintSet(positional={3: final_c}),
|
| 308 |
-
length=4,
|
| 309 |
-
policy=LongestFeasiblePolicy(),
|
| 310 |
-
)
|
| 311 |
-
|
| 312 |
-
backend_a = plan.for_prefix(sequence[:2])
|
| 313 |
-
backend_b = plan.for_prefix(sequence[2:4])
|
| 314 |
-
|
| 315 |
-
sample_a = backend_a.sample(rng=0)
|
| 316 |
-
sample_b = backend_b.sample(rng=1)
|
| 317 |
-
```
|
| 318 |
-
|
| 319 |
-
For regular constraints, the plan owns the shared graph and backward-message
|
| 320 |
-
caches. Binding a prefix warms the lazy beta messages reachable from that prefix,
|
| 321 |
-
and later prefixes reuse overlapping cached product states. The old
|
| 322 |
-
`prepare_constrained_order_stack(...)` API is now a convenience wrapper around
|
| 323 |
-
this plan path when the model supports prefix-independent graph preparation.
|
| 324 |
-
|
| 325 |
-
For variable-length Continuator-style suffixes, use
|
| 326 |
-
`prepare_until_order_stack(...)`. It prepares one fixed-length constrained
|
| 327 |
-
backend for each feasible length in `[min_length, max_length]`, samples a
|
| 328 |
-
length by the sum of its positive start-order masses, then samples the suffix
|
| 329 |
-
from that length backend. If only a backend success indicator is available, the
|
| 330 |
-
length receives unit weight. The returned suffix does not include the prefix:
|
| 331 |
-
the prefix is conditioning context only.
|
| 332 |
-
|
| 333 |
-
```python
|
| 334 |
-
from vo_regular_bp import OrderStackModel, prepare_until_order_stack
|
| 335 |
-
|
| 336 |
-
model = OrderStackModel.from_sequences([("A", "B", "C")], max_order=1)
|
| 337 |
-
backend = prepare_until_order_stack(
|
| 338 |
-
model,
|
| 339 |
-
prefix=("A",),
|
| 340 |
-
stop="C",
|
| 341 |
-
min_length=1,
|
| 342 |
-
max_length=3,
|
| 343 |
-
)
|
| 344 |
-
|
| 345 |
-
suffix = backend.sample(rng=0)
|
| 346 |
-
assert suffix == ("B", "C")
|
| 347 |
-
```
|
| 348 |
-
|
| 349 |
-
`stop` may be one symbol, an iterable/set of symbols, or a predicate
|
| 350 |
-
`stop(symbol) -> bool`. The returned suffix includes the first generated stop
|
| 351 |
-
symbol at the final position, and earlier positions are constrained not to
|
| 352 |
-
satisfy `stop`. `prepare_until_end_order_stack(..., end_symbol=...)` is a
|
| 353 |
-
convenience alias for learned END/sentinel stops. END remains forbidden in
|
| 354 |
-
ordinary fixed-length sampling, but first-hit END generation explicitly allows
|
| 355 |
-
the sentinel at the final stop position.
|
| 356 |
-
|
| 357 |
-
`ConstraintSet` supports:
|
| 358 |
-
|
| 359 |
-
- `positional`: per-time symbol masks or predicates.
|
| 360 |
-
- `forbidden_substrings`: exact forbidden substring / MAXORDER constraints,
|
| 361 |
-
compiled to a dense DFA when possible.
|
| 362 |
-
- `regular_acceptors`: caller-supplied `DFA` or `WeightedDFA` instances. Soft
|
| 363 |
-
transition weights are multiplied into BP; missing transitions or weight
|
| 364 |
-
`0.0` reject the symbol.
|
| 365 |
-
- `meter`: a `MeterConstraint` for finite per-symbol meter/class patterns.
|
| 366 |
-
- `cumulative_meter`: a `CumulativeMeterConstraint` for duration/cost
|
| 367 |
-
accumulation, bar-boundary predicates, final total cost, and optional padding
|
| 368 |
-
symbols.
|
| 369 |
-
|
| 370 |
-
The core API is intentionally not Continuator-specific; adapters for other
|
| 371 |
-
projects can map their own event objects to symbols, meter classes, costs, or
|
| 372 |
-
regular acceptors.
|
| 373 |
-
|
| 374 |
-
All high-level constraints are over the generated suffix. Positional indices are
|
| 375 |
-
zero-based within the generated sequence, not within the training corpus and not
|
| 376 |
-
within `prefix + generated`.
|
| 377 |
-
|
| 378 |
-
### Constraint Builders
|
| 379 |
-
|
| 380 |
-
Common constraints can be built and combined without manually constructing
|
| 381 |
-
`ConstraintSet` objects:
|
| 382 |
-
|
| 383 |
-
```python
|
| 384 |
-
from vo_regular_bp import (
|
| 385 |
-
combine_constraints,
|
| 386 |
-
final_pitch_class,
|
| 387 |
-
avoid_copied_ngrams,
|
| 388 |
-
)
|
| 389 |
-
|
| 390 |
-
constraints = combine_constraints(
|
| 391 |
-
final_pitch_class(0, length=32),
|
| 392 |
-
avoid_copied_ngrams(reference_pitches, 5),
|
| 393 |
-
)
|
| 394 |
-
```
|
| 395 |
-
|
| 396 |
-
Builder helpers include:
|
| 397 |
-
|
| 398 |
-
- `at_position(...)`
|
| 399 |
-
- `final_symbol(...)` and `final_symbols(...)`
|
| 400 |
-
- `final_pitch_class(...)`
|
| 401 |
-
- `avoid_copied_ngrams(...)`
|
| 402 |
-
- `meter_pattern(...)`
|
| 403 |
-
- `cumulative_meter(...)`
|
| 404 |
-
- `padded_duration_total(...)`
|
| 405 |
-
- `combine_constraints(...)`
|
| 406 |
-
|
| 407 |
-
### Event Adapters
|
| 408 |
-
|
| 409 |
-
External projects can keep rich event objects at their boundary and encode them
|
| 410 |
-
as hashable symbols for the backend:
|
| 411 |
-
|
| 412 |
-
```python
|
| 413 |
-
from dataclasses import dataclass
|
| 414 |
-
from vo_regular_bp import (
|
| 415 |
-
EventCodec,
|
| 416 |
-
combine_constraints,
|
| 417 |
-
final_pitch_class,
|
| 418 |
-
prepare_constrained_order_stack_from_events,
|
| 419 |
-
)
|
| 420 |
-
|
| 421 |
-
@dataclass(frozen=True)
|
| 422 |
-
class Note:
|
| 423 |
-
pitch: int
|
| 424 |
-
duration: int
|
| 425 |
-
|
| 426 |
-
codec = EventCodec(
|
| 427 |
-
event_to_symbol=lambda note: (note.pitch, note.duration),
|
| 428 |
-
symbol_to_event=lambda symbol: Note(symbol[0], symbol[1]),
|
| 429 |
-
)
|
| 430 |
-
|
| 431 |
-
backend = prepare_constrained_order_stack_from_events(
|
| 432 |
-
[training_notes],
|
| 433 |
-
combine_constraints(
|
| 434 |
-
final_pitch_class(0, length=8, symbol_to_pitch=lambda symbol: symbol[0]),
|
| 435 |
-
),
|
| 436 |
-
codec=codec,
|
| 437 |
-
max_order=3,
|
| 438 |
-
length=8,
|
| 439 |
-
prefix=prefix_notes,
|
| 440 |
-
)
|
| 441 |
-
|
| 442 |
-
generated = backend.sample_events_with_orders(rng=0)
|
| 443 |
-
print(generated.events)
|
| 444 |
-
print(generated.orders)
|
| 445 |
-
```
|
| 446 |
-
|
| 447 |
-
See `examples/event_order_stack_backend.py` for a complete small example.
|
| 448 |
-
|
| 449 |
-
### Continuator-Style Facade
|
| 450 |
-
|
| 451 |
-
For projects that want a Continuator-shaped entry point, the dependency-free
|
| 452 |
-
facade in `vo_regular_bp.continuator` uses event sequences, a prefix, a horizon,
|
| 453 |
-
and constraints:
|
| 454 |
-
|
| 455 |
-
```python
|
| 456 |
-
from vo_regular_bp import (
|
| 457 |
-
combine_constraints,
|
| 458 |
-
duration_total_constraint,
|
| 459 |
-
final_pitch_class_constraint,
|
| 460 |
-
prepare_continuation_backend,
|
| 461 |
-
prepare_continuation_plan,
|
| 462 |
-
)
|
| 463 |
-
|
| 464 |
-
constraints = combine_constraints(
|
| 465 |
-
final_pitch_class_constraint(
|
| 466 |
-
0,
|
| 467 |
-
horizon=8,
|
| 468 |
-
symbol_to_pitch=lambda symbol: symbol[0],
|
| 469 |
-
),
|
| 470 |
-
duration_total_constraint(
|
| 471 |
-
8,
|
| 472 |
-
horizon=8,
|
| 473 |
-
symbol_to_duration=lambda symbol: symbol[1],
|
| 474 |
-
),
|
| 475 |
-
)
|
| 476 |
-
|
| 477 |
-
backend = prepare_continuation_backend(
|
| 478 |
-
[training_events],
|
| 479 |
-
prefix=prefix_events,
|
| 480 |
-
horizon=8,
|
| 481 |
-
max_order=4,
|
| 482 |
-
constraints=constraints,
|
| 483 |
-
event_to_symbol=lambda event: (event.pitch, event.duration),
|
| 484 |
-
)
|
| 485 |
-
|
| 486 |
-
generated = backend.sample_events_with_orders(rng=0)
|
| 487 |
-
print(generated.events)
|
| 488 |
-
print(backend.diagnostics.as_dict())
|
| 489 |
-
```
|
| 490 |
-
|
| 491 |
-
The facade defaults to `SingletonAvoidingBackoffPolicy`, matching the
|
| 492 |
-
Continuator-style policy-backoff interpretation. Pass `policy=...` to use a
|
| 493 |
-
different order-selection policy. See `examples/continuator_style_backend.py`
|
| 494 |
-
for a complete dependency-free example.
|
| 495 |
-
|
| 496 |
-
When making many calls with the same training material, horizon, and constraints,
|
| 497 |
-
use `prepare_continuation_plan(...)` once and bind each prefix later:
|
| 498 |
-
|
| 499 |
-
```python
|
| 500 |
-
plan = prepare_continuation_plan(
|
| 501 |
-
[training_events],
|
| 502 |
-
horizon=8,
|
| 503 |
-
max_order=4,
|
| 504 |
-
constraints=constraints,
|
| 505 |
-
event_to_symbol=lambda event: (event.pitch, event.duration),
|
| 506 |
-
)
|
| 507 |
-
|
| 508 |
-
backend = plan.for_prefix(prefix_events)
|
| 509 |
-
generated = backend.sample_events_with_orders(rng=0)
|
| 510 |
-
```
|
| 511 |
-
|
| 512 |
-
For variable musical length in a fixed BP horizon, use
|
| 513 |
-
`padded_duration_total_constraint(...)` with explicit zero-duration PAD symbols
|
| 514 |
-
added to phrase/bar training sequences via `append_padding(...)`. See
|
| 515 |
-
`examples/padded_duration_order_stack_backend.py`.
|
| 516 |
-
|
| 517 |
-
## Brute Force and Metrics
|
| 518 |
-
|
| 519 |
-
For small examples, the package includes exact enumeration helpers:
|
| 520 |
-
|
| 521 |
-
- `brute_force_distribution(...)`
|
| 522 |
-
- `brute_force_partition_function(...)`
|
| 523 |
-
- `conditional_distribution(...)`
|
| 524 |
-
|
| 525 |
-
For empirical checks:
|
| 526 |
-
|
| 527 |
-
- `empirical_distribution(...)`
|
| 528 |
-
- `total_variation(...)`
|
| 529 |
-
|
| 530 |
-
These are intended for tests, toy examples, and exactness validation.
|
| 531 |
-
|
| 532 |
-
## Paper Experiments
|
| 533 |
-
|
| 534 |
-
The reusable library lives in `vo_regular_bp/`. Paper-specific validation and
|
| 535 |
-
benchmark implementations are kept under `paper/variable_order_regular_bp/`.
|
| 536 |
-
The top-level `scripts/` files remain as compatibility launchers, so existing
|
| 537 |
-
commands still work:
|
| 538 |
-
|
| 539 |
-
```bash
|
| 540 |
-
python scripts/eval_tiny_exactness.py
|
| 541 |
-
python scripts/eval_scalability.py --help
|
| 542 |
-
python scripts/eval_bach_scalability.py --help
|
| 543 |
-
python scripts/eval_neurips_ablation.py --help
|
| 544 |
-
python scripts/eval_virtual_augmentation.py --help
|
| 545 |
-
python scripts/eval_bach_continuator_compare.py --help
|
| 546 |
-
python scripts/eval_bach_positional_direct.py --help
|
| 547 |
-
```
|
| 548 |
-
|
| 549 |
-
The Bach data lives in `data/bach_prelude_c_major_pitches.txt`.
|
| 550 |
-
|
| 551 |
-
Rendered experiment notes are in `reports/`, including:
|
| 552 |
-
|
| 553 |
-
- `reports/evaluation_report.md`
|
| 554 |
-
- `reports/optimization_report.md`
|
| 555 |
-
- `reports/bach_contextbp_final_compare.md`
|
| 556 |
-
- `reports/bach_policy_backoff_paper_results.md`
|
| 557 |
-
|
| 558 |
-
## Semantics Note
|
| 559 |
-
|
| 560 |
-
The direct product-BP path (`ContextGraph` plus `run_bp`) samples exactly from a
|
| 561 |
-
single fixed stochastic source conditioned on a regular language:
|
| 562 |
-
|
| 563 |
-
```text
|
| 564 |
-
P_source(x | x in L(A))
|
| 565 |
-
```
|
| 566 |
-
|
| 567 |
-
The order-stack path is exact for a different object: a constrained generation
|
| 568 |
-
policy that prefers higher orders when they have positive feasible future mass
|
| 569 |
-
and backs off only when needed. Its `success_mass` is therefore a policy success
|
| 570 |
-
indicator/mass, not the partition function of one fixed source graph.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/pyproject.toml
DELETED
|
@@ -1,51 +0,0 @@
|
|
| 1 |
-
[build-system]
|
| 2 |
-
requires = ["setuptools>=68"]
|
| 3 |
-
build-backend = "setuptools.build_meta"
|
| 4 |
-
|
| 5 |
-
[project]
|
| 6 |
-
name = "vo-regular-bp"
|
| 7 |
-
version = "0.1.1"
|
| 8 |
-
description = "Exact constrained sampling from sparse variable-order context models under regular constraints."
|
| 9 |
-
readme = "README.md"
|
| 10 |
-
requires-python = ">=3.10"
|
| 11 |
-
license = { file = "LICENSE" }
|
| 12 |
-
authors = [
|
| 13 |
-
{ name = "Francois Pachet" }
|
| 14 |
-
]
|
| 15 |
-
dependencies = []
|
| 16 |
-
keywords = [
|
| 17 |
-
"constrained-sampling",
|
| 18 |
-
"variable-order-markov",
|
| 19 |
-
"belief-propagation",
|
| 20 |
-
"regular-constraints",
|
| 21 |
-
"music-generation",
|
| 22 |
-
]
|
| 23 |
-
classifiers = [
|
| 24 |
-
"Development Status :: 3 - Alpha",
|
| 25 |
-
"Intended Audience :: Developers",
|
| 26 |
-
"Intended Audience :: Science/Research",
|
| 27 |
-
"License :: OSI Approved :: MIT License",
|
| 28 |
-
"Programming Language :: Python :: 3",
|
| 29 |
-
"Programming Language :: Python :: 3 :: Only",
|
| 30 |
-
"Programming Language :: Python :: 3.10",
|
| 31 |
-
"Programming Language :: Python :: 3.11",
|
| 32 |
-
"Programming Language :: Python :: 3.12",
|
| 33 |
-
"Programming Language :: Python :: 3.13",
|
| 34 |
-
"Topic :: Scientific/Engineering",
|
| 35 |
-
"Topic :: Software Development :: Libraries :: Python Modules",
|
| 36 |
-
]
|
| 37 |
-
|
| 38 |
-
[project.optional-dependencies]
|
| 39 |
-
test = [
|
| 40 |
-
"pytest>=8",
|
| 41 |
-
]
|
| 42 |
-
|
| 43 |
-
[project.urls]
|
| 44 |
-
Repository = "https://github.com/fpachet/vo-regular-bp"
|
| 45 |
-
|
| 46 |
-
[tool.setuptools.packages.find]
|
| 47 |
-
include = ["vo_regular_bp*"]
|
| 48 |
-
|
| 49 |
-
[tool.pytest.ini_options]
|
| 50 |
-
testpaths = ["tests"]
|
| 51 |
-
pythonpath = ["."]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/__init__.py
DELETED
|
@@ -1,256 +0,0 @@
|
|
| 1 |
-
"""Exact constrained sampling for sparse variable-order context models.
|
| 2 |
-
|
| 3 |
-
The top-level package exports the stable library surface: generic product BP,
|
| 4 |
-
Continuator-style order-stack backends, event adapters, constraint builders, and
|
| 5 |
-
diagnostic helpers. Lower-level modules remain importable for experiments, but
|
| 6 |
-
external projects should prefer the names exported here.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
from importlib.metadata import PackageNotFoundError as _PackageNotFoundError
|
| 10 |
-
from importlib.metadata import version as _metadata_version
|
| 11 |
-
|
| 12 |
-
try:
|
| 13 |
-
__version__ = _metadata_version("vo-regular-bp")
|
| 14 |
-
except _PackageNotFoundError:
|
| 15 |
-
__version__ = "0+unknown"
|
| 16 |
-
|
| 17 |
-
from .acceptors import (
|
| 18 |
-
DFA,
|
| 19 |
-
DenseForbiddenSubstringDFA,
|
| 20 |
-
SupportsWeightedTransitions,
|
| 21 |
-
WeightedDFA,
|
| 22 |
-
all_of,
|
| 23 |
-
cumulative_meter_acceptor,
|
| 24 |
-
dense_forbidden_substring_acceptor,
|
| 25 |
-
forbidden_substring_acceptor,
|
| 26 |
-
max_order_acceptor,
|
| 27 |
-
meter_acceptor,
|
| 28 |
-
positional_acceptor,
|
| 29 |
-
true_acceptor,
|
| 30 |
-
)
|
| 31 |
-
from .adapters import (
|
| 32 |
-
EventCodec,
|
| 33 |
-
EventOrderStackBackend,
|
| 34 |
-
EventOrderStackPlan,
|
| 35 |
-
GeneratedEvents,
|
| 36 |
-
infer_symbol_to_event,
|
| 37 |
-
prepare_constrained_order_stack_from_events,
|
| 38 |
-
prepare_constrained_order_stack_plan_from_events,
|
| 39 |
-
)
|
| 40 |
-
from .augmentation import (
|
| 41 |
-
SymbolTransform,
|
| 42 |
-
VirtualAugmentedOrderStackModel,
|
| 43 |
-
integer_shift_transform,
|
| 44 |
-
integer_shift_transforms,
|
| 45 |
-
materialize_transformed_sequences,
|
| 46 |
-
)
|
| 47 |
-
from .brute_force import (
|
| 48 |
-
brute_force_distribution,
|
| 49 |
-
brute_force_partition_function,
|
| 50 |
-
conditional_distribution,
|
| 51 |
-
)
|
| 52 |
-
from .backend import (
|
| 53 |
-
BackendDiagnostics,
|
| 54 |
-
ConstrainedOrderStackBackend,
|
| 55 |
-
ConstrainedOrderStackPlan,
|
| 56 |
-
ConstrainedOrderStackSupportPlan,
|
| 57 |
-
GeneratedSequence,
|
| 58 |
-
UntilLengthDiagnostics,
|
| 59 |
-
UntilOrderStackBackend,
|
| 60 |
-
UntilOrderStackDiagnostics,
|
| 61 |
-
prepare_constrained_order_stack,
|
| 62 |
-
prepare_constrained_order_stack_plan,
|
| 63 |
-
prepare_constrained_order_stack_plan_from_sequences,
|
| 64 |
-
prepare_constrained_order_stack_support_plan,
|
| 65 |
-
prepare_constrained_order_stack_support_plan_from_sequences,
|
| 66 |
-
prepare_constrained_order_stack_from_sequences,
|
| 67 |
-
prepare_until_end_order_stack,
|
| 68 |
-
prepare_until_order_stack,
|
| 69 |
-
run_constrained_order_stack,
|
| 70 |
-
)
|
| 71 |
-
from .constraint_builders import (
|
| 72 |
-
at_position,
|
| 73 |
-
avoid_copied_ngrams,
|
| 74 |
-
combine_constraints,
|
| 75 |
-
cumulative_meter,
|
| 76 |
-
final_pitch_class,
|
| 77 |
-
final_symbol,
|
| 78 |
-
final_symbols,
|
| 79 |
-
meter_pattern,
|
| 80 |
-
padded_duration_total,
|
| 81 |
-
)
|
| 82 |
-
from .continuator import (
|
| 83 |
-
duration_total_constraint,
|
| 84 |
-
final_pitch_class_constraint,
|
| 85 |
-
meter_cycle_constraint,
|
| 86 |
-
padded_duration_total_constraint,
|
| 87 |
-
prepare_continuation_backend,
|
| 88 |
-
prepare_continuation_plan,
|
| 89 |
-
)
|
| 90 |
-
from .constraints import (
|
| 91 |
-
CompiledConstraints,
|
| 92 |
-
ConstraintSet,
|
| 93 |
-
CumulativeMeterConstraint,
|
| 94 |
-
MeterConstraint,
|
| 95 |
-
compile_constraints,
|
| 96 |
-
)
|
| 97 |
-
from .context import Context, ContextGraph, Edge, Symbol
|
| 98 |
-
from .metrics import empirical_distribution, total_variation
|
| 99 |
-
from .minimization import (
|
| 100 |
-
ExactQuotientStats,
|
| 101 |
-
exact_context_graph_quotient_stats,
|
| 102 |
-
exact_fixed_order_graph_quotient_stats,
|
| 103 |
-
minimize_context_graph,
|
| 104 |
-
minimize_fixed_order_graph,
|
| 105 |
-
minimize_fixed_order_graphs,
|
| 106 |
-
)
|
| 107 |
-
from .order_stack_bp import (
|
| 108 |
-
DurationViewQuotientDiagnostics,
|
| 109 |
-
DurationViewQuotientOrderStats,
|
| 110 |
-
LongestFeasiblePolicy,
|
| 111 |
-
OrderStackBPPlan,
|
| 112 |
-
OrderStackBPResult,
|
| 113 |
-
OrderStackModel,
|
| 114 |
-
RegularOrderStackBPPlan,
|
| 115 |
-
RegularOrderStackBPResult,
|
| 116 |
-
SingletonAvoidingBackoffPolicy,
|
| 117 |
-
padded_melody_duration_view_quotient_diagnostics,
|
| 118 |
-
prepare_order_stack_bp,
|
| 119 |
-
prepare_order_stack_dfa_bp,
|
| 120 |
-
prepare_order_stack_masked_dfa_bp,
|
| 121 |
-
run_order_stack_dfa_bp,
|
| 122 |
-
run_order_stack_masked_dfa_bp,
|
| 123 |
-
run_order_stack_bp,
|
| 124 |
-
)
|
| 125 |
-
from .orbit_diagnostics import (
|
| 126 |
-
ForbiddenPatternOrbitStats,
|
| 127 |
-
RegularProductOrbitStats,
|
| 128 |
-
RegularRowSignatureStats,
|
| 129 |
-
canonical_integer_shift_key,
|
| 130 |
-
forbidden_pattern_orbit_stats,
|
| 131 |
-
regular_product_orbit_stats,
|
| 132 |
-
regular_row_signature_stats,
|
| 133 |
-
)
|
| 134 |
-
from .padding import append_padding
|
| 135 |
-
from .positional_bp import LazyBackoffContextModel, PositionalBPResult, run_positional_bp
|
| 136 |
-
from .product_bp import ProductBPResult, run_bp, sample_exact
|
| 137 |
-
|
| 138 |
-
__all__ = [
|
| 139 |
-
"__version__",
|
| 140 |
-
# Core symbolic graph and DFA API.
|
| 141 |
-
"Context",
|
| 142 |
-
"ContextGraph",
|
| 143 |
-
"DFA",
|
| 144 |
-
"DenseForbiddenSubstringDFA",
|
| 145 |
-
"Edge",
|
| 146 |
-
"SupportsWeightedTransitions",
|
| 147 |
-
"Symbol",
|
| 148 |
-
"WeightedDFA",
|
| 149 |
-
"all_of",
|
| 150 |
-
"cumulative_meter_acceptor",
|
| 151 |
-
"dense_forbidden_substring_acceptor",
|
| 152 |
-
"forbidden_substring_acceptor",
|
| 153 |
-
"max_order_acceptor",
|
| 154 |
-
"meter_acceptor",
|
| 155 |
-
"positional_acceptor",
|
| 156 |
-
"true_acceptor",
|
| 157 |
-
# General exact product BP.
|
| 158 |
-
"LazyBackoffContextModel",
|
| 159 |
-
"PositionalBPResult",
|
| 160 |
-
"ProductBPResult",
|
| 161 |
-
"run_bp",
|
| 162 |
-
"run_positional_bp",
|
| 163 |
-
"sample_exact",
|
| 164 |
-
# Library-facing order-stack backend.
|
| 165 |
-
"BackendDiagnostics",
|
| 166 |
-
"ConstrainedOrderStackBackend",
|
| 167 |
-
"ConstrainedOrderStackPlan",
|
| 168 |
-
"ConstrainedOrderStackSupportPlan",
|
| 169 |
-
"DurationViewQuotientDiagnostics",
|
| 170 |
-
"DurationViewQuotientOrderStats",
|
| 171 |
-
"GeneratedSequence",
|
| 172 |
-
"LongestFeasiblePolicy",
|
| 173 |
-
"OrderStackBPPlan",
|
| 174 |
-
"OrderStackBPResult",
|
| 175 |
-
"OrderStackModel",
|
| 176 |
-
"RegularOrderStackBPPlan",
|
| 177 |
-
"RegularOrderStackBPResult",
|
| 178 |
-
"SingletonAvoidingBackoffPolicy",
|
| 179 |
-
"UntilLengthDiagnostics",
|
| 180 |
-
"UntilOrderStackBackend",
|
| 181 |
-
"UntilOrderStackDiagnostics",
|
| 182 |
-
"prepare_constrained_order_stack",
|
| 183 |
-
"prepare_constrained_order_stack_plan",
|
| 184 |
-
"prepare_constrained_order_stack_plan_from_sequences",
|
| 185 |
-
"prepare_constrained_order_stack_support_plan",
|
| 186 |
-
"prepare_constrained_order_stack_support_plan_from_sequences",
|
| 187 |
-
"prepare_constrained_order_stack_from_sequences",
|
| 188 |
-
"prepare_order_stack_bp",
|
| 189 |
-
"prepare_order_stack_dfa_bp",
|
| 190 |
-
"prepare_order_stack_masked_dfa_bp",
|
| 191 |
-
"padded_melody_duration_view_quotient_diagnostics",
|
| 192 |
-
"prepare_until_end_order_stack",
|
| 193 |
-
"prepare_until_order_stack",
|
| 194 |
-
"run_constrained_order_stack",
|
| 195 |
-
"run_order_stack_bp",
|
| 196 |
-
"run_order_stack_dfa_bp",
|
| 197 |
-
"run_order_stack_masked_dfa_bp",
|
| 198 |
-
# Exact source graph minimization.
|
| 199 |
-
"ExactQuotientStats",
|
| 200 |
-
"exact_context_graph_quotient_stats",
|
| 201 |
-
"exact_fixed_order_graph_quotient_stats",
|
| 202 |
-
"minimize_context_graph",
|
| 203 |
-
"minimize_fixed_order_graph",
|
| 204 |
-
"minimize_fixed_order_graphs",
|
| 205 |
-
# Transformation-orbit diagnostics.
|
| 206 |
-
"ForbiddenPatternOrbitStats",
|
| 207 |
-
"RegularProductOrbitStats",
|
| 208 |
-
"RegularRowSignatureStats",
|
| 209 |
-
"canonical_integer_shift_key",
|
| 210 |
-
"forbidden_pattern_orbit_stats",
|
| 211 |
-
"regular_product_orbit_stats",
|
| 212 |
-
"regular_row_signature_stats",
|
| 213 |
-
# Virtual data augmentation.
|
| 214 |
-
"SymbolTransform",
|
| 215 |
-
"VirtualAugmentedOrderStackModel",
|
| 216 |
-
"integer_shift_transform",
|
| 217 |
-
"integer_shift_transforms",
|
| 218 |
-
"materialize_transformed_sequences",
|
| 219 |
-
# Fixed-horizon padding helpers.
|
| 220 |
-
"append_padding",
|
| 221 |
-
# Constraint specifications and builders.
|
| 222 |
-
"CompiledConstraints",
|
| 223 |
-
"ConstraintSet",
|
| 224 |
-
"CumulativeMeterConstraint",
|
| 225 |
-
"MeterConstraint",
|
| 226 |
-
"at_position",
|
| 227 |
-
"avoid_copied_ngrams",
|
| 228 |
-
"combine_constraints",
|
| 229 |
-
"compile_constraints",
|
| 230 |
-
"cumulative_meter",
|
| 231 |
-
"final_pitch_class",
|
| 232 |
-
"final_symbol",
|
| 233 |
-
"final_symbols",
|
| 234 |
-
"meter_pattern",
|
| 235 |
-
"padded_duration_total",
|
| 236 |
-
# Event and Continuator-style adapters.
|
| 237 |
-
"EventCodec",
|
| 238 |
-
"EventOrderStackBackend",
|
| 239 |
-
"EventOrderStackPlan",
|
| 240 |
-
"GeneratedEvents",
|
| 241 |
-
"duration_total_constraint",
|
| 242 |
-
"final_pitch_class_constraint",
|
| 243 |
-
"infer_symbol_to_event",
|
| 244 |
-
"meter_cycle_constraint",
|
| 245 |
-
"padded_duration_total_constraint",
|
| 246 |
-
"prepare_constrained_order_stack_from_events",
|
| 247 |
-
"prepare_constrained_order_stack_plan_from_events",
|
| 248 |
-
"prepare_continuation_backend",
|
| 249 |
-
"prepare_continuation_plan",
|
| 250 |
-
# Analysis/test helpers that are useful for exactness checks.
|
| 251 |
-
"brute_force_distribution",
|
| 252 |
-
"brute_force_partition_function",
|
| 253 |
-
"conditional_distribution",
|
| 254 |
-
"empirical_distribution",
|
| 255 |
-
"total_variation",
|
| 256 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/acceptors.py
DELETED
|
@@ -1,660 +0,0 @@
|
|
| 1 |
-
"""Deterministic regular acceptors used by product BP."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import math
|
| 6 |
-
from itertools import product
|
| 7 |
-
from typing import Callable, Hashable, Iterable, Mapping, Protocol, Sequence
|
| 8 |
-
|
| 9 |
-
Symbol = Hashable
|
| 10 |
-
State = Hashable
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class SupportsWeightedTransitions(Protocol):
|
| 14 |
-
"""Structural acceptor interface with optional soft transition weights."""
|
| 15 |
-
|
| 16 |
-
start_state: State
|
| 17 |
-
|
| 18 |
-
def next_state(self, state: State, symbol: Symbol) -> State | None:
|
| 19 |
-
...
|
| 20 |
-
|
| 21 |
-
def is_accepting(self, state: State) -> bool:
|
| 22 |
-
...
|
| 23 |
-
|
| 24 |
-
def transition_weight(self, state: State, symbol: Symbol) -> float:
|
| 25 |
-
...
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
class DFA:
|
| 29 |
-
"""Small deterministic acceptor interface.
|
| 30 |
-
|
| 31 |
-
A transition returning ``None`` means that the emitted symbol is rejected
|
| 32 |
-
from that state. Legal transitions can optionally carry nonnegative
|
| 33 |
-
multiplicative weights through ``transition_weight``; the default is
|
| 34 |
-
``1.0``, so ordinary hard DFAs are unchanged.
|
| 35 |
-
"""
|
| 36 |
-
|
| 37 |
-
def __init__(
|
| 38 |
-
self,
|
| 39 |
-
*,
|
| 40 |
-
start_state: State,
|
| 41 |
-
accept_states: Iterable[State] | None = None,
|
| 42 |
-
transitions: Mapping[State, Mapping[Symbol, State]] | None = None,
|
| 43 |
-
states: Iterable[State] | None = None,
|
| 44 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 45 |
-
transition_func: Callable[[State, Symbol], State | None] | None = None,
|
| 46 |
-
transition_weights: Mapping[State, Mapping[Symbol, float]] | None = None,
|
| 47 |
-
transition_weight_func: Callable[[State, Symbol], float] | None = None,
|
| 48 |
-
accept_func: Callable[[State], bool] | None = None,
|
| 49 |
-
name: str = "dfa",
|
| 50 |
-
) -> None:
|
| 51 |
-
if accept_states is None and accept_func is None:
|
| 52 |
-
raise ValueError("either accept_states or accept_func is required")
|
| 53 |
-
if transitions is None and transition_func is None:
|
| 54 |
-
raise ValueError("either transitions or transition_func is required")
|
| 55 |
-
|
| 56 |
-
self.start_state = start_state
|
| 57 |
-
self.accept_states = frozenset(accept_states or ())
|
| 58 |
-
self.transitions = {
|
| 59 |
-
state: dict(symbols)
|
| 60 |
-
for state, symbols in (transitions or {}).items()
|
| 61 |
-
}
|
| 62 |
-
self._transition_weights = {
|
| 63 |
-
state: dict(symbols)
|
| 64 |
-
for state, symbols in (transition_weights or {}).items()
|
| 65 |
-
}
|
| 66 |
-
self.states = None if states is None else frozenset(states)
|
| 67 |
-
self.alphabet = None if alphabet is None else frozenset(alphabet)
|
| 68 |
-
self._transition_func = transition_func
|
| 69 |
-
self._transition_weight_func = transition_weight_func
|
| 70 |
-
self._accept_func = accept_func
|
| 71 |
-
self.name = name
|
| 72 |
-
|
| 73 |
-
def next_state(self, state: State, symbol: Symbol) -> State | None:
|
| 74 |
-
if self._transition_func is not None:
|
| 75 |
-
return self._transition_func(state, symbol)
|
| 76 |
-
return self.transitions.get(state, {}).get(symbol)
|
| 77 |
-
|
| 78 |
-
def transition_weight(self, state: State, symbol: Symbol) -> float:
|
| 79 |
-
transition_weight_func = getattr(self, "_transition_weight_func", None)
|
| 80 |
-
if transition_weight_func is not None:
|
| 81 |
-
return float(transition_weight_func(state, symbol))
|
| 82 |
-
transition_weights = getattr(self, "_transition_weights", {})
|
| 83 |
-
return float(transition_weights.get(state, {}).get(symbol, 1.0))
|
| 84 |
-
|
| 85 |
-
def is_accepting(self, state: State) -> bool:
|
| 86 |
-
if self._accept_func is not None:
|
| 87 |
-
return bool(self._accept_func(state))
|
| 88 |
-
return state in self.accept_states
|
| 89 |
-
|
| 90 |
-
def accepts(self, sequence: Sequence[Symbol]) -> bool:
|
| 91 |
-
state = self.start_state
|
| 92 |
-
for symbol in sequence:
|
| 93 |
-
current_state = state
|
| 94 |
-
state = self.next_state(state, symbol)
|
| 95 |
-
if state is None:
|
| 96 |
-
return False
|
| 97 |
-
if transition_weight(self, current_state, symbol) <= 0.0:
|
| 98 |
-
return False
|
| 99 |
-
return self.is_accepting(state)
|
| 100 |
-
|
| 101 |
-
def state_count(self) -> int | None:
|
| 102 |
-
if self.states is None:
|
| 103 |
-
return None
|
| 104 |
-
return len(self.states)
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
class WeightedDFA(DFA):
|
| 108 |
-
"""DFA subclass documenting weighted regular-constraint semantics.
|
| 109 |
-
|
| 110 |
-
Subclasses may override ``transition_weight``. The inherited implementation
|
| 111 |
-
supports either a transition-weight mapping or a callable passed to
|
| 112 |
-
``DFA.__init__``.
|
| 113 |
-
"""
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
class DenseForbiddenSubstringDFA(DFA):
|
| 117 |
-
"""Finite-alphabet forbidden-substring DFA with dense integer states.
|
| 118 |
-
|
| 119 |
-
States are integer ids for the same proper-prefix states used by
|
| 120 |
-
:func:`forbidden_substring_acceptor`. A transition value of ``-1`` means
|
| 121 |
-
rejection. The public methods mirror :class:`DFA`, while
|
| 122 |
-
``dense_transition_by_symbol`` gives hot loops a direct table lookup path.
|
| 123 |
-
"""
|
| 124 |
-
|
| 125 |
-
rejected_state = -1
|
| 126 |
-
|
| 127 |
-
def __init__(
|
| 128 |
-
self,
|
| 129 |
-
*,
|
| 130 |
-
prefixes: Sequence[tuple[Symbol, ...]],
|
| 131 |
-
alphabet: Iterable[Symbol],
|
| 132 |
-
transition_table: Sequence[Sequence[int]],
|
| 133 |
-
name: str = "dense_forbidden_substring",
|
| 134 |
-
) -> None:
|
| 135 |
-
alphabet_tuple = tuple(alphabet)
|
| 136 |
-
self.start_state = 0
|
| 137 |
-
self.prefixes = tuple(prefixes)
|
| 138 |
-
self.states = frozenset(range(len(self.prefixes)))
|
| 139 |
-
self.accept_states = self.states
|
| 140 |
-
self.alphabet = frozenset(alphabet_tuple)
|
| 141 |
-
self.transition_table = tuple(tuple(int(next_state) for next_state in row) for row in transition_table)
|
| 142 |
-
self.symbol_to_index = {symbol: index for index, symbol in enumerate(alphabet_tuple)}
|
| 143 |
-
self.dense_transition_by_symbol = {
|
| 144 |
-
symbol: tuple(row[index] for row in self.transition_table)
|
| 145 |
-
for symbol, index in self.symbol_to_index.items()
|
| 146 |
-
}
|
| 147 |
-
self.name = name
|
| 148 |
-
|
| 149 |
-
def next_state(self, state: State, symbol: Symbol) -> State | None:
|
| 150 |
-
if not isinstance(state, int):
|
| 151 |
-
raise TypeError("dense forbidden-substring states must be integers")
|
| 152 |
-
transitions = self.dense_transition_by_symbol.get(symbol)
|
| 153 |
-
if transitions is None:
|
| 154 |
-
return None
|
| 155 |
-
next_state = transitions[state]
|
| 156 |
-
if next_state == self.rejected_state:
|
| 157 |
-
return None
|
| 158 |
-
return next_state
|
| 159 |
-
|
| 160 |
-
def is_accepting(self, state: State) -> bool:
|
| 161 |
-
return isinstance(state, int) and 0 <= state < len(self.prefixes)
|
| 162 |
-
|
| 163 |
-
def accepts(self, sequence: Sequence[Symbol]) -> bool:
|
| 164 |
-
state = self.start_state
|
| 165 |
-
for symbol in sequence:
|
| 166 |
-
next_state = self.next_state(state, symbol)
|
| 167 |
-
if next_state is None:
|
| 168 |
-
return False
|
| 169 |
-
if transition_weight(self, state, symbol) <= 0.0:
|
| 170 |
-
return False
|
| 171 |
-
state = next_state
|
| 172 |
-
return self.is_accepting(state)
|
| 173 |
-
|
| 174 |
-
def state_count(self) -> int:
|
| 175 |
-
return len(self.prefixes)
|
| 176 |
-
|
| 177 |
-
def edge_count(self) -> int:
|
| 178 |
-
return sum(
|
| 179 |
-
1
|
| 180 |
-
for row in self.transition_table
|
| 181 |
-
for next_state in row
|
| 182 |
-
if next_state != self.rejected_state
|
| 183 |
-
)
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
def true_acceptor(*, name: str = "true") -> DFA:
|
| 187 |
-
"""Accept every finite sequence over any alphabet."""
|
| 188 |
-
|
| 189 |
-
return DFA(
|
| 190 |
-
start_state=0,
|
| 191 |
-
accept_states={0},
|
| 192 |
-
states={0},
|
| 193 |
-
transition_func=lambda state, symbol: 0,
|
| 194 |
-
name=name,
|
| 195 |
-
)
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
def all_of(*acceptors: DFA, name: str = "all_of") -> DFA:
|
| 199 |
-
"""Intersection of deterministic acceptors."""
|
| 200 |
-
|
| 201 |
-
if not acceptors:
|
| 202 |
-
return true_acceptor(name=name)
|
| 203 |
-
|
| 204 |
-
start = tuple(acceptor.start_state for acceptor in acceptors)
|
| 205 |
-
|
| 206 |
-
def transition(state: State, symbol: Symbol) -> State | None:
|
| 207 |
-
next_parts = []
|
| 208 |
-
for acceptor, part in zip(acceptors, state):
|
| 209 |
-
next_part = acceptor.next_state(part, symbol)
|
| 210 |
-
if next_part is None:
|
| 211 |
-
return None
|
| 212 |
-
next_parts.append(next_part)
|
| 213 |
-
return tuple(next_parts)
|
| 214 |
-
|
| 215 |
-
def accepting(state: State) -> bool:
|
| 216 |
-
return all(acceptor.is_accepting(part) for acceptor, part in zip(acceptors, state))
|
| 217 |
-
|
| 218 |
-
def weight(state: State, symbol: Symbol) -> float:
|
| 219 |
-
total = 1.0
|
| 220 |
-
for acceptor, part in zip(acceptors, state):
|
| 221 |
-
total *= transition_weight(acceptor, part, symbol)
|
| 222 |
-
return total
|
| 223 |
-
|
| 224 |
-
states = None
|
| 225 |
-
if all(acceptor.states is not None for acceptor in acceptors):
|
| 226 |
-
sizes = [len(acceptor.states or ()) for acceptor in acceptors]
|
| 227 |
-
product_size = 1
|
| 228 |
-
for size in sizes:
|
| 229 |
-
product_size *= size
|
| 230 |
-
if product_size <= 100_000:
|
| 231 |
-
state_sets = [acceptor.states or () for acceptor in acceptors]
|
| 232 |
-
states = set(product(*state_sets))
|
| 233 |
-
|
| 234 |
-
alphabets = [acceptor.alphabet for acceptor in acceptors if acceptor.alphabet is not None]
|
| 235 |
-
alphabet = set.intersection(*map(set, alphabets)) if alphabets else None
|
| 236 |
-
|
| 237 |
-
dfa = DFA(
|
| 238 |
-
start_state=start,
|
| 239 |
-
states=states,
|
| 240 |
-
alphabet=alphabet,
|
| 241 |
-
transition_func=transition,
|
| 242 |
-
transition_weight_func=weight,
|
| 243 |
-
accept_func=accepting,
|
| 244 |
-
name=name,
|
| 245 |
-
)
|
| 246 |
-
dfa.component_acceptors = tuple(acceptors) # type: ignore[attr-defined]
|
| 247 |
-
return dfa
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
def transition_weight(acceptor: object, state: State, symbol: Symbol) -> float:
|
| 251 |
-
"""Return a validated multiplicative weight for a legal transition.
|
| 252 |
-
|
| 253 |
-
Acceptors without ``transition_weight`` are treated as ordinary hard DFAs
|
| 254 |
-
with weight ``1.0``. A weight of ``0.0`` is allowed and acts as hard
|
| 255 |
-
rejection in BP; negative or non-finite weights are invalid.
|
| 256 |
-
"""
|
| 257 |
-
|
| 258 |
-
method = getattr(acceptor, "transition_weight", None)
|
| 259 |
-
raw_weight = 1.0 if method is None else method(state, symbol)
|
| 260 |
-
weight = float(raw_weight)
|
| 261 |
-
if not math.isfinite(weight) or weight < 0.0:
|
| 262 |
-
raise ValueError(
|
| 263 |
-
f"transition weight for state {state!r} and symbol {symbol!r} "
|
| 264 |
-
f"must be a finite nonnegative number, got {raw_weight!r}"
|
| 265 |
-
)
|
| 266 |
-
return weight
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
def has_custom_transition_weights(acceptor: object) -> bool:
|
| 270 |
-
method = getattr(type(acceptor), "transition_weight", None)
|
| 271 |
-
default_method = getattr(DFA, "transition_weight")
|
| 272 |
-
if method is not None and method is not default_method:
|
| 273 |
-
return True
|
| 274 |
-
return bool(
|
| 275 |
-
getattr(acceptor, "_transition_weights", None)
|
| 276 |
-
or getattr(acceptor, "_transition_weight_func", None) is not None
|
| 277 |
-
)
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
def positional_acceptor(
|
| 281 |
-
length: int,
|
| 282 |
-
constraints: Mapping[int, Iterable[Symbol] | Symbol] | None = None,
|
| 283 |
-
*,
|
| 284 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 285 |
-
name: str = "positional",
|
| 286 |
-
) -> DFA:
|
| 287 |
-
"""Accept length-``n`` strings satisfying per-position allowed sets.
|
| 288 |
-
|
| 289 |
-
Positions are zero-based. Constraint values may be a single symbol or an
|
| 290 |
-
iterable of allowed symbols.
|
| 291 |
-
"""
|
| 292 |
-
|
| 293 |
-
if length < 0:
|
| 294 |
-
raise ValueError("length must be non-negative")
|
| 295 |
-
|
| 296 |
-
allowed_by_pos: dict[int, set[Symbol]] = {}
|
| 297 |
-
for position, allowed in (constraints or {}).items():
|
| 298 |
-
if position < 0 or position >= length:
|
| 299 |
-
raise ValueError(f"position {position} is outside length {length}")
|
| 300 |
-
if isinstance(allowed, (str, bytes)):
|
| 301 |
-
allowed_by_pos[position] = {allowed}
|
| 302 |
-
else:
|
| 303 |
-
try:
|
| 304 |
-
allowed_by_pos[position] = set(allowed) # type: ignore[arg-type]
|
| 305 |
-
except TypeError:
|
| 306 |
-
allowed_by_pos[position] = {allowed} # type: ignore[list-item]
|
| 307 |
-
|
| 308 |
-
def transition(position: State, symbol: Symbol) -> State | None:
|
| 309 |
-
if not isinstance(position, int) or position >= length:
|
| 310 |
-
return None
|
| 311 |
-
allowed = allowed_by_pos.get(position)
|
| 312 |
-
if allowed is not None and symbol not in allowed:
|
| 313 |
-
return None
|
| 314 |
-
return position + 1
|
| 315 |
-
|
| 316 |
-
return DFA(
|
| 317 |
-
start_state=0,
|
| 318 |
-
accept_states={length},
|
| 319 |
-
states=set(range(length + 1)),
|
| 320 |
-
alphabet=alphabet,
|
| 321 |
-
transition_func=transition,
|
| 322 |
-
name=name,
|
| 323 |
-
)
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
def meter_acceptor(
|
| 327 |
-
pattern: Sequence[Hashable | Iterable[Hashable] | None],
|
| 328 |
-
symbol_to_meter: Mapping[Symbol, Hashable] | Callable[[Symbol], Hashable],
|
| 329 |
-
*,
|
| 330 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 331 |
-
name: str = "meter",
|
| 332 |
-
) -> DFA:
|
| 333 |
-
"""Accept strings whose per-symbol meter classes match ``pattern``.
|
| 334 |
-
|
| 335 |
-
Each pattern entry may be a single class, an iterable of allowed classes, or
|
| 336 |
-
``None`` as a wildcard. This intentionally keeps meter generic: callers can
|
| 337 |
-
map symbols to stress, duration, pitch class, or any other finite class.
|
| 338 |
-
"""
|
| 339 |
-
|
| 340 |
-
allowed_by_pos: list[set[Hashable] | None] = []
|
| 341 |
-
for expected in pattern:
|
| 342 |
-
if expected is None:
|
| 343 |
-
allowed_by_pos.append(None)
|
| 344 |
-
elif isinstance(expected, (str, bytes)):
|
| 345 |
-
allowed_by_pos.append({expected})
|
| 346 |
-
else:
|
| 347 |
-
try:
|
| 348 |
-
allowed_by_pos.append(set(expected)) # type: ignore[arg-type]
|
| 349 |
-
except TypeError:
|
| 350 |
-
allowed_by_pos.append({expected}) # type: ignore[list-item]
|
| 351 |
-
|
| 352 |
-
if callable(symbol_to_meter):
|
| 353 |
-
meter_of = symbol_to_meter
|
| 354 |
-
else:
|
| 355 |
-
meter_map = dict(symbol_to_meter)
|
| 356 |
-
meter_of = lambda symbol: meter_map[symbol]
|
| 357 |
-
|
| 358 |
-
length = len(pattern)
|
| 359 |
-
|
| 360 |
-
def transition(position: State, symbol: Symbol) -> State | None:
|
| 361 |
-
if not isinstance(position, int) or position >= length:
|
| 362 |
-
return None
|
| 363 |
-
allowed = allowed_by_pos[position]
|
| 364 |
-
if allowed is not None and meter_of(symbol) not in allowed:
|
| 365 |
-
return None
|
| 366 |
-
return position + 1
|
| 367 |
-
|
| 368 |
-
return DFA(
|
| 369 |
-
start_state=0,
|
| 370 |
-
accept_states={length},
|
| 371 |
-
states=set(range(length + 1)),
|
| 372 |
-
alphabet=alphabet,
|
| 373 |
-
transition_func=transition,
|
| 374 |
-
name=name,
|
| 375 |
-
)
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
def cumulative_meter_acceptor(
|
| 379 |
-
length: int,
|
| 380 |
-
cost: Mapping[Symbol, int] | Callable[[Symbol], int],
|
| 381 |
-
predicate: Callable[[int, Symbol, int], bool] | None = None,
|
| 382 |
-
*,
|
| 383 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 384 |
-
max_cost: int | None = None,
|
| 385 |
-
accept_costs: Iterable[int] | Callable[[int], bool] | None = None,
|
| 386 |
-
end_symbol: Symbol | None = None,
|
| 387 |
-
name: str = "cumulative_meter",
|
| 388 |
-
) -> DFA:
|
| 389 |
-
"""Accept strings satisfying a cumulative meter predicate.
|
| 390 |
-
|
| 391 |
-
This is the regular-constraint form of the paper-style meter predicate
|
| 392 |
-
``pi(c, x, k)``: before emitting symbol ``x`` at 1-based position ``k``,
|
| 393 |
-
the DFA state stores the cumulative cost ``c`` of previous symbols. The
|
| 394 |
-
transition is accepted iff ``predicate(c, x, k)`` holds, then the symbol
|
| 395 |
-
cost is added to the state.
|
| 396 |
-
|
| 397 |
-
``accept_costs`` can be used for final total-cost requirements. If
|
| 398 |
-
``end_symbol`` is provided, once it appears, all later symbols must be the
|
| 399 |
-
same padding symbol. Passing ``max_cost`` bounds transitions and lets the
|
| 400 |
-
DFA expose an explicit finite state set.
|
| 401 |
-
"""
|
| 402 |
-
|
| 403 |
-
if length < 0:
|
| 404 |
-
raise ValueError("length must be non-negative")
|
| 405 |
-
if max_cost is not None and max_cost < 0:
|
| 406 |
-
raise ValueError("max_cost must be non-negative")
|
| 407 |
-
|
| 408 |
-
if callable(cost):
|
| 409 |
-
cost_of = cost
|
| 410 |
-
else:
|
| 411 |
-
cost_map = dict(cost)
|
| 412 |
-
|
| 413 |
-
def cost_of(symbol: Symbol) -> int:
|
| 414 |
-
return cost_map[symbol]
|
| 415 |
-
|
| 416 |
-
if accept_costs is None:
|
| 417 |
-
accepts_cost = lambda total: True
|
| 418 |
-
elif callable(accept_costs):
|
| 419 |
-
accepts_cost = accept_costs
|
| 420 |
-
else:
|
| 421 |
-
accepted_totals = frozenset(int(total) for total in accept_costs)
|
| 422 |
-
accepts_cost = lambda total: total in accepted_totals
|
| 423 |
-
|
| 424 |
-
def transition(state: State, symbol: Symbol) -> State | None:
|
| 425 |
-
if not (
|
| 426 |
-
isinstance(state, tuple)
|
| 427 |
-
and len(state) == 3
|
| 428 |
-
and isinstance(state[0], int)
|
| 429 |
-
and isinstance(state[1], int)
|
| 430 |
-
and isinstance(state[2], bool)
|
| 431 |
-
):
|
| 432 |
-
raise TypeError("cumulative-meter states must be (position, total_cost, ended)")
|
| 433 |
-
|
| 434 |
-
position, total_cost, ended = state
|
| 435 |
-
if position >= length:
|
| 436 |
-
return None
|
| 437 |
-
if ended and symbol != end_symbol:
|
| 438 |
-
return None
|
| 439 |
-
|
| 440 |
-
symbol_cost = _nonnegative_int_cost(cost_of(symbol), symbol)
|
| 441 |
-
next_position = position + 1
|
| 442 |
-
next_total = total_cost + symbol_cost
|
| 443 |
-
if max_cost is not None and next_total > max_cost:
|
| 444 |
-
return None
|
| 445 |
-
if predicate is not None and not predicate(total_cost, symbol, next_position):
|
| 446 |
-
return None
|
| 447 |
-
|
| 448 |
-
next_ended = ended or (end_symbol is not None and symbol == end_symbol)
|
| 449 |
-
return (next_position, next_total, next_ended)
|
| 450 |
-
|
| 451 |
-
def accepting(state: State) -> bool:
|
| 452 |
-
return (
|
| 453 |
-
isinstance(state, tuple)
|
| 454 |
-
and len(state) == 3
|
| 455 |
-
and state[0] == length
|
| 456 |
-
and isinstance(state[1], int)
|
| 457 |
-
and bool(accepts_cost(state[1]))
|
| 458 |
-
)
|
| 459 |
-
|
| 460 |
-
states = None
|
| 461 |
-
if max_cost is not None:
|
| 462 |
-
ended_values = (False, True) if end_symbol is not None else (False,)
|
| 463 |
-
states = {
|
| 464 |
-
(position, total, ended)
|
| 465 |
-
for position in range(length + 1)
|
| 466 |
-
for total in range(max_cost + 1)
|
| 467 |
-
for ended in ended_values
|
| 468 |
-
}
|
| 469 |
-
|
| 470 |
-
return DFA(
|
| 471 |
-
start_state=(0, 0, False),
|
| 472 |
-
states=states,
|
| 473 |
-
alphabet=alphabet,
|
| 474 |
-
transition_func=transition,
|
| 475 |
-
accept_func=accepting,
|
| 476 |
-
name=name,
|
| 477 |
-
)
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
def forbidden_substring_acceptor(
|
| 481 |
-
forbidden_patterns: Iterable[Sequence[Symbol]],
|
| 482 |
-
*,
|
| 483 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 484 |
-
name: str = "forbidden_substring",
|
| 485 |
-
) -> DFA:
|
| 486 |
-
"""Accept strings that contain none of the forbidden substrings.
|
| 487 |
-
|
| 488 |
-
The states are Aho-Corasick-style prefixes: the current state stores the
|
| 489 |
-
longest suffix of the generated sequence that is a proper prefix of a
|
| 490 |
-
forbidden pattern. Completing a forbidden pattern rejects the transition.
|
| 491 |
-
"""
|
| 492 |
-
|
| 493 |
-
patterns = tuple(tuple(pattern) for pattern in forbidden_patterns)
|
| 494 |
-
if any(len(pattern) == 0 for pattern in patterns):
|
| 495 |
-
raise ValueError("empty forbidden patterns would reject every sequence")
|
| 496 |
-
if not patterns:
|
| 497 |
-
return true_acceptor(name=name)
|
| 498 |
-
|
| 499 |
-
prefixes: set[tuple[Symbol, ...]] = {()}
|
| 500 |
-
for pattern in patterns:
|
| 501 |
-
for prefix_len in range(1, len(pattern)):
|
| 502 |
-
prefixes.add(pattern[:prefix_len])
|
| 503 |
-
|
| 504 |
-
max_prefix_len = max((len(prefix) for prefix in prefixes), default=0)
|
| 505 |
-
patterns_by_len: dict[int, set[tuple[Symbol, ...]]] = {}
|
| 506 |
-
for pattern in patterns:
|
| 507 |
-
patterns_by_len.setdefault(len(pattern), set()).add(pattern)
|
| 508 |
-
|
| 509 |
-
if len(patterns_by_len) == 1:
|
| 510 |
-
forbidden_len, forbidden_set = next(iter(patterns_by_len.items()))
|
| 511 |
-
|
| 512 |
-
def completes_forbidden(candidate: tuple[Symbol, ...]) -> bool:
|
| 513 |
-
return len(candidate) >= forbidden_len and candidate[-forbidden_len:] in forbidden_set
|
| 514 |
-
|
| 515 |
-
else:
|
| 516 |
-
|
| 517 |
-
def completes_forbidden(candidate: tuple[Symbol, ...]) -> bool:
|
| 518 |
-
return any(
|
| 519 |
-
len(candidate) >= pattern_len and candidate[-pattern_len:] in pattern_set
|
| 520 |
-
for pattern_len, pattern_set in patterns_by_len.items()
|
| 521 |
-
)
|
| 522 |
-
|
| 523 |
-
def next_prefix(state: State, symbol: Symbol) -> State | None:
|
| 524 |
-
if not isinstance(state, tuple):
|
| 525 |
-
raise TypeError("forbidden-substring states must be tuples")
|
| 526 |
-
candidate = state + (symbol,)
|
| 527 |
-
if completes_forbidden(candidate):
|
| 528 |
-
return None
|
| 529 |
-
limit = min(len(candidate), max_prefix_len)
|
| 530 |
-
for size in range(limit, -1, -1):
|
| 531 |
-
suffix = candidate[-size:] if size else ()
|
| 532 |
-
if suffix in prefixes:
|
| 533 |
-
return suffix
|
| 534 |
-
return ()
|
| 535 |
-
|
| 536 |
-
transitions = None
|
| 537 |
-
if alphabet is not None:
|
| 538 |
-
alphabet_set = frozenset(alphabet)
|
| 539 |
-
transitions = {
|
| 540 |
-
state: {
|
| 541 |
-
symbol: next_state
|
| 542 |
-
for symbol in alphabet_set
|
| 543 |
-
if (next_state := next_prefix(state, symbol)) is not None
|
| 544 |
-
}
|
| 545 |
-
for state in prefixes
|
| 546 |
-
}
|
| 547 |
-
else:
|
| 548 |
-
alphabet_set = None
|
| 549 |
-
|
| 550 |
-
return DFA(
|
| 551 |
-
start_state=(),
|
| 552 |
-
accept_states=prefixes,
|
| 553 |
-
transitions=transitions,
|
| 554 |
-
states=prefixes,
|
| 555 |
-
alphabet=alphabet_set,
|
| 556 |
-
transition_func=None if transitions is not None else next_prefix,
|
| 557 |
-
name=name,
|
| 558 |
-
)
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
def dense_forbidden_substring_acceptor(
|
| 562 |
-
forbidden_patterns: Iterable[Sequence[Symbol]],
|
| 563 |
-
*,
|
| 564 |
-
alphabet: Iterable[Symbol],
|
| 565 |
-
name: str = "dense_forbidden_substring",
|
| 566 |
-
) -> DenseForbiddenSubstringDFA:
|
| 567 |
-
"""Dense finite-alphabet acceptor for strings with no forbidden substring."""
|
| 568 |
-
|
| 569 |
-
patterns = tuple(tuple(pattern) for pattern in forbidden_patterns)
|
| 570 |
-
if any(len(pattern) == 0 for pattern in patterns):
|
| 571 |
-
raise ValueError("empty forbidden patterns would reject every sequence")
|
| 572 |
-
|
| 573 |
-
alphabet_tuple = tuple(alphabet)
|
| 574 |
-
prefix_to_id: dict[tuple[Symbol, ...], int] = {(): 0}
|
| 575 |
-
prefixes: list[tuple[Symbol, ...]] = [()]
|
| 576 |
-
for pattern in patterns:
|
| 577 |
-
for prefix_len in range(1, len(pattern)):
|
| 578 |
-
prefix = pattern[:prefix_len]
|
| 579 |
-
if prefix not in prefix_to_id:
|
| 580 |
-
prefix_to_id[prefix] = len(prefixes)
|
| 581 |
-
prefixes.append(prefix)
|
| 582 |
-
|
| 583 |
-
max_prefix_len = max((len(prefix) for prefix in prefixes), default=0)
|
| 584 |
-
patterns_by_len: dict[int, set[tuple[Symbol, ...]]] = {}
|
| 585 |
-
for pattern in patterns:
|
| 586 |
-
patterns_by_len.setdefault(len(pattern), set()).add(pattern)
|
| 587 |
-
|
| 588 |
-
if len(patterns_by_len) == 1:
|
| 589 |
-
forbidden_len, forbidden_set = next(iter(patterns_by_len.items()))
|
| 590 |
-
|
| 591 |
-
def completes_forbidden(candidate: tuple[Symbol, ...]) -> bool:
|
| 592 |
-
return len(candidate) >= forbidden_len and candidate[-forbidden_len:] in forbidden_set
|
| 593 |
-
|
| 594 |
-
else:
|
| 595 |
-
|
| 596 |
-
def completes_forbidden(candidate: tuple[Symbol, ...]) -> bool:
|
| 597 |
-
return any(
|
| 598 |
-
len(candidate) >= pattern_len and candidate[-pattern_len:] in pattern_set
|
| 599 |
-
for pattern_len, pattern_set in patterns_by_len.items()
|
| 600 |
-
)
|
| 601 |
-
|
| 602 |
-
def next_prefix(state: tuple[Symbol, ...], symbol: Symbol) -> tuple[Symbol, ...] | None:
|
| 603 |
-
candidate = state + (symbol,)
|
| 604 |
-
if completes_forbidden(candidate):
|
| 605 |
-
return None
|
| 606 |
-
limit = min(len(candidate), max_prefix_len)
|
| 607 |
-
for size in range(limit, -1, -1):
|
| 608 |
-
suffix = candidate[-size:] if size else ()
|
| 609 |
-
if suffix in prefix_to_id:
|
| 610 |
-
return suffix
|
| 611 |
-
return ()
|
| 612 |
-
|
| 613 |
-
transition_table: list[list[int]] = []
|
| 614 |
-
for prefix in prefixes:
|
| 615 |
-
row: list[int] = []
|
| 616 |
-
for symbol in alphabet_tuple:
|
| 617 |
-
next_state = next_prefix(prefix, symbol)
|
| 618 |
-
row.append(DenseForbiddenSubstringDFA.rejected_state if next_state is None else prefix_to_id[next_state])
|
| 619 |
-
transition_table.append(row)
|
| 620 |
-
|
| 621 |
-
return DenseForbiddenSubstringDFA(
|
| 622 |
-
prefixes=prefixes,
|
| 623 |
-
alphabet=alphabet_tuple,
|
| 624 |
-
transition_table=transition_table,
|
| 625 |
-
name=name,
|
| 626 |
-
)
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
def max_order_acceptor(
|
| 630 |
-
reference_sequences: Iterable[Sequence[Symbol]],
|
| 631 |
-
max_order: int,
|
| 632 |
-
*,
|
| 633 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 634 |
-
name: str = "max_order",
|
| 635 |
-
) -> DFA:
|
| 636 |
-
"""Forbid every reference substring of length ``max_order + 1``.
|
| 637 |
-
|
| 638 |
-
This is the standard regular way to enforce "do not copy beyond max order":
|
| 639 |
-
any generated window longer than ``max_order`` that appears in the reference
|
| 640 |
-
material is rejected.
|
| 641 |
-
"""
|
| 642 |
-
|
| 643 |
-
if max_order < 0:
|
| 644 |
-
raise ValueError("max_order must be non-negative")
|
| 645 |
-
|
| 646 |
-
window = max_order + 1
|
| 647 |
-
forbidden: set[tuple[Symbol, ...]] = set()
|
| 648 |
-
for sequence in reference_sequences:
|
| 649 |
-
tokens = tuple(sequence)
|
| 650 |
-
for index in range(0, len(tokens) - window + 1):
|
| 651 |
-
forbidden.add(tokens[index : index + window])
|
| 652 |
-
return forbidden_substring_acceptor(forbidden, alphabet=alphabet, name=name)
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
def _nonnegative_int_cost(value: int, symbol: Symbol) -> int:
|
| 656 |
-
if not isinstance(value, int):
|
| 657 |
-
raise TypeError(f"cost for symbol {symbol!r} must be an integer")
|
| 658 |
-
if value < 0:
|
| 659 |
-
raise ValueError(f"cost for symbol {symbol!r} must be non-negative")
|
| 660 |
-
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/adapters.py
DELETED
|
@@ -1,270 +0,0 @@
|
|
| 1 |
-
"""Adapters for projects that use rich event objects instead of symbols."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from collections.abc import Callable, Iterable, Mapping, Sequence
|
| 6 |
-
from dataclasses import dataclass
|
| 7 |
-
import random
|
| 8 |
-
from typing import Generic, TypeVar
|
| 9 |
-
|
| 10 |
-
from .backend import (
|
| 11 |
-
BackendDiagnostics,
|
| 12 |
-
ConstrainedOrderStackBackend,
|
| 13 |
-
ConstrainedOrderStackPlan,
|
| 14 |
-
prepare_constrained_order_stack_plan,
|
| 15 |
-
)
|
| 16 |
-
from .constraints import ConstraintSet
|
| 17 |
-
from .context import Symbol
|
| 18 |
-
from .order_stack_bp import OrderPolicy, OrderStackModel
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
EventT = TypeVar("EventT")
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
@dataclass(frozen=True)
|
| 25 |
-
class EventCodec(Generic[EventT]):
|
| 26 |
-
"""Encode project-specific events as hashable symbols.
|
| 27 |
-
|
| 28 |
-
``symbol_to_event`` is optional. It is only required when callers want to
|
| 29 |
-
decode generated symbols back into event objects.
|
| 30 |
-
"""
|
| 31 |
-
|
| 32 |
-
event_to_symbol: Callable[[EventT], Symbol]
|
| 33 |
-
symbol_to_event: Mapping[Symbol, EventT] | Callable[[Symbol], EventT] | None = None
|
| 34 |
-
|
| 35 |
-
@classmethod
|
| 36 |
-
def identity(cls) -> "EventCodec[Symbol]":
|
| 37 |
-
return cls(lambda event: event, lambda symbol: symbol)
|
| 38 |
-
|
| 39 |
-
def encode_event(self, event: EventT) -> Symbol:
|
| 40 |
-
return self.event_to_symbol(event)
|
| 41 |
-
|
| 42 |
-
def encode_sequence(self, sequence: Sequence[EventT]) -> tuple[Symbol, ...]:
|
| 43 |
-
return tuple(self.encode_event(event) for event in sequence)
|
| 44 |
-
|
| 45 |
-
def encode_sequences(
|
| 46 |
-
self,
|
| 47 |
-
sequences: Iterable[Sequence[EventT]],
|
| 48 |
-
) -> tuple[tuple[Symbol, ...], ...]:
|
| 49 |
-
return tuple(self.encode_sequence(sequence) for sequence in sequences)
|
| 50 |
-
|
| 51 |
-
def decode_symbol(self, symbol: Symbol) -> EventT:
|
| 52 |
-
decoder = self.symbol_to_event
|
| 53 |
-
if decoder is None:
|
| 54 |
-
raise ValueError("EventCodec has no symbol_to_event decoder")
|
| 55 |
-
if isinstance(decoder, Mapping):
|
| 56 |
-
return decoder[symbol]
|
| 57 |
-
return decoder(symbol)
|
| 58 |
-
|
| 59 |
-
def decode_sequence(self, sequence: Sequence[Symbol]) -> tuple[EventT, ...]:
|
| 60 |
-
return tuple(self.decode_symbol(symbol) for symbol in sequence)
|
| 61 |
-
|
| 62 |
-
def symbol_property(
|
| 63 |
-
self,
|
| 64 |
-
event_property: Callable[[EventT], object],
|
| 65 |
-
) -> Callable[[Symbol], object]:
|
| 66 |
-
"""Lift an event property function to generated symbols."""
|
| 67 |
-
|
| 68 |
-
def property_from_symbol(symbol: Symbol) -> object:
|
| 69 |
-
return event_property(self.decode_symbol(symbol))
|
| 70 |
-
|
| 71 |
-
return property_from_symbol
|
| 72 |
-
|
| 73 |
-
def with_decoder(
|
| 74 |
-
self,
|
| 75 |
-
symbol_to_event: Mapping[Symbol, EventT] | Callable[[Symbol], EventT],
|
| 76 |
-
) -> "EventCodec[EventT]":
|
| 77 |
-
return EventCodec(self.event_to_symbol, symbol_to_event)
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
@dataclass(frozen=True)
|
| 81 |
-
class GeneratedEvents(Generic[EventT]):
|
| 82 |
-
"""Generated events with their encoded symbols and optional order trace."""
|
| 83 |
-
|
| 84 |
-
events: tuple[EventT, ...]
|
| 85 |
-
symbols: tuple[Symbol, ...]
|
| 86 |
-
orders: tuple[int, ...] = ()
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
@dataclass(frozen=True)
|
| 90 |
-
class EventOrderStackBackend(Generic[EventT]):
|
| 91 |
-
"""Prepared constrained order-stack sampler for event objects."""
|
| 92 |
-
|
| 93 |
-
backend: ConstrainedOrderStackBackend
|
| 94 |
-
codec: EventCodec[EventT]
|
| 95 |
-
|
| 96 |
-
@property
|
| 97 |
-
def diagnostics(self) -> BackendDiagnostics:
|
| 98 |
-
return self.backend.diagnostics
|
| 99 |
-
|
| 100 |
-
def sample_symbols(self, *, rng: random.Random | int | None = None) -> tuple[Symbol, ...]:
|
| 101 |
-
return self.backend.sample(rng=rng)
|
| 102 |
-
|
| 103 |
-
def sample_events(self, *, rng: random.Random | int | None = None) -> tuple[EventT, ...]:
|
| 104 |
-
return self.codec.decode_sequence(self.sample_symbols(rng=rng))
|
| 105 |
-
|
| 106 |
-
def sample(self, *, rng: random.Random | int | None = None) -> tuple[EventT, ...]:
|
| 107 |
-
return self.sample_events(rng=rng)
|
| 108 |
-
|
| 109 |
-
def sample_events_with_orders(
|
| 110 |
-
self,
|
| 111 |
-
*,
|
| 112 |
-
rng: random.Random | int | None = None,
|
| 113 |
-
) -> GeneratedEvents[EventT]:
|
| 114 |
-
generated = self.backend.sample_with_orders(rng=rng)
|
| 115 |
-
return GeneratedEvents(
|
| 116 |
-
events=self.codec.decode_sequence(generated.sequence),
|
| 117 |
-
symbols=generated.sequence,
|
| 118 |
-
orders=generated.orders,
|
| 119 |
-
)
|
| 120 |
-
|
| 121 |
-
def sample_many_events(
|
| 122 |
-
self,
|
| 123 |
-
count: int,
|
| 124 |
-
*,
|
| 125 |
-
rng: random.Random | int | None = None,
|
| 126 |
-
) -> list[tuple[EventT, ...]]:
|
| 127 |
-
return [
|
| 128 |
-
self.codec.decode_sequence(sequence)
|
| 129 |
-
for sequence in self.backend.sample_many(count, rng=rng)
|
| 130 |
-
]
|
| 131 |
-
|
| 132 |
-
def sample_many_events_with_orders(
|
| 133 |
-
self,
|
| 134 |
-
count: int,
|
| 135 |
-
*,
|
| 136 |
-
rng: random.Random | int | None = None,
|
| 137 |
-
) -> list[GeneratedEvents[EventT]]:
|
| 138 |
-
return [
|
| 139 |
-
GeneratedEvents(
|
| 140 |
-
events=self.codec.decode_sequence(generated.sequence),
|
| 141 |
-
symbols=generated.sequence,
|
| 142 |
-
orders=generated.orders,
|
| 143 |
-
)
|
| 144 |
-
for generated in self.backend.sample_many_with_orders(count, rng=rng)
|
| 145 |
-
]
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
@dataclass(frozen=True)
|
| 149 |
-
class EventOrderStackPlan(Generic[EventT]):
|
| 150 |
-
"""Prefix-independent event sampler plan with encode/decode glue."""
|
| 151 |
-
|
| 152 |
-
plan: ConstrainedOrderStackPlan
|
| 153 |
-
codec: EventCodec[EventT]
|
| 154 |
-
|
| 155 |
-
@property
|
| 156 |
-
def length(self) -> int:
|
| 157 |
-
return self.plan.length
|
| 158 |
-
|
| 159 |
-
def for_prefix(self, prefix: Sequence[EventT]) -> EventOrderStackBackend[EventT]:
|
| 160 |
-
backend = self.plan.for_prefix(self.codec.encode_sequence(prefix))
|
| 161 |
-
return EventOrderStackBackend(backend=backend, codec=self.codec)
|
| 162 |
-
|
| 163 |
-
def sample_events(
|
| 164 |
-
self,
|
| 165 |
-
*,
|
| 166 |
-
prefix: Sequence[EventT],
|
| 167 |
-
rng: random.Random | int | None = None,
|
| 168 |
-
) -> tuple[EventT, ...]:
|
| 169 |
-
return self.for_prefix(prefix).sample_events(rng=rng)
|
| 170 |
-
|
| 171 |
-
def sample_events_with_orders(
|
| 172 |
-
self,
|
| 173 |
-
*,
|
| 174 |
-
prefix: Sequence[EventT],
|
| 175 |
-
rng: random.Random | int | None = None,
|
| 176 |
-
) -> GeneratedEvents[EventT]:
|
| 177 |
-
return self.for_prefix(prefix).sample_events_with_orders(rng=rng)
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
def prepare_constrained_order_stack_from_events(
|
| 181 |
-
sequences: Iterable[Sequence[EventT]],
|
| 182 |
-
constraints: ConstraintSet | None = None,
|
| 183 |
-
*,
|
| 184 |
-
codec: EventCodec[EventT],
|
| 185 |
-
max_order: int,
|
| 186 |
-
length: int,
|
| 187 |
-
prefix: Sequence[EventT],
|
| 188 |
-
policy: OrderPolicy | None = None,
|
| 189 |
-
start_event: EventT | None = None,
|
| 190 |
-
end_event: EventT | None = None,
|
| 191 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 192 |
-
prefer_dense_forbidden: bool = True,
|
| 193 |
-
minimize_source_graphs: bool = False,
|
| 194 |
-
) -> EventOrderStackBackend[EventT]:
|
| 195 |
-
"""Build and prepare a constrained order-stack backend from event sequences."""
|
| 196 |
-
|
| 197 |
-
plan = prepare_constrained_order_stack_plan_from_events(
|
| 198 |
-
sequences,
|
| 199 |
-
constraints,
|
| 200 |
-
codec=codec,
|
| 201 |
-
max_order=max_order,
|
| 202 |
-
length=length,
|
| 203 |
-
policy=policy,
|
| 204 |
-
start_event=start_event,
|
| 205 |
-
end_event=end_event,
|
| 206 |
-
alphabet=alphabet,
|
| 207 |
-
prefer_dense_forbidden=prefer_dense_forbidden,
|
| 208 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 209 |
-
)
|
| 210 |
-
return plan.for_prefix(prefix)
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
def prepare_constrained_order_stack_plan_from_events(
|
| 214 |
-
sequences: Iterable[Sequence[EventT]],
|
| 215 |
-
constraints: ConstraintSet | None = None,
|
| 216 |
-
*,
|
| 217 |
-
codec: EventCodec[EventT],
|
| 218 |
-
max_order: int,
|
| 219 |
-
length: int,
|
| 220 |
-
policy: OrderPolicy | None = None,
|
| 221 |
-
start_event: EventT | None = None,
|
| 222 |
-
end_event: EventT | None = None,
|
| 223 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 224 |
-
prefer_dense_forbidden: bool = True,
|
| 225 |
-
minimize_source_graphs: bool = False,
|
| 226 |
-
) -> EventOrderStackPlan[EventT]:
|
| 227 |
-
"""Build an order-stack model from events and prepare a prefixless plan."""
|
| 228 |
-
|
| 229 |
-
encoded_sequences = codec.encode_sequences(sequences)
|
| 230 |
-
model = OrderStackModel.from_sequences(
|
| 231 |
-
encoded_sequences,
|
| 232 |
-
max_order=max_order,
|
| 233 |
-
start_symbol=None if start_event is None else codec.encode_event(start_event),
|
| 234 |
-
end_symbol=None if end_event is None else codec.encode_event(end_event),
|
| 235 |
-
)
|
| 236 |
-
plan = prepare_constrained_order_stack_plan(
|
| 237 |
-
model,
|
| 238 |
-
constraints,
|
| 239 |
-
length=length,
|
| 240 |
-
policy=policy,
|
| 241 |
-
alphabet=alphabet,
|
| 242 |
-
prefer_dense_forbidden=prefer_dense_forbidden,
|
| 243 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 244 |
-
)
|
| 245 |
-
return EventOrderStackPlan(plan=plan, codec=codec)
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
def infer_symbol_to_event(
|
| 249 |
-
sequences: Iterable[Sequence[EventT]],
|
| 250 |
-
event_to_symbol: Callable[[EventT], Symbol],
|
| 251 |
-
*,
|
| 252 |
-
strict: bool = True,
|
| 253 |
-
) -> dict[Symbol, EventT]:
|
| 254 |
-
"""Infer a decoder lookup from training events.
|
| 255 |
-
|
| 256 |
-
In strict mode, two unequal events mapping to the same symbol raise because
|
| 257 |
-
decoding would be ambiguous. With ``strict=False``, the first event seen for
|
| 258 |
-
each symbol is kept.
|
| 259 |
-
"""
|
| 260 |
-
|
| 261 |
-
lookup: dict[Symbol, EventT] = {}
|
| 262 |
-
for sequence in sequences:
|
| 263 |
-
for event in sequence:
|
| 264 |
-
symbol = event_to_symbol(event)
|
| 265 |
-
if symbol in lookup:
|
| 266 |
-
if strict and lookup[symbol] != event:
|
| 267 |
-
raise ValueError(f"symbol {symbol!r} maps to multiple events")
|
| 268 |
-
continue
|
| 269 |
-
lookup[symbol] = event
|
| 270 |
-
return lookup
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/augmentation.py
DELETED
|
@@ -1,505 +0,0 @@
|
|
| 1 |
-
"""Virtual data augmentation for order-stack models.
|
| 2 |
-
|
| 3 |
-
The classes here keep the generated symbol space absolute. Transformations are
|
| 4 |
-
used only to evaluate continuation counts as if transformed copies of the
|
| 5 |
-
training sequences had been materialized.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
-
from collections import Counter
|
| 11 |
-
from collections.abc import Callable, Iterable, Iterator, Sequence
|
| 12 |
-
from dataclasses import dataclass, field
|
| 13 |
-
import time
|
| 14 |
-
|
| 15 |
-
from .context import Context, Symbol, _as_context
|
| 16 |
-
from .order_stack_bp import FixedOrderContextGraph, OrderStackModel
|
| 17 |
-
from .order_stack_bp import StackEdge
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
@dataclass(frozen=True)
|
| 21 |
-
class SymbolTransform:
|
| 22 |
-
"""A deterministic finite augmentation transform over symbols."""
|
| 23 |
-
|
| 24 |
-
name: str
|
| 25 |
-
apply_symbol: Callable[[Symbol], Symbol]
|
| 26 |
-
inverse_symbol: Callable[[Symbol], Symbol]
|
| 27 |
-
weight: float = 1.0
|
| 28 |
-
|
| 29 |
-
def apply_context(self, context: Sequence[Symbol]) -> Context:
|
| 30 |
-
return tuple(self.apply_symbol(symbol) for symbol in context)
|
| 31 |
-
|
| 32 |
-
def inverse_context(self, context: Sequence[Symbol]) -> Context:
|
| 33 |
-
return tuple(self.inverse_symbol(symbol) for symbol in context)
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def integer_shift_transform(
|
| 37 |
-
offset: int,
|
| 38 |
-
*,
|
| 39 |
-
fixed_symbols: Iterable[Symbol] = (),
|
| 40 |
-
name: str | None = None,
|
| 41 |
-
) -> SymbolTransform:
|
| 42 |
-
"""Return a symbol transform that adds an integer offset.
|
| 43 |
-
|
| 44 |
-
``fixed_symbols`` are left unchanged. This is useful for start/end sentinel
|
| 45 |
-
symbols in pitch models.
|
| 46 |
-
"""
|
| 47 |
-
|
| 48 |
-
fixed = frozenset(fixed_symbols)
|
| 49 |
-
shift = int(offset)
|
| 50 |
-
|
| 51 |
-
def apply_symbol(symbol: Symbol) -> Symbol:
|
| 52 |
-
if symbol in fixed:
|
| 53 |
-
return symbol
|
| 54 |
-
return int(symbol) + shift
|
| 55 |
-
|
| 56 |
-
def inverse_symbol(symbol: Symbol) -> Symbol:
|
| 57 |
-
if symbol in fixed:
|
| 58 |
-
return symbol
|
| 59 |
-
return int(symbol) - shift
|
| 60 |
-
|
| 61 |
-
return SymbolTransform(
|
| 62 |
-
name=name or f"shift_{shift:+d}",
|
| 63 |
-
apply_symbol=apply_symbol,
|
| 64 |
-
inverse_symbol=inverse_symbol,
|
| 65 |
-
)
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def integer_shift_transforms(
|
| 69 |
-
offsets: Iterable[int],
|
| 70 |
-
*,
|
| 71 |
-
fixed_symbols: Iterable[Symbol] = (),
|
| 72 |
-
) -> tuple[SymbolTransform, ...]:
|
| 73 |
-
"""Return integer-shift transforms for all requested offsets."""
|
| 74 |
-
|
| 75 |
-
return tuple(
|
| 76 |
-
integer_shift_transform(offset, fixed_symbols=fixed_symbols)
|
| 77 |
-
for offset in offsets
|
| 78 |
-
)
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def materialize_transformed_sequences(
|
| 82 |
-
sequences: Iterable[Sequence[Symbol]],
|
| 83 |
-
transforms: Iterable[SymbolTransform],
|
| 84 |
-
) -> tuple[tuple[Symbol, ...], ...]:
|
| 85 |
-
"""Materialize transformed copies for validation and comparison."""
|
| 86 |
-
|
| 87 |
-
material = tuple(tuple(sequence) for sequence in sequences)
|
| 88 |
-
return tuple(
|
| 89 |
-
transform.apply_context(sequence)
|
| 90 |
-
for transform in transforms
|
| 91 |
-
for sequence in material
|
| 92 |
-
)
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
@dataclass
|
| 96 |
-
class VirtualAugmentedOrderStackModel:
|
| 97 |
-
"""Order-stack model with exact virtual transformed-corpus counts.
|
| 98 |
-
|
| 99 |
-
The model answers continuation queries with the same counts as an explicit
|
| 100 |
-
augmented corpus:
|
| 101 |
-
|
| 102 |
-
``C_aug(c -> y) = sum_g C_base(g^-1(c) -> g^-1(y))``.
|
| 103 |
-
"""
|
| 104 |
-
|
| 105 |
-
base_model: OrderStackModel
|
| 106 |
-
transforms: tuple[SymbolTransform, ...]
|
| 107 |
-
_counts_cache: dict[Context, Counter[Symbol]] = field(default_factory=dict, init=False)
|
| 108 |
-
_distribution_cache: dict[tuple[Context, int | None], tuple[tuple[tuple[Symbol, float], ...], int | None]] = field(
|
| 109 |
-
default_factory=dict,
|
| 110 |
-
init=False,
|
| 111 |
-
repr=False,
|
| 112 |
-
)
|
| 113 |
-
_graph_cache: dict[int, FixedOrderContextGraph] = field(default_factory=dict, init=False)
|
| 114 |
-
_contexts_cache: dict[int, tuple[Context, ...]] = field(default_factory=dict, init=False)
|
| 115 |
-
_lazy_graph_cache: dict[tuple[Context, int], dict[int, "LazyVirtualFixedOrderContextGraph"]] = field(
|
| 116 |
-
default_factory=dict,
|
| 117 |
-
init=False,
|
| 118 |
-
repr=False,
|
| 119 |
-
)
|
| 120 |
-
_lazy_plan_graph_cache: dict[int, dict[int, "LazyVirtualFixedOrderContextGraph"]] = field(
|
| 121 |
-
default_factory=dict,
|
| 122 |
-
init=False,
|
| 123 |
-
repr=False,
|
| 124 |
-
)
|
| 125 |
-
_apply_symbol_cache: dict[tuple[int, Symbol], Symbol] = field(
|
| 126 |
-
default_factory=dict,
|
| 127 |
-
init=False,
|
| 128 |
-
repr=False,
|
| 129 |
-
)
|
| 130 |
-
_inverse_symbol_cache: dict[tuple[int, Symbol], Symbol] = field(
|
| 131 |
-
default_factory=dict,
|
| 132 |
-
init=False,
|
| 133 |
-
repr=False,
|
| 134 |
-
)
|
| 135 |
-
_inverse_context_cache: dict[tuple[int, Context], Context] = field(
|
| 136 |
-
default_factory=dict,
|
| 137 |
-
init=False,
|
| 138 |
-
repr=False,
|
| 139 |
-
)
|
| 140 |
-
_context_materialization_seconds: float = field(default=0.0, init=False, repr=False)
|
| 141 |
-
_context_materialization_calls: int = field(default=0, init=False, repr=False)
|
| 142 |
-
_context_materialization_cache_hits: int = field(default=0, init=False, repr=False)
|
| 143 |
-
_context_materialization_cache_misses: int = field(default=0, init=False, repr=False)
|
| 144 |
-
_augmented_count_calls: int = field(default=0, init=False, repr=False)
|
| 145 |
-
_augmented_count_cache_hits: int = field(default=0, init=False, repr=False)
|
| 146 |
-
_augmented_count_cache_misses: int = field(default=0, init=False, repr=False)
|
| 147 |
-
|
| 148 |
-
def __post_init__(self) -> None:
|
| 149 |
-
if not self.transforms:
|
| 150 |
-
raise ValueError("at least one transform is required")
|
| 151 |
-
if any(transform.weight <= 0.0 for transform in self.transforms):
|
| 152 |
-
raise ValueError("transform weights must be positive")
|
| 153 |
-
self.max_order = self.base_model.max_order
|
| 154 |
-
self.forbidden_symbols = frozenset(
|
| 155 |
-
self._apply_symbol(transform_index, symbol)
|
| 156 |
-
for transform_index, _transform in enumerate(self.transforms)
|
| 157 |
-
for symbol in self.base_model.forbidden_symbols
|
| 158 |
-
)
|
| 159 |
-
self.alphabet = frozenset(
|
| 160 |
-
self._apply_symbol(transform_index, symbol)
|
| 161 |
-
for transform_index, _transform in enumerate(self.transforms)
|
| 162 |
-
for symbol in self.base_model.alphabet
|
| 163 |
-
)
|
| 164 |
-
|
| 165 |
-
@classmethod
|
| 166 |
-
def from_sequences(
|
| 167 |
-
cls,
|
| 168 |
-
sequences: Iterable[Sequence[Symbol]],
|
| 169 |
-
*,
|
| 170 |
-
max_order: int,
|
| 171 |
-
transforms: Iterable[SymbolTransform],
|
| 172 |
-
start_symbol: Symbol | None = None,
|
| 173 |
-
end_symbol: Symbol | None = None,
|
| 174 |
-
) -> "VirtualAugmentedOrderStackModel":
|
| 175 |
-
base_model = OrderStackModel.from_sequences(
|
| 176 |
-
sequences,
|
| 177 |
-
max_order=max_order,
|
| 178 |
-
start_symbol=start_symbol,
|
| 179 |
-
end_symbol=end_symbol,
|
| 180 |
-
)
|
| 181 |
-
return cls(base_model=base_model, transforms=tuple(transforms))
|
| 182 |
-
|
| 183 |
-
@property
|
| 184 |
-
def base_context_count(self) -> int:
|
| 185 |
-
return len(self.base_model.counts)
|
| 186 |
-
|
| 187 |
-
@property
|
| 188 |
-
def virtual_event_multiplier(self) -> int:
|
| 189 |
-
return len(self.transforms)
|
| 190 |
-
|
| 191 |
-
def virtual_context_count(self, order: int | None = None) -> int:
|
| 192 |
-
if order is None:
|
| 193 |
-
order = self.max_order
|
| 194 |
-
return sum(1 for _context in self.iter_contexts(order))
|
| 195 |
-
|
| 196 |
-
def iter_contexts(self, order: int) -> Iterable[Context]:
|
| 197 |
-
self._context_materialization_calls += 1
|
| 198 |
-
cached = self._contexts_cache.get(order)
|
| 199 |
-
if cached is not None:
|
| 200 |
-
self._context_materialization_cache_hits += 1
|
| 201 |
-
return cached
|
| 202 |
-
self._context_materialization_cache_misses += 1
|
| 203 |
-
started = time.perf_counter()
|
| 204 |
-
contexts: set[Context] = set()
|
| 205 |
-
for context in self.base_model.counts:
|
| 206 |
-
if len(context) > order:
|
| 207 |
-
continue
|
| 208 |
-
for transform_index, _transform in enumerate(self.transforms):
|
| 209 |
-
contexts.add(
|
| 210 |
-
tuple(
|
| 211 |
-
self._apply_symbol(transform_index, symbol)
|
| 212 |
-
for symbol in context
|
| 213 |
-
)
|
| 214 |
-
)
|
| 215 |
-
result = tuple(contexts)
|
| 216 |
-
self._contexts_cache[order] = result
|
| 217 |
-
self._context_materialization_seconds += time.perf_counter() - started
|
| 218 |
-
return result
|
| 219 |
-
|
| 220 |
-
def augmented_counts(self, context: Iterable[Symbol] | Context) -> Counter[Symbol]:
|
| 221 |
-
self._augmented_count_calls += 1
|
| 222 |
-
state = _as_context(context)
|
| 223 |
-
cached = self._counts_cache.get(state)
|
| 224 |
-
if cached is not None:
|
| 225 |
-
self._augmented_count_cache_hits += 1
|
| 226 |
-
return cached
|
| 227 |
-
|
| 228 |
-
self._augmented_count_cache_misses += 1
|
| 229 |
-
counts: Counter[Symbol] = Counter()
|
| 230 |
-
for transform_index, _transform in enumerate(self.transforms):
|
| 231 |
-
inverse_context = self._inverse_context(transform_index, state)
|
| 232 |
-
base_counts = self.base_model.counts.get(inverse_context)
|
| 233 |
-
if not base_counts:
|
| 234 |
-
continue
|
| 235 |
-
for base_symbol, count in base_counts.items():
|
| 236 |
-
counts[self._apply_symbol(transform_index, base_symbol)] += (
|
| 237 |
-
count * self.transforms[transform_index].weight
|
| 238 |
-
)
|
| 239 |
-
self._counts_cache[state] = counts
|
| 240 |
-
return counts
|
| 241 |
-
|
| 242 |
-
def virtual_diagnostics(self) -> dict[str, int | float]:
|
| 243 |
-
return {
|
| 244 |
-
"virtual_context_materialization_seconds": self._context_materialization_seconds,
|
| 245 |
-
"virtual_context_materialization_calls": self._context_materialization_calls,
|
| 246 |
-
"virtual_context_materialization_cache_hits": (
|
| 247 |
-
self._context_materialization_cache_hits
|
| 248 |
-
),
|
| 249 |
-
"virtual_context_materialization_cache_misses": (
|
| 250 |
-
self._context_materialization_cache_misses
|
| 251 |
-
),
|
| 252 |
-
"augmented_count_calls": self._augmented_count_calls,
|
| 253 |
-
"augmented_count_cache_hits": self._augmented_count_cache_hits,
|
| 254 |
-
"augmented_count_cache_misses": self._augmented_count_cache_misses,
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
def _apply_symbol(self, transform_index: int, symbol: Symbol) -> Symbol:
|
| 258 |
-
key = (transform_index, symbol)
|
| 259 |
-
cached = self._apply_symbol_cache.get(key)
|
| 260 |
-
if cached is not None:
|
| 261 |
-
return cached
|
| 262 |
-
transformed = self.transforms[transform_index].apply_symbol(symbol)
|
| 263 |
-
self._apply_symbol_cache[key] = transformed
|
| 264 |
-
return transformed
|
| 265 |
-
|
| 266 |
-
def _inverse_symbol(self, transform_index: int, symbol: Symbol) -> Symbol:
|
| 267 |
-
key = (transform_index, symbol)
|
| 268 |
-
cached = self._inverse_symbol_cache.get(key)
|
| 269 |
-
if cached is not None:
|
| 270 |
-
return cached
|
| 271 |
-
transformed = self.transforms[transform_index].inverse_symbol(symbol)
|
| 272 |
-
self._inverse_symbol_cache[key] = transformed
|
| 273 |
-
return transformed
|
| 274 |
-
|
| 275 |
-
def _inverse_context(self, transform_index: int, context: Sequence[Symbol]) -> Context:
|
| 276 |
-
state = _as_context(context)
|
| 277 |
-
key = (transform_index, state)
|
| 278 |
-
cached = self._inverse_context_cache.get(key)
|
| 279 |
-
if cached is not None:
|
| 280 |
-
return cached
|
| 281 |
-
transformed = tuple(self._inverse_symbol(transform_index, symbol) for symbol in state)
|
| 282 |
-
self._inverse_context_cache[key] = transformed
|
| 283 |
-
return transformed
|
| 284 |
-
|
| 285 |
-
def longest_available_suffix(
|
| 286 |
-
self,
|
| 287 |
-
context: Iterable[Symbol] | Context,
|
| 288 |
-
*,
|
| 289 |
-
max_order: int | None = None,
|
| 290 |
-
) -> Context | None:
|
| 291 |
-
state = _as_context(context)
|
| 292 |
-
order_limit = min(self.max_order if max_order is None else max_order, len(state))
|
| 293 |
-
for order in range(order_limit, 0, -1):
|
| 294 |
-
suffix = state[-order:]
|
| 295 |
-
if self.augmented_counts(suffix):
|
| 296 |
-
return suffix
|
| 297 |
-
return None
|
| 298 |
-
|
| 299 |
-
def continuation_distribution_with_order(
|
| 300 |
-
self,
|
| 301 |
-
context: Iterable[Symbol] | Context,
|
| 302 |
-
*,
|
| 303 |
-
max_order: int | None = None,
|
| 304 |
-
) -> tuple[tuple[tuple[Symbol, float], ...], int | None]:
|
| 305 |
-
state = _as_context(context)
|
| 306 |
-
cache_key = (state, max_order)
|
| 307 |
-
cached = self._distribution_cache.get(cache_key)
|
| 308 |
-
if cached is not None:
|
| 309 |
-
return cached
|
| 310 |
-
|
| 311 |
-
suffix = self.longest_available_suffix(state, max_order=max_order)
|
| 312 |
-
if suffix is None:
|
| 313 |
-
result = ((), None)
|
| 314 |
-
self._distribution_cache[cache_key] = result
|
| 315 |
-
return result
|
| 316 |
-
|
| 317 |
-
counts = self.augmented_counts(suffix)
|
| 318 |
-
total = float(sum(counts.values()))
|
| 319 |
-
if total <= 0.0:
|
| 320 |
-
result = ((), None)
|
| 321 |
-
else:
|
| 322 |
-
result = (
|
| 323 |
-
tuple((symbol, float(count) / total) for symbol, count in counts.items()),
|
| 324 |
-
len(suffix),
|
| 325 |
-
)
|
| 326 |
-
self._distribution_cache[cache_key] = result
|
| 327 |
-
return result
|
| 328 |
-
|
| 329 |
-
def compile_graph(self, order: int) -> FixedOrderContextGraph:
|
| 330 |
-
cached = self._graph_cache.get(order)
|
| 331 |
-
if cached is not None:
|
| 332 |
-
return cached
|
| 333 |
-
graph = FixedOrderContextGraph.from_model(self, order=order)
|
| 334 |
-
self._graph_cache[order] = graph
|
| 335 |
-
return graph
|
| 336 |
-
|
| 337 |
-
def compile_graphs_for_plan(
|
| 338 |
-
self,
|
| 339 |
-
*,
|
| 340 |
-
length: int,
|
| 341 |
-
) -> dict[int, "LazyVirtualFixedOrderContextGraph"]:
|
| 342 |
-
if length < 0:
|
| 343 |
-
raise ValueError("length must be non-negative")
|
| 344 |
-
cache_key = int(length)
|
| 345 |
-
cached = self._lazy_plan_graph_cache.get(cache_key)
|
| 346 |
-
if cached is not None:
|
| 347 |
-
return cached
|
| 348 |
-
|
| 349 |
-
graphs = self._new_lazy_graphs()
|
| 350 |
-
self._lazy_plan_graph_cache[cache_key] = graphs
|
| 351 |
-
return graphs
|
| 352 |
-
|
| 353 |
-
def compile_graphs_for_prefix(
|
| 354 |
-
self,
|
| 355 |
-
*,
|
| 356 |
-
prefix: Sequence[Symbol],
|
| 357 |
-
length: int,
|
| 358 |
-
) -> dict[int, "LazyVirtualFixedOrderContextGraph"]:
|
| 359 |
-
if length < 0:
|
| 360 |
-
raise ValueError("length must be non-negative")
|
| 361 |
-
prefix_context = tuple(prefix)
|
| 362 |
-
cache_key = (prefix_context, int(length))
|
| 363 |
-
cached = self._lazy_graph_cache.get(cache_key)
|
| 364 |
-
if cached is not None:
|
| 365 |
-
return cached
|
| 366 |
-
|
| 367 |
-
graphs = self._new_lazy_graphs()
|
| 368 |
-
self._lazy_graph_cache[cache_key] = graphs
|
| 369 |
-
return graphs
|
| 370 |
-
|
| 371 |
-
def _new_lazy_graphs(self) -> dict[int, "LazyVirtualFixedOrderContextGraph"]:
|
| 372 |
-
return {
|
| 373 |
-
order: LazyVirtualFixedOrderContextGraph(
|
| 374 |
-
self,
|
| 375 |
-
order=order,
|
| 376 |
-
)
|
| 377 |
-
for order in range(1, self.max_order + 1)
|
| 378 |
-
}
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
class _LazyOutgoing:
|
| 382 |
-
def __init__(self, graph: "LazyVirtualFixedOrderContextGraph") -> None:
|
| 383 |
-
self.graph = graph
|
| 384 |
-
|
| 385 |
-
def __getitem__(self, state: int) -> list[StackEdge]:
|
| 386 |
-
return self.graph.outgoing_for_state(state)
|
| 387 |
-
|
| 388 |
-
def __iter__(self) -> Iterator[list[StackEdge]]:
|
| 389 |
-
for state in range(len(self.graph.contexts)):
|
| 390 |
-
yield self.graph.outgoing_for_state(state)
|
| 391 |
-
|
| 392 |
-
def __len__(self) -> int:
|
| 393 |
-
return len(self.graph.contexts)
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
class LazyVirtualFixedOrderContextGraph:
|
| 397 |
-
"""Lazily materialized fixed-order graph for virtual augmentation."""
|
| 398 |
-
|
| 399 |
-
def __init__(
|
| 400 |
-
self,
|
| 401 |
-
model: VirtualAugmentedOrderStackModel,
|
| 402 |
-
*,
|
| 403 |
-
order: int,
|
| 404 |
-
allowed_contexts: Iterable[Context] | None = None,
|
| 405 |
-
) -> None:
|
| 406 |
-
if order < 1 or order > model.max_order:
|
| 407 |
-
raise ValueError(f"order must be between 1 and {model.max_order}")
|
| 408 |
-
self.model = model
|
| 409 |
-
self.order = int(order)
|
| 410 |
-
self._allowed_contexts_source = (
|
| 411 |
-
None if allowed_contexts is None else tuple(allowed_contexts)
|
| 412 |
-
)
|
| 413 |
-
self._allowed_contexts: set[Context] | None = None
|
| 414 |
-
self.contexts: list[Context] = []
|
| 415 |
-
self.context_to_id: dict[Context, int] = {}
|
| 416 |
-
self._outgoing_cache: dict[int, list[StackEdge]] = {}
|
| 417 |
-
self.outgoing = _LazyOutgoing(self)
|
| 418 |
-
self.outgoing_row_calls = 0
|
| 419 |
-
self.outgoing_row_cache_hits = 0
|
| 420 |
-
self.outgoing_row_cache_misses = 0
|
| 421 |
-
self.outgoing_row_materialization_seconds = 0.0
|
| 422 |
-
|
| 423 |
-
@property
|
| 424 |
-
def allowed_contexts(self) -> set[Context]:
|
| 425 |
-
return self._ensure_allowed_contexts()
|
| 426 |
-
|
| 427 |
-
def truncate_context(self, context: Iterable[Symbol] | Context) -> Context:
|
| 428 |
-
state = _as_context(context)
|
| 429 |
-
if len(state) <= self.order:
|
| 430 |
-
return state
|
| 431 |
-
return state[-self.order:]
|
| 432 |
-
|
| 433 |
-
def next_context(self, context: Iterable[Symbol] | Context, symbol: Symbol) -> Context:
|
| 434 |
-
return self.truncate_context(_as_context(context) + (symbol,))
|
| 435 |
-
|
| 436 |
-
def state_id(self, context: Iterable[Symbol] | Context) -> int | None:
|
| 437 |
-
state = self.truncate_context(context)
|
| 438 |
-
if state not in self._ensure_allowed_contexts():
|
| 439 |
-
return None
|
| 440 |
-
return self._add_context(state)
|
| 441 |
-
|
| 442 |
-
@property
|
| 443 |
-
def edge_count(self) -> int:
|
| 444 |
-
return sum(len(edges) for edges in self.outgoing)
|
| 445 |
-
|
| 446 |
-
def outgoing_for_state(self, state: int) -> list[StackEdge]:
|
| 447 |
-
self.outgoing_row_calls += 1
|
| 448 |
-
cached = self._outgoing_cache.get(state)
|
| 449 |
-
if cached is not None:
|
| 450 |
-
self.outgoing_row_cache_hits += 1
|
| 451 |
-
return cached
|
| 452 |
-
self.outgoing_row_cache_misses += 1
|
| 453 |
-
started = time.perf_counter()
|
| 454 |
-
context = self.contexts[state]
|
| 455 |
-
distribution, effective_order = self.model.continuation_distribution_with_order(
|
| 456 |
-
context,
|
| 457 |
-
max_order=self.order,
|
| 458 |
-
)
|
| 459 |
-
edges: list[StackEdge] = []
|
| 460 |
-
order = self.order
|
| 461 |
-
allowed_contexts = self._ensure_allowed_contexts()
|
| 462 |
-
for symbol, probability in distribution:
|
| 463 |
-
candidate_context = context + (symbol,)
|
| 464 |
-
dst_context = (
|
| 465 |
-
candidate_context
|
| 466 |
-
if len(candidate_context) <= order
|
| 467 |
-
else candidate_context[-order:]
|
| 468 |
-
)
|
| 469 |
-
if dst_context not in allowed_contexts:
|
| 470 |
-
continue
|
| 471 |
-
dst = self._add_context(dst_context)
|
| 472 |
-
edges.append(
|
| 473 |
-
StackEdge(
|
| 474 |
-
src=state,
|
| 475 |
-
dst=dst,
|
| 476 |
-
symbol=symbol,
|
| 477 |
-
probability=float(probability),
|
| 478 |
-
order=effective_order or 0,
|
| 479 |
-
)
|
| 480 |
-
)
|
| 481 |
-
self._outgoing_cache[state] = edges
|
| 482 |
-
self.outgoing_row_materialization_seconds += time.perf_counter() - started
|
| 483 |
-
return edges
|
| 484 |
-
|
| 485 |
-
def _ensure_allowed_contexts(self) -> set[Context]:
|
| 486 |
-
allowed_contexts = self._allowed_contexts
|
| 487 |
-
if allowed_contexts is not None:
|
| 488 |
-
return allowed_contexts
|
| 489 |
-
source = (
|
| 490 |
-
self.model.iter_contexts(self.order)
|
| 491 |
-
if self._allowed_contexts_source is None
|
| 492 |
-
else self._allowed_contexts_source
|
| 493 |
-
)
|
| 494 |
-
allowed_contexts = {self.truncate_context(context) for context in source}
|
| 495 |
-
self._allowed_contexts = allowed_contexts
|
| 496 |
-
return allowed_contexts
|
| 497 |
-
|
| 498 |
-
def _add_context(self, context: Context) -> int:
|
| 499 |
-
found = self.context_to_id.get(context)
|
| 500 |
-
if found is not None:
|
| 501 |
-
return found
|
| 502 |
-
context_id = len(self.contexts)
|
| 503 |
-
self.context_to_id[context] = context_id
|
| 504 |
-
self.contexts.append(context)
|
| 505 |
-
return context_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/backend.py
DELETED
|
@@ -1,1066 +0,0 @@
|
|
| 1 |
-
"""Stable public backend entry points.
|
| 2 |
-
|
| 3 |
-
This module is the library-facing layer. It compiles generic constraint
|
| 4 |
-
specifications and delegates to the current BP engines. Future optimized
|
| 5 |
-
engines should be wired behind these functions without changing the public
|
| 6 |
-
surface.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
from __future__ import annotations
|
| 10 |
-
|
| 11 |
-
import bisect
|
| 12 |
-
from collections.abc import Callable, Hashable, Iterable, Sequence
|
| 13 |
-
from dataclasses import dataclass
|
| 14 |
-
import random
|
| 15 |
-
|
| 16 |
-
from .acceptors import DFA
|
| 17 |
-
from .constraint_builders import combine_constraints
|
| 18 |
-
from .constraints import ConstraintSet, compile_constraints
|
| 19 |
-
from .context import Symbol
|
| 20 |
-
from .order_stack_bp import (
|
| 21 |
-
OrderPolicy,
|
| 22 |
-
OrderSampleStep,
|
| 23 |
-
OrderStackBPPlan,
|
| 24 |
-
OrderStackModel,
|
| 25 |
-
RegularOrderStackBPResult,
|
| 26 |
-
RegularOrderStackBPPlan,
|
| 27 |
-
OrderStackBPResult,
|
| 28 |
-
prepare_order_stack_bp,
|
| 29 |
-
prepare_order_stack_masked_dfa_bp,
|
| 30 |
-
run_order_stack_bp,
|
| 31 |
-
run_order_stack_masked_dfa_bp,
|
| 32 |
-
)
|
| 33 |
-
from .positional_bp import AllowedForbiddenSymbols
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
@dataclass(frozen=True)
|
| 37 |
-
class GeneratedSequence:
|
| 38 |
-
"""A generated sequence plus optional order diagnostics."""
|
| 39 |
-
|
| 40 |
-
sequence: tuple[Symbol, ...]
|
| 41 |
-
orders: tuple[int, ...] = ()
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
@dataclass(frozen=True)
|
| 45 |
-
class BackendDiagnostics:
|
| 46 |
-
"""Runtime-independent diagnostics exposed by prepared backends."""
|
| 47 |
-
|
| 48 |
-
backend: str
|
| 49 |
-
length: int
|
| 50 |
-
max_order: int
|
| 51 |
-
context_states: int
|
| 52 |
-
context_edges: int
|
| 53 |
-
regular_product_states: int | None = None
|
| 54 |
-
regular_product_states_time_indexed: int | None = None
|
| 55 |
-
regular_product_edges: int | None = None
|
| 56 |
-
regular_transition_rows: int | None = None
|
| 57 |
-
regular_transition_row_cache_hits: int | None = None
|
| 58 |
-
regular_transition_row_cache_misses: int | None = None
|
| 59 |
-
regular_accepted_transitions: int | None = None
|
| 60 |
-
regular_beta_state_expansions: int | None = None
|
| 61 |
-
regular_beta_cache_hits: int | None = None
|
| 62 |
-
regular_beta_cache_misses: int | None = None
|
| 63 |
-
regular_acceptor_symbol_transition_cache_hits: int | None = None
|
| 64 |
-
regular_acceptor_symbol_transition_cache_misses: int | None = None
|
| 65 |
-
duration_view_quotient_states: int | None = None
|
| 66 |
-
duration_view_quotient_classes: int | None = None
|
| 67 |
-
duration_view_quotient_state_reduction: float | None = None
|
| 68 |
-
duration_view_quotient_edges: int | None = None
|
| 69 |
-
duration_view_quotient_projected_edges: int | None = None
|
| 70 |
-
duration_view_quotient_ignored_edges: int | None = None
|
| 71 |
-
duration_view_quotient_quotient_edges: int | None = None
|
| 72 |
-
duration_view_quotient_projected_edge_reduction: float | None = None
|
| 73 |
-
duration_view_quotient_max_class_size: int | None = None
|
| 74 |
-
duration_view_quotient_refinement_rounds: int | None = None
|
| 75 |
-
duration_view_quotient_seconds: float | None = None
|
| 76 |
-
duration_view_quotient_order_stats: tuple[dict[str, object], ...] = ()
|
| 77 |
-
virtual_context_materialization_seconds: float | None = None
|
| 78 |
-
virtual_context_materialization_calls: int | None = None
|
| 79 |
-
virtual_context_materialization_cache_hits: int | None = None
|
| 80 |
-
virtual_context_materialization_cache_misses: int | None = None
|
| 81 |
-
augmented_count_calls: int | None = None
|
| 82 |
-
augmented_count_cache_hits: int | None = None
|
| 83 |
-
augmented_count_cache_misses: int | None = None
|
| 84 |
-
virtual_outgoing_row_calls: int | None = None
|
| 85 |
-
virtual_outgoing_row_cache_hits: int | None = None
|
| 86 |
-
virtual_outgoing_row_cache_misses: int | None = None
|
| 87 |
-
success_mass: float | None = None
|
| 88 |
-
start_order_masses: tuple[tuple[int, float], ...] = ()
|
| 89 |
-
|
| 90 |
-
def as_dict(self) -> dict[str, object]:
|
| 91 |
-
return {
|
| 92 |
-
"backend": self.backend,
|
| 93 |
-
"length": self.length,
|
| 94 |
-
"max_order": self.max_order,
|
| 95 |
-
"context_states": self.context_states,
|
| 96 |
-
"context_edges": self.context_edges,
|
| 97 |
-
"regular_product_states": self.regular_product_states,
|
| 98 |
-
"regular_product_states_time_indexed": self.regular_product_states_time_indexed,
|
| 99 |
-
"regular_product_edges": self.regular_product_edges,
|
| 100 |
-
"regular_transition_rows": self.regular_transition_rows,
|
| 101 |
-
"regular_transition_row_cache_hits": self.regular_transition_row_cache_hits,
|
| 102 |
-
"regular_transition_row_cache_misses": self.regular_transition_row_cache_misses,
|
| 103 |
-
"regular_accepted_transitions": self.regular_accepted_transitions,
|
| 104 |
-
"regular_beta_state_expansions": self.regular_beta_state_expansions,
|
| 105 |
-
"regular_beta_cache_hits": self.regular_beta_cache_hits,
|
| 106 |
-
"regular_beta_cache_misses": self.regular_beta_cache_misses,
|
| 107 |
-
"regular_acceptor_symbol_transition_cache_hits": (
|
| 108 |
-
self.regular_acceptor_symbol_transition_cache_hits
|
| 109 |
-
),
|
| 110 |
-
"regular_acceptor_symbol_transition_cache_misses": (
|
| 111 |
-
self.regular_acceptor_symbol_transition_cache_misses
|
| 112 |
-
),
|
| 113 |
-
"duration_view_quotient_states": self.duration_view_quotient_states,
|
| 114 |
-
"duration_view_quotient_classes": self.duration_view_quotient_classes,
|
| 115 |
-
"duration_view_quotient_state_reduction": (
|
| 116 |
-
self.duration_view_quotient_state_reduction
|
| 117 |
-
),
|
| 118 |
-
"duration_view_quotient_edges": self.duration_view_quotient_edges,
|
| 119 |
-
"duration_view_quotient_projected_edges": (
|
| 120 |
-
self.duration_view_quotient_projected_edges
|
| 121 |
-
),
|
| 122 |
-
"duration_view_quotient_ignored_edges": (
|
| 123 |
-
self.duration_view_quotient_ignored_edges
|
| 124 |
-
),
|
| 125 |
-
"duration_view_quotient_quotient_edges": (
|
| 126 |
-
self.duration_view_quotient_quotient_edges
|
| 127 |
-
),
|
| 128 |
-
"duration_view_quotient_projected_edge_reduction": (
|
| 129 |
-
self.duration_view_quotient_projected_edge_reduction
|
| 130 |
-
),
|
| 131 |
-
"duration_view_quotient_max_class_size": (
|
| 132 |
-
self.duration_view_quotient_max_class_size
|
| 133 |
-
),
|
| 134 |
-
"duration_view_quotient_refinement_rounds": (
|
| 135 |
-
self.duration_view_quotient_refinement_rounds
|
| 136 |
-
),
|
| 137 |
-
"duration_view_quotient_seconds": self.duration_view_quotient_seconds,
|
| 138 |
-
"duration_view_quotient_order_stats": (
|
| 139 |
-
self.duration_view_quotient_order_stats
|
| 140 |
-
),
|
| 141 |
-
"virtual_context_materialization_seconds": (
|
| 142 |
-
self.virtual_context_materialization_seconds
|
| 143 |
-
),
|
| 144 |
-
"virtual_context_materialization_calls": self.virtual_context_materialization_calls,
|
| 145 |
-
"virtual_context_materialization_cache_hits": (
|
| 146 |
-
self.virtual_context_materialization_cache_hits
|
| 147 |
-
),
|
| 148 |
-
"virtual_context_materialization_cache_misses": (
|
| 149 |
-
self.virtual_context_materialization_cache_misses
|
| 150 |
-
),
|
| 151 |
-
"augmented_count_calls": self.augmented_count_calls,
|
| 152 |
-
"augmented_count_cache_hits": self.augmented_count_cache_hits,
|
| 153 |
-
"augmented_count_cache_misses": self.augmented_count_cache_misses,
|
| 154 |
-
"virtual_outgoing_row_calls": self.virtual_outgoing_row_calls,
|
| 155 |
-
"virtual_outgoing_row_cache_hits": self.virtual_outgoing_row_cache_hits,
|
| 156 |
-
"virtual_outgoing_row_cache_misses": self.virtual_outgoing_row_cache_misses,
|
| 157 |
-
"success_mass": self.success_mass,
|
| 158 |
-
"start_order_masses": self.start_order_masses,
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
@dataclass(frozen=True)
|
| 163 |
-
class UntilLengthDiagnostics:
|
| 164 |
-
"""Diagnostics for one feasible first-hit continuation length."""
|
| 165 |
-
|
| 166 |
-
length: int
|
| 167 |
-
weight: float
|
| 168 |
-
backend: BackendDiagnostics
|
| 169 |
-
|
| 170 |
-
def as_dict(self) -> dict[str, object]:
|
| 171 |
-
return {
|
| 172 |
-
"length": self.length,
|
| 173 |
-
"weight": self.weight,
|
| 174 |
-
"backend": self.backend.as_dict(),
|
| 175 |
-
}
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
@dataclass(frozen=True)
|
| 179 |
-
class UntilOrderStackDiagnostics:
|
| 180 |
-
"""Diagnostics exposed by variable-length first-hit backends."""
|
| 181 |
-
|
| 182 |
-
backend: str
|
| 183 |
-
min_length: int
|
| 184 |
-
max_length: int
|
| 185 |
-
feasible_lengths: tuple[int, ...]
|
| 186 |
-
length_weights: tuple[tuple[int, float], ...]
|
| 187 |
-
length_diagnostics: tuple[UntilLengthDiagnostics, ...]
|
| 188 |
-
|
| 189 |
-
def as_dict(self) -> dict[str, object]:
|
| 190 |
-
return {
|
| 191 |
-
"backend": self.backend,
|
| 192 |
-
"min_length": self.min_length,
|
| 193 |
-
"max_length": self.max_length,
|
| 194 |
-
"feasible_lengths": self.feasible_lengths,
|
| 195 |
-
"length_weights": self.length_weights,
|
| 196 |
-
"length_diagnostics": tuple(
|
| 197 |
-
diagnostic.as_dict() for diagnostic in self.length_diagnostics
|
| 198 |
-
),
|
| 199 |
-
}
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
def _virtual_model_diagnostics(model: object) -> dict[str, object]:
|
| 203 |
-
diagnostics = getattr(model, "virtual_diagnostics", None)
|
| 204 |
-
if callable(diagnostics):
|
| 205 |
-
return dict(diagnostics())
|
| 206 |
-
return {}
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
def _virtual_graph_outgoing_diagnostics(
|
| 210 |
-
graphs: object,
|
| 211 |
-
) -> dict[str, int]:
|
| 212 |
-
values = tuple(graphs.values()) if isinstance(graphs, dict) else ()
|
| 213 |
-
return {
|
| 214 |
-
"virtual_outgoing_row_calls": sum(
|
| 215 |
-
int(getattr(graph, "outgoing_row_calls", 0)) for graph in values
|
| 216 |
-
),
|
| 217 |
-
"virtual_outgoing_row_cache_hits": sum(
|
| 218 |
-
int(getattr(graph, "outgoing_row_cache_hits", 0))
|
| 219 |
-
for graph in values
|
| 220 |
-
),
|
| 221 |
-
"virtual_outgoing_row_cache_misses": sum(
|
| 222 |
-
int(getattr(graph, "outgoing_row_cache_misses", 0))
|
| 223 |
-
for graph in values
|
| 224 |
-
),
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
@dataclass(frozen=True)
|
| 229 |
-
class ConstrainedOrderStackBackend:
|
| 230 |
-
"""Prepared reusable constrained order-stack sampler.
|
| 231 |
-
|
| 232 |
-
External projects can keep this object around after compilation/BP and call
|
| 233 |
-
the sampling methods repeatedly. The concrete result remains available for
|
| 234 |
-
advanced inspection, but regular users should prefer this stable wrapper.
|
| 235 |
-
"""
|
| 236 |
-
|
| 237 |
-
result: OrderStackBPResult | RegularOrderStackBPResult
|
| 238 |
-
|
| 239 |
-
@property
|
| 240 |
-
def diagnostics(self) -> BackendDiagnostics:
|
| 241 |
-
is_regular = isinstance(self.result, RegularOrderStackBPResult)
|
| 242 |
-
virtual_diagnostics = _virtual_model_diagnostics(self.result.model)
|
| 243 |
-
virtual_outgoing_diagnostics = _virtual_graph_outgoing_diagnostics(
|
| 244 |
-
self.result.graphs,
|
| 245 |
-
)
|
| 246 |
-
duration_view = (
|
| 247 |
-
self.result.duration_view_quotient_diagnostics
|
| 248 |
-
if is_regular
|
| 249 |
-
else None
|
| 250 |
-
)
|
| 251 |
-
return BackendDiagnostics(
|
| 252 |
-
backend="order_stack_regular" if is_regular else "order_stack_positional",
|
| 253 |
-
length=self.result.length,
|
| 254 |
-
max_order=self.result.model.max_order,
|
| 255 |
-
context_states=self.result.context_state_count,
|
| 256 |
-
context_edges=self.result.context_edge_count,
|
| 257 |
-
regular_product_states=self.result.product_state_count if is_regular else None,
|
| 258 |
-
regular_product_states_time_indexed=(
|
| 259 |
-
self.result.time_indexed_product_state_count if is_regular else None
|
| 260 |
-
),
|
| 261 |
-
regular_product_edges=self.result.product_edge_count if is_regular else None,
|
| 262 |
-
regular_transition_rows=(
|
| 263 |
-
self.result.regular_transition_row_count if is_regular else None
|
| 264 |
-
),
|
| 265 |
-
regular_transition_row_cache_hits=(
|
| 266 |
-
self.result.regular_transition_row_cache_hits if is_regular else None
|
| 267 |
-
),
|
| 268 |
-
regular_transition_row_cache_misses=(
|
| 269 |
-
self.result.regular_transition_row_cache_misses if is_regular else None
|
| 270 |
-
),
|
| 271 |
-
regular_accepted_transitions=(
|
| 272 |
-
self.result.regular_accepted_transition_count if is_regular else None
|
| 273 |
-
),
|
| 274 |
-
regular_beta_state_expansions=(
|
| 275 |
-
self.result.regular_beta_state_expansions if is_regular else None
|
| 276 |
-
),
|
| 277 |
-
regular_beta_cache_hits=(
|
| 278 |
-
self.result.regular_beta_cache_hits if is_regular else None
|
| 279 |
-
),
|
| 280 |
-
regular_beta_cache_misses=(
|
| 281 |
-
self.result.regular_beta_cache_misses if is_regular else None
|
| 282 |
-
),
|
| 283 |
-
regular_acceptor_symbol_transition_cache_hits=(
|
| 284 |
-
self.result.regular_acceptor_symbol_transition_cache_hits
|
| 285 |
-
if is_regular
|
| 286 |
-
else None
|
| 287 |
-
),
|
| 288 |
-
regular_acceptor_symbol_transition_cache_misses=(
|
| 289 |
-
self.result.regular_acceptor_symbol_transition_cache_misses
|
| 290 |
-
if is_regular
|
| 291 |
-
else None
|
| 292 |
-
),
|
| 293 |
-
duration_view_quotient_states=(
|
| 294 |
-
duration_view.states if duration_view is not None else None
|
| 295 |
-
),
|
| 296 |
-
duration_view_quotient_classes=(
|
| 297 |
-
duration_view.classes if duration_view is not None else None
|
| 298 |
-
),
|
| 299 |
-
duration_view_quotient_state_reduction=(
|
| 300 |
-
duration_view.state_reduction if duration_view is not None else None
|
| 301 |
-
),
|
| 302 |
-
duration_view_quotient_edges=(
|
| 303 |
-
duration_view.edges if duration_view is not None else None
|
| 304 |
-
),
|
| 305 |
-
duration_view_quotient_projected_edges=(
|
| 306 |
-
duration_view.projected_edges if duration_view is not None else None
|
| 307 |
-
),
|
| 308 |
-
duration_view_quotient_ignored_edges=(
|
| 309 |
-
duration_view.ignored_edges if duration_view is not None else None
|
| 310 |
-
),
|
| 311 |
-
duration_view_quotient_quotient_edges=(
|
| 312 |
-
duration_view.quotient_edges if duration_view is not None else None
|
| 313 |
-
),
|
| 314 |
-
duration_view_quotient_projected_edge_reduction=(
|
| 315 |
-
duration_view.projected_edge_reduction
|
| 316 |
-
if duration_view is not None
|
| 317 |
-
else None
|
| 318 |
-
),
|
| 319 |
-
duration_view_quotient_max_class_size=(
|
| 320 |
-
duration_view.max_class_size if duration_view is not None else None
|
| 321 |
-
),
|
| 322 |
-
duration_view_quotient_refinement_rounds=(
|
| 323 |
-
duration_view.refinement_rounds if duration_view is not None else None
|
| 324 |
-
),
|
| 325 |
-
duration_view_quotient_seconds=(
|
| 326 |
-
duration_view.seconds if duration_view is not None else None
|
| 327 |
-
),
|
| 328 |
-
duration_view_quotient_order_stats=(
|
| 329 |
-
tuple(
|
| 330 |
-
stats.as_dict()
|
| 331 |
-
for stats in duration_view.orders
|
| 332 |
-
)
|
| 333 |
-
if duration_view is not None
|
| 334 |
-
else ()
|
| 335 |
-
),
|
| 336 |
-
virtual_context_materialization_seconds=virtual_diagnostics.get(
|
| 337 |
-
"virtual_context_materialization_seconds",
|
| 338 |
-
),
|
| 339 |
-
virtual_context_materialization_calls=virtual_diagnostics.get(
|
| 340 |
-
"virtual_context_materialization_calls",
|
| 341 |
-
),
|
| 342 |
-
virtual_context_materialization_cache_hits=virtual_diagnostics.get(
|
| 343 |
-
"virtual_context_materialization_cache_hits",
|
| 344 |
-
),
|
| 345 |
-
virtual_context_materialization_cache_misses=virtual_diagnostics.get(
|
| 346 |
-
"virtual_context_materialization_cache_misses",
|
| 347 |
-
),
|
| 348 |
-
augmented_count_calls=virtual_diagnostics.get("augmented_count_calls"),
|
| 349 |
-
augmented_count_cache_hits=virtual_diagnostics.get("augmented_count_cache_hits"),
|
| 350 |
-
augmented_count_cache_misses=virtual_diagnostics.get(
|
| 351 |
-
"augmented_count_cache_misses",
|
| 352 |
-
),
|
| 353 |
-
virtual_outgoing_row_calls=virtual_outgoing_diagnostics[
|
| 354 |
-
"virtual_outgoing_row_calls"
|
| 355 |
-
],
|
| 356 |
-
virtual_outgoing_row_cache_hits=virtual_outgoing_diagnostics[
|
| 357 |
-
"virtual_outgoing_row_cache_hits"
|
| 358 |
-
],
|
| 359 |
-
virtual_outgoing_row_cache_misses=virtual_outgoing_diagnostics[
|
| 360 |
-
"virtual_outgoing_row_cache_misses"
|
| 361 |
-
],
|
| 362 |
-
success_mass=self.result.success_mass,
|
| 363 |
-
start_order_masses=self.result.start_order_masses(),
|
| 364 |
-
)
|
| 365 |
-
|
| 366 |
-
def sample(self, *, rng: random.Random | int | None = None) -> tuple[Symbol, ...]:
|
| 367 |
-
return self.result.sample(rng=rng)
|
| 368 |
-
|
| 369 |
-
def sample_many(
|
| 370 |
-
self,
|
| 371 |
-
count: int,
|
| 372 |
-
*,
|
| 373 |
-
rng: random.Random | int | None = None,
|
| 374 |
-
) -> list[tuple[Symbol, ...]]:
|
| 375 |
-
return [
|
| 376 |
-
sequence
|
| 377 |
-
for sequence, _orders in self.result.sample_many_with_orders(count, rng=rng)
|
| 378 |
-
]
|
| 379 |
-
|
| 380 |
-
def sample_with_orders(self, *, rng: random.Random | int | None = None) -> GeneratedSequence:
|
| 381 |
-
sequence, orders = self.result.sample_with_orders(rng=rng)
|
| 382 |
-
return GeneratedSequence(sequence=sequence, orders=orders)
|
| 383 |
-
|
| 384 |
-
def sample_many_with_orders(
|
| 385 |
-
self,
|
| 386 |
-
count: int,
|
| 387 |
-
*,
|
| 388 |
-
rng: random.Random | int | None = None,
|
| 389 |
-
) -> list[GeneratedSequence]:
|
| 390 |
-
return [
|
| 391 |
-
GeneratedSequence(sequence=sequence, orders=orders)
|
| 392 |
-
for sequence, orders in self.result.sample_many_with_orders(count, rng=rng)
|
| 393 |
-
]
|
| 394 |
-
|
| 395 |
-
def sample_with_trace(
|
| 396 |
-
self,
|
| 397 |
-
*,
|
| 398 |
-
rng: random.Random | int | None = None,
|
| 399 |
-
) -> tuple[tuple[Symbol, ...], tuple[OrderSampleStep, ...]]:
|
| 400 |
-
return self.result.sample_with_trace(rng=rng)
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
@dataclass(frozen=True)
|
| 404 |
-
class ConstrainedOrderStackPlan:
|
| 405 |
-
"""Prefix-independent constrained order-stack preparation.
|
| 406 |
-
|
| 407 |
-
Keep this object around when the model, horizon, and constraints are fixed
|
| 408 |
-
but callers need to sample from many different prefixes. ``for_prefix`` is
|
| 409 |
-
cheap and returns the existing prefix-bound backend wrapper.
|
| 410 |
-
"""
|
| 411 |
-
|
| 412 |
-
plan: OrderStackBPPlan | RegularOrderStackBPPlan
|
| 413 |
-
|
| 414 |
-
@property
|
| 415 |
-
def length(self) -> int:
|
| 416 |
-
return self.plan.length
|
| 417 |
-
|
| 418 |
-
@property
|
| 419 |
-
def is_regular(self) -> bool:
|
| 420 |
-
return isinstance(self.plan, RegularOrderStackBPPlan)
|
| 421 |
-
|
| 422 |
-
def for_prefix(self, prefix: Sequence[Symbol]) -> ConstrainedOrderStackBackend:
|
| 423 |
-
return ConstrainedOrderStackBackend(self.plan.for_prefix(prefix))
|
| 424 |
-
|
| 425 |
-
def sample(
|
| 426 |
-
self,
|
| 427 |
-
*,
|
| 428 |
-
prefix: Sequence[Symbol],
|
| 429 |
-
rng: random.Random | int | None = None,
|
| 430 |
-
) -> tuple[Symbol, ...]:
|
| 431 |
-
return self.for_prefix(prefix).sample(rng=rng)
|
| 432 |
-
|
| 433 |
-
def sample_with_orders(
|
| 434 |
-
self,
|
| 435 |
-
*,
|
| 436 |
-
prefix: Sequence[Symbol],
|
| 437 |
-
rng: random.Random | int | None = None,
|
| 438 |
-
) -> GeneratedSequence:
|
| 439 |
-
return self.for_prefix(prefix).sample_with_orders(rng=rng)
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
@dataclass(frozen=True)
|
| 443 |
-
class ConstrainedOrderStackSupportPlan:
|
| 444 |
-
"""Reusable hard-support plan for dynamic soft regular acceptors.
|
| 445 |
-
|
| 446 |
-
The wrapped hard plan owns the compiled source/order-stack graphs and stable
|
| 447 |
-
constraints. ``with_soft_acceptor`` creates a prefix-bound backend with fresh
|
| 448 |
-
regular caches so changing soft weights or topology cannot reuse stale beta
|
| 449 |
-
values.
|
| 450 |
-
"""
|
| 451 |
-
|
| 452 |
-
plan: ConstrainedOrderStackPlan
|
| 453 |
-
|
| 454 |
-
@property
|
| 455 |
-
def length(self) -> int:
|
| 456 |
-
return self.plan.length
|
| 457 |
-
|
| 458 |
-
@property
|
| 459 |
-
def is_regular(self) -> bool:
|
| 460 |
-
return self.plan.is_regular
|
| 461 |
-
|
| 462 |
-
def for_prefix(self, prefix: Sequence[Symbol]) -> ConstrainedOrderStackBackend:
|
| 463 |
-
"""Bind the hard-only support plan to a prefix."""
|
| 464 |
-
|
| 465 |
-
return self.plan.for_prefix(prefix)
|
| 466 |
-
|
| 467 |
-
def with_soft_acceptor(
|
| 468 |
-
self,
|
| 469 |
-
soft_acceptor: DFA | None,
|
| 470 |
-
*,
|
| 471 |
-
prefix: Sequence[Symbol],
|
| 472 |
-
soft_start_acceptor_state: Hashable | None = None,
|
| 473 |
-
) -> ConstrainedOrderStackBackend:
|
| 474 |
-
"""Bind a dynamic soft acceptor on top of the cached hard support.
|
| 475 |
-
|
| 476 |
-
Passing ``None`` samples from the hard support only. For a real soft
|
| 477 |
-
acceptor, the returned backend has new regular transition-row and beta
|
| 478 |
-
caches but reuses the source/order-stack graphs and stable masks.
|
| 479 |
-
"""
|
| 480 |
-
|
| 481 |
-
if soft_acceptor is None:
|
| 482 |
-
return self.for_prefix(prefix)
|
| 483 |
-
|
| 484 |
-
lower_plan = self.plan.plan
|
| 485 |
-
soft0 = (
|
| 486 |
-
soft_acceptor.start_state
|
| 487 |
-
if soft_start_acceptor_state is None
|
| 488 |
-
else soft_start_acceptor_state
|
| 489 |
-
)
|
| 490 |
-
if isinstance(lower_plan, RegularOrderStackBPPlan):
|
| 491 |
-
soft_plan = lower_plan.with_soft_acceptor(
|
| 492 |
-
soft_acceptor,
|
| 493 |
-
start_acceptor_state=(lower_plan.start_acceptor_state, soft0),
|
| 494 |
-
)
|
| 495 |
-
else:
|
| 496 |
-
soft_plan = lower_plan.with_regular_acceptor(
|
| 497 |
-
soft_acceptor,
|
| 498 |
-
start_acceptor_state=soft0,
|
| 499 |
-
)
|
| 500 |
-
return ConstrainedOrderStackBackend(soft_plan.for_prefix(prefix))
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
@dataclass(frozen=True)
|
| 504 |
-
class UntilOrderStackBackend:
|
| 505 |
-
"""Prepared reusable first-hit order-stack sampler."""
|
| 506 |
-
|
| 507 |
-
backends: tuple[ConstrainedOrderStackBackend, ...]
|
| 508 |
-
length_weights: tuple[float, ...]
|
| 509 |
-
min_length: int
|
| 510 |
-
max_length: int
|
| 511 |
-
|
| 512 |
-
def __post_init__(self) -> None:
|
| 513 |
-
if not self.backends:
|
| 514 |
-
raise ValueError("at least one feasible length backend is required")
|
| 515 |
-
if len(self.backends) != len(self.length_weights):
|
| 516 |
-
raise ValueError("backends and length_weights must have the same length")
|
| 517 |
-
if any(weight < 0.0 for weight in self.length_weights):
|
| 518 |
-
raise ValueError("length weights must be non-negative")
|
| 519 |
-
if sum(self.length_weights) <= 0.0:
|
| 520 |
-
raise ValueError("at least one length weight must be positive")
|
| 521 |
-
|
| 522 |
-
@property
|
| 523 |
-
def feasible_lengths(self) -> tuple[int, ...]:
|
| 524 |
-
return tuple(backend.result.length for backend in self.backends)
|
| 525 |
-
|
| 526 |
-
@property
|
| 527 |
-
def diagnostics(self) -> UntilOrderStackDiagnostics:
|
| 528 |
-
length_diagnostics = tuple(
|
| 529 |
-
UntilLengthDiagnostics(
|
| 530 |
-
length=backend.result.length,
|
| 531 |
-
weight=weight,
|
| 532 |
-
backend=backend.diagnostics,
|
| 533 |
-
)
|
| 534 |
-
for backend, weight in zip(self.backends, self.length_weights)
|
| 535 |
-
)
|
| 536 |
-
return UntilOrderStackDiagnostics(
|
| 537 |
-
backend="order_stack_until",
|
| 538 |
-
min_length=self.min_length,
|
| 539 |
-
max_length=self.max_length,
|
| 540 |
-
feasible_lengths=self.feasible_lengths,
|
| 541 |
-
length_weights=tuple(
|
| 542 |
-
(backend.result.length, weight)
|
| 543 |
-
for backend, weight in zip(self.backends, self.length_weights)
|
| 544 |
-
),
|
| 545 |
-
length_diagnostics=length_diagnostics,
|
| 546 |
-
)
|
| 547 |
-
|
| 548 |
-
def sample(self, *, rng: random.Random | int | None = None) -> tuple[Symbol, ...]:
|
| 549 |
-
generator = _coerce_backend_rng(rng)
|
| 550 |
-
return self._choose_backend(generator).sample(rng=generator)
|
| 551 |
-
|
| 552 |
-
def sample_many(
|
| 553 |
-
self,
|
| 554 |
-
count: int,
|
| 555 |
-
*,
|
| 556 |
-
rng: random.Random | int | None = None,
|
| 557 |
-
) -> list[tuple[Symbol, ...]]:
|
| 558 |
-
generator = _coerce_backend_rng(rng)
|
| 559 |
-
return [self.sample(rng=generator) for _ in range(count)]
|
| 560 |
-
|
| 561 |
-
def sample_with_orders(
|
| 562 |
-
self,
|
| 563 |
-
*,
|
| 564 |
-
rng: random.Random | int | None = None,
|
| 565 |
-
) -> GeneratedSequence:
|
| 566 |
-
generator = _coerce_backend_rng(rng)
|
| 567 |
-
return self._choose_backend(generator).sample_with_orders(rng=generator)
|
| 568 |
-
|
| 569 |
-
def sample_many_with_orders(
|
| 570 |
-
self,
|
| 571 |
-
count: int,
|
| 572 |
-
*,
|
| 573 |
-
rng: random.Random | int | None = None,
|
| 574 |
-
) -> list[GeneratedSequence]:
|
| 575 |
-
generator = _coerce_backend_rng(rng)
|
| 576 |
-
return [self.sample_with_orders(rng=generator) for _ in range(count)]
|
| 577 |
-
|
| 578 |
-
def sample_with_trace(
|
| 579 |
-
self,
|
| 580 |
-
*,
|
| 581 |
-
rng: random.Random | int | None = None,
|
| 582 |
-
) -> tuple[tuple[Symbol, ...], tuple[OrderSampleStep, ...]]:
|
| 583 |
-
generator = _coerce_backend_rng(rng)
|
| 584 |
-
return self._choose_backend(generator).sample_with_trace(rng=generator)
|
| 585 |
-
|
| 586 |
-
def _choose_backend(self, rng: random.Random) -> ConstrainedOrderStackBackend:
|
| 587 |
-
cumulative = _cumulative_weights(self.length_weights)
|
| 588 |
-
index = bisect.bisect_left(cumulative, rng.random() * cumulative[-1])
|
| 589 |
-
if index >= len(self.backends):
|
| 590 |
-
index = len(self.backends) - 1
|
| 591 |
-
return self.backends[index]
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
def run_constrained_order_stack(
|
| 595 |
-
model: OrderStackModel,
|
| 596 |
-
constraints: ConstraintSet | None = None,
|
| 597 |
-
*,
|
| 598 |
-
length: int,
|
| 599 |
-
prefix: Sequence[Symbol],
|
| 600 |
-
policy: OrderPolicy | None = None,
|
| 601 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 602 |
-
prefer_dense_forbidden: bool = True,
|
| 603 |
-
allowed_forbidden_symbols: AllowedForbiddenSymbols | None = None,
|
| 604 |
-
minimize_source_graphs: bool = False,
|
| 605 |
-
) -> OrderStackBPResult | RegularOrderStackBPResult:
|
| 606 |
-
"""Run constrained order-stack BP through the public constraint API.
|
| 607 |
-
|
| 608 |
-
Purely positional constraints use the positional order-stack backend. Any
|
| 609 |
-
regular component uses the regular backend with positional constraints kept
|
| 610 |
-
as masks instead of being folded into the DFA state.
|
| 611 |
-
"""
|
| 612 |
-
|
| 613 |
-
compiled = compile_constraints(
|
| 614 |
-
constraints,
|
| 615 |
-
length=length,
|
| 616 |
-
alphabet=tuple(model.alphabet if alphabet is None else alphabet),
|
| 617 |
-
prefer_dense_forbidden=prefer_dense_forbidden,
|
| 618 |
-
)
|
| 619 |
-
if compiled.regular_acceptor is None:
|
| 620 |
-
return run_order_stack_bp(
|
| 621 |
-
model,
|
| 622 |
-
length=length,
|
| 623 |
-
prefix=prefix,
|
| 624 |
-
constraints=compiled.positional,
|
| 625 |
-
allowed_forbidden_symbols=allowed_forbidden_symbols,
|
| 626 |
-
policy=policy,
|
| 627 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 628 |
-
)
|
| 629 |
-
return run_order_stack_masked_dfa_bp(
|
| 630 |
-
model,
|
| 631 |
-
compiled.regular_acceptor,
|
| 632 |
-
length=length,
|
| 633 |
-
prefix=prefix,
|
| 634 |
-
constraints=compiled.positional,
|
| 635 |
-
allowed_forbidden_symbols=allowed_forbidden_symbols,
|
| 636 |
-
policy=policy,
|
| 637 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 638 |
-
)
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
def prepare_constrained_order_stack_plan(
|
| 642 |
-
model: OrderStackModel,
|
| 643 |
-
constraints: ConstraintSet | None = None,
|
| 644 |
-
*,
|
| 645 |
-
length: int,
|
| 646 |
-
policy: OrderPolicy | None = None,
|
| 647 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 648 |
-
prefer_dense_forbidden: bool = True,
|
| 649 |
-
allowed_forbidden_symbols: AllowedForbiddenSymbols | None = None,
|
| 650 |
-
minimize_source_graphs: bool = False,
|
| 651 |
-
) -> ConstrainedOrderStackPlan:
|
| 652 |
-
"""Prepare reusable constrained order-stack BP without binding a prefix."""
|
| 653 |
-
|
| 654 |
-
compiled = compile_constraints(
|
| 655 |
-
constraints,
|
| 656 |
-
length=length,
|
| 657 |
-
alphabet=tuple(model.alphabet if alphabet is None else alphabet),
|
| 658 |
-
prefer_dense_forbidden=prefer_dense_forbidden,
|
| 659 |
-
)
|
| 660 |
-
if compiled.regular_acceptor is None:
|
| 661 |
-
return ConstrainedOrderStackPlan(
|
| 662 |
-
prepare_order_stack_bp(
|
| 663 |
-
model,
|
| 664 |
-
length=length,
|
| 665 |
-
constraints=compiled.positional,
|
| 666 |
-
allowed_forbidden_symbols=allowed_forbidden_symbols,
|
| 667 |
-
policy=policy,
|
| 668 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 669 |
-
)
|
| 670 |
-
)
|
| 671 |
-
return ConstrainedOrderStackPlan(
|
| 672 |
-
prepare_order_stack_masked_dfa_bp(
|
| 673 |
-
model,
|
| 674 |
-
compiled.regular_acceptor,
|
| 675 |
-
length=length,
|
| 676 |
-
constraints=compiled.positional,
|
| 677 |
-
allowed_forbidden_symbols=allowed_forbidden_symbols,
|
| 678 |
-
policy=policy,
|
| 679 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 680 |
-
)
|
| 681 |
-
)
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
def prepare_constrained_order_stack_support_plan(
|
| 685 |
-
model: OrderStackModel,
|
| 686 |
-
constraints: ConstraintSet | None = None,
|
| 687 |
-
*,
|
| 688 |
-
length: int,
|
| 689 |
-
policy: OrderPolicy | None = None,
|
| 690 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 691 |
-
prefer_dense_forbidden: bool = True,
|
| 692 |
-
allowed_forbidden_symbols: AllowedForbiddenSymbols | None = None,
|
| 693 |
-
minimize_source_graphs: bool = False,
|
| 694 |
-
) -> ConstrainedOrderStackSupportPlan:
|
| 695 |
-
"""Prepare hard support once for repeated dynamic soft regular weighting."""
|
| 696 |
-
|
| 697 |
-
return ConstrainedOrderStackSupportPlan(
|
| 698 |
-
prepare_constrained_order_stack_plan(
|
| 699 |
-
model,
|
| 700 |
-
constraints,
|
| 701 |
-
length=length,
|
| 702 |
-
policy=policy,
|
| 703 |
-
alphabet=alphabet,
|
| 704 |
-
prefer_dense_forbidden=prefer_dense_forbidden,
|
| 705 |
-
allowed_forbidden_symbols=allowed_forbidden_symbols,
|
| 706 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 707 |
-
)
|
| 708 |
-
)
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
def prepare_constrained_order_stack(
|
| 712 |
-
model: OrderStackModel,
|
| 713 |
-
constraints: ConstraintSet | None = None,
|
| 714 |
-
*,
|
| 715 |
-
length: int,
|
| 716 |
-
prefix: Sequence[Symbol],
|
| 717 |
-
policy: OrderPolicy | None = None,
|
| 718 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 719 |
-
prefer_dense_forbidden: bool = True,
|
| 720 |
-
allowed_forbidden_symbols: AllowedForbiddenSymbols | None = None,
|
| 721 |
-
minimize_source_graphs: bool = False,
|
| 722 |
-
) -> ConstrainedOrderStackBackend:
|
| 723 |
-
"""Compile constraints and return a reusable order-stack backend."""
|
| 724 |
-
|
| 725 |
-
try:
|
| 726 |
-
plan = prepare_constrained_order_stack_plan(
|
| 727 |
-
model,
|
| 728 |
-
constraints,
|
| 729 |
-
length=length,
|
| 730 |
-
policy=policy,
|
| 731 |
-
alphabet=alphabet,
|
| 732 |
-
prefer_dense_forbidden=prefer_dense_forbidden,
|
| 733 |
-
allowed_forbidden_symbols=allowed_forbidden_symbols,
|
| 734 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 735 |
-
)
|
| 736 |
-
except ValueError as error:
|
| 737 |
-
if "prefix-independent graph compilation" not in str(error):
|
| 738 |
-
raise
|
| 739 |
-
return ConstrainedOrderStackBackend(
|
| 740 |
-
run_constrained_order_stack(
|
| 741 |
-
model,
|
| 742 |
-
constraints,
|
| 743 |
-
length=length,
|
| 744 |
-
prefix=prefix,
|
| 745 |
-
policy=policy,
|
| 746 |
-
alphabet=alphabet,
|
| 747 |
-
prefer_dense_forbidden=prefer_dense_forbidden,
|
| 748 |
-
allowed_forbidden_symbols=allowed_forbidden_symbols,
|
| 749 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 750 |
-
)
|
| 751 |
-
)
|
| 752 |
-
return plan.for_prefix(prefix)
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
def prepare_constrained_order_stack_plan_from_sequences(
|
| 756 |
-
sequences: Iterable[Sequence[Symbol]],
|
| 757 |
-
constraints: ConstraintSet | None = None,
|
| 758 |
-
*,
|
| 759 |
-
max_order: int,
|
| 760 |
-
length: int,
|
| 761 |
-
policy: OrderPolicy | None = None,
|
| 762 |
-
start_symbol: Symbol | None = None,
|
| 763 |
-
end_symbol: Symbol | None = None,
|
| 764 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 765 |
-
prefer_dense_forbidden: bool = True,
|
| 766 |
-
minimize_source_graphs: bool = False,
|
| 767 |
-
) -> ConstrainedOrderStackPlan:
|
| 768 |
-
"""Build an order-stack model from sequences and prepare a prefixless plan."""
|
| 769 |
-
|
| 770 |
-
model = OrderStackModel.from_sequences(
|
| 771 |
-
sequences,
|
| 772 |
-
max_order=max_order,
|
| 773 |
-
start_symbol=start_symbol,
|
| 774 |
-
end_symbol=end_symbol,
|
| 775 |
-
)
|
| 776 |
-
return prepare_constrained_order_stack_plan(
|
| 777 |
-
model,
|
| 778 |
-
constraints,
|
| 779 |
-
length=length,
|
| 780 |
-
policy=policy,
|
| 781 |
-
alphabet=alphabet,
|
| 782 |
-
prefer_dense_forbidden=prefer_dense_forbidden,
|
| 783 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 784 |
-
)
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
def prepare_constrained_order_stack_support_plan_from_sequences(
|
| 788 |
-
sequences: Iterable[Sequence[Symbol]],
|
| 789 |
-
constraints: ConstraintSet | None = None,
|
| 790 |
-
*,
|
| 791 |
-
max_order: int,
|
| 792 |
-
length: int,
|
| 793 |
-
policy: OrderPolicy | None = None,
|
| 794 |
-
start_symbol: Symbol | None = None,
|
| 795 |
-
end_symbol: Symbol | None = None,
|
| 796 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 797 |
-
prefer_dense_forbidden: bool = True,
|
| 798 |
-
minimize_source_graphs: bool = False,
|
| 799 |
-
) -> ConstrainedOrderStackSupportPlan:
|
| 800 |
-
"""Build an order-stack model once and prepare reusable hard support."""
|
| 801 |
-
|
| 802 |
-
return ConstrainedOrderStackSupportPlan(
|
| 803 |
-
prepare_constrained_order_stack_plan_from_sequences(
|
| 804 |
-
sequences,
|
| 805 |
-
constraints,
|
| 806 |
-
max_order=max_order,
|
| 807 |
-
length=length,
|
| 808 |
-
policy=policy,
|
| 809 |
-
start_symbol=start_symbol,
|
| 810 |
-
end_symbol=end_symbol,
|
| 811 |
-
alphabet=alphabet,
|
| 812 |
-
prefer_dense_forbidden=prefer_dense_forbidden,
|
| 813 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 814 |
-
)
|
| 815 |
-
)
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
def prepare_until_order_stack(
|
| 819 |
-
model: OrderStackModel,
|
| 820 |
-
*,
|
| 821 |
-
prefix: Sequence[Symbol],
|
| 822 |
-
stop: Symbol | Iterable[Symbol] | Callable[[Symbol], bool],
|
| 823 |
-
min_length: int = 1,
|
| 824 |
-
max_length: int = 64,
|
| 825 |
-
constraints: ConstraintSet | None = None,
|
| 826 |
-
policy: OrderPolicy | None = None,
|
| 827 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 828 |
-
prefer_dense_forbidden: bool = True,
|
| 829 |
-
minimize_source_graphs: bool = False,
|
| 830 |
-
) -> UntilOrderStackBackend:
|
| 831 |
-
"""Prepare variable-length first-hit continuation for the order stack.
|
| 832 |
-
|
| 833 |
-
The generated suffix has length in ``[min_length, max_length]``. The final
|
| 834 |
-
emitted symbol satisfies ``stop`` and no earlier emitted symbol does.
|
| 835 |
-
Lengths are sampled proportionally to the sum of each feasible backend's
|
| 836 |
-
positive start-order masses, falling back to unit weight if only a success
|
| 837 |
-
indicator is available.
|
| 838 |
-
"""
|
| 839 |
-
|
| 840 |
-
if min_length < 1:
|
| 841 |
-
raise ValueError("min_length must be at least 1")
|
| 842 |
-
if max_length < min_length:
|
| 843 |
-
raise ValueError("max_length must be greater than or equal to min_length")
|
| 844 |
-
if not prefix:
|
| 845 |
-
raise ValueError("order-stack BP requires a non-empty prefix")
|
| 846 |
-
|
| 847 |
-
stop_predicate, stop_symbols = _coerce_stop_condition(
|
| 848 |
-
stop,
|
| 849 |
-
known_symbols=model.alphabet,
|
| 850 |
-
)
|
| 851 |
-
prepared: list[ConstrainedOrderStackBackend] = []
|
| 852 |
-
weights: list[float] = []
|
| 853 |
-
|
| 854 |
-
for length in range(min_length, max_length + 1):
|
| 855 |
-
if not _constraints_compatible_with_length(constraints, length):
|
| 856 |
-
continue
|
| 857 |
-
first_hit = _first_hit_constraints(stop_predicate, length)
|
| 858 |
-
combined = combine_constraints(constraints, first_hit)
|
| 859 |
-
allowed_forbidden = _allowed_forbidden_stop_symbols(
|
| 860 |
-
model,
|
| 861 |
-
stop_predicate,
|
| 862 |
-
stop_symbols,
|
| 863 |
-
length,
|
| 864 |
-
)
|
| 865 |
-
backend = prepare_constrained_order_stack(
|
| 866 |
-
model,
|
| 867 |
-
combined,
|
| 868 |
-
length=length,
|
| 869 |
-
prefix=prefix,
|
| 870 |
-
policy=policy,
|
| 871 |
-
alphabet=alphabet,
|
| 872 |
-
prefer_dense_forbidden=prefer_dense_forbidden,
|
| 873 |
-
allowed_forbidden_symbols=allowed_forbidden,
|
| 874 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 875 |
-
)
|
| 876 |
-
weight = _length_weight(backend)
|
| 877 |
-
if weight <= 0.0:
|
| 878 |
-
continue
|
| 879 |
-
prepared.append(backend)
|
| 880 |
-
weights.append(weight)
|
| 881 |
-
|
| 882 |
-
if not prepared:
|
| 883 |
-
raise ValueError("No feasible first-hit continuation length satisfies the constraints.")
|
| 884 |
-
|
| 885 |
-
return UntilOrderStackBackend(
|
| 886 |
-
backends=tuple(prepared),
|
| 887 |
-
length_weights=tuple(weights),
|
| 888 |
-
min_length=min_length,
|
| 889 |
-
max_length=max_length,
|
| 890 |
-
)
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
def prepare_until_end_order_stack(
|
| 894 |
-
model: OrderStackModel,
|
| 895 |
-
*,
|
| 896 |
-
prefix: Sequence[Symbol],
|
| 897 |
-
end_symbol: Symbol,
|
| 898 |
-
min_length: int = 1,
|
| 899 |
-
max_length: int = 64,
|
| 900 |
-
constraints: ConstraintSet | None = None,
|
| 901 |
-
policy: OrderPolicy | None = None,
|
| 902 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 903 |
-
prefer_dense_forbidden: bool = True,
|
| 904 |
-
minimize_source_graphs: bool = False,
|
| 905 |
-
) -> UntilOrderStackBackend:
|
| 906 |
-
"""Prepare first-hit continuation that stops at ``end_symbol``."""
|
| 907 |
-
|
| 908 |
-
return prepare_until_order_stack(
|
| 909 |
-
model,
|
| 910 |
-
prefix=prefix,
|
| 911 |
-
stop=end_symbol,
|
| 912 |
-
min_length=min_length,
|
| 913 |
-
max_length=max_length,
|
| 914 |
-
constraints=constraints,
|
| 915 |
-
policy=policy,
|
| 916 |
-
alphabet=alphabet,
|
| 917 |
-
prefer_dense_forbidden=prefer_dense_forbidden,
|
| 918 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 919 |
-
)
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
def prepare_constrained_order_stack_from_sequences(
|
| 923 |
-
sequences: Iterable[Sequence[Symbol]],
|
| 924 |
-
constraints: ConstraintSet | None = None,
|
| 925 |
-
*,
|
| 926 |
-
max_order: int,
|
| 927 |
-
length: int,
|
| 928 |
-
prefix: Sequence[Symbol],
|
| 929 |
-
policy: OrderPolicy | None = None,
|
| 930 |
-
start_symbol: Symbol | None = None,
|
| 931 |
-
end_symbol: Symbol | None = None,
|
| 932 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 933 |
-
prefer_dense_forbidden: bool = True,
|
| 934 |
-
minimize_source_graphs: bool = False,
|
| 935 |
-
) -> ConstrainedOrderStackBackend:
|
| 936 |
-
"""Build an order-stack model from sequences and prepare a backend."""
|
| 937 |
-
|
| 938 |
-
model = OrderStackModel.from_sequences(
|
| 939 |
-
sequences,
|
| 940 |
-
max_order=max_order,
|
| 941 |
-
start_symbol=start_symbol,
|
| 942 |
-
end_symbol=end_symbol,
|
| 943 |
-
)
|
| 944 |
-
return prepare_constrained_order_stack(
|
| 945 |
-
model,
|
| 946 |
-
constraints,
|
| 947 |
-
length=length,
|
| 948 |
-
prefix=prefix,
|
| 949 |
-
policy=policy,
|
| 950 |
-
alphabet=alphabet,
|
| 951 |
-
prefer_dense_forbidden=prefer_dense_forbidden,
|
| 952 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 953 |
-
)
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
def _coerce_stop_condition(
|
| 957 |
-
stop: Symbol | Iterable[Symbol] | Callable[[Symbol], bool],
|
| 958 |
-
*,
|
| 959 |
-
known_symbols: Iterable[Symbol] = (),
|
| 960 |
-
) -> tuple[Callable[[Symbol], bool], frozenset[Symbol] | None]:
|
| 961 |
-
if callable(stop):
|
| 962 |
-
return lambda symbol: bool(stop(symbol)), None
|
| 963 |
-
known = frozenset(known_symbols)
|
| 964 |
-
if isinstance(stop, (str, bytes)) or _is_known_symbol(stop, known):
|
| 965 |
-
symbols = frozenset({stop})
|
| 966 |
-
else:
|
| 967 |
-
try:
|
| 968 |
-
symbols = frozenset(stop) # type: ignore[arg-type]
|
| 969 |
-
except TypeError:
|
| 970 |
-
symbols = frozenset({stop}) # type: ignore[list-item]
|
| 971 |
-
if not symbols:
|
| 972 |
-
raise ValueError("stop symbol set must not be empty")
|
| 973 |
-
return lambda symbol: symbol in symbols, symbols
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
def _constraints_compatible_with_length(
|
| 977 |
-
constraints: ConstraintSet | None,
|
| 978 |
-
length: int,
|
| 979 |
-
) -> bool:
|
| 980 |
-
if constraints is None:
|
| 981 |
-
return True
|
| 982 |
-
for position in constraints.positional:
|
| 983 |
-
if position < 0:
|
| 984 |
-
raise IndexError(f"constraint position {position} is outside length {length}")
|
| 985 |
-
if position >= length:
|
| 986 |
-
return False
|
| 987 |
-
if constraints.meter is not None and len(constraints.meter.pattern) != length:
|
| 988 |
-
return False
|
| 989 |
-
cumulative = constraints.cumulative_meter
|
| 990 |
-
if cumulative is not None and cumulative.length is not None and cumulative.length != length:
|
| 991 |
-
return False
|
| 992 |
-
return True
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
def _is_known_symbol(value: object, known_symbols: frozenset[Symbol]) -> bool:
|
| 996 |
-
if not isinstance(value, Hashable):
|
| 997 |
-
return False
|
| 998 |
-
try:
|
| 999 |
-
return value in known_symbols
|
| 1000 |
-
except TypeError:
|
| 1001 |
-
return False
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
def _first_hit_constraints(
|
| 1005 |
-
stop_predicate: Callable[[Symbol], bool],
|
| 1006 |
-
length: int,
|
| 1007 |
-
) -> ConstraintSet:
|
| 1008 |
-
positional: dict[int, Callable[[Symbol], bool]] = {
|
| 1009 |
-
position: _negate_predicate(stop_predicate)
|
| 1010 |
-
for position in range(length - 1)
|
| 1011 |
-
}
|
| 1012 |
-
positional[length - 1] = stop_predicate
|
| 1013 |
-
return ConstraintSet(positional=positional)
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
def _negate_predicate(predicate: Callable[[Symbol], bool]) -> Callable[[Symbol], bool]:
|
| 1017 |
-
return lambda symbol: not predicate(symbol)
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
def _allowed_forbidden_stop_symbols(
|
| 1021 |
-
model: OrderStackModel,
|
| 1022 |
-
stop_predicate: Callable[[Symbol], bool],
|
| 1023 |
-
stop_symbols: frozenset[Symbol] | None,
|
| 1024 |
-
length: int,
|
| 1025 |
-
) -> dict[int, frozenset[Symbol]]:
|
| 1026 |
-
if stop_symbols is None:
|
| 1027 |
-
allowed = frozenset(
|
| 1028 |
-
symbol
|
| 1029 |
-
for symbol in model.forbidden_symbols
|
| 1030 |
-
if _predicate_accepts(stop_predicate, symbol)
|
| 1031 |
-
)
|
| 1032 |
-
else:
|
| 1033 |
-
allowed = frozenset(symbol for symbol in stop_symbols if symbol in model.forbidden_symbols)
|
| 1034 |
-
return {length - 1: allowed} if allowed else {}
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
def _predicate_accepts(predicate: Callable[[Symbol], bool], symbol: Symbol) -> bool:
|
| 1038 |
-
try:
|
| 1039 |
-
return bool(predicate(symbol))
|
| 1040 |
-
except Exception:
|
| 1041 |
-
return False
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
def _length_weight(backend: ConstrainedOrderStackBackend) -> float:
|
| 1045 |
-
mass = sum(
|
| 1046 |
-
max(0.0, float(start_mass))
|
| 1047 |
-
for _order, start_mass in backend.result.start_order_masses()
|
| 1048 |
-
)
|
| 1049 |
-
if mass > 0.0:
|
| 1050 |
-
return mass
|
| 1051 |
-
return 1.0 if backend.result.success_mass > 0.0 else 0.0
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
def _coerce_backend_rng(rng: random.Random | int | None) -> random.Random:
|
| 1055 |
-
if isinstance(rng, random.Random):
|
| 1056 |
-
return rng
|
| 1057 |
-
return random.Random(rng)
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
def _cumulative_weights(weights: Sequence[float]) -> tuple[float, ...]:
|
| 1061 |
-
total = 0.0
|
| 1062 |
-
cumulative: list[float] = []
|
| 1063 |
-
for weight in weights:
|
| 1064 |
-
total += float(weight)
|
| 1065 |
-
cumulative.append(total)
|
| 1066 |
-
return tuple(cumulative)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/brute_force.py
DELETED
|
@@ -1,116 +0,0 @@
|
|
| 1 |
-
"""Tiny exhaustive checks for exactness tests."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from collections import defaultdict
|
| 6 |
-
from itertools import product
|
| 7 |
-
from typing import Hashable, Iterable, Mapping
|
| 8 |
-
|
| 9 |
-
from .acceptors import DFA, transition_weight as regular_transition_weight
|
| 10 |
-
from .context import Context, ContextGraph, Symbol, _as_context
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
def brute_force_distribution(
|
| 14 |
-
graph: ContextGraph,
|
| 15 |
-
acceptor: DFA,
|
| 16 |
-
*,
|
| 17 |
-
length: int,
|
| 18 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 19 |
-
start_context: Iterable[Symbol] | Context | None = None,
|
| 20 |
-
start_acceptor_state: Hashable | None = None,
|
| 21 |
-
) -> dict[tuple[Symbol, ...], float]:
|
| 22 |
-
"""Enumerate all accepted length-``n`` strings and unnormalized masses."""
|
| 23 |
-
|
| 24 |
-
if length < 0:
|
| 25 |
-
raise ValueError("length must be non-negative")
|
| 26 |
-
|
| 27 |
-
context0 = graph.start_state if start_context is None else _as_context(start_context)
|
| 28 |
-
acceptor0 = acceptor.start_state if start_acceptor_state is None else start_acceptor_state
|
| 29 |
-
masses: dict[tuple[Symbol, ...], float] = defaultdict(float)
|
| 30 |
-
|
| 31 |
-
if alphabet is not None:
|
| 32 |
-
for sequence in product(tuple(alphabet), repeat=length):
|
| 33 |
-
context_state = context0
|
| 34 |
-
acceptor_state = acceptor0
|
| 35 |
-
probability = 1.0
|
| 36 |
-
accepted = True
|
| 37 |
-
for symbol in sequence:
|
| 38 |
-
edge = next(
|
| 39 |
-
(edge for edge in graph.outgoing(context_state) if edge.symbol == symbol),
|
| 40 |
-
None,
|
| 41 |
-
)
|
| 42 |
-
if edge is None:
|
| 43 |
-
accepted = False
|
| 44 |
-
break
|
| 45 |
-
next_acceptor_state = acceptor.next_state(acceptor_state, symbol)
|
| 46 |
-
if next_acceptor_state is None:
|
| 47 |
-
accepted = False
|
| 48 |
-
break
|
| 49 |
-
dfa_weight = regular_transition_weight(acceptor, acceptor_state, symbol)
|
| 50 |
-
if dfa_weight <= 0.0:
|
| 51 |
-
accepted = False
|
| 52 |
-
break
|
| 53 |
-
probability *= edge.probability * dfa_weight
|
| 54 |
-
context_state = edge.next_state
|
| 55 |
-
acceptor_state = next_acceptor_state
|
| 56 |
-
if accepted and acceptor.is_accepting(acceptor_state) and probability > 0.0:
|
| 57 |
-
masses[sequence] += probability
|
| 58 |
-
return dict(masses)
|
| 59 |
-
|
| 60 |
-
def visit(
|
| 61 |
-
time: int,
|
| 62 |
-
context_state: Context,
|
| 63 |
-
acceptor_state: Hashable,
|
| 64 |
-
prefix: tuple[Symbol, ...],
|
| 65 |
-
probability: float,
|
| 66 |
-
) -> None:
|
| 67 |
-
if time == length:
|
| 68 |
-
if acceptor.is_accepting(acceptor_state):
|
| 69 |
-
masses[prefix] += probability
|
| 70 |
-
return
|
| 71 |
-
|
| 72 |
-
for edge in graph.outgoing(context_state):
|
| 73 |
-
next_acceptor_state = acceptor.next_state(acceptor_state, edge.symbol)
|
| 74 |
-
if next_acceptor_state is None:
|
| 75 |
-
continue
|
| 76 |
-
dfa_weight = regular_transition_weight(acceptor, acceptor_state, edge.symbol)
|
| 77 |
-
if dfa_weight <= 0.0:
|
| 78 |
-
continue
|
| 79 |
-
visit(
|
| 80 |
-
time + 1,
|
| 81 |
-
edge.next_state,
|
| 82 |
-
next_acceptor_state,
|
| 83 |
-
prefix + (edge.symbol,),
|
| 84 |
-
probability * edge.probability * dfa_weight,
|
| 85 |
-
)
|
| 86 |
-
|
| 87 |
-
visit(0, context0, acceptor0, (), 1.0)
|
| 88 |
-
return dict(masses)
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def brute_force_partition_function(
|
| 92 |
-
graph: ContextGraph,
|
| 93 |
-
acceptor: DFA,
|
| 94 |
-
*,
|
| 95 |
-
length: int,
|
| 96 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 97 |
-
start_context: Iterable[Symbol] | Context | None = None,
|
| 98 |
-
start_acceptor_state: Hashable | None = None,
|
| 99 |
-
) -> float:
|
| 100 |
-
return sum(
|
| 101 |
-
brute_force_distribution(
|
| 102 |
-
graph,
|
| 103 |
-
acceptor,
|
| 104 |
-
length=length,
|
| 105 |
-
alphabet=alphabet,
|
| 106 |
-
start_context=start_context,
|
| 107 |
-
start_acceptor_state=start_acceptor_state,
|
| 108 |
-
).values()
|
| 109 |
-
)
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
def conditional_distribution(masses: Mapping[tuple[Symbol, ...], float]) -> dict[tuple[Symbol, ...], float]:
|
| 113 |
-
total = float(sum(masses.values()))
|
| 114 |
-
if total <= 0.0:
|
| 115 |
-
return {}
|
| 116 |
-
return {sequence: mass / total for sequence, mass in masses.items()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/constraint_builders.py
DELETED
|
@@ -1,238 +0,0 @@
|
|
| 1 |
-
"""Convenience builders for common fixed-horizon constraints."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from collections.abc import Callable, Iterable, Mapping, Sequence
|
| 6 |
-
|
| 7 |
-
from .constraints import ConstraintSet, CumulativeMeterConstraint, MeterConstraint
|
| 8 |
-
from .context import Symbol
|
| 9 |
-
from .positional_bp import PositionConstraint, _allows
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
def combine_constraints(*constraints: ConstraintSet | None) -> ConstraintSet:
|
| 13 |
-
"""Merge constraint sets into one specification.
|
| 14 |
-
|
| 15 |
-
Positional constraints at the same position are intersected. The current
|
| 16 |
-
public ``ConstraintSet`` supports one meter and one cumulative-meter
|
| 17 |
-
constraint; passing multiple distinct constraints of either kind raises.
|
| 18 |
-
"""
|
| 19 |
-
|
| 20 |
-
positional: dict[int, PositionConstraint] = {}
|
| 21 |
-
forbidden_substrings: list[Sequence[Symbol]] = []
|
| 22 |
-
regular_acceptors = []
|
| 23 |
-
meter = None
|
| 24 |
-
cumulative_meter = None
|
| 25 |
-
|
| 26 |
-
for spec in constraints:
|
| 27 |
-
if spec is None:
|
| 28 |
-
continue
|
| 29 |
-
for position, constraint in spec.positional.items():
|
| 30 |
-
if position in positional:
|
| 31 |
-
positional[position] = _intersect_position_constraints(
|
| 32 |
-
positional[position],
|
| 33 |
-
constraint,
|
| 34 |
-
)
|
| 35 |
-
else:
|
| 36 |
-
positional[position] = constraint
|
| 37 |
-
forbidden_substrings.extend(spec.forbidden_substrings)
|
| 38 |
-
regular_acceptors.extend(spec.regular_acceptors)
|
| 39 |
-
if spec.meter is not None:
|
| 40 |
-
if meter is not None and spec.meter != meter:
|
| 41 |
-
raise ValueError("combine_constraints supports at most one meter constraint")
|
| 42 |
-
meter = spec.meter
|
| 43 |
-
if spec.cumulative_meter is not None:
|
| 44 |
-
if cumulative_meter is not None and spec.cumulative_meter != cumulative_meter:
|
| 45 |
-
raise ValueError(
|
| 46 |
-
"combine_constraints supports at most one cumulative meter constraint"
|
| 47 |
-
)
|
| 48 |
-
cumulative_meter = spec.cumulative_meter
|
| 49 |
-
|
| 50 |
-
return ConstraintSet(
|
| 51 |
-
positional=positional,
|
| 52 |
-
forbidden_substrings=tuple(forbidden_substrings),
|
| 53 |
-
regular_acceptors=tuple(regular_acceptors),
|
| 54 |
-
meter=meter,
|
| 55 |
-
cumulative_meter=cumulative_meter,
|
| 56 |
-
)
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def at_position(position: int, allowed: PositionConstraint | Symbol) -> ConstraintSet:
|
| 60 |
-
"""Constrain one zero-based generated position."""
|
| 61 |
-
|
| 62 |
-
return ConstraintSet(positional={int(position): _position_constraint(allowed)})
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def final_symbol(symbol: Symbol, *, length: int) -> ConstraintSet:
|
| 66 |
-
"""Constrain the final generated symbol to one value."""
|
| 67 |
-
|
| 68 |
-
return final_symbols({symbol}, length=length)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def final_symbols(allowed: PositionConstraint | Symbol, *, length: int) -> ConstraintSet:
|
| 72 |
-
"""Constrain the final generated position."""
|
| 73 |
-
|
| 74 |
-
if length <= 0:
|
| 75 |
-
raise ValueError("length must be positive for a final-position constraint")
|
| 76 |
-
return at_position(length - 1, allowed)
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def final_pitch_class(
|
| 80 |
-
pitch_class: int,
|
| 81 |
-
*,
|
| 82 |
-
length: int,
|
| 83 |
-
modulo: int = 12,
|
| 84 |
-
symbol_to_pitch: Mapping[Symbol, int] | Callable[[Symbol], int] | None = None,
|
| 85 |
-
) -> ConstraintSet:
|
| 86 |
-
"""Constrain the final generated symbol by pitch class.
|
| 87 |
-
|
| 88 |
-
By default the symbol itself is interpreted as an integer pitch. Richer
|
| 89 |
-
symbol types can pass a mapping or callable through ``symbol_to_pitch``.
|
| 90 |
-
"""
|
| 91 |
-
|
| 92 |
-
if modulo <= 0:
|
| 93 |
-
raise ValueError("modulo must be positive")
|
| 94 |
-
target = int(pitch_class) % int(modulo)
|
| 95 |
-
|
| 96 |
-
def allows(symbol: Symbol) -> bool:
|
| 97 |
-
pitch = _lookup(symbol_to_pitch, symbol)
|
| 98 |
-
return int(pitch) % int(modulo) == target
|
| 99 |
-
|
| 100 |
-
return final_symbols(allows, length=length)
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
def avoid_copied_ngrams(reference: Sequence[Symbol], ngram_length: int) -> ConstraintSet:
|
| 104 |
-
"""Reject generated substrings copied from a reference sequence."""
|
| 105 |
-
|
| 106 |
-
if ngram_length <= 0:
|
| 107 |
-
raise ValueError("ngram_length must be positive")
|
| 108 |
-
forbidden = tuple(
|
| 109 |
-
dict.fromkeys(
|
| 110 |
-
tuple(reference[index : index + ngram_length])
|
| 111 |
-
for index in range(0, len(reference) - ngram_length + 1)
|
| 112 |
-
)
|
| 113 |
-
)
|
| 114 |
-
return ConstraintSet(forbidden_substrings=forbidden)
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
def meter_pattern(
|
| 118 |
-
pattern: Sequence[object | Iterable[object] | None],
|
| 119 |
-
symbol_to_meter: Mapping[Symbol, object] | Callable[[Symbol], object],
|
| 120 |
-
*,
|
| 121 |
-
name: str = "meter",
|
| 122 |
-
) -> ConstraintSet:
|
| 123 |
-
"""Constrain per-position meter/classes."""
|
| 124 |
-
|
| 125 |
-
return ConstraintSet(
|
| 126 |
-
meter=MeterConstraint(
|
| 127 |
-
pattern=pattern,
|
| 128 |
-
symbol_to_meter=symbol_to_meter,
|
| 129 |
-
name=name,
|
| 130 |
-
)
|
| 131 |
-
)
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
def cumulative_meter(
|
| 135 |
-
cost: Mapping[Symbol, int] | Callable[[Symbol], int],
|
| 136 |
-
predicate: Callable[[int, Symbol, int], bool] | None = None,
|
| 137 |
-
*,
|
| 138 |
-
length: int | None = None,
|
| 139 |
-
max_cost: int | None = None,
|
| 140 |
-
accept_costs: Iterable[int] | Callable[[int], bool] | None = None,
|
| 141 |
-
end_symbol: Symbol | None = None,
|
| 142 |
-
name: str = "cumulative_meter",
|
| 143 |
-
) -> ConstraintSet:
|
| 144 |
-
"""Constrain cumulative symbolic cost such as duration or beat position."""
|
| 145 |
-
|
| 146 |
-
return ConstraintSet(
|
| 147 |
-
cumulative_meter=CumulativeMeterConstraint(
|
| 148 |
-
cost=cost,
|
| 149 |
-
predicate=predicate,
|
| 150 |
-
length=length,
|
| 151 |
-
max_cost=max_cost,
|
| 152 |
-
accept_costs=accept_costs,
|
| 153 |
-
end_symbol=end_symbol,
|
| 154 |
-
name=name,
|
| 155 |
-
)
|
| 156 |
-
)
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
def padded_duration_total(
|
| 160 |
-
total: int,
|
| 161 |
-
*,
|
| 162 |
-
length: int,
|
| 163 |
-
pad_symbol: Symbol,
|
| 164 |
-
symbol_to_duration: Mapping[Symbol, int] | Callable[[Symbol], int],
|
| 165 |
-
allow_zero_duration_events: bool = False,
|
| 166 |
-
name: str = "padded_duration_total",
|
| 167 |
-
) -> ConstraintSet:
|
| 168 |
-
"""Constrain generated duration with trailing PAD symbols.
|
| 169 |
-
|
| 170 |
-
The generated sequence has fixed length, but its musical prefix may be
|
| 171 |
-
shorter. The PAD symbol has duration zero, may appear only once ``total`` is
|
| 172 |
-
reached, and is absorbing once emitted.
|
| 173 |
-
"""
|
| 174 |
-
|
| 175 |
-
if total < 0:
|
| 176 |
-
raise ValueError("total must be non-negative")
|
| 177 |
-
if length < 0:
|
| 178 |
-
raise ValueError("length must be non-negative")
|
| 179 |
-
target = int(total)
|
| 180 |
-
|
| 181 |
-
def duration_of(symbol: Symbol) -> int:
|
| 182 |
-
if symbol == pad_symbol:
|
| 183 |
-
return 0
|
| 184 |
-
return _lookup(symbol_to_duration, symbol)
|
| 185 |
-
|
| 186 |
-
def predicate(current_total: int, symbol: Symbol, one_based_position: int) -> bool:
|
| 187 |
-
del one_based_position
|
| 188 |
-
if symbol == pad_symbol:
|
| 189 |
-
return current_total == target
|
| 190 |
-
duration = duration_of(symbol)
|
| 191 |
-
if duration == 0 and not allow_zero_duration_events:
|
| 192 |
-
return False
|
| 193 |
-
if current_total == target:
|
| 194 |
-
return False
|
| 195 |
-
return True
|
| 196 |
-
|
| 197 |
-
return cumulative_meter(
|
| 198 |
-
duration_of,
|
| 199 |
-
predicate,
|
| 200 |
-
length=length,
|
| 201 |
-
max_cost=target,
|
| 202 |
-
accept_costs={target},
|
| 203 |
-
end_symbol=pad_symbol,
|
| 204 |
-
name=name,
|
| 205 |
-
)
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
def _intersect_position_constraints(
|
| 209 |
-
left: PositionConstraint,
|
| 210 |
-
right: PositionConstraint,
|
| 211 |
-
) -> PositionConstraint:
|
| 212 |
-
def allows(symbol: Symbol) -> bool:
|
| 213 |
-
return _allows(left, symbol) and _allows(right, symbol)
|
| 214 |
-
|
| 215 |
-
return allows
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
def _position_constraint(allowed: PositionConstraint | Symbol) -> PositionConstraint:
|
| 219 |
-
if callable(allowed):
|
| 220 |
-
return allowed
|
| 221 |
-
if isinstance(allowed, (str, bytes)):
|
| 222 |
-
return {allowed}
|
| 223 |
-
try:
|
| 224 |
-
iter(allowed) # type: ignore[arg-type]
|
| 225 |
-
except TypeError:
|
| 226 |
-
return {allowed} # type: ignore[return-value]
|
| 227 |
-
return allowed # type: ignore[return-value]
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
def _lookup(
|
| 231 |
-
mapping_or_callable: Mapping[Symbol, int] | Callable[[Symbol], int] | None,
|
| 232 |
-
symbol: Symbol,
|
| 233 |
-
) -> int:
|
| 234 |
-
if mapping_or_callable is None:
|
| 235 |
-
return int(symbol)
|
| 236 |
-
if isinstance(mapping_or_callable, Mapping):
|
| 237 |
-
return int(mapping_or_callable[symbol])
|
| 238 |
-
return int(mapping_or_callable(symbol))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/constraints.py
DELETED
|
@@ -1,173 +0,0 @@
|
|
| 1 |
-
"""Public constraint specifications and compiler helpers.
|
| 2 |
-
|
| 3 |
-
The classes in this module are intentionally project-agnostic: symbols can be
|
| 4 |
-
pitches, durations, tokens, events, or any other hashable objects. The compiler
|
| 5 |
-
keeps purely positional constraints as time-indexed masks and compiles regular
|
| 6 |
-
constraints into deterministic acceptors.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
from __future__ import annotations
|
| 10 |
-
|
| 11 |
-
from collections.abc import Callable, Iterable, Mapping, Sequence
|
| 12 |
-
from dataclasses import dataclass, field
|
| 13 |
-
from typing import Hashable
|
| 14 |
-
|
| 15 |
-
from .acceptors import (
|
| 16 |
-
DFA,
|
| 17 |
-
all_of,
|
| 18 |
-
cumulative_meter_acceptor,
|
| 19 |
-
dense_forbidden_substring_acceptor,
|
| 20 |
-
forbidden_substring_acceptor,
|
| 21 |
-
meter_acceptor,
|
| 22 |
-
)
|
| 23 |
-
from .context import Symbol
|
| 24 |
-
from .positional_bp import PositionConstraint, PositionConstraints
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
MeterClass = Hashable
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
@dataclass(frozen=True)
|
| 31 |
-
class MeterConstraint:
|
| 32 |
-
"""Per-position meter/classes for emitted symbols.
|
| 33 |
-
|
| 34 |
-
``pattern`` entries may be a single class, an iterable of allowed classes,
|
| 35 |
-
or ``None`` as a wildcard. ``symbol_to_meter`` maps each emitted symbol to
|
| 36 |
-
its meter class.
|
| 37 |
-
"""
|
| 38 |
-
|
| 39 |
-
pattern: Sequence[MeterClass | Iterable[MeterClass] | None]
|
| 40 |
-
symbol_to_meter: Mapping[Symbol, MeterClass] | Callable[[Symbol], MeterClass]
|
| 41 |
-
name: str = "meter"
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
@dataclass(frozen=True)
|
| 45 |
-
class CumulativeMeterConstraint:
|
| 46 |
-
"""Cumulative-cost meter predicate.
|
| 47 |
-
|
| 48 |
-
The DFA state stores the cumulative cost before each emission. ``predicate``
|
| 49 |
-
is called as ``predicate(total_cost_before, symbol, one_based_position)``.
|
| 50 |
-
If ``length`` is omitted, the compiler uses the requested generation length.
|
| 51 |
-
"""
|
| 52 |
-
|
| 53 |
-
cost: Mapping[Symbol, int] | Callable[[Symbol], int]
|
| 54 |
-
predicate: Callable[[int, Symbol, int], bool] | None = None
|
| 55 |
-
length: int | None = None
|
| 56 |
-
max_cost: int | None = None
|
| 57 |
-
accept_costs: Iterable[int] | Callable[[int], bool] | None = None
|
| 58 |
-
end_symbol: Symbol | None = None
|
| 59 |
-
name: str = "cumulative_meter"
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
@dataclass(frozen=True)
|
| 63 |
-
class ConstraintSet:
|
| 64 |
-
"""Constraints for fixed-horizon symbolic generation.
|
| 65 |
-
|
| 66 |
-
Positional constraints are represented as masks and do not inflate regular
|
| 67 |
-
DFA state. Regular constraints are compiled into acceptors and intersected.
|
| 68 |
-
"""
|
| 69 |
-
|
| 70 |
-
positional: PositionConstraints = field(default_factory=dict)
|
| 71 |
-
forbidden_substrings: Iterable[Sequence[Symbol]] = field(default_factory=tuple)
|
| 72 |
-
regular_acceptors: Sequence[DFA] = field(default_factory=tuple)
|
| 73 |
-
meter: MeterConstraint | None = None
|
| 74 |
-
cumulative_meter: CumulativeMeterConstraint | None = None
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
@dataclass(frozen=True)
|
| 78 |
-
class CompiledConstraints:
|
| 79 |
-
"""Compiled constraints consumed by BP backends."""
|
| 80 |
-
|
| 81 |
-
positional: dict[int, PositionConstraint]
|
| 82 |
-
regular_acceptor: DFA | None = None
|
| 83 |
-
|
| 84 |
-
@property
|
| 85 |
-
def has_regular(self) -> bool:
|
| 86 |
-
return self.regular_acceptor is not None
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
def compile_constraints(
|
| 90 |
-
constraints: ConstraintSet | None,
|
| 91 |
-
*,
|
| 92 |
-
length: int,
|
| 93 |
-
alphabet: Iterable[Symbol],
|
| 94 |
-
prefer_dense_forbidden: bool = True,
|
| 95 |
-
) -> CompiledConstraints:
|
| 96 |
-
"""Compile public constraint specs into masks plus an optional DFA."""
|
| 97 |
-
|
| 98 |
-
if length < 0:
|
| 99 |
-
raise ValueError("length must be non-negative")
|
| 100 |
-
spec = constraints or ConstraintSet()
|
| 101 |
-
alphabet_tuple = tuple(alphabet)
|
| 102 |
-
|
| 103 |
-
positional = dict(spec.positional)
|
| 104 |
-
_validate_positions(positional, length)
|
| 105 |
-
|
| 106 |
-
regulars: list[DFA] = list(spec.regular_acceptors)
|
| 107 |
-
forbidden = tuple(tuple(pattern) for pattern in spec.forbidden_substrings)
|
| 108 |
-
if forbidden:
|
| 109 |
-
if prefer_dense_forbidden:
|
| 110 |
-
regulars.append(
|
| 111 |
-
dense_forbidden_substring_acceptor(
|
| 112 |
-
forbidden,
|
| 113 |
-
alphabet=alphabet_tuple,
|
| 114 |
-
name="dense_forbidden_substring",
|
| 115 |
-
)
|
| 116 |
-
)
|
| 117 |
-
else:
|
| 118 |
-
regulars.append(
|
| 119 |
-
forbidden_substring_acceptor(
|
| 120 |
-
forbidden,
|
| 121 |
-
alphabet=alphabet_tuple,
|
| 122 |
-
name="forbidden_substring",
|
| 123 |
-
)
|
| 124 |
-
)
|
| 125 |
-
|
| 126 |
-
if spec.meter is not None:
|
| 127 |
-
if len(spec.meter.pattern) != length:
|
| 128 |
-
raise ValueError("meter pattern length must match generation length")
|
| 129 |
-
regulars.append(
|
| 130 |
-
meter_acceptor(
|
| 131 |
-
spec.meter.pattern,
|
| 132 |
-
spec.meter.symbol_to_meter,
|
| 133 |
-
alphabet=alphabet_tuple,
|
| 134 |
-
name=spec.meter.name,
|
| 135 |
-
)
|
| 136 |
-
)
|
| 137 |
-
|
| 138 |
-
if spec.cumulative_meter is not None:
|
| 139 |
-
cumulative = spec.cumulative_meter
|
| 140 |
-
if cumulative.length is not None and cumulative.length != length:
|
| 141 |
-
raise ValueError("cumulative meter length must match generation length")
|
| 142 |
-
regulars.append(
|
| 143 |
-
cumulative_meter_acceptor(
|
| 144 |
-
cumulative.length if cumulative.length is not None else length,
|
| 145 |
-
cumulative.cost,
|
| 146 |
-
cumulative.predicate,
|
| 147 |
-
alphabet=alphabet_tuple,
|
| 148 |
-
max_cost=cumulative.max_cost,
|
| 149 |
-
accept_costs=cumulative.accept_costs,
|
| 150 |
-
end_symbol=cumulative.end_symbol,
|
| 151 |
-
name=cumulative.name,
|
| 152 |
-
)
|
| 153 |
-
)
|
| 154 |
-
|
| 155 |
-
regular_acceptor = _combine_regulars(regulars)
|
| 156 |
-
return CompiledConstraints(
|
| 157 |
-
positional=positional,
|
| 158 |
-
regular_acceptor=regular_acceptor,
|
| 159 |
-
)
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
def _combine_regulars(regulars: Sequence[DFA]) -> DFA | None:
|
| 163 |
-
if not regulars:
|
| 164 |
-
return None
|
| 165 |
-
if len(regulars) == 1:
|
| 166 |
-
return regulars[0]
|
| 167 |
-
return all_of(*regulars, name="constraint_set")
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
def _validate_positions(constraints: Mapping[int, PositionConstraint], length: int) -> None:
|
| 171 |
-
for position in constraints:
|
| 172 |
-
if position < 0 or position >= length:
|
| 173 |
-
raise IndexError(f"constraint position {position} is outside length {length}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/context.py
DELETED
|
@@ -1,397 +0,0 @@
|
|
| 1 |
-
"""Sparse variable-order context graphs."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from collections import Counter, defaultdict
|
| 6 |
-
from dataclasses import dataclass
|
| 7 |
-
import math
|
| 8 |
-
from typing import Hashable, Iterable, Mapping, Sequence
|
| 9 |
-
|
| 10 |
-
Symbol = Hashable
|
| 11 |
-
Context = tuple[Symbol, ...]
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
@dataclass(frozen=True)
|
| 15 |
-
class Edge:
|
| 16 |
-
"""A labeled probabilistic transition in a context graph."""
|
| 17 |
-
|
| 18 |
-
symbol: Symbol
|
| 19 |
-
probability: float
|
| 20 |
-
next_state: Context
|
| 21 |
-
order_weights: tuple[tuple[int, float], ...] = ()
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def _as_context(value: Iterable[Symbol] | Context | None) -> Context:
|
| 25 |
-
if value is None:
|
| 26 |
-
return ()
|
| 27 |
-
if isinstance(value, tuple):
|
| 28 |
-
return value
|
| 29 |
-
return tuple(value)
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
class ContextGraph:
|
| 33 |
-
"""Sparse context graph induced by a variable-order/backoff model.
|
| 34 |
-
|
| 35 |
-
Nodes are canonical contexts represented as tuples of symbols. Each outgoing
|
| 36 |
-
edge emits one symbol and moves to the canonical suffix context after that
|
| 37 |
-
emission.
|
| 38 |
-
"""
|
| 39 |
-
|
| 40 |
-
def __init__(
|
| 41 |
-
self,
|
| 42 |
-
edges_by_state: Mapping[Iterable[Symbol] | Context, Sequence[Edge]],
|
| 43 |
-
*,
|
| 44 |
-
start_state: Iterable[Symbol] | Context = (),
|
| 45 |
-
max_order: int | None = None,
|
| 46 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 47 |
-
validate: bool = True,
|
| 48 |
-
) -> None:
|
| 49 |
-
normalized: dict[Context, tuple[Edge, ...]] = {}
|
| 50 |
-
states: set[Context] = {_as_context(start_state)}
|
| 51 |
-
emitted: set[Symbol] = set(alphabet or ())
|
| 52 |
-
|
| 53 |
-
for raw_state, raw_edges in edges_by_state.items():
|
| 54 |
-
state = _as_context(raw_state)
|
| 55 |
-
edges = tuple(
|
| 56 |
-
Edge(
|
| 57 |
-
edge.symbol,
|
| 58 |
-
float(edge.probability),
|
| 59 |
-
_as_context(edge.next_state),
|
| 60 |
-
tuple((int(order), float(weight)) for order, weight in edge.order_weights),
|
| 61 |
-
)
|
| 62 |
-
for edge in raw_edges
|
| 63 |
-
)
|
| 64 |
-
normalized[state] = edges
|
| 65 |
-
states.add(state)
|
| 66 |
-
for edge in edges:
|
| 67 |
-
states.add(edge.next_state)
|
| 68 |
-
emitted.add(edge.symbol)
|
| 69 |
-
|
| 70 |
-
self._edges = normalized
|
| 71 |
-
self.start_state = _as_context(start_state)
|
| 72 |
-
self.states = frozenset(states)
|
| 73 |
-
self.alphabet = frozenset(emitted)
|
| 74 |
-
self._alias_to_state: dict[Context, Context] = {}
|
| 75 |
-
self._continuation_counts: dict[Context, dict[Symbol, float]] = {}
|
| 76 |
-
self._state_supports: dict[Context, float] = {}
|
| 77 |
-
self.max_order = (
|
| 78 |
-
max_order
|
| 79 |
-
if max_order is not None
|
| 80 |
-
else max((len(state) for state in self.states), default=0)
|
| 81 |
-
)
|
| 82 |
-
|
| 83 |
-
if self.max_order < 0:
|
| 84 |
-
raise ValueError("max_order must be non-negative")
|
| 85 |
-
if validate:
|
| 86 |
-
self._validate()
|
| 87 |
-
|
| 88 |
-
@classmethod
|
| 89 |
-
def from_counts(
|
| 90 |
-
cls,
|
| 91 |
-
continuation_counts: Mapping[Iterable[Symbol] | Context, Mapping[Symbol, int | float]],
|
| 92 |
-
*,
|
| 93 |
-
max_order: int | None = None,
|
| 94 |
-
start_state: Iterable[Symbol] | Context = (),
|
| 95 |
-
) -> "ContextGraph":
|
| 96 |
-
"""Build a graph by normalizing explicit continuation counts."""
|
| 97 |
-
|
| 98 |
-
contexts = {_as_context(context) for context in continuation_counts}
|
| 99 |
-
contexts.add(_as_context(start_state))
|
| 100 |
-
if max_order is None:
|
| 101 |
-
max_order = max((len(context) for context in contexts), default=0)
|
| 102 |
-
|
| 103 |
-
edges_by_state: dict[Context, list[Edge]] = {}
|
| 104 |
-
count_metadata: dict[Context, dict[Symbol, float]] = {}
|
| 105 |
-
state_supports: dict[Context, float] = {}
|
| 106 |
-
for raw_context, counts in continuation_counts.items():
|
| 107 |
-
context = _as_context(raw_context)
|
| 108 |
-
total = float(sum(counts.values()))
|
| 109 |
-
if total <= 0.0:
|
| 110 |
-
raise ValueError(f"context {context!r} has no positive continuation mass")
|
| 111 |
-
state_supports[context] = total
|
| 112 |
-
count_metadata[context] = {
|
| 113 |
-
symbol: float(count)
|
| 114 |
-
for symbol, count in counts.items()
|
| 115 |
-
if count > 0
|
| 116 |
-
}
|
| 117 |
-
edges_by_state[context] = [
|
| 118 |
-
Edge(symbol, float(count) / total, cls._canon(context, symbol, contexts, max_order))
|
| 119 |
-
for symbol, count in counts.items()
|
| 120 |
-
if count > 0
|
| 121 |
-
]
|
| 122 |
-
graph = cls(
|
| 123 |
-
edges_by_state,
|
| 124 |
-
start_state=start_state,
|
| 125 |
-
max_order=max_order,
|
| 126 |
-
validate=True,
|
| 127 |
-
)
|
| 128 |
-
graph._continuation_counts = count_metadata
|
| 129 |
-
graph._state_supports = state_supports
|
| 130 |
-
return graph
|
| 131 |
-
|
| 132 |
-
@classmethod
|
| 133 |
-
def from_probabilities(
|
| 134 |
-
cls,
|
| 135 |
-
probabilities: Mapping[Iterable[Symbol] | Context, Mapping[Symbol, float]],
|
| 136 |
-
*,
|
| 137 |
-
max_order: int | None = None,
|
| 138 |
-
start_state: Iterable[Symbol] | Context = (),
|
| 139 |
-
) -> "ContextGraph":
|
| 140 |
-
"""Build a graph from explicit conditional probabilities."""
|
| 141 |
-
|
| 142 |
-
contexts = {_as_context(context) for context in probabilities}
|
| 143 |
-
contexts.add(_as_context(start_state))
|
| 144 |
-
if max_order is None:
|
| 145 |
-
max_order = max((len(context) for context in contexts), default=0)
|
| 146 |
-
|
| 147 |
-
edges_by_state: dict[Context, list[Edge]] = {}
|
| 148 |
-
continuation_counts: dict[Context, dict[Symbol, float]] = {}
|
| 149 |
-
state_supports: dict[Context, float] = {}
|
| 150 |
-
for raw_context, probs in probabilities.items():
|
| 151 |
-
context = _as_context(raw_context)
|
| 152 |
-
continuation_counts[context] = {
|
| 153 |
-
symbol: float(prob)
|
| 154 |
-
for symbol, prob in probs.items()
|
| 155 |
-
if prob > 0.0
|
| 156 |
-
}
|
| 157 |
-
state_supports[context] = float(sum(continuation_counts[context].values()))
|
| 158 |
-
edges_by_state[context] = [
|
| 159 |
-
Edge(symbol, float(prob), cls._canon(context, symbol, contexts, max_order))
|
| 160 |
-
for symbol, prob in probs.items()
|
| 161 |
-
if prob > 0.0
|
| 162 |
-
]
|
| 163 |
-
graph = cls(
|
| 164 |
-
edges_by_state,
|
| 165 |
-
start_state=start_state,
|
| 166 |
-
max_order=max_order,
|
| 167 |
-
validate=True,
|
| 168 |
-
)
|
| 169 |
-
graph._continuation_counts = continuation_counts
|
| 170 |
-
graph._state_supports = state_supports
|
| 171 |
-
return graph
|
| 172 |
-
|
| 173 |
-
@classmethod
|
| 174 |
-
def from_sequences(
|
| 175 |
-
cls,
|
| 176 |
-
sequences: Iterable[Sequence[Symbol]],
|
| 177 |
-
*,
|
| 178 |
-
max_order: int,
|
| 179 |
-
start_state: Iterable[Symbol] | Context = (),
|
| 180 |
-
) -> "ContextGraph":
|
| 181 |
-
"""Estimate normalized continuation counts for all suffix contexts.
|
| 182 |
-
|
| 183 |
-
For every token position, the implementation records the continuation
|
| 184 |
-
from each suffix context of orders 0..max_order available before that
|
| 185 |
-
token. This gives a sparse variable-order graph whose transitions are
|
| 186 |
-
normalized continuation counts at each observed context.
|
| 187 |
-
"""
|
| 188 |
-
|
| 189 |
-
if max_order < 0:
|
| 190 |
-
raise ValueError("max_order must be non-negative")
|
| 191 |
-
|
| 192 |
-
return cls.from_weighted_sequences(
|
| 193 |
-
((1.0, sequence) for sequence in sequences),
|
| 194 |
-
max_order=max_order,
|
| 195 |
-
start_state=start_state,
|
| 196 |
-
)
|
| 197 |
-
|
| 198 |
-
@classmethod
|
| 199 |
-
def from_weighted_sequences(
|
| 200 |
-
cls,
|
| 201 |
-
weighted_sequences: Iterable[tuple[int | float, Sequence[Symbol]]],
|
| 202 |
-
*,
|
| 203 |
-
max_order: int,
|
| 204 |
-
start_state: Iterable[Symbol] | Context = (),
|
| 205 |
-
) -> "ContextGraph":
|
| 206 |
-
"""Estimate continuation counts from a weighted sequence multiset."""
|
| 207 |
-
|
| 208 |
-
if max_order < 0:
|
| 209 |
-
raise ValueError("max_order must be non-negative")
|
| 210 |
-
|
| 211 |
-
counts: dict[Context, Counter[Symbol]] = defaultdict(Counter)
|
| 212 |
-
|
| 213 |
-
for weight, sequence in weighted_sequences:
|
| 214 |
-
if weight <= 0:
|
| 215 |
-
continue
|
| 216 |
-
tokens = tuple(sequence)
|
| 217 |
-
for index, symbol in enumerate(tokens):
|
| 218 |
-
order_limit = min(max_order, index)
|
| 219 |
-
for order in range(order_limit + 1):
|
| 220 |
-
context = tokens[index - order : index] if order else ()
|
| 221 |
-
counts[context][symbol] += weight
|
| 222 |
-
|
| 223 |
-
return cls.from_counts(counts, max_order=max_order, start_state=start_state)
|
| 224 |
-
|
| 225 |
-
@classmethod
|
| 226 |
-
def from_backoff_sequences(
|
| 227 |
-
cls,
|
| 228 |
-
sequences: Iterable[Sequence[Symbol]],
|
| 229 |
-
*,
|
| 230 |
-
max_order: int,
|
| 231 |
-
backoff_weight: float = 0.25,
|
| 232 |
-
start_state: Iterable[Symbol] | Context = (),
|
| 233 |
-
) -> "ContextGraph":
|
| 234 |
-
"""Build a sparse graph with explicit lower-order backoff support.
|
| 235 |
-
|
| 236 |
-
Each context receives a weighted mixture of normalized continuation
|
| 237 |
-
distributions from itself and its suffixes. This is useful for exact
|
| 238 |
-
experiments where a strict longest-context MLE model would make every
|
| 239 |
-
``K+1``-gram transition copied from training by construction.
|
| 240 |
-
"""
|
| 241 |
-
|
| 242 |
-
if max_order < 0:
|
| 243 |
-
raise ValueError("max_order must be non-negative")
|
| 244 |
-
if not 0.0 <= backoff_weight <= 1.0:
|
| 245 |
-
raise ValueError("backoff_weight must be in [0, 1]")
|
| 246 |
-
|
| 247 |
-
counts: dict[Context, Counter[Symbol]] = defaultdict(Counter)
|
| 248 |
-
for sequence in sequences:
|
| 249 |
-
tokens = tuple(sequence)
|
| 250 |
-
for index, symbol in enumerate(tokens):
|
| 251 |
-
order_limit = min(max_order, index)
|
| 252 |
-
for order in range(order_limit + 1):
|
| 253 |
-
context = tokens[index - order : index] if order else ()
|
| 254 |
-
counts[context][symbol] += 1
|
| 255 |
-
|
| 256 |
-
contexts = set(counts)
|
| 257 |
-
contexts.add(_as_context(start_state))
|
| 258 |
-
edges_by_state: dict[Context, list[Edge]] = {}
|
| 259 |
-
continuation_counts: dict[Context, dict[Symbol, float]] = {}
|
| 260 |
-
state_supports: dict[Context, float] = {}
|
| 261 |
-
|
| 262 |
-
for context in contexts:
|
| 263 |
-
scores: Counter[Symbol] = Counter()
|
| 264 |
-
order_scores: dict[Symbol, Counter[int]] = defaultdict(Counter)
|
| 265 |
-
context_order = len(context)
|
| 266 |
-
for order in range(context_order, -1, -1):
|
| 267 |
-
suffix = context[-order:] if order else ()
|
| 268 |
-
if suffix not in counts:
|
| 269 |
-
continue
|
| 270 |
-
total = float(sum(counts[suffix].values()))
|
| 271 |
-
if total <= 0.0:
|
| 272 |
-
continue
|
| 273 |
-
weight = backoff_weight ** (context_order - order)
|
| 274 |
-
for symbol, count in counts[suffix].items():
|
| 275 |
-
contribution = weight * (float(count) / total)
|
| 276 |
-
scores[symbol] += contribution
|
| 277 |
-
order_scores[symbol][order] += contribution
|
| 278 |
-
|
| 279 |
-
total_score = float(sum(scores.values()))
|
| 280 |
-
if total_score <= 0.0:
|
| 281 |
-
continue
|
| 282 |
-
state_supports[context] = total_score
|
| 283 |
-
continuation_counts[context] = {
|
| 284 |
-
symbol: float(score)
|
| 285 |
-
for symbol, score in scores.items()
|
| 286 |
-
if score > 0.0
|
| 287 |
-
}
|
| 288 |
-
edges_by_state[context] = [
|
| 289 |
-
Edge(
|
| 290 |
-
symbol,
|
| 291 |
-
score / total_score,
|
| 292 |
-
cls._canon(context, symbol, contexts, max_order),
|
| 293 |
-
tuple(
|
| 294 |
-
sorted(
|
| 295 |
-
(
|
| 296 |
-
(order, contribution / score)
|
| 297 |
-
for order, contribution in order_scores[symbol].items()
|
| 298 |
-
),
|
| 299 |
-
reverse=True,
|
| 300 |
-
)
|
| 301 |
-
),
|
| 302 |
-
)
|
| 303 |
-
for symbol, score in scores.items()
|
| 304 |
-
if score > 0.0
|
| 305 |
-
]
|
| 306 |
-
|
| 307 |
-
graph = cls(
|
| 308 |
-
edges_by_state,
|
| 309 |
-
start_state=start_state,
|
| 310 |
-
max_order=max_order,
|
| 311 |
-
validate=True,
|
| 312 |
-
)
|
| 313 |
-
graph._continuation_counts = continuation_counts
|
| 314 |
-
graph._state_supports = state_supports
|
| 315 |
-
return graph
|
| 316 |
-
|
| 317 |
-
@staticmethod
|
| 318 |
-
def _canon(
|
| 319 |
-
context: Context,
|
| 320 |
-
symbol: Symbol,
|
| 321 |
-
known_contexts: set[Context] | frozenset[Context],
|
| 322 |
-
max_order: int,
|
| 323 |
-
) -> Context:
|
| 324 |
-
candidate = context + (symbol,)
|
| 325 |
-
limit = min(max_order, len(candidate))
|
| 326 |
-
for order in range(limit, -1, -1):
|
| 327 |
-
suffix = candidate[-order:] if order else ()
|
| 328 |
-
if suffix in known_contexts:
|
| 329 |
-
return suffix
|
| 330 |
-
return ()
|
| 331 |
-
|
| 332 |
-
def canonical_context(self, context: Iterable[Symbol] | Context, symbol: Symbol) -> Context:
|
| 333 |
-
"""Return the longest known suffix after emitting ``symbol``."""
|
| 334 |
-
|
| 335 |
-
candidate = _as_context(context) + (symbol,)
|
| 336 |
-
limit = min(self.max_order, len(candidate))
|
| 337 |
-
for order in range(limit, -1, -1):
|
| 338 |
-
suffix = candidate[-order:] if order else ()
|
| 339 |
-
state = self._resolve_state(suffix)
|
| 340 |
-
if state in self.states:
|
| 341 |
-
return state
|
| 342 |
-
return ()
|
| 343 |
-
|
| 344 |
-
def outgoing(self, state: Iterable[Symbol] | Context) -> tuple[Edge, ...]:
|
| 345 |
-
"""Outgoing edges for a context, or an empty tuple for dead contexts."""
|
| 346 |
-
|
| 347 |
-
return self._edges.get(self._resolve_state(state), ())
|
| 348 |
-
|
| 349 |
-
def _resolve_state(self, state: Iterable[Symbol] | Context) -> Context:
|
| 350 |
-
context = _as_context(state)
|
| 351 |
-
return self._alias_to_state.get(context, context)
|
| 352 |
-
|
| 353 |
-
def edge_count(self) -> int:
|
| 354 |
-
return sum(len(edges) for edges in self._edges.values())
|
| 355 |
-
|
| 356 |
-
def probability(self, sequence: Sequence[Symbol], *, start_state: Iterable[Symbol] | Context | None = None) -> float:
|
| 357 |
-
"""Unconstrained probability of a sequence under the context graph."""
|
| 358 |
-
|
| 359 |
-
state = self.start_state if start_state is None else _as_context(start_state)
|
| 360 |
-
prob = 1.0
|
| 361 |
-
for symbol in sequence:
|
| 362 |
-
edge = next((edge for edge in self.outgoing(state) if edge.symbol == symbol), None)
|
| 363 |
-
if edge is None:
|
| 364 |
-
return 0.0
|
| 365 |
-
prob *= edge.probability
|
| 366 |
-
state = edge.next_state
|
| 367 |
-
return prob
|
| 368 |
-
|
| 369 |
-
def _validate(self) -> None:
|
| 370 |
-
if self.start_state not in self.states:
|
| 371 |
-
raise ValueError("start_state must be present in graph states")
|
| 372 |
-
if self.max_order < max((len(state) for state in self.states), default=0):
|
| 373 |
-
raise ValueError("max_order is smaller than at least one context state")
|
| 374 |
-
|
| 375 |
-
for state, edges in self._edges.items():
|
| 376 |
-
seen_symbols: set[Symbol] = set()
|
| 377 |
-
total = 0.0
|
| 378 |
-
for edge in edges:
|
| 379 |
-
if edge.symbol in seen_symbols:
|
| 380 |
-
raise ValueError(f"state {state!r} has duplicate edge for symbol {edge.symbol!r}")
|
| 381 |
-
seen_symbols.add(edge.symbol)
|
| 382 |
-
if edge.next_state not in self.states:
|
| 383 |
-
raise ValueError(f"edge from {state!r} points to unknown state {edge.next_state!r}")
|
| 384 |
-
if not math.isfinite(edge.probability) or edge.probability < 0.0:
|
| 385 |
-
raise ValueError(f"invalid probability {edge.probability!r} on edge {edge!r}")
|
| 386 |
-
order_total = 0.0
|
| 387 |
-
for order, weight in edge.order_weights:
|
| 388 |
-
if order < 0:
|
| 389 |
-
raise ValueError(f"invalid negative order {order!r} on edge {edge!r}")
|
| 390 |
-
if not math.isfinite(weight) or weight < 0.0:
|
| 391 |
-
raise ValueError(f"invalid order weight {weight!r} on edge {edge!r}")
|
| 392 |
-
order_total += weight
|
| 393 |
-
if edge.order_weights and not math.isclose(order_total, 1.0, rel_tol=1e-9, abs_tol=1e-9):
|
| 394 |
-
raise ValueError(f"order weights on edge {edge!r} sum to {order_total}, not 1")
|
| 395 |
-
total += edge.probability
|
| 396 |
-
if edges and not math.isclose(total, 1.0, rel_tol=1e-9, abs_tol=1e-9):
|
| 397 |
-
raise ValueError(f"outgoing probabilities from {state!r} sum to {total}, not 1")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/continuator.py
DELETED
|
@@ -1,238 +0,0 @@
|
|
| 1 |
-
"""Continuator-style facade over the reusable constrained BP backend.
|
| 2 |
-
|
| 3 |
-
This module does not import Continuator. It provides a small compatibility
|
| 4 |
-
surface with Continuator vocabulary so an external Continuator project can swap
|
| 5 |
-
its generation backend to ``vo_regular_bp`` with minimal glue code.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
-
from collections.abc import Callable, Iterable, Mapping, Sequence
|
| 11 |
-
from typing import TypeVar
|
| 12 |
-
|
| 13 |
-
from .adapters import (
|
| 14 |
-
EventCodec,
|
| 15 |
-
EventOrderStackBackend,
|
| 16 |
-
EventOrderStackPlan,
|
| 17 |
-
infer_symbol_to_event as infer_symbol_decoder,
|
| 18 |
-
prepare_constrained_order_stack_plan_from_events,
|
| 19 |
-
)
|
| 20 |
-
from .constraint_builders import (
|
| 21 |
-
cumulative_meter,
|
| 22 |
-
final_pitch_class,
|
| 23 |
-
meter_pattern,
|
| 24 |
-
padded_duration_total,
|
| 25 |
-
)
|
| 26 |
-
from .constraints import ConstraintSet
|
| 27 |
-
from .context import Symbol
|
| 28 |
-
from .order_stack_bp import OrderPolicy, SingletonAvoidingBackoffPolicy
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
EventT = TypeVar("EventT")
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def prepare_continuation_backend(
|
| 35 |
-
training_events: Iterable[Sequence[EventT]],
|
| 36 |
-
*,
|
| 37 |
-
prefix: Sequence[EventT],
|
| 38 |
-
horizon: int,
|
| 39 |
-
max_order: int,
|
| 40 |
-
constraints: ConstraintSet | None = None,
|
| 41 |
-
event_to_symbol: Callable[[EventT], Symbol] | None = None,
|
| 42 |
-
symbol_to_event: Mapping[Symbol, EventT] | Callable[[Symbol], EventT] | None = None,
|
| 43 |
-
infer_decoder: bool = True,
|
| 44 |
-
strict_decoder: bool = True,
|
| 45 |
-
policy: OrderPolicy | None = None,
|
| 46 |
-
start_event: EventT | None = None,
|
| 47 |
-
end_event: EventT | None = None,
|
| 48 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 49 |
-
prefer_dense_forbidden: bool = True,
|
| 50 |
-
minimize_source_graphs: bool = False,
|
| 51 |
-
) -> EventOrderStackBackend[EventT]:
|
| 52 |
-
"""Prepare a constrained order-stack backend in Continuator vocabulary.
|
| 53 |
-
|
| 54 |
-
``training_events`` and ``prefix`` are project-level event objects.
|
| 55 |
-
``event_to_symbol`` maps those events to hashable symbols consumed by the
|
| 56 |
-
backend. If no explicit ``symbol_to_event`` decoder is supplied, the
|
| 57 |
-
function can infer one from the training material when symbols identify
|
| 58 |
-
unique events.
|
| 59 |
-
"""
|
| 60 |
-
|
| 61 |
-
if horizon < 0:
|
| 62 |
-
raise ValueError("horizon must be non-negative")
|
| 63 |
-
material = tuple(tuple(sequence) for sequence in training_events)
|
| 64 |
-
prefix_tuple = tuple(prefix)
|
| 65 |
-
|
| 66 |
-
codec = _build_codec(
|
| 67 |
-
material,
|
| 68 |
-
prefix_tuple,
|
| 69 |
-
event_to_symbol=event_to_symbol,
|
| 70 |
-
symbol_to_event=symbol_to_event,
|
| 71 |
-
infer_decoder=infer_decoder,
|
| 72 |
-
strict_decoder=strict_decoder,
|
| 73 |
-
)
|
| 74 |
-
active_policy = policy if policy is not None else SingletonAvoidingBackoffPolicy()
|
| 75 |
-
|
| 76 |
-
plan = prepare_constrained_order_stack_plan_from_events(
|
| 77 |
-
material,
|
| 78 |
-
constraints,
|
| 79 |
-
codec=codec,
|
| 80 |
-
max_order=max_order,
|
| 81 |
-
length=horizon,
|
| 82 |
-
policy=active_policy,
|
| 83 |
-
start_event=start_event,
|
| 84 |
-
end_event=end_event,
|
| 85 |
-
alphabet=alphabet,
|
| 86 |
-
prefer_dense_forbidden=prefer_dense_forbidden,
|
| 87 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 88 |
-
)
|
| 89 |
-
return plan.for_prefix(prefix_tuple)
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def prepare_continuation_plan(
|
| 93 |
-
training_events: Iterable[Sequence[EventT]],
|
| 94 |
-
*,
|
| 95 |
-
horizon: int,
|
| 96 |
-
max_order: int,
|
| 97 |
-
constraints: ConstraintSet | None = None,
|
| 98 |
-
event_to_symbol: Callable[[EventT], Symbol] | None = None,
|
| 99 |
-
symbol_to_event: Mapping[Symbol, EventT] | Callable[[Symbol], EventT] | None = None,
|
| 100 |
-
infer_decoder: bool = True,
|
| 101 |
-
strict_decoder: bool = True,
|
| 102 |
-
policy: OrderPolicy | None = None,
|
| 103 |
-
start_event: EventT | None = None,
|
| 104 |
-
end_event: EventT | None = None,
|
| 105 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 106 |
-
prefer_dense_forbidden: bool = True,
|
| 107 |
-
minimize_source_graphs: bool = False,
|
| 108 |
-
) -> EventOrderStackPlan[EventT]:
|
| 109 |
-
"""Prepare a reusable Continuator-shaped plan without binding a prefix."""
|
| 110 |
-
|
| 111 |
-
if horizon < 0:
|
| 112 |
-
raise ValueError("horizon must be non-negative")
|
| 113 |
-
material = tuple(tuple(sequence) for sequence in training_events)
|
| 114 |
-
codec = _build_codec(
|
| 115 |
-
material,
|
| 116 |
-
(),
|
| 117 |
-
event_to_symbol=event_to_symbol,
|
| 118 |
-
symbol_to_event=symbol_to_event,
|
| 119 |
-
infer_decoder=infer_decoder,
|
| 120 |
-
strict_decoder=strict_decoder,
|
| 121 |
-
)
|
| 122 |
-
active_policy = policy if policy is not None else SingletonAvoidingBackoffPolicy()
|
| 123 |
-
|
| 124 |
-
return prepare_constrained_order_stack_plan_from_events(
|
| 125 |
-
material,
|
| 126 |
-
constraints,
|
| 127 |
-
codec=codec,
|
| 128 |
-
max_order=max_order,
|
| 129 |
-
length=horizon,
|
| 130 |
-
policy=active_policy,
|
| 131 |
-
start_event=start_event,
|
| 132 |
-
end_event=end_event,
|
| 133 |
-
alphabet=alphabet,
|
| 134 |
-
prefer_dense_forbidden=prefer_dense_forbidden,
|
| 135 |
-
minimize_source_graphs=minimize_source_graphs,
|
| 136 |
-
)
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
def final_pitch_class_constraint(
|
| 140 |
-
pitch_class: int,
|
| 141 |
-
*,
|
| 142 |
-
horizon: int,
|
| 143 |
-
modulo: int = 12,
|
| 144 |
-
symbol_to_pitch: Mapping[Symbol, int] | Callable[[Symbol], int] | None = None,
|
| 145 |
-
) -> ConstraintSet:
|
| 146 |
-
"""Constrain the final generated symbol to a pitch class."""
|
| 147 |
-
|
| 148 |
-
return final_pitch_class(
|
| 149 |
-
pitch_class,
|
| 150 |
-
length=horizon,
|
| 151 |
-
modulo=modulo,
|
| 152 |
-
symbol_to_pitch=symbol_to_pitch,
|
| 153 |
-
)
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
def duration_total_constraint(
|
| 157 |
-
total: int,
|
| 158 |
-
*,
|
| 159 |
-
horizon: int | None = None,
|
| 160 |
-
symbol_to_duration: Mapping[Symbol, int] | Callable[[Symbol], int],
|
| 161 |
-
max_total: int | None = None,
|
| 162 |
-
name: str = "duration_total",
|
| 163 |
-
) -> ConstraintSet:
|
| 164 |
-
"""Constrain the total generated duration/cost."""
|
| 165 |
-
|
| 166 |
-
if total < 0:
|
| 167 |
-
raise ValueError("total must be non-negative")
|
| 168 |
-
return cumulative_meter(
|
| 169 |
-
symbol_to_duration,
|
| 170 |
-
length=horizon,
|
| 171 |
-
max_cost=total if max_total is None else max_total,
|
| 172 |
-
accept_costs={total},
|
| 173 |
-
name=name,
|
| 174 |
-
)
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
def padded_duration_total_constraint(
|
| 178 |
-
total: int,
|
| 179 |
-
*,
|
| 180 |
-
horizon: int,
|
| 181 |
-
pad_symbol: Symbol,
|
| 182 |
-
symbol_to_duration: Mapping[Symbol, int] | Callable[[Symbol], int],
|
| 183 |
-
allow_zero_duration_events: bool = False,
|
| 184 |
-
name: str = "padded_duration_total",
|
| 185 |
-
) -> ConstraintSet:
|
| 186 |
-
"""Constrain generated duration with trailing PAD symbols."""
|
| 187 |
-
|
| 188 |
-
return padded_duration_total(
|
| 189 |
-
total,
|
| 190 |
-
length=horizon,
|
| 191 |
-
pad_symbol=pad_symbol,
|
| 192 |
-
symbol_to_duration=symbol_to_duration,
|
| 193 |
-
allow_zero_duration_events=allow_zero_duration_events,
|
| 194 |
-
name=name,
|
| 195 |
-
)
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
def meter_cycle_constraint(
|
| 199 |
-
cycle: Sequence[object | Iterable[object] | None],
|
| 200 |
-
*,
|
| 201 |
-
horizon: int,
|
| 202 |
-
symbol_to_meter: Mapping[Symbol, object] | Callable[[Symbol], object],
|
| 203 |
-
offset: int = 0,
|
| 204 |
-
name: str = "meter_cycle",
|
| 205 |
-
) -> ConstraintSet:
|
| 206 |
-
"""Constrain generated positions by a repeating meter/class cycle."""
|
| 207 |
-
|
| 208 |
-
if horizon < 0:
|
| 209 |
-
raise ValueError("horizon must be non-negative")
|
| 210 |
-
if not cycle:
|
| 211 |
-
raise ValueError("cycle must not be empty")
|
| 212 |
-
pattern = tuple(cycle[(offset + position) % len(cycle)] for position in range(horizon))
|
| 213 |
-
return meter_pattern(pattern, symbol_to_meter, name=name)
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
def _build_codec(
|
| 217 |
-
training_events: Sequence[Sequence[EventT]],
|
| 218 |
-
prefix: Sequence[EventT],
|
| 219 |
-
*,
|
| 220 |
-
event_to_symbol: Callable[[EventT], Symbol] | None,
|
| 221 |
-
symbol_to_event: Mapping[Symbol, EventT] | Callable[[Symbol], EventT] | None,
|
| 222 |
-
infer_decoder: bool,
|
| 223 |
-
strict_decoder: bool,
|
| 224 |
-
) -> EventCodec[EventT]:
|
| 225 |
-
if event_to_symbol is None:
|
| 226 |
-
codec = EventCodec.identity()
|
| 227 |
-
if symbol_to_event is not None:
|
| 228 |
-
return codec.with_decoder(symbol_to_event) # type: ignore[arg-type, return-value]
|
| 229 |
-
return codec # type: ignore[return-value]
|
| 230 |
-
|
| 231 |
-
decoder = symbol_to_event
|
| 232 |
-
if decoder is None and infer_decoder:
|
| 233 |
-
decoder = infer_symbol_decoder(
|
| 234 |
-
tuple(training_events) + (tuple(prefix),),
|
| 235 |
-
event_to_symbol,
|
| 236 |
-
strict=strict_decoder,
|
| 237 |
-
)
|
| 238 |
-
return EventCodec(event_to_symbol=event_to_symbol, symbol_to_event=decoder)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/experimental.py
DELETED
|
@@ -1,986 +0,0 @@
|
|
| 1 |
-
"""Experimental source-model transformations.
|
| 2 |
-
|
| 3 |
-
The functions in this module are explicit modeling operations. They may change
|
| 4 |
-
the source distribution, unlike exact minimization in :mod:`vo_regular_bp.minimization`.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
from __future__ import annotations
|
| 8 |
-
|
| 9 |
-
from collections import Counter, defaultdict
|
| 10 |
-
from collections.abc import Callable, Hashable, Mapping
|
| 11 |
-
from dataclasses import dataclass
|
| 12 |
-
import math
|
| 13 |
-
import time
|
| 14 |
-
from typing import Any
|
| 15 |
-
|
| 16 |
-
from .context import Context, ContextGraph, Edge, Symbol
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
@dataclass(frozen=True)
|
| 20 |
-
class AlergiaMergeMetadata:
|
| 21 |
-
"""Diagnostics for an experimental ALERGIA-style source merge."""
|
| 22 |
-
|
| 23 |
-
method: str
|
| 24 |
-
alpha: float
|
| 25 |
-
min_support: float
|
| 26 |
-
recursive: bool
|
| 27 |
-
original_state_count: int
|
| 28 |
-
merged_state_count: int
|
| 29 |
-
original_edge_count: int
|
| 30 |
-
merged_edge_count: int
|
| 31 |
-
state_to_class: dict[Context, int]
|
| 32 |
-
classes: tuple[tuple[Context, ...], ...]
|
| 33 |
-
conflicting_symbol_destinations: int = 0
|
| 34 |
-
symbol_projection: str | None = None
|
| 35 |
-
transition_projection: str | None = None
|
| 36 |
-
projection_kind: str = "identity"
|
| 37 |
-
|
| 38 |
-
@property
|
| 39 |
-
def state_compression_ratio(self) -> float:
|
| 40 |
-
return _ratio(self.original_state_count, self.merged_state_count)
|
| 41 |
-
|
| 42 |
-
@property
|
| 43 |
-
def edge_compression_ratio(self) -> float:
|
| 44 |
-
return _ratio(self.original_edge_count, self.merged_edge_count)
|
| 45 |
-
|
| 46 |
-
def as_dict(self) -> dict[str, object]:
|
| 47 |
-
return {
|
| 48 |
-
"method": self.method,
|
| 49 |
-
"alpha": self.alpha,
|
| 50 |
-
"min_support": self.min_support,
|
| 51 |
-
"recursive": self.recursive,
|
| 52 |
-
"original_state_count": self.original_state_count,
|
| 53 |
-
"merged_state_count": self.merged_state_count,
|
| 54 |
-
"original_edge_count": self.original_edge_count,
|
| 55 |
-
"merged_edge_count": self.merged_edge_count,
|
| 56 |
-
"state_compression_ratio": self.state_compression_ratio,
|
| 57 |
-
"edge_compression_ratio": self.edge_compression_ratio,
|
| 58 |
-
"conflicting_symbol_destinations": self.conflicting_symbol_destinations,
|
| 59 |
-
"symbol_projection": self.symbol_projection,
|
| 60 |
-
"transition_projection": self.transition_projection,
|
| 61 |
-
"projection_kind": self.projection_kind,
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
@dataclass(frozen=True)
|
| 66 |
-
class AlergiaFixedOrderMergeMetadata:
|
| 67 |
-
"""Diagnostics for an experimental ALERGIA merge of one fixed-order graph."""
|
| 68 |
-
|
| 69 |
-
method: str
|
| 70 |
-
order: int
|
| 71 |
-
alpha: float
|
| 72 |
-
min_support: float
|
| 73 |
-
recursive: bool
|
| 74 |
-
original_state_count: int
|
| 75 |
-
merged_state_count: int
|
| 76 |
-
original_edge_count: int
|
| 77 |
-
merged_edge_count: int
|
| 78 |
-
state_to_class: dict[Context, int]
|
| 79 |
-
classes: tuple[tuple[Context, ...], ...]
|
| 80 |
-
conflicting_symbol_destinations: int = 0
|
| 81 |
-
conflicting_edge_orders: int = 0
|
| 82 |
-
symbol_projection: str | None = None
|
| 83 |
-
transition_projection: str | None = None
|
| 84 |
-
projection_kind: str = "identity"
|
| 85 |
-
merge_time_seconds: float = 0.0
|
| 86 |
-
|
| 87 |
-
@property
|
| 88 |
-
def state_compression_ratio(self) -> float:
|
| 89 |
-
return _ratio(self.original_state_count, self.merged_state_count)
|
| 90 |
-
|
| 91 |
-
@property
|
| 92 |
-
def edge_compression_ratio(self) -> float:
|
| 93 |
-
return _ratio(self.original_edge_count, self.merged_edge_count)
|
| 94 |
-
|
| 95 |
-
def as_dict(self) -> dict[str, object]:
|
| 96 |
-
return {
|
| 97 |
-
"method": self.method,
|
| 98 |
-
"order": self.order,
|
| 99 |
-
"alpha": self.alpha,
|
| 100 |
-
"min_support": self.min_support,
|
| 101 |
-
"recursive": self.recursive,
|
| 102 |
-
"original_state_count": self.original_state_count,
|
| 103 |
-
"merged_state_count": self.merged_state_count,
|
| 104 |
-
"original_edge_count": self.original_edge_count,
|
| 105 |
-
"merged_edge_count": self.merged_edge_count,
|
| 106 |
-
"state_compression_ratio": self.state_compression_ratio,
|
| 107 |
-
"edge_compression_ratio": self.edge_compression_ratio,
|
| 108 |
-
"conflicting_symbol_destinations": self.conflicting_symbol_destinations,
|
| 109 |
-
"conflicting_edge_orders": self.conflicting_edge_orders,
|
| 110 |
-
"symbol_projection": self.symbol_projection,
|
| 111 |
-
"transition_projection": self.transition_projection,
|
| 112 |
-
"projection_kind": self.projection_kind,
|
| 113 |
-
"merge_time_seconds": self.merge_time_seconds,
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
@dataclass(frozen=True)
|
| 118 |
-
class AlergiaOrderStackMergeMetadata:
|
| 119 |
-
"""Diagnostics for an experimental ALERGIA-merged order-stack model."""
|
| 120 |
-
|
| 121 |
-
method: str
|
| 122 |
-
alpha: float
|
| 123 |
-
min_support: float
|
| 124 |
-
recursive: bool
|
| 125 |
-
orders: dict[int, AlergiaFixedOrderMergeMetadata]
|
| 126 |
-
|
| 127 |
-
@property
|
| 128 |
-
def original_state_count(self) -> int:
|
| 129 |
-
return sum(metadata.original_state_count for metadata in self.orders.values())
|
| 130 |
-
|
| 131 |
-
@property
|
| 132 |
-
def merged_state_count(self) -> int:
|
| 133 |
-
return sum(metadata.merged_state_count for metadata in self.orders.values())
|
| 134 |
-
|
| 135 |
-
@property
|
| 136 |
-
def original_edge_count(self) -> int:
|
| 137 |
-
return sum(metadata.original_edge_count for metadata in self.orders.values())
|
| 138 |
-
|
| 139 |
-
@property
|
| 140 |
-
def merged_edge_count(self) -> int:
|
| 141 |
-
return sum(metadata.merged_edge_count for metadata in self.orders.values())
|
| 142 |
-
|
| 143 |
-
@property
|
| 144 |
-
def merge_time_seconds(self) -> float:
|
| 145 |
-
return sum(metadata.merge_time_seconds for metadata in self.orders.values())
|
| 146 |
-
|
| 147 |
-
@property
|
| 148 |
-
def state_compression_ratio(self) -> float:
|
| 149 |
-
return _ratio(self.original_state_count, self.merged_state_count)
|
| 150 |
-
|
| 151 |
-
@property
|
| 152 |
-
def edge_compression_ratio(self) -> float:
|
| 153 |
-
return _ratio(self.original_edge_count, self.merged_edge_count)
|
| 154 |
-
|
| 155 |
-
def as_dict(self) -> dict[str, object]:
|
| 156 |
-
return {
|
| 157 |
-
"method": self.method,
|
| 158 |
-
"alpha": self.alpha,
|
| 159 |
-
"min_support": self.min_support,
|
| 160 |
-
"recursive": self.recursive,
|
| 161 |
-
"original_state_count": self.original_state_count,
|
| 162 |
-
"merged_state_count": self.merged_state_count,
|
| 163 |
-
"original_edge_count": self.original_edge_count,
|
| 164 |
-
"merged_edge_count": self.merged_edge_count,
|
| 165 |
-
"state_compression_ratio": self.state_compression_ratio,
|
| 166 |
-
"edge_compression_ratio": self.edge_compression_ratio,
|
| 167 |
-
"merge_time_seconds": self.merge_time_seconds,
|
| 168 |
-
"orders": {
|
| 169 |
-
order: metadata.as_dict()
|
| 170 |
-
for order, metadata in sorted(self.orders.items())
|
| 171 |
-
},
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
@dataclass(frozen=True)
|
| 176 |
-
class _ProjectionCache:
|
| 177 |
-
counts: dict[Context, dict[Hashable, float]]
|
| 178 |
-
dominant_next: dict[Context, dict[Hashable, Context]]
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
_PairMemo = dict[tuple[Context, Context], tuple[bool, tuple[tuple[Context, Context], ...]]]
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
def alergia_merge(
|
| 185 |
-
graph: ContextGraph,
|
| 186 |
-
*,
|
| 187 |
-
alpha: float = 0.01,
|
| 188 |
-
min_support: int | float = 10,
|
| 189 |
-
recursive: bool = True,
|
| 190 |
-
symbol_projection: Callable[[Symbol], Hashable] | None = None,
|
| 191 |
-
transition_projection: Callable[[Context, Symbol, Edge], Hashable] | None = None,
|
| 192 |
-
) -> ContextGraph:
|
| 193 |
-
"""Return an ALERGIA-like approximate source-state merge.
|
| 194 |
-
|
| 195 |
-
This function acts only on the unconstrained source graph. ``symbol_projection``
|
| 196 |
-
lets a caller compare emitted symbols through a feature map. For
|
| 197 |
-
context-aware semantics, ``transition_projection`` can instead map
|
| 198 |
-
``(state, symbol, edge)`` to a comparison feature, for example an interval
|
| 199 |
-
from the current context to the emitted symbol. If both projections are
|
| 200 |
-
supplied, ``transition_projection`` takes precedence. The returned graph
|
| 201 |
-
still emits concrete symbols and remains a regular :class:`ContextGraph`,
|
| 202 |
-
so existing BP remains exact with respect to the new merged source model.
|
| 203 |
-
"""
|
| 204 |
-
|
| 205 |
-
if alpha <= 0.0 or alpha >= 1.0:
|
| 206 |
-
raise ValueError("alpha must be in (0, 1)")
|
| 207 |
-
if min_support < 0:
|
| 208 |
-
raise ValueError("min_support must be non-negative")
|
| 209 |
-
|
| 210 |
-
counts = _state_counts(graph)
|
| 211 |
-
project = _transition_projector(
|
| 212 |
-
symbol_projection=symbol_projection,
|
| 213 |
-
transition_projection=transition_projection,
|
| 214 |
-
)
|
| 215 |
-
projection_cache = _precompute_projection_cache(graph, counts, project)
|
| 216 |
-
supports = {
|
| 217 |
-
state: float(sum(symbol_counts.values()))
|
| 218 |
-
for state, symbol_counts in counts.items()
|
| 219 |
-
}
|
| 220 |
-
states = tuple(sorted(graph.states, key=_context_sort_key))
|
| 221 |
-
classes = _ClassRegistry(states)
|
| 222 |
-
pair_memo: _PairMemo = {}
|
| 223 |
-
|
| 224 |
-
for state in states:
|
| 225 |
-
if supports.get(state, 0.0) < min_support:
|
| 226 |
-
continue
|
| 227 |
-
for root in classes.roots():
|
| 228 |
-
if classes.find(state) == root:
|
| 229 |
-
break
|
| 230 |
-
members = classes.members(root)
|
| 231 |
-
compatible, pairs = _compatible_with_class(
|
| 232 |
-
graph,
|
| 233 |
-
counts,
|
| 234 |
-
supports,
|
| 235 |
-
state,
|
| 236 |
-
members,
|
| 237 |
-
alpha=float(alpha),
|
| 238 |
-
min_support=float(min_support),
|
| 239 |
-
recursive=recursive,
|
| 240 |
-
projection_cache=projection_cache,
|
| 241 |
-
pair_memo=pair_memo,
|
| 242 |
-
)
|
| 243 |
-
if compatible:
|
| 244 |
-
classes.union(state, members[0])
|
| 245 |
-
for left, right in pairs:
|
| 246 |
-
classes.union(left, right)
|
| 247 |
-
break
|
| 248 |
-
|
| 249 |
-
return _build_merged_graph(
|
| 250 |
-
graph,
|
| 251 |
-
counts,
|
| 252 |
-
classes.union_find,
|
| 253 |
-
alpha=float(alpha),
|
| 254 |
-
min_support=float(min_support),
|
| 255 |
-
recursive=recursive,
|
| 256 |
-
symbol_projection_name=_projection_name(symbol_projection),
|
| 257 |
-
transition_projection_name=_projection_name(transition_projection),
|
| 258 |
-
projection_kind=_projection_kind(symbol_projection, transition_projection),
|
| 259 |
-
)
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
def alergia_metadata(graph: object) -> (
|
| 263 |
-
AlergiaMergeMetadata
|
| 264 |
-
| AlergiaFixedOrderMergeMetadata
|
| 265 |
-
| AlergiaOrderStackMergeMetadata
|
| 266 |
-
| None
|
| 267 |
-
):
|
| 268 |
-
"""Return ALERGIA metadata if ``graph`` was produced by an experimental merge."""
|
| 269 |
-
|
| 270 |
-
return getattr(graph, "alergia_metadata", None)
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
def alergia_merge_fixed_order_graph(
|
| 274 |
-
graph: object,
|
| 275 |
-
*,
|
| 276 |
-
alpha: float = 0.01,
|
| 277 |
-
min_support: int | float = 10,
|
| 278 |
-
recursive: bool = True,
|
| 279 |
-
symbol_projection: Callable[[Symbol], Hashable] | None = None,
|
| 280 |
-
transition_projection: Callable[[Context, Symbol, Edge], Hashable] | None = None,
|
| 281 |
-
continuation_counts: Mapping[Context, Mapping[Symbol, int | float]] | None = None,
|
| 282 |
-
) -> object:
|
| 283 |
-
"""Return an experimental ALERGIA-merged fixed-order order-stack graph.
|
| 284 |
-
|
| 285 |
-
The merge is applied to one unconstrained fixed-order source graph. The
|
| 286 |
-
result is still a ``FixedOrderContextGraph`` and can be used by the existing
|
| 287 |
-
order-stack BP backends without a special sampling path.
|
| 288 |
-
"""
|
| 289 |
-
|
| 290 |
-
started = time.perf_counter()
|
| 291 |
-
context_graph = _fixed_order_to_context_graph(
|
| 292 |
-
graph,
|
| 293 |
-
continuation_counts=continuation_counts,
|
| 294 |
-
)
|
| 295 |
-
merged_context_graph = alergia_merge(
|
| 296 |
-
context_graph,
|
| 297 |
-
alpha=alpha,
|
| 298 |
-
min_support=min_support,
|
| 299 |
-
recursive=recursive,
|
| 300 |
-
symbol_projection=symbol_projection,
|
| 301 |
-
transition_projection=transition_projection,
|
| 302 |
-
)
|
| 303 |
-
merged_graph, conflicting_edge_orders = _context_graph_to_fixed_order_graph(
|
| 304 |
-
merged_context_graph,
|
| 305 |
-
graph,
|
| 306 |
-
)
|
| 307 |
-
source_metadata = alergia_metadata(merged_context_graph)
|
| 308 |
-
if not isinstance(source_metadata, AlergiaMergeMetadata):
|
| 309 |
-
raise RuntimeError("missing ContextGraph ALERGIA metadata")
|
| 310 |
-
merged_graph.alergia_metadata = AlergiaFixedOrderMergeMetadata(
|
| 311 |
-
method="alergia_fixed_order",
|
| 312 |
-
order=int(graph.order),
|
| 313 |
-
alpha=float(alpha),
|
| 314 |
-
min_support=float(min_support),
|
| 315 |
-
recursive=recursive,
|
| 316 |
-
original_state_count=source_metadata.original_state_count,
|
| 317 |
-
merged_state_count=source_metadata.merged_state_count,
|
| 318 |
-
original_edge_count=source_metadata.original_edge_count,
|
| 319 |
-
merged_edge_count=source_metadata.merged_edge_count,
|
| 320 |
-
state_to_class=source_metadata.state_to_class,
|
| 321 |
-
classes=source_metadata.classes,
|
| 322 |
-
conflicting_symbol_destinations=source_metadata.conflicting_symbol_destinations,
|
| 323 |
-
conflicting_edge_orders=conflicting_edge_orders,
|
| 324 |
-
symbol_projection=source_metadata.symbol_projection,
|
| 325 |
-
transition_projection=source_metadata.transition_projection,
|
| 326 |
-
projection_kind=source_metadata.projection_kind,
|
| 327 |
-
merge_time_seconds=time.perf_counter() - started,
|
| 328 |
-
)
|
| 329 |
-
return merged_graph
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
def alergia_merge_order_stack_model(
|
| 333 |
-
model: object,
|
| 334 |
-
*,
|
| 335 |
-
alpha: float = 0.01,
|
| 336 |
-
min_support: int | float = 10,
|
| 337 |
-
recursive: bool = True,
|
| 338 |
-
symbol_projection: Callable[[Symbol], Hashable] | None = None,
|
| 339 |
-
transition_projection: Callable[[Context, Symbol, Edge], Hashable] | None = None,
|
| 340 |
-
) -> "AlergiaOrderStackModel":
|
| 341 |
-
"""Return an experimental order-stack model with ALERGIA-merged sources.
|
| 342 |
-
|
| 343 |
-
Each fixed-order graph is merged independently. The returned model exposes
|
| 344 |
-
the same ``compile_graph(order)`` interface used by the existing order-stack
|
| 345 |
-
BP preparation functions, so sampling APIs do not need a special path.
|
| 346 |
-
"""
|
| 347 |
-
|
| 348 |
-
graphs = {}
|
| 349 |
-
metadata_by_order = {}
|
| 350 |
-
for order in range(1, int(model.max_order) + 1):
|
| 351 |
-
graph = model.compile_graph(order)
|
| 352 |
-
merged_graph = alergia_merge_fixed_order_graph(
|
| 353 |
-
graph,
|
| 354 |
-
alpha=alpha,
|
| 355 |
-
min_support=min_support,
|
| 356 |
-
recursive=recursive,
|
| 357 |
-
symbol_projection=symbol_projection,
|
| 358 |
-
transition_projection=transition_projection,
|
| 359 |
-
continuation_counts=_fixed_order_counts_from_model(model, graph),
|
| 360 |
-
)
|
| 361 |
-
graphs[order] = merged_graph
|
| 362 |
-
metadata = alergia_metadata(merged_graph)
|
| 363 |
-
if not isinstance(metadata, AlergiaFixedOrderMergeMetadata):
|
| 364 |
-
raise RuntimeError("missing fixed-order ALERGIA metadata")
|
| 365 |
-
metadata_by_order[order] = metadata
|
| 366 |
-
|
| 367 |
-
return AlergiaOrderStackModel(
|
| 368 |
-
base_model=model,
|
| 369 |
-
graphs=graphs,
|
| 370 |
-
metadata=AlergiaOrderStackMergeMetadata(
|
| 371 |
-
method="alergia_order_stack",
|
| 372 |
-
alpha=float(alpha),
|
| 373 |
-
min_support=float(min_support),
|
| 374 |
-
recursive=recursive,
|
| 375 |
-
orders=metadata_by_order,
|
| 376 |
-
),
|
| 377 |
-
)
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
class AlergiaOrderStackModel:
|
| 381 |
-
"""Experimental order-stack model backed by ALERGIA-merged fixed-order graphs."""
|
| 382 |
-
|
| 383 |
-
def __init__(
|
| 384 |
-
self,
|
| 385 |
-
*,
|
| 386 |
-
base_model: object,
|
| 387 |
-
graphs: dict[int, object],
|
| 388 |
-
metadata: AlergiaOrderStackMergeMetadata,
|
| 389 |
-
) -> None:
|
| 390 |
-
self.base_model = base_model
|
| 391 |
-
self.max_order = int(base_model.max_order)
|
| 392 |
-
self.forbidden_symbols = frozenset(getattr(base_model, "forbidden_symbols", ()))
|
| 393 |
-
self.alphabet = frozenset(getattr(base_model, "alphabet", ()))
|
| 394 |
-
self._graphs = dict(graphs)
|
| 395 |
-
self.alergia_metadata = metadata
|
| 396 |
-
|
| 397 |
-
def compile_graph(self, order: int) -> object:
|
| 398 |
-
if order < 1 or order > self.max_order:
|
| 399 |
-
raise ValueError(f"order must be between 1 and {self.max_order}")
|
| 400 |
-
return self._graphs[int(order)]
|
| 401 |
-
|
| 402 |
-
def compile_graphs_for_plan(self, *, length: int) -> dict[int, object]:
|
| 403 |
-
return dict(self._graphs)
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
def _compatible_with_class(
|
| 407 |
-
graph: ContextGraph,
|
| 408 |
-
counts: dict[Context, dict[Symbol, float]],
|
| 409 |
-
supports: dict[Context, float],
|
| 410 |
-
state: Context,
|
| 411 |
-
members: list[Context],
|
| 412 |
-
*,
|
| 413 |
-
alpha: float,
|
| 414 |
-
min_support: float,
|
| 415 |
-
recursive: bool,
|
| 416 |
-
projection_cache: _ProjectionCache,
|
| 417 |
-
pair_memo: _PairMemo,
|
| 418 |
-
) -> tuple[bool, list[tuple[Context, Context]]]:
|
| 419 |
-
pairs: list[tuple[Context, Context]] = []
|
| 420 |
-
for member in members:
|
| 421 |
-
compatible, member_pairs = _compatible_pair(
|
| 422 |
-
graph,
|
| 423 |
-
counts,
|
| 424 |
-
supports,
|
| 425 |
-
state,
|
| 426 |
-
member,
|
| 427 |
-
alpha=alpha,
|
| 428 |
-
min_support=min_support,
|
| 429 |
-
recursive=recursive,
|
| 430 |
-
projection_cache=projection_cache,
|
| 431 |
-
pair_memo=pair_memo,
|
| 432 |
-
seen=set(),
|
| 433 |
-
)
|
| 434 |
-
if not compatible:
|
| 435 |
-
return False, []
|
| 436 |
-
pairs.extend(member_pairs)
|
| 437 |
-
return True, pairs
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
def _compatible_pair(
|
| 441 |
-
graph: ContextGraph,
|
| 442 |
-
counts: dict[Context, dict[Symbol, float]],
|
| 443 |
-
supports: dict[Context, float],
|
| 444 |
-
left: Context,
|
| 445 |
-
right: Context,
|
| 446 |
-
*,
|
| 447 |
-
alpha: float,
|
| 448 |
-
min_support: float,
|
| 449 |
-
recursive: bool,
|
| 450 |
-
projection_cache: _ProjectionCache,
|
| 451 |
-
pair_memo: _PairMemo,
|
| 452 |
-
seen: set[tuple[Context, Context]],
|
| 453 |
-
) -> tuple[bool, list[tuple[Context, Context]]]:
|
| 454 |
-
if left == right:
|
| 455 |
-
return True, []
|
| 456 |
-
pair = (left, right) if _context_sort_key(left) <= _context_sort_key(right) else (right, left)
|
| 457 |
-
if pair in seen:
|
| 458 |
-
return True, []
|
| 459 |
-
cache_positive = not seen
|
| 460 |
-
cached = pair_memo.get(pair)
|
| 461 |
-
if cached is not None:
|
| 462 |
-
compatible, pairs = cached
|
| 463 |
-
if not compatible or cache_positive:
|
| 464 |
-
return compatible, list(pairs)
|
| 465 |
-
seen.add(pair)
|
| 466 |
-
|
| 467 |
-
left_counts = counts.get(left, {})
|
| 468 |
-
right_counts = counts.get(right, {})
|
| 469 |
-
left_support = supports.get(left, 0.0)
|
| 470 |
-
right_support = supports.get(right, 0.0)
|
| 471 |
-
if left_support == 0.0 and right_support == 0.0 and not left_counts and not right_counts:
|
| 472 |
-
return _cache_pair_result(pair_memo, pair, True, [pair], cache_positive)
|
| 473 |
-
if left_support <= 0.0 or right_support <= 0.0:
|
| 474 |
-
return _cache_pair_result(pair_memo, pair, False, [], cache_positive)
|
| 475 |
-
if left_support < min_support or right_support < min_support:
|
| 476 |
-
return _cache_pair_result(pair_memo, pair, False, [], cache_positive)
|
| 477 |
-
|
| 478 |
-
left_projected = projection_cache.counts.get(left, {})
|
| 479 |
-
right_projected = projection_cache.counts.get(right, {})
|
| 480 |
-
bound = _hoeffding_bound(left_support, right_support, alpha)
|
| 481 |
-
for projected_symbol in set(left_projected) | set(right_projected):
|
| 482 |
-
left_probability = left_projected.get(projected_symbol, 0.0) / left_support
|
| 483 |
-
right_probability = right_projected.get(projected_symbol, 0.0) / right_support
|
| 484 |
-
if abs(left_probability - right_probability) > bound:
|
| 485 |
-
return _cache_pair_result(pair_memo, pair, False, [], cache_positive)
|
| 486 |
-
|
| 487 |
-
pairs = [pair]
|
| 488 |
-
if recursive:
|
| 489 |
-
left_next = projection_cache.dominant_next.get(left, {})
|
| 490 |
-
right_next = projection_cache.dominant_next.get(right, {})
|
| 491 |
-
for projected_symbol in set(left_next) & set(right_next):
|
| 492 |
-
compatible, child_pairs = _compatible_pair(
|
| 493 |
-
graph,
|
| 494 |
-
counts,
|
| 495 |
-
supports,
|
| 496 |
-
left_next[projected_symbol],
|
| 497 |
-
right_next[projected_symbol],
|
| 498 |
-
alpha=alpha,
|
| 499 |
-
min_support=min_support,
|
| 500 |
-
recursive=True,
|
| 501 |
-
projection_cache=projection_cache,
|
| 502 |
-
pair_memo=pair_memo,
|
| 503 |
-
seen=seen,
|
| 504 |
-
)
|
| 505 |
-
if not compatible:
|
| 506 |
-
return _cache_pair_result(pair_memo, pair, False, [], cache_positive)
|
| 507 |
-
pairs.extend(child_pairs)
|
| 508 |
-
return _cache_pair_result(pair_memo, pair, True, pairs, cache_positive)
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
def _build_merged_graph(
|
| 512 |
-
graph: ContextGraph,
|
| 513 |
-
counts: dict[Context, dict[Symbol, float]],
|
| 514 |
-
union: "_UnionFind",
|
| 515 |
-
*,
|
| 516 |
-
alpha: float,
|
| 517 |
-
min_support: float,
|
| 518 |
-
recursive: bool,
|
| 519 |
-
symbol_projection_name: str | None,
|
| 520 |
-
transition_projection_name: str | None,
|
| 521 |
-
projection_kind: str,
|
| 522 |
-
) -> ContextGraph:
|
| 523 |
-
states = tuple(sorted(graph.states, key=_context_sort_key))
|
| 524 |
-
root_to_members: dict[Context, list[Context]] = defaultdict(list)
|
| 525 |
-
for state in states:
|
| 526 |
-
root_to_members[union.find(state)].append(state)
|
| 527 |
-
|
| 528 |
-
roots = tuple(
|
| 529 |
-
sorted(
|
| 530 |
-
root_to_members,
|
| 531 |
-
key=lambda root: _context_sort_key(root_to_members[root][0]),
|
| 532 |
-
)
|
| 533 |
-
)
|
| 534 |
-
root_to_class = {root: class_id for class_id, root in enumerate(roots)}
|
| 535 |
-
representative_by_root = {
|
| 536 |
-
root: tuple(sorted(members, key=_context_sort_key))[0]
|
| 537 |
-
for root, members in root_to_members.items()
|
| 538 |
-
}
|
| 539 |
-
|
| 540 |
-
edges_by_state: dict[Context, list[Edge]] = {}
|
| 541 |
-
merged_counts: dict[Context, dict[Symbol, float]] = {}
|
| 542 |
-
merged_supports: dict[Context, float] = {}
|
| 543 |
-
conflicting_symbol_destinations = 0
|
| 544 |
-
|
| 545 |
-
for root in roots:
|
| 546 |
-
representative = representative_by_root[root]
|
| 547 |
-
symbol_counts: Counter[Symbol] = Counter()
|
| 548 |
-
destination_counts: dict[Symbol, Counter[Context]] = defaultdict(Counter)
|
| 549 |
-
order_scores: dict[Symbol, Counter[int]] = defaultdict(Counter)
|
| 550 |
-
|
| 551 |
-
for member in root_to_members[root]:
|
| 552 |
-
member_counts = counts.get(member, {})
|
| 553 |
-
for edge in graph.outgoing(member):
|
| 554 |
-
count = member_counts.get(edge.symbol, 0.0)
|
| 555 |
-
if count <= 0.0:
|
| 556 |
-
continue
|
| 557 |
-
symbol_counts[edge.symbol] += count
|
| 558 |
-
destination_counts[edge.symbol][union.find(edge.next_state)] += count
|
| 559 |
-
for order, weight in edge.order_weights:
|
| 560 |
-
order_scores[edge.symbol][int(order)] += count * float(weight)
|
| 561 |
-
|
| 562 |
-
total = float(sum(symbol_counts.values()))
|
| 563 |
-
merged_supports[representative] = total
|
| 564 |
-
merged_counts[representative] = {
|
| 565 |
-
symbol: float(count)
|
| 566 |
-
for symbol, count in symbol_counts.items()
|
| 567 |
-
if count > 0.0
|
| 568 |
-
}
|
| 569 |
-
if total <= 0.0:
|
| 570 |
-
edges_by_state[representative] = []
|
| 571 |
-
continue
|
| 572 |
-
|
| 573 |
-
state_edges: list[Edge] = []
|
| 574 |
-
for symbol, count in sorted(symbol_counts.items(), key=lambda item: _hashable_key(item[0])):
|
| 575 |
-
destinations = destination_counts[symbol]
|
| 576 |
-
if len(destinations) > 1:
|
| 577 |
-
conflicting_symbol_destinations += 1
|
| 578 |
-
destination_root = max(
|
| 579 |
-
destinations,
|
| 580 |
-
key=lambda candidate: (destinations[candidate], repr(candidate)),
|
| 581 |
-
)
|
| 582 |
-
order_weights = ()
|
| 583 |
-
if order_scores[symbol]:
|
| 584 |
-
order_total = float(sum(order_scores[symbol].values()))
|
| 585 |
-
if order_total > 0.0:
|
| 586 |
-
order_weights = tuple(
|
| 587 |
-
sorted(
|
| 588 |
-
(
|
| 589 |
-
(order, score / order_total)
|
| 590 |
-
for order, score in order_scores[symbol].items()
|
| 591 |
-
),
|
| 592 |
-
reverse=True,
|
| 593 |
-
)
|
| 594 |
-
)
|
| 595 |
-
state_edges.append(
|
| 596 |
-
Edge(
|
| 597 |
-
symbol=symbol,
|
| 598 |
-
probability=float(count) / total,
|
| 599 |
-
next_state=representative_by_root[destination_root],
|
| 600 |
-
order_weights=order_weights,
|
| 601 |
-
)
|
| 602 |
-
)
|
| 603 |
-
edges_by_state[representative] = state_edges
|
| 604 |
-
|
| 605 |
-
start_state = representative_by_root[union.find(graph._resolve_state(graph.start_state))]
|
| 606 |
-
merged = ContextGraph(
|
| 607 |
-
edges_by_state,
|
| 608 |
-
start_state=start_state,
|
| 609 |
-
max_order=graph.max_order,
|
| 610 |
-
alphabet=graph.alphabet,
|
| 611 |
-
validate=True,
|
| 612 |
-
)
|
| 613 |
-
|
| 614 |
-
alias_to_state = {
|
| 615 |
-
state: representative_by_root[union.find(state)]
|
| 616 |
-
for state in states
|
| 617 |
-
}
|
| 618 |
-
for alias, resolved in getattr(graph, "_alias_to_state", {}).items():
|
| 619 |
-
alias_to_state[alias] = representative_by_root[union.find(resolved)]
|
| 620 |
-
merged._alias_to_state = alias_to_state
|
| 621 |
-
merged._continuation_counts = merged_counts
|
| 622 |
-
merged._state_supports = merged_supports
|
| 623 |
-
|
| 624 |
-
state_to_class = {
|
| 625 |
-
state: root_to_class[union.find(state)]
|
| 626 |
-
for state in states
|
| 627 |
-
}
|
| 628 |
-
classes = tuple(
|
| 629 |
-
tuple(sorted(root_to_members[root], key=_context_sort_key))
|
| 630 |
-
for root in roots
|
| 631 |
-
)
|
| 632 |
-
merged.alergia_metadata = AlergiaMergeMetadata(
|
| 633 |
-
method="alergia",
|
| 634 |
-
alpha=alpha,
|
| 635 |
-
min_support=min_support,
|
| 636 |
-
recursive=recursive,
|
| 637 |
-
original_state_count=len(graph.states),
|
| 638 |
-
merged_state_count=len(merged.states),
|
| 639 |
-
original_edge_count=graph.edge_count(),
|
| 640 |
-
merged_edge_count=merged.edge_count(),
|
| 641 |
-
state_to_class=state_to_class,
|
| 642 |
-
classes=classes,
|
| 643 |
-
conflicting_symbol_destinations=conflicting_symbol_destinations,
|
| 644 |
-
symbol_projection=symbol_projection_name,
|
| 645 |
-
transition_projection=transition_projection_name,
|
| 646 |
-
projection_kind=projection_kind,
|
| 647 |
-
)
|
| 648 |
-
return merged
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
def _fixed_order_to_context_graph(
|
| 652 |
-
graph: object,
|
| 653 |
-
*,
|
| 654 |
-
continuation_counts: Mapping[Context, Mapping[Symbol, int | float]] | None,
|
| 655 |
-
) -> ContextGraph:
|
| 656 |
-
contexts = tuple(graph.contexts)
|
| 657 |
-
edges_by_state: dict[Context, list[Edge]] = {}
|
| 658 |
-
count_metadata: dict[Context, dict[Symbol, float]] = {}
|
| 659 |
-
state_supports: dict[Context, float] = {}
|
| 660 |
-
|
| 661 |
-
for state_id, context in enumerate(contexts):
|
| 662 |
-
raw_counts = continuation_counts.get(context) if continuation_counts is not None else None
|
| 663 |
-
support = (
|
| 664 |
-
float(sum(count for count in raw_counts.values() if count > 0))
|
| 665 |
-
if raw_counts is not None
|
| 666 |
-
else 1.0
|
| 667 |
-
)
|
| 668 |
-
if support <= 0.0 and graph.outgoing[state_id]:
|
| 669 |
-
support = 1.0
|
| 670 |
-
state_counts: dict[Symbol, float] = {}
|
| 671 |
-
state_edges: list[Edge] = []
|
| 672 |
-
for edge in graph.outgoing[state_id]:
|
| 673 |
-
count = (
|
| 674 |
-
float(raw_counts.get(edge.symbol, edge.probability * support))
|
| 675 |
-
if raw_counts is not None
|
| 676 |
-
else edge.probability * support
|
| 677 |
-
)
|
| 678 |
-
if count > 0.0:
|
| 679 |
-
state_counts[edge.symbol] = count
|
| 680 |
-
state_edges.append(
|
| 681 |
-
Edge(
|
| 682 |
-
symbol=edge.symbol,
|
| 683 |
-
probability=edge.probability,
|
| 684 |
-
next_state=contexts[edge.dst],
|
| 685 |
-
order_weights=((int(edge.order), 1.0),),
|
| 686 |
-
)
|
| 687 |
-
)
|
| 688 |
-
edges_by_state[context] = state_edges
|
| 689 |
-
count_metadata[context] = state_counts
|
| 690 |
-
state_supports[context] = float(sum(state_counts.values()))
|
| 691 |
-
|
| 692 |
-
start_state = contexts[0] if contexts else ()
|
| 693 |
-
context_graph = ContextGraph(
|
| 694 |
-
edges_by_state,
|
| 695 |
-
start_state=start_state,
|
| 696 |
-
max_order=int(graph.order),
|
| 697 |
-
validate=True,
|
| 698 |
-
)
|
| 699 |
-
alias_to_state = {
|
| 700 |
-
alias: contexts[state_id]
|
| 701 |
-
for alias, state_id in graph.context_to_id.items()
|
| 702 |
-
if state_id < len(contexts)
|
| 703 |
-
}
|
| 704 |
-
for state_id, aliases in enumerate(getattr(graph, "state_aliases", ())):
|
| 705 |
-
if state_id >= len(contexts):
|
| 706 |
-
continue
|
| 707 |
-
for alias in aliases:
|
| 708 |
-
alias_to_state[alias] = contexts[state_id]
|
| 709 |
-
context_graph._alias_to_state = alias_to_state
|
| 710 |
-
context_graph._continuation_counts = count_metadata
|
| 711 |
-
context_graph._state_supports = state_supports
|
| 712 |
-
return context_graph
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
def _context_graph_to_fixed_order_graph(
|
| 716 |
-
merged: ContextGraph,
|
| 717 |
-
source_graph: object,
|
| 718 |
-
) -> tuple[object, int]:
|
| 719 |
-
from .order_stack_bp import FixedOrderContextGraph, StackEdge
|
| 720 |
-
|
| 721 |
-
metadata = alergia_metadata(merged)
|
| 722 |
-
if not isinstance(metadata, AlergiaMergeMetadata):
|
| 723 |
-
raise RuntimeError("missing ContextGraph ALERGIA metadata")
|
| 724 |
-
|
| 725 |
-
fixed = FixedOrderContextGraph(int(source_graph.order))
|
| 726 |
-
representatives = [class_members[0] for class_members in metadata.classes]
|
| 727 |
-
fixed.contexts = representatives
|
| 728 |
-
fixed.context_to_id = {
|
| 729 |
-
context: state_id
|
| 730 |
-
for state_id, context in enumerate(representatives)
|
| 731 |
-
}
|
| 732 |
-
fixed.outgoing = [[] for _context in representatives]
|
| 733 |
-
|
| 734 |
-
aliases: list[set[Context]] = [set(class_members) for class_members in metadata.classes]
|
| 735 |
-
for alias, resolved in getattr(merged, "_alias_to_state", {}).items():
|
| 736 |
-
state_id = fixed.context_to_id.get(resolved)
|
| 737 |
-
if state_id is None:
|
| 738 |
-
continue
|
| 739 |
-
fixed.context_to_id[alias] = state_id
|
| 740 |
-
aliases[state_id].add(alias)
|
| 741 |
-
fixed.state_aliases = tuple(
|
| 742 |
-
tuple(sorted(group, key=_context_sort_key))
|
| 743 |
-
for group in aliases
|
| 744 |
-
)
|
| 745 |
-
|
| 746 |
-
conflicting_edge_orders = 0
|
| 747 |
-
for src, context in enumerate(fixed.contexts):
|
| 748 |
-
for edge in merged.outgoing(context):
|
| 749 |
-
order, has_conflict = _dominant_order(edge.order_weights, int(source_graph.order))
|
| 750 |
-
if has_conflict:
|
| 751 |
-
conflicting_edge_orders += 1
|
| 752 |
-
fixed.outgoing[src].append(
|
| 753 |
-
StackEdge(
|
| 754 |
-
src=src,
|
| 755 |
-
dst=fixed.context_to_id[edge.next_state],
|
| 756 |
-
symbol=edge.symbol,
|
| 757 |
-
probability=edge.probability,
|
| 758 |
-
order=order,
|
| 759 |
-
)
|
| 760 |
-
)
|
| 761 |
-
return fixed, conflicting_edge_orders
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
def _fixed_order_counts_from_model(
|
| 765 |
-
model: object,
|
| 766 |
-
graph: object,
|
| 767 |
-
) -> dict[Context, dict[Symbol, float]] | None:
|
| 768 |
-
model_counts = getattr(model, "counts", None)
|
| 769 |
-
longest_available_suffix = getattr(model, "longest_available_suffix", None)
|
| 770 |
-
if not isinstance(model_counts, Mapping) or longest_available_suffix is None:
|
| 771 |
-
return None
|
| 772 |
-
|
| 773 |
-
result: dict[Context, dict[Symbol, float]] = {}
|
| 774 |
-
for state_id, context in enumerate(graph.contexts):
|
| 775 |
-
suffix = longest_available_suffix(context, max_order=int(graph.order))
|
| 776 |
-
suffix_counts = model_counts.get(suffix, {}) if suffix is not None else {}
|
| 777 |
-
state_counts = {
|
| 778 |
-
edge.symbol: float(suffix_counts.get(edge.symbol, 0.0))
|
| 779 |
-
for edge in graph.outgoing[state_id]
|
| 780 |
-
if suffix_counts.get(edge.symbol, 0.0) > 0.0
|
| 781 |
-
}
|
| 782 |
-
if not state_counts and graph.outgoing[state_id]:
|
| 783 |
-
state_counts = {
|
| 784 |
-
edge.symbol: float(edge.probability)
|
| 785 |
-
for edge in graph.outgoing[state_id]
|
| 786 |
-
if edge.probability > 0.0
|
| 787 |
-
}
|
| 788 |
-
result[context] = state_counts
|
| 789 |
-
return result
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
def _dominant_order(
|
| 793 |
-
order_weights: tuple[tuple[int, float], ...],
|
| 794 |
-
default_order: int,
|
| 795 |
-
) -> tuple[int, bool]:
|
| 796 |
-
if not order_weights:
|
| 797 |
-
return default_order, False
|
| 798 |
-
order, _weight = max(order_weights, key=lambda item: (item[1], item[0]))
|
| 799 |
-
return int(order), len(order_weights) > 1
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
def _state_counts(graph: ContextGraph) -> dict[Context, dict[Symbol, float]]:
|
| 803 |
-
metadata = getattr(graph, "_continuation_counts", {})
|
| 804 |
-
result: dict[Context, dict[Symbol, float]] = {}
|
| 805 |
-
supports = getattr(graph, "_state_supports", {})
|
| 806 |
-
for state in graph.states:
|
| 807 |
-
if state in metadata:
|
| 808 |
-
result[state] = dict(metadata[state])
|
| 809 |
-
continue
|
| 810 |
-
support = float(supports.get(state, 0.0))
|
| 811 |
-
edges = graph.outgoing(state)
|
| 812 |
-
if support <= 0.0 and edges:
|
| 813 |
-
support = 1.0
|
| 814 |
-
result[state] = {
|
| 815 |
-
edge.symbol: edge.probability * support
|
| 816 |
-
for edge in edges
|
| 817 |
-
if edge.probability > 0.0
|
| 818 |
-
}
|
| 819 |
-
return result
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
def _precompute_projection_cache(
|
| 823 |
-
graph: ContextGraph,
|
| 824 |
-
counts: dict[Context, dict[Symbol, float]],
|
| 825 |
-
transition_projection: Callable[[Context, Symbol, Edge], Hashable],
|
| 826 |
-
) -> _ProjectionCache:
|
| 827 |
-
projected_counts: dict[Context, dict[Hashable, float]] = {}
|
| 828 |
-
dominant_next: dict[Context, dict[Hashable, Context]] = {}
|
| 829 |
-
for state in graph.states:
|
| 830 |
-
state_projected: Counter[Hashable] = Counter()
|
| 831 |
-
destination_counts: dict[Hashable, Counter[Context]] = defaultdict(Counter)
|
| 832 |
-
state_counts = counts.get(state, {})
|
| 833 |
-
for edge in graph.outgoing(state):
|
| 834 |
-
count = state_counts.get(edge.symbol, 0.0)
|
| 835 |
-
if count <= 0.0:
|
| 836 |
-
continue
|
| 837 |
-
projected_symbol = transition_projection(state, edge.symbol, edge)
|
| 838 |
-
state_projected[projected_symbol] += count
|
| 839 |
-
destination_counts[projected_symbol][edge.next_state] += count
|
| 840 |
-
projected_counts[state] = dict(state_projected)
|
| 841 |
-
dominant_next[state] = {
|
| 842 |
-
projected_symbol: max(
|
| 843 |
-
destinations,
|
| 844 |
-
key=lambda candidate: (destinations[candidate], repr(candidate)),
|
| 845 |
-
)
|
| 846 |
-
for projected_symbol, destinations in destination_counts.items()
|
| 847 |
-
}
|
| 848 |
-
return _ProjectionCache(counts=projected_counts, dominant_next=dominant_next)
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
def _cache_pair_result(
|
| 852 |
-
pair_memo: _PairMemo,
|
| 853 |
-
pair: tuple[Context, Context],
|
| 854 |
-
compatible: bool,
|
| 855 |
-
pairs: list[tuple[Context, Context]],
|
| 856 |
-
cache_positive: bool,
|
| 857 |
-
) -> tuple[bool, list[tuple[Context, Context]]]:
|
| 858 |
-
frozen_pairs = tuple(pairs)
|
| 859 |
-
if not compatible or cache_positive:
|
| 860 |
-
pair_memo[pair] = (compatible, frozen_pairs)
|
| 861 |
-
return compatible, list(frozen_pairs)
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
def _hoeffding_bound(left_support: float, right_support: float, alpha: float) -> float:
|
| 865 |
-
scale = math.log(2.0 / alpha) / 2.0
|
| 866 |
-
return math.sqrt(scale / left_support) + math.sqrt(scale / right_support)
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
class _ClassRegistry:
|
| 870 |
-
def __init__(self, states: tuple[Context, ...]) -> None:
|
| 871 |
-
self.union_find = _UnionFind(states)
|
| 872 |
-
self._state_index = {state: index for index, state in enumerate(states)}
|
| 873 |
-
self._active_roots = list(states)
|
| 874 |
-
self._members_by_root = {state: [state] for state in states}
|
| 875 |
-
|
| 876 |
-
def find(self, state: Context) -> Context:
|
| 877 |
-
return self.union_find.find(state)
|
| 878 |
-
|
| 879 |
-
def roots(self) -> tuple[Context, ...]:
|
| 880 |
-
return tuple(self._active_roots)
|
| 881 |
-
|
| 882 |
-
def members(self, root: Context) -> list[Context]:
|
| 883 |
-
return self._members_by_root[self.find(root)]
|
| 884 |
-
|
| 885 |
-
def union(self, left: Context, right: Context) -> Context:
|
| 886 |
-
left_root = self.union_find.find(left)
|
| 887 |
-
right_root = self.union_find.find(right)
|
| 888 |
-
if left_root == right_root:
|
| 889 |
-
return left_root
|
| 890 |
-
|
| 891 |
-
root = self.union_find.union(left_root, right_root)
|
| 892 |
-
removed_root = right_root if root == left_root else left_root
|
| 893 |
-
self._members_by_root[root] = self._merge_members(
|
| 894 |
-
self._members_by_root[root],
|
| 895 |
-
self._members_by_root.pop(removed_root),
|
| 896 |
-
)
|
| 897 |
-
self._active_roots.remove(removed_root)
|
| 898 |
-
return root
|
| 899 |
-
|
| 900 |
-
def _merge_members(self, left: list[Context], right: list[Context]) -> list[Context]:
|
| 901 |
-
merged: list[Context] = []
|
| 902 |
-
left_index = 0
|
| 903 |
-
right_index = 0
|
| 904 |
-
while left_index < len(left) and right_index < len(right):
|
| 905 |
-
if self._state_index[left[left_index]] <= self._state_index[right[right_index]]:
|
| 906 |
-
merged.append(left[left_index])
|
| 907 |
-
left_index += 1
|
| 908 |
-
else:
|
| 909 |
-
merged.append(right[right_index])
|
| 910 |
-
right_index += 1
|
| 911 |
-
merged.extend(left[left_index:])
|
| 912 |
-
merged.extend(right[right_index:])
|
| 913 |
-
return merged
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
def _context_sort_key(context: Context) -> tuple[int, str]:
|
| 917 |
-
return (len(context), repr(context))
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
def _hashable_key(value: Any) -> Hashable:
|
| 921 |
-
try:
|
| 922 |
-
hash(value)
|
| 923 |
-
except TypeError:
|
| 924 |
-
return (type(value).__name__, repr(value))
|
| 925 |
-
return (type(value).__name__, value)
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
def _identity_projection(_state: Context, symbol: Symbol, _edge: Edge) -> Hashable:
|
| 929 |
-
return symbol
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
def _transition_projector(
|
| 933 |
-
*,
|
| 934 |
-
symbol_projection: Callable[[Symbol], Hashable] | None,
|
| 935 |
-
transition_projection: Callable[[Context, Symbol, Edge], Hashable] | None,
|
| 936 |
-
) -> Callable[[Context, Symbol, Edge], Hashable]:
|
| 937 |
-
if transition_projection is not None:
|
| 938 |
-
return transition_projection
|
| 939 |
-
if symbol_projection is not None:
|
| 940 |
-
return lambda _state, symbol, _edge: symbol_projection(symbol)
|
| 941 |
-
return _identity_projection
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
def _projection_name(projection: Callable[..., Hashable] | None) -> str | None:
|
| 945 |
-
if projection is None:
|
| 946 |
-
return None
|
| 947 |
-
return getattr(projection, "__name__", repr(projection))
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
def _projection_kind(
|
| 951 |
-
symbol_projection: Callable[[Symbol], Hashable] | None,
|
| 952 |
-
transition_projection: Callable[[Context, Symbol, Edge], Hashable] | None,
|
| 953 |
-
) -> str:
|
| 954 |
-
if transition_projection is not None:
|
| 955 |
-
return "transition"
|
| 956 |
-
if symbol_projection is not None:
|
| 957 |
-
return "symbol"
|
| 958 |
-
return "identity"
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
def _ratio(numerator: int, denominator: int) -> float:
|
| 962 |
-
if denominator == 0:
|
| 963 |
-
return float("inf") if numerator else 1.0
|
| 964 |
-
return float(numerator) / float(denominator)
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
class _UnionFind:
|
| 968 |
-
def __init__(self, states: tuple[Context, ...]) -> None:
|
| 969 |
-
self.parent = {state: state for state in states}
|
| 970 |
-
|
| 971 |
-
def find(self, state: Context) -> Context:
|
| 972 |
-
parent = self.parent[state]
|
| 973 |
-
if parent != state:
|
| 974 |
-
parent = self.find(parent)
|
| 975 |
-
self.parent[state] = parent
|
| 976 |
-
return parent
|
| 977 |
-
|
| 978 |
-
def union(self, left: Context, right: Context) -> Context:
|
| 979 |
-
left_root = self.find(left)
|
| 980 |
-
right_root = self.find(right)
|
| 981 |
-
if left_root == right_root:
|
| 982 |
-
return left_root
|
| 983 |
-
if _context_sort_key(right_root) < _context_sort_key(left_root):
|
| 984 |
-
left_root, right_root = right_root, left_root
|
| 985 |
-
self.parent[right_root] = left_root
|
| 986 |
-
return left_root
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/metrics.py
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 1 |
-
"""Small metrics for exactness experiments."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from collections import Counter
|
| 6 |
-
from typing import Hashable, Iterable, Mapping
|
| 7 |
-
|
| 8 |
-
Symbol = Hashable
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def total_variation(
|
| 12 |
-
p: Mapping[tuple[Symbol, ...], float],
|
| 13 |
-
q: Mapping[tuple[Symbol, ...], float],
|
| 14 |
-
) -> float:
|
| 15 |
-
support = set(p) | set(q)
|
| 16 |
-
return 0.5 * sum(abs(float(p.get(item, 0.0)) - float(q.get(item, 0.0))) for item in support)
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def empirical_distribution(samples: Iterable[tuple[Symbol, ...]]) -> dict[tuple[Symbol, ...], float]:
|
| 20 |
-
counts = Counter(samples)
|
| 21 |
-
total = sum(counts.values())
|
| 22 |
-
if total == 0:
|
| 23 |
-
return {}
|
| 24 |
-
return {sample: count / total for sample, count in counts.items()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/minimization.py
DELETED
|
@@ -1,326 +0,0 @@
|
|
| 1 |
-
"""Exact quotient/minimization helpers for stochastic context graphs."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from collections.abc import Hashable, Iterable, Sequence
|
| 6 |
-
from dataclasses import dataclass
|
| 7 |
-
from typing import Any
|
| 8 |
-
|
| 9 |
-
from .context import Context, ContextGraph, Edge
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
@dataclass(frozen=True)
|
| 13 |
-
class ExactQuotientStats:
|
| 14 |
-
"""State/edge counts for an exact graph quotient."""
|
| 15 |
-
|
| 16 |
-
states: int
|
| 17 |
-
classes: int
|
| 18 |
-
edges: int
|
| 19 |
-
quotient_edges: int
|
| 20 |
-
max_class_size: int
|
| 21 |
-
refinement_rounds: int
|
| 22 |
-
|
| 23 |
-
@property
|
| 24 |
-
def state_reduction(self) -> float:
|
| 25 |
-
return _ratio(self.states, self.classes)
|
| 26 |
-
|
| 27 |
-
@property
|
| 28 |
-
def edge_reduction(self) -> float:
|
| 29 |
-
return _ratio(self.edges, self.quotient_edges)
|
| 30 |
-
|
| 31 |
-
def as_dict(self) -> dict[str, int | float]:
|
| 32 |
-
return {
|
| 33 |
-
"states": self.states,
|
| 34 |
-
"classes": self.classes,
|
| 35 |
-
"edges": self.edges,
|
| 36 |
-
"quotient_edges": self.quotient_edges,
|
| 37 |
-
"max_class_size": self.max_class_size,
|
| 38 |
-
"refinement_rounds": self.refinement_rounds,
|
| 39 |
-
"state_reduction": self.state_reduction,
|
| 40 |
-
"edge_reduction": self.edge_reduction,
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def exact_context_graph_quotient_stats(graph: ContextGraph) -> ExactQuotientStats:
|
| 45 |
-
"""Return exact quotient counts for a :class:`ContextGraph`."""
|
| 46 |
-
|
| 47 |
-
partition = _context_graph_partition(graph)
|
| 48 |
-
return partition.stats
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def minimize_context_graph(graph: ContextGraph) -> ContextGraph:
|
| 52 |
-
"""Return a read-only exact quotient of ``graph``.
|
| 53 |
-
|
| 54 |
-
The quotient keeps one representative context per equivalence class and
|
| 55 |
-
records aliases so callers may still ask for outgoing edges using original
|
| 56 |
-
context labels.
|
| 57 |
-
"""
|
| 58 |
-
|
| 59 |
-
partition = _context_graph_partition(graph)
|
| 60 |
-
if partition.stats.classes == partition.stats.states:
|
| 61 |
-
return graph
|
| 62 |
-
|
| 63 |
-
representative_by_class = partition.representatives
|
| 64 |
-
representative_for_state = {
|
| 65 |
-
state: representative_by_class[partition.class_by_state[state]]
|
| 66 |
-
for state in partition.states
|
| 67 |
-
}
|
| 68 |
-
edges_by_state: dict[Context, list[Edge]] = {}
|
| 69 |
-
for class_id, representative in enumerate(representative_by_class):
|
| 70 |
-
edges_by_state[representative] = [
|
| 71 |
-
Edge(
|
| 72 |
-
edge.symbol,
|
| 73 |
-
edge.probability,
|
| 74 |
-
representative_for_state[edge.next_state],
|
| 75 |
-
edge.order_weights,
|
| 76 |
-
)
|
| 77 |
-
for edge in graph.outgoing(representative)
|
| 78 |
-
]
|
| 79 |
-
|
| 80 |
-
result = ContextGraph(
|
| 81 |
-
edges_by_state,
|
| 82 |
-
start_state=representative_for_state[graph.start_state],
|
| 83 |
-
max_order=graph.max_order,
|
| 84 |
-
alphabet=graph.alphabet,
|
| 85 |
-
validate=True,
|
| 86 |
-
)
|
| 87 |
-
result._alias_to_state = representative_for_state # type: ignore[attr-defined]
|
| 88 |
-
result._quotient_stats = partition.stats # type: ignore[attr-defined]
|
| 89 |
-
result._quotient_classes = tuple( # type: ignore[attr-defined]
|
| 90 |
-
tuple(
|
| 91 |
-
state
|
| 92 |
-
for state in partition.states
|
| 93 |
-
if partition.class_by_state[state] == class_id
|
| 94 |
-
)
|
| 95 |
-
for class_id in range(len(representative_by_class))
|
| 96 |
-
)
|
| 97 |
-
return result
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
def exact_fixed_order_graph_quotient_stats(graph: object) -> ExactQuotientStats:
|
| 101 |
-
"""Return exact quotient counts for a fixed-order context graph."""
|
| 102 |
-
|
| 103 |
-
partition = _fixed_order_partition(graph)
|
| 104 |
-
return partition.stats
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
def minimize_fixed_order_graph(graph: object) -> object:
|
| 108 |
-
"""Return a minimized fixed-order graph, preserving original context lookup."""
|
| 109 |
-
|
| 110 |
-
partition = _fixed_order_partition(graph)
|
| 111 |
-
if partition.stats.classes == partition.stats.states:
|
| 112 |
-
return graph
|
| 113 |
-
|
| 114 |
-
from .order_stack_bp import FixedOrderContextGraph, StackEdge
|
| 115 |
-
|
| 116 |
-
minimized = FixedOrderContextGraph(graph.order)
|
| 117 |
-
representative_by_class = partition.representatives
|
| 118 |
-
minimized.contexts = [graph.contexts[index] for index in representative_by_class]
|
| 119 |
-
minimized.outgoing = [[] for _ in representative_by_class]
|
| 120 |
-
|
| 121 |
-
for original_context, original_state in graph.context_to_id.items():
|
| 122 |
-
minimized.context_to_id[original_context] = partition.class_by_state[original_state]
|
| 123 |
-
|
| 124 |
-
aliases: list[list[Context]] = [[] for _ in representative_by_class]
|
| 125 |
-
for state, context in enumerate(graph.contexts):
|
| 126 |
-
aliases[partition.class_by_state[state]].append(context)
|
| 127 |
-
minimized.state_aliases = tuple(tuple(group) for group in aliases)
|
| 128 |
-
minimized.quotient_stats = partition.stats
|
| 129 |
-
|
| 130 |
-
for class_id, representative_state in enumerate(representative_by_class):
|
| 131 |
-
minimized.outgoing[class_id] = [
|
| 132 |
-
StackEdge(
|
| 133 |
-
src=class_id,
|
| 134 |
-
dst=partition.class_by_state[edge.dst],
|
| 135 |
-
symbol=edge.symbol,
|
| 136 |
-
probability=edge.probability,
|
| 137 |
-
order=edge.order,
|
| 138 |
-
)
|
| 139 |
-
for edge in graph.outgoing[representative_state]
|
| 140 |
-
]
|
| 141 |
-
|
| 142 |
-
return minimized
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
def minimize_fixed_order_graphs(graphs: dict[int, object]) -> dict[int, object]:
|
| 146 |
-
"""Minimize every materialized fixed-order graph in ``graphs``."""
|
| 147 |
-
|
| 148 |
-
return {
|
| 149 |
-
order: minimize_fixed_order_graph(graph)
|
| 150 |
-
for order, graph in graphs.items()
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
@dataclass(frozen=True)
|
| 155 |
-
class _ContextGraphPartition:
|
| 156 |
-
states: tuple[Context, ...]
|
| 157 |
-
class_by_state: dict[Context, int]
|
| 158 |
-
representatives: tuple[Context, ...]
|
| 159 |
-
stats: ExactQuotientStats
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
@dataclass(frozen=True)
|
| 163 |
-
class _FixedOrderPartition:
|
| 164 |
-
class_by_state: tuple[int, ...]
|
| 165 |
-
representatives: tuple[int, ...]
|
| 166 |
-
stats: ExactQuotientStats
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
def _context_graph_partition(graph: ContextGraph) -> _ContextGraphPartition:
|
| 170 |
-
states = tuple(sorted(graph.states, key=_context_sort_key))
|
| 171 |
-
state_index = {state: index for index, state in enumerate(states)}
|
| 172 |
-
classes = [0 for _state in states]
|
| 173 |
-
rounds = 0
|
| 174 |
-
|
| 175 |
-
while True:
|
| 176 |
-
signatures = [
|
| 177 |
-
tuple(
|
| 178 |
-
sorted(
|
| 179 |
-
(
|
| 180 |
-
_hashable_key(edge.symbol),
|
| 181 |
-
_float_key(edge.probability),
|
| 182 |
-
_order_weights_key(edge.order_weights),
|
| 183 |
-
classes[state_index[edge.next_state]],
|
| 184 |
-
)
|
| 185 |
-
for edge in graph.outgoing(state)
|
| 186 |
-
)
|
| 187 |
-
)
|
| 188 |
-
for state in states
|
| 189 |
-
]
|
| 190 |
-
new_classes, counts = _classes_from_signatures(signatures)
|
| 191 |
-
rounds += 1
|
| 192 |
-
if new_classes == classes:
|
| 193 |
-
representatives = _representative_states(new_classes)
|
| 194 |
-
quotient_edges = sum(
|
| 195 |
-
len(graph.outgoing(states[state]))
|
| 196 |
-
for state in representatives
|
| 197 |
-
)
|
| 198 |
-
return _ContextGraphPartition(
|
| 199 |
-
states=states,
|
| 200 |
-
class_by_state={
|
| 201 |
-
state: class_id
|
| 202 |
-
for state, class_id in zip(states, new_classes)
|
| 203 |
-
},
|
| 204 |
-
representatives=tuple(states[state] for state in representatives),
|
| 205 |
-
stats=ExactQuotientStats(
|
| 206 |
-
states=len(states),
|
| 207 |
-
classes=len(counts),
|
| 208 |
-
edges=graph.edge_count(),
|
| 209 |
-
quotient_edges=quotient_edges,
|
| 210 |
-
max_class_size=max(counts or [0]),
|
| 211 |
-
refinement_rounds=rounds,
|
| 212 |
-
),
|
| 213 |
-
)
|
| 214 |
-
classes = new_classes
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
def _fixed_order_partition(graph: object) -> _FixedOrderPartition:
|
| 218 |
-
_require_materialized_fixed_order_graph(graph)
|
| 219 |
-
state_count = len(graph.contexts)
|
| 220 |
-
classes = [0 for _state in graph.contexts]
|
| 221 |
-
rounds = 0
|
| 222 |
-
|
| 223 |
-
while True:
|
| 224 |
-
signatures = [
|
| 225 |
-
tuple(
|
| 226 |
-
sorted(
|
| 227 |
-
(
|
| 228 |
-
_hashable_key(edge.symbol),
|
| 229 |
-
_float_key(edge.probability),
|
| 230 |
-
int(edge.order),
|
| 231 |
-
classes[edge.dst],
|
| 232 |
-
)
|
| 233 |
-
for edge in edges
|
| 234 |
-
)
|
| 235 |
-
)
|
| 236 |
-
for edges in graph.outgoing
|
| 237 |
-
]
|
| 238 |
-
new_classes, counts = _classes_from_signatures(signatures)
|
| 239 |
-
rounds += 1
|
| 240 |
-
if new_classes == classes:
|
| 241 |
-
representatives = _representative_states(new_classes)
|
| 242 |
-
quotient_edges = sum(
|
| 243 |
-
len(graph.outgoing[state])
|
| 244 |
-
for state in representatives
|
| 245 |
-
)
|
| 246 |
-
return _FixedOrderPartition(
|
| 247 |
-
class_by_state=tuple(new_classes),
|
| 248 |
-
representatives=tuple(representatives),
|
| 249 |
-
stats=ExactQuotientStats(
|
| 250 |
-
states=state_count,
|
| 251 |
-
classes=len(counts),
|
| 252 |
-
edges=graph.edge_count,
|
| 253 |
-
quotient_edges=quotient_edges,
|
| 254 |
-
max_class_size=max(counts or [0]),
|
| 255 |
-
refinement_rounds=rounds,
|
| 256 |
-
),
|
| 257 |
-
)
|
| 258 |
-
classes = new_classes
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
def _classes_from_signatures(
|
| 262 |
-
signatures: Sequence[Hashable],
|
| 263 |
-
) -> tuple[list[int], list[int]]:
|
| 264 |
-
class_ids: dict[Hashable, int] = {}
|
| 265 |
-
classes: list[int] = []
|
| 266 |
-
counts: list[int] = []
|
| 267 |
-
for signature in signatures:
|
| 268 |
-
class_id = class_ids.get(signature)
|
| 269 |
-
if class_id is None:
|
| 270 |
-
class_id = len(class_ids)
|
| 271 |
-
class_ids[signature] = class_id
|
| 272 |
-
counts.append(0)
|
| 273 |
-
classes.append(class_id)
|
| 274 |
-
counts[class_id] += 1
|
| 275 |
-
return classes, counts
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
def _representative_states(classes: Sequence[int]) -> list[int]:
|
| 279 |
-
representatives: dict[int, int] = {}
|
| 280 |
-
for state, class_id in enumerate(classes):
|
| 281 |
-
representatives.setdefault(class_id, state)
|
| 282 |
-
return [representatives[class_id] for class_id in range(len(representatives))]
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
def _require_materialized_fixed_order_graph(graph: object) -> None:
|
| 286 |
-
from .order_stack_bp import FixedOrderContextGraph
|
| 287 |
-
|
| 288 |
-
if not isinstance(graph, FixedOrderContextGraph):
|
| 289 |
-
raise TypeError(
|
| 290 |
-
"exact source-graph minimization currently supports materialized "
|
| 291 |
-
"FixedOrderContextGraph instances"
|
| 292 |
-
)
|
| 293 |
-
required = ("order", "contexts", "context_to_id", "outgoing", "edge_count")
|
| 294 |
-
if not all(hasattr(graph, name) for name in required):
|
| 295 |
-
raise TypeError("expected a materialized fixed-order context graph")
|
| 296 |
-
if len(graph.outgoing) != len(graph.contexts):
|
| 297 |
-
raise ValueError(
|
| 298 |
-
"exact minimization requires materialized graph outgoing rows; "
|
| 299 |
-
"lazy graph views are not supported"
|
| 300 |
-
)
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
def _context_sort_key(context: Context) -> tuple[int, str]:
|
| 304 |
-
return (len(context), repr(context))
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
def _hashable_key(value: Any) -> Hashable:
|
| 308 |
-
try:
|
| 309 |
-
hash(value)
|
| 310 |
-
except TypeError:
|
| 311 |
-
return (type(value).__name__, repr(value))
|
| 312 |
-
return (type(value).__name__, value)
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
def _float_key(value: float) -> tuple[int, int]:
|
| 316 |
-
return float(value).as_integer_ratio()
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
def _order_weights_key(weights: Iterable[tuple[int, float]]) -> tuple[tuple[int, tuple[int, int]], ...]:
|
| 320 |
-
return tuple((int(order), _float_key(weight)) for order, weight in weights)
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
def _ratio(numerator: int, denominator: int) -> float:
|
| 324 |
-
if denominator == 0:
|
| 325 |
-
return float("inf") if numerator else 1.0
|
| 326 |
-
return float(numerator) / float(denominator)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/orbit_diagnostics.py
DELETED
|
@@ -1,574 +0,0 @@
|
|
| 1 |
-
"""Diagnostics for transformation-orbit structure in regular BP products."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from collections import Counter
|
| 6 |
-
from collections.abc import Hashable, Iterable, Mapping
|
| 7 |
-
from dataclasses import dataclass
|
| 8 |
-
from numbers import Integral
|
| 9 |
-
from typing import Any
|
| 10 |
-
|
| 11 |
-
from .context import Symbol
|
| 12 |
-
from .order_stack_bp import RegularOrderStackBPResult
|
| 13 |
-
from .positional_bp import PositionConstraint
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
@dataclass(frozen=True)
|
| 17 |
-
class ForbiddenPatternOrbitStats:
|
| 18 |
-
"""Orbit counts for forbidden patterns and their DFA prefix states."""
|
| 19 |
-
|
| 20 |
-
pattern_count: int
|
| 21 |
-
pattern_orbit_count: int
|
| 22 |
-
prefix_state_count: int
|
| 23 |
-
prefix_state_orbit_count: int
|
| 24 |
-
alphabet_size: int | None = None
|
| 25 |
-
alphabet_orbit_count: int | None = None
|
| 26 |
-
|
| 27 |
-
@property
|
| 28 |
-
def pattern_reduction_factor(self) -> float:
|
| 29 |
-
return _ratio(self.pattern_count, self.pattern_orbit_count)
|
| 30 |
-
|
| 31 |
-
@property
|
| 32 |
-
def prefix_state_reduction_factor(self) -> float:
|
| 33 |
-
return _ratio(self.prefix_state_count, self.prefix_state_orbit_count)
|
| 34 |
-
|
| 35 |
-
@property
|
| 36 |
-
def alphabet_reduction_factor(self) -> float | None:
|
| 37 |
-
if self.alphabet_size is None or self.alphabet_orbit_count is None:
|
| 38 |
-
return None
|
| 39 |
-
return _ratio(self.alphabet_size, self.alphabet_orbit_count)
|
| 40 |
-
|
| 41 |
-
def as_dict(self) -> dict[str, int | float | None]:
|
| 42 |
-
return {
|
| 43 |
-
"forbidden_patterns": self.pattern_count,
|
| 44 |
-
"forbidden_pattern_orbits": self.pattern_orbit_count,
|
| 45 |
-
"forbidden_pattern_reduction": self.pattern_reduction_factor,
|
| 46 |
-
"dfa_prefix_states": self.prefix_state_count,
|
| 47 |
-
"dfa_prefix_state_orbits": self.prefix_state_orbit_count,
|
| 48 |
-
"dfa_prefix_state_reduction": self.prefix_state_reduction_factor,
|
| 49 |
-
"alphabet_size": self.alphabet_size,
|
| 50 |
-
"alphabet_orbits": self.alphabet_orbit_count,
|
| 51 |
-
"alphabet_reduction": self.alphabet_reduction_factor,
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
@dataclass(frozen=True)
|
| 56 |
-
class RegularProductOrbitStats:
|
| 57 |
-
"""Orbit counts for the currently expanded regular BP product."""
|
| 58 |
-
|
| 59 |
-
time_indexed_product_states: int
|
| 60 |
-
time_indexed_product_state_orbits: int
|
| 61 |
-
product_states: int
|
| 62 |
-
product_state_orbits: int
|
| 63 |
-
product_edges: int
|
| 64 |
-
time_indexed_product_edge_orbits: int
|
| 65 |
-
product_edge_orbits: int
|
| 66 |
-
expanded_rows: int
|
| 67 |
-
transition_row_shape_orbits: int
|
| 68 |
-
max_time_state_orbit_size: int
|
| 69 |
-
max_product_state_orbit_size: int
|
| 70 |
-
max_time_edge_orbit_size: int
|
| 71 |
-
max_edge_orbit_size: int
|
| 72 |
-
|
| 73 |
-
@property
|
| 74 |
-
def time_indexed_product_state_reduction_factor(self) -> float:
|
| 75 |
-
return _ratio(
|
| 76 |
-
self.time_indexed_product_states,
|
| 77 |
-
self.time_indexed_product_state_orbits,
|
| 78 |
-
)
|
| 79 |
-
|
| 80 |
-
@property
|
| 81 |
-
def product_state_reduction_factor(self) -> float:
|
| 82 |
-
return _ratio(self.product_states, self.product_state_orbits)
|
| 83 |
-
|
| 84 |
-
@property
|
| 85 |
-
def time_indexed_product_edge_reduction_factor(self) -> float:
|
| 86 |
-
return _ratio(self.product_edges, self.time_indexed_product_edge_orbits)
|
| 87 |
-
|
| 88 |
-
@property
|
| 89 |
-
def product_edge_reduction_factor(self) -> float:
|
| 90 |
-
return _ratio(self.product_edges, self.product_edge_orbits)
|
| 91 |
-
|
| 92 |
-
@property
|
| 93 |
-
def transition_row_shape_reduction_factor(self) -> float:
|
| 94 |
-
return _ratio(self.expanded_rows, self.transition_row_shape_orbits)
|
| 95 |
-
|
| 96 |
-
def as_dict(self) -> dict[str, int | float]:
|
| 97 |
-
return {
|
| 98 |
-
"time_indexed_product_states": self.time_indexed_product_states,
|
| 99 |
-
"time_indexed_product_state_orbits": self.time_indexed_product_state_orbits,
|
| 100 |
-
"time_indexed_product_state_reduction": (
|
| 101 |
-
self.time_indexed_product_state_reduction_factor
|
| 102 |
-
),
|
| 103 |
-
"product_states": self.product_states,
|
| 104 |
-
"product_state_orbits": self.product_state_orbits,
|
| 105 |
-
"product_state_reduction": self.product_state_reduction_factor,
|
| 106 |
-
"product_edges": self.product_edges,
|
| 107 |
-
"time_indexed_product_edge_orbits": self.time_indexed_product_edge_orbits,
|
| 108 |
-
"time_indexed_product_edge_reduction": (
|
| 109 |
-
self.time_indexed_product_edge_reduction_factor
|
| 110 |
-
),
|
| 111 |
-
"product_edge_orbits": self.product_edge_orbits,
|
| 112 |
-
"product_edge_reduction": self.product_edge_reduction_factor,
|
| 113 |
-
"expanded_rows": self.expanded_rows,
|
| 114 |
-
"transition_row_shape_orbits": self.transition_row_shape_orbits,
|
| 115 |
-
"transition_row_shape_reduction": self.transition_row_shape_reduction_factor,
|
| 116 |
-
"max_time_state_orbit_size": self.max_time_state_orbit_size,
|
| 117 |
-
"max_product_state_orbit_size": self.max_product_state_orbit_size,
|
| 118 |
-
"max_time_edge_orbit_size": self.max_time_edge_orbit_size,
|
| 119 |
-
"max_edge_orbit_size": self.max_edge_orbit_size,
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
@dataclass(frozen=True)
|
| 124 |
-
class RegularRowSignatureStats:
|
| 125 |
-
"""Exact shifted-row reuse counts for already-expanded regular BP rows."""
|
| 126 |
-
|
| 127 |
-
product_rows: int
|
| 128 |
-
product_row_signatures: int
|
| 129 |
-
reusable_product_rows: int
|
| 130 |
-
max_product_row_signature_size: int
|
| 131 |
-
time_masked_rows: int
|
| 132 |
-
time_masked_row_signatures: int
|
| 133 |
-
reusable_time_masked_rows: int
|
| 134 |
-
max_time_masked_row_signature_size: int
|
| 135 |
-
time_masked_edges: int
|
| 136 |
-
|
| 137 |
-
@property
|
| 138 |
-
def product_row_reduction_factor(self) -> float:
|
| 139 |
-
return _ratio(self.product_rows, self.product_row_signatures)
|
| 140 |
-
|
| 141 |
-
@property
|
| 142 |
-
def reusable_product_row_fraction(self) -> float:
|
| 143 |
-
return _ratio(self.reusable_product_rows, self.product_rows)
|
| 144 |
-
|
| 145 |
-
@property
|
| 146 |
-
def time_masked_row_reduction_factor(self) -> float:
|
| 147 |
-
return _ratio(self.time_masked_rows, self.time_masked_row_signatures)
|
| 148 |
-
|
| 149 |
-
@property
|
| 150 |
-
def reusable_time_masked_row_fraction(self) -> float:
|
| 151 |
-
return _ratio(self.reusable_time_masked_rows, self.time_masked_rows)
|
| 152 |
-
|
| 153 |
-
def as_dict(self) -> dict[str, int | float]:
|
| 154 |
-
return {
|
| 155 |
-
"exact_product_rows": self.product_rows,
|
| 156 |
-
"exact_product_row_signatures": self.product_row_signatures,
|
| 157 |
-
"exact_product_row_reduction": self.product_row_reduction_factor,
|
| 158 |
-
"exact_reusable_product_rows": self.reusable_product_rows,
|
| 159 |
-
"exact_reusable_product_row_fraction": self.reusable_product_row_fraction,
|
| 160 |
-
"exact_max_product_row_signature_size": self.max_product_row_signature_size,
|
| 161 |
-
"exact_time_masked_rows": self.time_masked_rows,
|
| 162 |
-
"exact_time_masked_row_signatures": self.time_masked_row_signatures,
|
| 163 |
-
"exact_time_masked_row_reduction": self.time_masked_row_reduction_factor,
|
| 164 |
-
"exact_reusable_time_masked_rows": self.reusable_time_masked_rows,
|
| 165 |
-
"exact_reusable_time_masked_row_fraction": (
|
| 166 |
-
self.reusable_time_masked_row_fraction
|
| 167 |
-
),
|
| 168 |
-
"exact_max_time_masked_row_signature_size": (
|
| 169 |
-
self.max_time_masked_row_signature_size
|
| 170 |
-
),
|
| 171 |
-
"exact_time_masked_edges": self.time_masked_edges,
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
def canonical_integer_shift_key(
|
| 176 |
-
value: Any,
|
| 177 |
-
*,
|
| 178 |
-
fixed_symbols: Iterable[Symbol] = (),
|
| 179 |
-
reference: int | None = None,
|
| 180 |
-
) -> Hashable:
|
| 181 |
-
"""Canonicalize nested symbols modulo a common integer shift.
|
| 182 |
-
|
| 183 |
-
Integer-like symbols are represented relative to the first integer symbol
|
| 184 |
-
unless ``reference`` is supplied. Non-integer symbols, such as sentinels,
|
| 185 |
-
are kept fixed. The function is intentionally diagnostic: it exposes how
|
| 186 |
-
much repeated structure is present under transposition without changing any
|
| 187 |
-
BP semantics.
|
| 188 |
-
"""
|
| 189 |
-
|
| 190 |
-
fixed = frozenset(fixed_symbols)
|
| 191 |
-
origin = reference
|
| 192 |
-
if origin is None:
|
| 193 |
-
first = _first_shiftable_symbol(value, fixed)
|
| 194 |
-
origin = 0 if first is None else first
|
| 195 |
-
return _canonicalize(value, fixed, int(origin))
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
def forbidden_pattern_orbit_stats(
|
| 199 |
-
patterns: Iterable[Iterable[Symbol]],
|
| 200 |
-
*,
|
| 201 |
-
alphabet: Iterable[Symbol] | None = None,
|
| 202 |
-
fixed_symbols: Iterable[Symbol] = (),
|
| 203 |
-
) -> ForbiddenPatternOrbitStats:
|
| 204 |
-
"""Measure integer-shift orbit structure in forbidden n-grams."""
|
| 205 |
-
|
| 206 |
-
fixed = frozenset(fixed_symbols)
|
| 207 |
-
pattern_tuple = tuple(dict.fromkeys(tuple(pattern) for pattern in patterns))
|
| 208 |
-
prefixes: set[tuple[Symbol, ...]] = {()}
|
| 209 |
-
for pattern in pattern_tuple:
|
| 210 |
-
for prefix_len in range(1, len(pattern)):
|
| 211 |
-
prefixes.add(pattern[:prefix_len])
|
| 212 |
-
|
| 213 |
-
pattern_orbits = {
|
| 214 |
-
canonical_integer_shift_key(pattern, fixed_symbols=fixed)
|
| 215 |
-
for pattern in pattern_tuple
|
| 216 |
-
}
|
| 217 |
-
prefix_orbits = {
|
| 218 |
-
canonical_integer_shift_key(prefix, fixed_symbols=fixed)
|
| 219 |
-
for prefix in prefixes
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
alphabet_size = None
|
| 223 |
-
alphabet_orbit_count = None
|
| 224 |
-
if alphabet is not None:
|
| 225 |
-
alphabet_tuple = tuple(dict.fromkeys(alphabet))
|
| 226 |
-
alphabet_size = len(alphabet_tuple)
|
| 227 |
-
alphabet_orbit_count = len(
|
| 228 |
-
{
|
| 229 |
-
canonical_integer_shift_key(symbol, fixed_symbols=fixed)
|
| 230 |
-
for symbol in alphabet_tuple
|
| 231 |
-
}
|
| 232 |
-
)
|
| 233 |
-
|
| 234 |
-
return ForbiddenPatternOrbitStats(
|
| 235 |
-
pattern_count=len(pattern_tuple),
|
| 236 |
-
pattern_orbit_count=len(pattern_orbits),
|
| 237 |
-
prefix_state_count=len(prefixes),
|
| 238 |
-
prefix_state_orbit_count=len(prefix_orbits),
|
| 239 |
-
alphabet_size=alphabet_size,
|
| 240 |
-
alphabet_orbit_count=alphabet_orbit_count,
|
| 241 |
-
)
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
def regular_product_orbit_stats(
|
| 245 |
-
result: RegularOrderStackBPResult,
|
| 246 |
-
*,
|
| 247 |
-
fixed_symbols: Iterable[Symbol] = (),
|
| 248 |
-
include_transition_row_shapes: bool = False,
|
| 249 |
-
) -> RegularProductOrbitStats:
|
| 250 |
-
"""Measure transposition-orbit repetition in an already-run regular BP."""
|
| 251 |
-
|
| 252 |
-
fixed = frozenset(fixed_symbols)
|
| 253 |
-
result.start_order_masses()
|
| 254 |
-
|
| 255 |
-
time_state_counts: Counter[Hashable] = Counter()
|
| 256 |
-
state_counts: Counter[Hashable] = Counter()
|
| 257 |
-
time_edge_counts: Counter[Hashable] = Counter()
|
| 258 |
-
edge_counts: Counter[Hashable] = Counter()
|
| 259 |
-
row_shapes: set[Hashable] = set()
|
| 260 |
-
state_seen: set[Hashable] = set()
|
| 261 |
-
state_key_cache: dict[tuple[int, int, Hashable], Hashable] = {}
|
| 262 |
-
acceptor_payload_cache: dict[Hashable, Hashable] = {}
|
| 263 |
-
edge_key_cache: dict[tuple[int, int, Hashable, int, Symbol, Hashable], Hashable] = {}
|
| 264 |
-
scanned_edge_count = 0
|
| 265 |
-
expanded_rows = 0
|
| 266 |
-
|
| 267 |
-
for order, cache in result.backwards.items():
|
| 268 |
-
graph = cache.graph
|
| 269 |
-
for time, graph_state, acceptor_state in cache.memo:
|
| 270 |
-
context = graph.contexts[graph_state]
|
| 271 |
-
acceptor_payload = acceptor_payload_cache.get(acceptor_state)
|
| 272 |
-
if acceptor_payload is None:
|
| 273 |
-
acceptor_payload = _acceptor_state_payload(result.acceptor, acceptor_state)
|
| 274 |
-
acceptor_payload_cache[acceptor_state] = acceptor_payload
|
| 275 |
-
|
| 276 |
-
raw_state_key = (order, graph_state, acceptor_state)
|
| 277 |
-
state_key = state_key_cache.get(raw_state_key)
|
| 278 |
-
if state_key is None:
|
| 279 |
-
state_payload = (context, acceptor_payload)
|
| 280 |
-
state_key = (
|
| 281 |
-
order,
|
| 282 |
-
canonical_integer_shift_key(state_payload, fixed_symbols=fixed),
|
| 283 |
-
)
|
| 284 |
-
state_key_cache[raw_state_key] = state_key
|
| 285 |
-
time_state_key = (time, *state_key)
|
| 286 |
-
time_state_counts[time_state_key] += 1
|
| 287 |
-
if raw_state_key not in state_seen:
|
| 288 |
-
state_seen.add(raw_state_key)
|
| 289 |
-
state_counts[state_key] += 1
|
| 290 |
-
|
| 291 |
-
if time == result.length:
|
| 292 |
-
continue
|
| 293 |
-
|
| 294 |
-
expanded_rows += 1
|
| 295 |
-
row_edges: list[Hashable] = []
|
| 296 |
-
reference = None
|
| 297 |
-
if include_transition_row_shapes:
|
| 298 |
-
reference = _first_shiftable_symbol((context, acceptor_payload), fixed)
|
| 299 |
-
for edge, next_acceptor_state, _transition_weight in cache.accepted_transitions(
|
| 300 |
-
graph_state,
|
| 301 |
-
acceptor_state,
|
| 302 |
-
):
|
| 303 |
-
if not _constraint_allows(result.constraints.get(time), edge.symbol):
|
| 304 |
-
continue
|
| 305 |
-
edge_cache_key = (
|
| 306 |
-
order,
|
| 307 |
-
graph_state,
|
| 308 |
-
acceptor_state,
|
| 309 |
-
edge.dst,
|
| 310 |
-
edge.symbol,
|
| 311 |
-
next_acceptor_state,
|
| 312 |
-
)
|
| 313 |
-
edge_key = edge_key_cache.get(edge_cache_key)
|
| 314 |
-
if edge_key is None:
|
| 315 |
-
next_payload = acceptor_payload_cache.get(next_acceptor_state)
|
| 316 |
-
if next_payload is None:
|
| 317 |
-
next_payload = _acceptor_state_payload(
|
| 318 |
-
result.acceptor,
|
| 319 |
-
next_acceptor_state,
|
| 320 |
-
)
|
| 321 |
-
acceptor_payload_cache[next_acceptor_state] = next_payload
|
| 322 |
-
dst_context = graph.contexts[edge.dst]
|
| 323 |
-
edge_payload = (
|
| 324 |
-
context,
|
| 325 |
-
acceptor_payload,
|
| 326 |
-
edge.symbol,
|
| 327 |
-
dst_context,
|
| 328 |
-
next_payload,
|
| 329 |
-
edge.probability,
|
| 330 |
-
)
|
| 331 |
-
edge_key = (
|
| 332 |
-
order,
|
| 333 |
-
canonical_integer_shift_key(edge_payload, fixed_symbols=fixed),
|
| 334 |
-
)
|
| 335 |
-
edge_key_cache[edge_cache_key] = edge_key
|
| 336 |
-
time_edge_key = (time, *edge_key)
|
| 337 |
-
edge_counts[edge_key] += 1
|
| 338 |
-
time_edge_counts[time_edge_key] += 1
|
| 339 |
-
scanned_edge_count += 1
|
| 340 |
-
|
| 341 |
-
if include_transition_row_shapes:
|
| 342 |
-
next_payload = acceptor_payload_cache[next_acceptor_state]
|
| 343 |
-
dst_context = graph.contexts[edge.dst]
|
| 344 |
-
row_reference = reference
|
| 345 |
-
if row_reference is None:
|
| 346 |
-
row_reference = _first_shiftable_symbol(
|
| 347 |
-
(edge.symbol, dst_context, next_payload),
|
| 348 |
-
fixed,
|
| 349 |
-
)
|
| 350 |
-
row_edges.append(
|
| 351 |
-
canonical_integer_shift_key(
|
| 352 |
-
(edge.symbol, dst_context, next_payload, edge.probability),
|
| 353 |
-
fixed_symbols=fixed,
|
| 354 |
-
reference=row_reference,
|
| 355 |
-
)
|
| 356 |
-
)
|
| 357 |
-
|
| 358 |
-
if include_transition_row_shapes:
|
| 359 |
-
row_key = (
|
| 360 |
-
order,
|
| 361 |
-
canonical_integer_shift_key(
|
| 362 |
-
(context, acceptor_payload),
|
| 363 |
-
fixed_symbols=fixed,
|
| 364 |
-
reference=reference,
|
| 365 |
-
),
|
| 366 |
-
tuple(sorted(row_edges, key=repr)),
|
| 367 |
-
)
|
| 368 |
-
row_shapes.add(row_key)
|
| 369 |
-
|
| 370 |
-
if scanned_edge_count != result.product_edge_count:
|
| 371 |
-
raise RuntimeError(
|
| 372 |
-
"orbit diagnostic edge scan did not match BP product-edge count: "
|
| 373 |
-
f"{scanned_edge_count} != {result.product_edge_count}"
|
| 374 |
-
)
|
| 375 |
-
|
| 376 |
-
return RegularProductOrbitStats(
|
| 377 |
-
time_indexed_product_states=result.time_indexed_product_state_count,
|
| 378 |
-
time_indexed_product_state_orbits=len(time_state_counts),
|
| 379 |
-
product_states=result.product_state_count,
|
| 380 |
-
product_state_orbits=len(state_counts),
|
| 381 |
-
product_edges=result.product_edge_count,
|
| 382 |
-
time_indexed_product_edge_orbits=len(time_edge_counts),
|
| 383 |
-
product_edge_orbits=len(edge_counts),
|
| 384 |
-
expanded_rows=expanded_rows,
|
| 385 |
-
transition_row_shape_orbits=len(row_shapes),
|
| 386 |
-
max_time_state_orbit_size=_max_count(time_state_counts),
|
| 387 |
-
max_product_state_orbit_size=_max_count(state_counts),
|
| 388 |
-
max_time_edge_orbit_size=_max_count(time_edge_counts),
|
| 389 |
-
max_edge_orbit_size=_max_count(edge_counts),
|
| 390 |
-
)
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
def regular_row_signature_stats(
|
| 394 |
-
result: RegularOrderStackBPResult,
|
| 395 |
-
*,
|
| 396 |
-
fixed_symbols: Iterable[Symbol] = (),
|
| 397 |
-
) -> RegularRowSignatureStats:
|
| 398 |
-
"""Count exact row-template reuse under integer-shift canonicalization.
|
| 399 |
-
|
| 400 |
-
Unlike the looser orbit edge counters, a row signature contains the
|
| 401 |
-
canonical source payload, every accepted symbol, successor context, successor
|
| 402 |
-
DFA payload, and transition probability. Finite-transform boundary effects
|
| 403 |
-
and nonuniform continuation probabilities therefore split into distinct
|
| 404 |
-
signatures instead of being merged.
|
| 405 |
-
"""
|
| 406 |
-
|
| 407 |
-
fixed = frozenset(fixed_symbols)
|
| 408 |
-
result.start_order_masses()
|
| 409 |
-
|
| 410 |
-
acceptor_payload_cache: dict[Hashable, Hashable] = {}
|
| 411 |
-
product_row_counts: Counter[Hashable] = Counter()
|
| 412 |
-
time_masked_row_counts: Counter[Hashable] = Counter()
|
| 413 |
-
product_seen: set[tuple[int, int, Hashable]] = set()
|
| 414 |
-
time_masked_edges = 0
|
| 415 |
-
|
| 416 |
-
for order, cache in result.backwards.items():
|
| 417 |
-
graph = cache.graph
|
| 418 |
-
for time, graph_state, acceptor_state in cache.memo:
|
| 419 |
-
if time == result.length:
|
| 420 |
-
continue
|
| 421 |
-
|
| 422 |
-
product_key = (order, graph_state, acceptor_state)
|
| 423 |
-
if product_key not in product_seen:
|
| 424 |
-
product_seen.add(product_key)
|
| 425 |
-
signature, _edge_count = _regular_row_signature(
|
| 426 |
-
result,
|
| 427 |
-
order=order,
|
| 428 |
-
graph_state=graph_state,
|
| 429 |
-
acceptor_state=acceptor_state,
|
| 430 |
-
constraint=None,
|
| 431 |
-
acceptor_payload_cache=acceptor_payload_cache,
|
| 432 |
-
fixed_symbols=fixed,
|
| 433 |
-
)
|
| 434 |
-
product_row_counts[signature] += 1
|
| 435 |
-
|
| 436 |
-
time_signature, edge_count = _regular_row_signature(
|
| 437 |
-
result,
|
| 438 |
-
order=order,
|
| 439 |
-
graph_state=graph_state,
|
| 440 |
-
acceptor_state=acceptor_state,
|
| 441 |
-
constraint=result.constraints.get(time),
|
| 442 |
-
acceptor_payload_cache=acceptor_payload_cache,
|
| 443 |
-
fixed_symbols=fixed,
|
| 444 |
-
)
|
| 445 |
-
time_masked_row_counts[time_signature] += 1
|
| 446 |
-
time_masked_edges += edge_count
|
| 447 |
-
|
| 448 |
-
if time_masked_edges != result.product_edge_count:
|
| 449 |
-
raise RuntimeError(
|
| 450 |
-
"row-signature edge scan did not match BP product-edge count: "
|
| 451 |
-
f"{time_masked_edges} != {result.product_edge_count}"
|
| 452 |
-
)
|
| 453 |
-
|
| 454 |
-
return RegularRowSignatureStats(
|
| 455 |
-
product_rows=len(product_seen),
|
| 456 |
-
product_row_signatures=len(product_row_counts),
|
| 457 |
-
reusable_product_rows=_reusable_count(product_row_counts),
|
| 458 |
-
max_product_row_signature_size=_max_count(product_row_counts),
|
| 459 |
-
time_masked_rows=sum(time_masked_row_counts.values()),
|
| 460 |
-
time_masked_row_signatures=len(time_masked_row_counts),
|
| 461 |
-
reusable_time_masked_rows=_reusable_count(time_masked_row_counts),
|
| 462 |
-
max_time_masked_row_signature_size=_max_count(time_masked_row_counts),
|
| 463 |
-
time_masked_edges=time_masked_edges,
|
| 464 |
-
)
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
def _acceptor_state_payload(acceptor: Any, state: Hashable) -> Hashable:
|
| 468 |
-
prefixes = getattr(acceptor, "prefixes", None)
|
| 469 |
-
if prefixes is not None and isinstance(state, int) and 0 <= state < len(prefixes):
|
| 470 |
-
return tuple(prefixes[state])
|
| 471 |
-
return ("dfa_state", state)
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
def _constraint_allows(constraint: PositionConstraint | None, symbol: Symbol) -> bool:
|
| 475 |
-
if constraint is None:
|
| 476 |
-
return True
|
| 477 |
-
if callable(constraint):
|
| 478 |
-
return bool(constraint(symbol))
|
| 479 |
-
return symbol in constraint
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
def _regular_row_signature(
|
| 483 |
-
result: RegularOrderStackBPResult,
|
| 484 |
-
*,
|
| 485 |
-
order: int,
|
| 486 |
-
graph_state: int,
|
| 487 |
-
acceptor_state: Hashable,
|
| 488 |
-
constraint: PositionConstraint | None,
|
| 489 |
-
acceptor_payload_cache: dict[Hashable, Hashable],
|
| 490 |
-
fixed_symbols: frozenset[Symbol],
|
| 491 |
-
) -> tuple[Hashable, int]:
|
| 492 |
-
cache = result.backwards[order]
|
| 493 |
-
graph = cache.graph
|
| 494 |
-
context = graph.contexts[graph_state]
|
| 495 |
-
acceptor_payload = acceptor_payload_cache.get(acceptor_state)
|
| 496 |
-
if acceptor_payload is None:
|
| 497 |
-
acceptor_payload = _acceptor_state_payload(result.acceptor, acceptor_state)
|
| 498 |
-
acceptor_payload_cache[acceptor_state] = acceptor_payload
|
| 499 |
-
|
| 500 |
-
source_payload = (context, acceptor_payload)
|
| 501 |
-
reference = _first_shiftable_symbol(source_payload, fixed_symbols)
|
| 502 |
-
source_key = canonical_integer_shift_key(
|
| 503 |
-
source_payload,
|
| 504 |
-
fixed_symbols=fixed_symbols,
|
| 505 |
-
reference=reference,
|
| 506 |
-
)
|
| 507 |
-
|
| 508 |
-
transition_keys: list[Hashable] = []
|
| 509 |
-
edge_count = 0
|
| 510 |
-
for edge, next_acceptor_state, _transition_weight in cache.accepted_transitions(
|
| 511 |
-
graph_state,
|
| 512 |
-
acceptor_state,
|
| 513 |
-
):
|
| 514 |
-
if not _constraint_allows(constraint, edge.symbol):
|
| 515 |
-
continue
|
| 516 |
-
next_payload = acceptor_payload_cache.get(next_acceptor_state)
|
| 517 |
-
if next_payload is None:
|
| 518 |
-
next_payload = _acceptor_state_payload(result.acceptor, next_acceptor_state)
|
| 519 |
-
acceptor_payload_cache[next_acceptor_state] = next_payload
|
| 520 |
-
dst_context = graph.contexts[edge.dst]
|
| 521 |
-
transition_keys.append(
|
| 522 |
-
canonical_integer_shift_key(
|
| 523 |
-
(
|
| 524 |
-
edge.symbol,
|
| 525 |
-
dst_context,
|
| 526 |
-
next_payload,
|
| 527 |
-
edge.probability,
|
| 528 |
-
),
|
| 529 |
-
fixed_symbols=fixed_symbols,
|
| 530 |
-
reference=reference,
|
| 531 |
-
)
|
| 532 |
-
)
|
| 533 |
-
edge_count += 1
|
| 534 |
-
|
| 535 |
-
return (order, source_key, tuple(sorted(transition_keys, key=repr))), edge_count
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
def _canonicalize(value: Any, fixed_symbols: frozenset[Symbol], reference: int) -> Hashable:
|
| 539 |
-
if _is_shiftable_integer(value, fixed_symbols):
|
| 540 |
-
return ("rel_int", int(value) - reference)
|
| 541 |
-
if isinstance(value, tuple):
|
| 542 |
-
return tuple(_canonicalize(item, fixed_symbols, reference) for item in value)
|
| 543 |
-
if isinstance(value, list):
|
| 544 |
-
return tuple(_canonicalize(item, fixed_symbols, reference) for item in value)
|
| 545 |
-
return ("fixed", value)
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
def _first_shiftable_symbol(value: Any, fixed_symbols: frozenset[Symbol]) -> int | None:
|
| 549 |
-
if _is_shiftable_integer(value, fixed_symbols):
|
| 550 |
-
return int(value)
|
| 551 |
-
if isinstance(value, (tuple, list)):
|
| 552 |
-
for item in value:
|
| 553 |
-
found = _first_shiftable_symbol(item, fixed_symbols)
|
| 554 |
-
if found is not None:
|
| 555 |
-
return found
|
| 556 |
-
return None
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
def _is_shiftable_integer(value: Any, fixed_symbols: frozenset[Symbol]) -> bool:
|
| 560 |
-
return isinstance(value, Integral) and not isinstance(value, bool) and value not in fixed_symbols
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
def _max_count(counter: Mapping[Hashable, int]) -> int:
|
| 564 |
-
return max(counter.values(), default=0)
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
def _reusable_count(counter: Mapping[Hashable, int]) -> int:
|
| 568 |
-
return sum(count for count in counter.values() if count > 1)
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
def _ratio(numerator: int, denominator: int) -> float:
|
| 572 |
-
if denominator == 0:
|
| 573 |
-
return 0.0
|
| 574 |
-
return float(numerator) / float(denominator)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/order_stack_bp.py
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
vendor/vo_regular_bp/vo_regular_bp/padding.py
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
"""Utilities for fixed-horizon padded generation."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from collections.abc import Iterable, Sequence
|
| 6 |
-
from typing import TypeVar
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
SymbolT = TypeVar("SymbolT")
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
def append_padding(
|
| 13 |
-
sequences: Iterable[Sequence[SymbolT]],
|
| 14 |
-
*,
|
| 15 |
-
pad_symbol: SymbolT,
|
| 16 |
-
pad_count: int,
|
| 17 |
-
) -> tuple[tuple[SymbolT, ...], ...]:
|
| 18 |
-
"""Append repeated PAD symbols to each training sequence.
|
| 19 |
-
|
| 20 |
-
This is the standard fixed-horizon encoding of variable-length generation:
|
| 21 |
-
train on phrase/bar sequences followed by enough PAD symbols for the chosen
|
| 22 |
-
model order, then constrain PAD to be zero-cost and absorbing.
|
| 23 |
-
|
| 24 |
-
Probabilities are those of the padded model: the first PAD transition is a
|
| 25 |
-
modeled stop/ending event, while trailing PAD self-loops should become
|
| 26 |
-
deterministic when enough padding is appended.
|
| 27 |
-
|
| 28 |
-
Use at least ``max_order + 1`` PAD symbols when building an order-stack
|
| 29 |
-
model so every fixed-order PAD context has a PAD self-loop.
|
| 30 |
-
"""
|
| 31 |
-
|
| 32 |
-
if pad_count < 1:
|
| 33 |
-
raise ValueError("pad_count must be at least 1")
|
| 34 |
-
padding = (pad_symbol,) * int(pad_count)
|
| 35 |
-
return tuple(tuple(sequence) + padding for sequence in sequences)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/positional_bp.py
DELETED
|
@@ -1,393 +0,0 @@
|
|
| 1 |
-
"""Exact BP for fixed-horizon positional constraints.
|
| 2 |
-
|
| 3 |
-
This module is the no-DFA specialization of product BP. Positional constraints
|
| 4 |
-
are represented as time-indexed symbol masks, so the recursion only ranges over
|
| 5 |
-
context states:
|
| 6 |
-
|
| 7 |
-
beta[t, s] = sum_y P(y | s) beta[t + 1, canon(s, y))
|
| 8 |
-
|
| 9 |
-
No approximation or pruning is introduced.
|
| 10 |
-
"""
|
| 11 |
-
|
| 12 |
-
from __future__ import annotations
|
| 13 |
-
|
| 14 |
-
from collections import Counter, defaultdict
|
| 15 |
-
from collections.abc import Callable, Iterable, Mapping, Sequence
|
| 16 |
-
from dataclasses import dataclass, field
|
| 17 |
-
import random
|
| 18 |
-
from typing import Protocol
|
| 19 |
-
|
| 20 |
-
from .context import Context, ContextGraph, Edge, Symbol, _as_context
|
| 21 |
-
from .product_bp import _sample_order
|
| 22 |
-
|
| 23 |
-
PositionConstraint = Callable[[Symbol], bool] | Iterable[Symbol]
|
| 24 |
-
PositionConstraints = Mapping[int, PositionConstraint]
|
| 25 |
-
AllowedForbiddenSymbols = Mapping[int, Iterable[Symbol]]
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
class ContextModel(Protocol):
|
| 29 |
-
start_state: Context
|
| 30 |
-
|
| 31 |
-
def outgoing(self, context: Iterable[Symbol] | Context) -> tuple[Edge, ...]:
|
| 32 |
-
...
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
class LazyBackoffContextModel:
|
| 36 |
-
"""Backoff continuation model with outgoing edges compiled on demand.
|
| 37 |
-
|
| 38 |
-
The stochastic semantics match ``ContextGraph.from_backoff_sequences``:
|
| 39 |
-
each context uses a normalized weighted mixture of its own continuation
|
| 40 |
-
distribution and lower-order suffix distributions. The difference is only
|
| 41 |
-
operational: edges are materialized when reached by BP.
|
| 42 |
-
"""
|
| 43 |
-
|
| 44 |
-
def __init__(
|
| 45 |
-
self,
|
| 46 |
-
counts: Mapping[Context, Counter[Symbol]],
|
| 47 |
-
*,
|
| 48 |
-
max_order: int,
|
| 49 |
-
backoff_weight: float,
|
| 50 |
-
start_state: Iterable[Symbol] | Context = (),
|
| 51 |
-
track_order_weights: bool = False,
|
| 52 |
-
) -> None:
|
| 53 |
-
if max_order < 0:
|
| 54 |
-
raise ValueError("max_order must be non-negative")
|
| 55 |
-
if not 0.0 <= backoff_weight <= 1.0:
|
| 56 |
-
raise ValueError("backoff_weight must be in [0, 1]")
|
| 57 |
-
|
| 58 |
-
self.counts = dict(counts)
|
| 59 |
-
self.max_order = int(max_order)
|
| 60 |
-
self.backoff_weight = float(backoff_weight)
|
| 61 |
-
self.start_state = _as_context(start_state)
|
| 62 |
-
self.track_order_weights = bool(track_order_weights)
|
| 63 |
-
contexts = set(self.counts)
|
| 64 |
-
contexts.add(self.start_state)
|
| 65 |
-
self.contexts = frozenset(contexts)
|
| 66 |
-
self.alphabet = frozenset(symbol for counter in self.counts.values() for symbol in counter)
|
| 67 |
-
self.distributions = {
|
| 68 |
-
context: tuple((symbol, float(count) / total) for symbol, count in counter.items())
|
| 69 |
-
for context, counter in self.counts.items()
|
| 70 |
-
for total in (float(sum(counter.values())),)
|
| 71 |
-
if total > 0.0
|
| 72 |
-
}
|
| 73 |
-
self._outgoing_cache: dict[Context, tuple[Edge, ...]] = {}
|
| 74 |
-
|
| 75 |
-
@classmethod
|
| 76 |
-
def from_sequences(
|
| 77 |
-
cls,
|
| 78 |
-
sequences: Iterable[Sequence[Symbol]],
|
| 79 |
-
*,
|
| 80 |
-
max_order: int,
|
| 81 |
-
backoff_weight: float,
|
| 82 |
-
start_state: Iterable[Symbol] | Context = (),
|
| 83 |
-
track_order_weights: bool = False,
|
| 84 |
-
) -> "LazyBackoffContextModel":
|
| 85 |
-
if max_order < 0:
|
| 86 |
-
raise ValueError("max_order must be non-negative")
|
| 87 |
-
|
| 88 |
-
counts: dict[Context, Counter[Symbol]] = defaultdict(Counter)
|
| 89 |
-
for sequence in sequences:
|
| 90 |
-
tokens = tuple(sequence)
|
| 91 |
-
for index, symbol in enumerate(tokens):
|
| 92 |
-
order_limit = min(max_order, index)
|
| 93 |
-
for order in range(order_limit + 1):
|
| 94 |
-
context = tokens[index - order : index] if order else ()
|
| 95 |
-
counts[context][symbol] += 1
|
| 96 |
-
return cls(
|
| 97 |
-
counts,
|
| 98 |
-
max_order=max_order,
|
| 99 |
-
backoff_weight=backoff_weight,
|
| 100 |
-
start_state=start_state,
|
| 101 |
-
track_order_weights=track_order_weights,
|
| 102 |
-
)
|
| 103 |
-
|
| 104 |
-
def outgoing(self, context: Iterable[Symbol] | Context) -> tuple[Edge, ...]:
|
| 105 |
-
state = _as_context(context)
|
| 106 |
-
cached = self._outgoing_cache.get(state)
|
| 107 |
-
if cached is not None:
|
| 108 |
-
return cached
|
| 109 |
-
|
| 110 |
-
if self.track_order_weights:
|
| 111 |
-
edges = self._outgoing_with_order_weights(state)
|
| 112 |
-
self._outgoing_cache[state] = edges
|
| 113 |
-
return edges
|
| 114 |
-
|
| 115 |
-
scores: dict[Symbol, float] = {}
|
| 116 |
-
context_order = len(state)
|
| 117 |
-
for order in range(context_order, -1, -1):
|
| 118 |
-
suffix = state[-order:] if order else ()
|
| 119 |
-
distribution = self.distributions.get(suffix)
|
| 120 |
-
if not distribution:
|
| 121 |
-
continue
|
| 122 |
-
weight = self.backoff_weight ** (context_order - order)
|
| 123 |
-
for symbol, probability in distribution:
|
| 124 |
-
contribution = weight * probability
|
| 125 |
-
scores[symbol] = scores.get(symbol, 0.0) + contribution
|
| 126 |
-
|
| 127 |
-
total_score = float(sum(scores.values()))
|
| 128 |
-
if total_score <= 0.0:
|
| 129 |
-
edges: tuple[Edge, ...] = ()
|
| 130 |
-
else:
|
| 131 |
-
edges = tuple(
|
| 132 |
-
Edge(
|
| 133 |
-
symbol,
|
| 134 |
-
score / total_score,
|
| 135 |
-
ContextGraph._canon(state, symbol, self.contexts, self.max_order),
|
| 136 |
-
)
|
| 137 |
-
for symbol, score in scores.items()
|
| 138 |
-
if score > 0.0
|
| 139 |
-
)
|
| 140 |
-
self._outgoing_cache[state] = edges
|
| 141 |
-
return edges
|
| 142 |
-
|
| 143 |
-
def _outgoing_with_order_weights(self, state: Context) -> tuple[Edge, ...]:
|
| 144 |
-
scores: dict[Symbol, float] = {}
|
| 145 |
-
order_scores: dict[Symbol, Counter[int]] = defaultdict(Counter)
|
| 146 |
-
context_order = len(state)
|
| 147 |
-
for order in range(context_order, -1, -1):
|
| 148 |
-
suffix = state[-order:] if order else ()
|
| 149 |
-
distribution = self.distributions.get(suffix)
|
| 150 |
-
if not distribution:
|
| 151 |
-
continue
|
| 152 |
-
weight = self.backoff_weight ** (context_order - order)
|
| 153 |
-
for symbol, probability in distribution:
|
| 154 |
-
contribution = weight * probability
|
| 155 |
-
scores[symbol] = scores.get(symbol, 0.0) + contribution
|
| 156 |
-
order_scores[symbol][order] += contribution
|
| 157 |
-
|
| 158 |
-
total_score = float(sum(scores.values()))
|
| 159 |
-
if total_score <= 0.0:
|
| 160 |
-
return ()
|
| 161 |
-
return tuple(
|
| 162 |
-
Edge(
|
| 163 |
-
symbol,
|
| 164 |
-
score / total_score,
|
| 165 |
-
ContextGraph._canon(state, symbol, self.contexts, self.max_order),
|
| 166 |
-
_normalized_order_weights(order_scores[symbol], score),
|
| 167 |
-
)
|
| 168 |
-
for symbol, score in scores.items()
|
| 169 |
-
if score > 0.0
|
| 170 |
-
)
|
| 171 |
-
|
| 172 |
-
def prefix_context(self, prefix: Sequence[Symbol]) -> Context:
|
| 173 |
-
for order in range(min(self.max_order, len(prefix)), -1, -1):
|
| 174 |
-
suffix = tuple(prefix[-order:]) if order else ()
|
| 175 |
-
if suffix in self.contexts:
|
| 176 |
-
return suffix
|
| 177 |
-
return self.start_state
|
| 178 |
-
|
| 179 |
-
@property
|
| 180 |
-
def materialized_edge_count(self) -> int:
|
| 181 |
-
return sum(len(edges) for edges in self._outgoing_cache.values())
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
@dataclass
|
| 185 |
-
class PositionalBPResult:
|
| 186 |
-
model: ContextModel
|
| 187 |
-
length: int
|
| 188 |
-
start_context: Context
|
| 189 |
-
constraints: PositionConstraints
|
| 190 |
-
betas: list[dict[Context, float]]
|
| 191 |
-
allowed_edge_counts: list[dict[Context, int]]
|
| 192 |
-
_transition_weight_cache: dict[tuple[int, Context], tuple[tuple[Edge, float], ...]] = field(
|
| 193 |
-
default_factory=dict,
|
| 194 |
-
init=False,
|
| 195 |
-
repr=False,
|
| 196 |
-
)
|
| 197 |
-
|
| 198 |
-
@property
|
| 199 |
-
def partition_function(self) -> float:
|
| 200 |
-
return self.betas[0].get(self.start_context, 0.0)
|
| 201 |
-
|
| 202 |
-
@property
|
| 203 |
-
def unique_state_count(self) -> int:
|
| 204 |
-
return len(set().union(*self.betas)) if self.betas else 0
|
| 205 |
-
|
| 206 |
-
@property
|
| 207 |
-
def time_indexed_state_count(self) -> int:
|
| 208 |
-
return sum(len(layer) for layer in self.betas)
|
| 209 |
-
|
| 210 |
-
@property
|
| 211 |
-
def edge_count(self) -> int:
|
| 212 |
-
return sum(sum(layer.values()) for layer in self.allowed_edge_counts)
|
| 213 |
-
|
| 214 |
-
def transition_weights(self, time: int, context: Iterable[Symbol] | Context) -> tuple[tuple[Edge, float], ...]:
|
| 215 |
-
if time < 0 or time >= self.length:
|
| 216 |
-
raise ValueError("time is outside the BP horizon")
|
| 217 |
-
state = _as_context(context)
|
| 218 |
-
cache_key = (time, state)
|
| 219 |
-
cached = self._transition_weight_cache.get(cache_key)
|
| 220 |
-
if cached is not None:
|
| 221 |
-
return cached
|
| 222 |
-
|
| 223 |
-
beta_next = self.betas[time + 1]
|
| 224 |
-
constraint = self.constraints.get(time)
|
| 225 |
-
weighted = []
|
| 226 |
-
for edge in self.model.outgoing(state):
|
| 227 |
-
if not _allows(constraint, edge.symbol):
|
| 228 |
-
continue
|
| 229 |
-
weight = edge.probability * beta_next.get(edge.next_state, 0.0)
|
| 230 |
-
if weight > 0.0:
|
| 231 |
-
weighted.append((edge, weight))
|
| 232 |
-
result = tuple(weighted)
|
| 233 |
-
self._transition_weight_cache[cache_key] = result
|
| 234 |
-
return result
|
| 235 |
-
|
| 236 |
-
def _sample_edge(self, time: int, context: Context, generator: random.Random) -> Edge:
|
| 237 |
-
weighted = self.transition_weights(time, context)
|
| 238 |
-
total = sum(weight for _, weight in weighted)
|
| 239 |
-
if total <= 0.0:
|
| 240 |
-
raise RuntimeError("BP table contains no positive continuation for a reachable state")
|
| 241 |
-
threshold = generator.random() * total
|
| 242 |
-
cumulative = 0.0
|
| 243 |
-
chosen = weighted[-1][0]
|
| 244 |
-
for edge, weight in weighted:
|
| 245 |
-
cumulative += weight
|
| 246 |
-
if threshold <= cumulative:
|
| 247 |
-
return edge
|
| 248 |
-
return chosen
|
| 249 |
-
|
| 250 |
-
def sample(self, *, rng: random.Random | int | None = None) -> tuple[Symbol, ...]:
|
| 251 |
-
generator = _coerce_rng(rng)
|
| 252 |
-
if self.partition_function <= 0.0:
|
| 253 |
-
raise ValueError("cannot sample because the constrained partition function is zero")
|
| 254 |
-
|
| 255 |
-
context = self.start_context
|
| 256 |
-
output: list[Symbol] = []
|
| 257 |
-
for time in range(self.length):
|
| 258 |
-
edge = self._sample_edge(time, context, generator)
|
| 259 |
-
output.append(edge.symbol)
|
| 260 |
-
context = edge.next_state
|
| 261 |
-
return tuple(output)
|
| 262 |
-
|
| 263 |
-
def sample_many(self, count: int, *, rng: random.Random | int | None = None) -> list[tuple[Symbol, ...]]:
|
| 264 |
-
generator = _coerce_rng(rng)
|
| 265 |
-
return [self.sample(rng=generator) for _ in range(count)]
|
| 266 |
-
|
| 267 |
-
def sample_with_orders(
|
| 268 |
-
self,
|
| 269 |
-
*,
|
| 270 |
-
rng: random.Random | int | None = None,
|
| 271 |
-
) -> tuple[tuple[Symbol, ...], tuple[int, ...]]:
|
| 272 |
-
generator = _coerce_rng(rng)
|
| 273 |
-
if self.partition_function <= 0.0:
|
| 274 |
-
raise ValueError("cannot sample because the constrained partition function is zero")
|
| 275 |
-
|
| 276 |
-
context = self.start_context
|
| 277 |
-
output: list[Symbol] = []
|
| 278 |
-
orders: list[int] = []
|
| 279 |
-
for time in range(self.length):
|
| 280 |
-
edge = self._sample_edge(time, context, generator)
|
| 281 |
-
output.append(edge.symbol)
|
| 282 |
-
orders.append(_sample_order(edge.order_weights or ((len(context), 1.0),), generator))
|
| 283 |
-
context = edge.next_state
|
| 284 |
-
return tuple(output), tuple(orders)
|
| 285 |
-
|
| 286 |
-
def conditional_probability(self, sequence: Sequence[Symbol]) -> float:
|
| 287 |
-
if len(sequence) != self.length:
|
| 288 |
-
raise ValueError("sequence length must match the BP horizon")
|
| 289 |
-
if self.partition_function <= 0.0:
|
| 290 |
-
return 0.0
|
| 291 |
-
|
| 292 |
-
context = self.start_context
|
| 293 |
-
probability = 1.0
|
| 294 |
-
for time, symbol in enumerate(sequence):
|
| 295 |
-
beta_now = self.betas[time].get(context, 0.0)
|
| 296 |
-
if beta_now <= 0.0:
|
| 297 |
-
return 0.0
|
| 298 |
-
edge = next(
|
| 299 |
-
(edge for edge, _ in self.transition_weights(time, context) if edge.symbol == symbol),
|
| 300 |
-
None,
|
| 301 |
-
)
|
| 302 |
-
if edge is None:
|
| 303 |
-
return 0.0
|
| 304 |
-
weight = edge.probability * self.betas[time + 1].get(edge.next_state, 0.0)
|
| 305 |
-
if weight <= 0.0:
|
| 306 |
-
return 0.0
|
| 307 |
-
probability *= weight / beta_now
|
| 308 |
-
context = edge.next_state
|
| 309 |
-
return probability
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
def run_positional_bp(
|
| 313 |
-
model: ContextModel,
|
| 314 |
-
*,
|
| 315 |
-
length: int,
|
| 316 |
-
start_context: Iterable[Symbol] | Context | None = None,
|
| 317 |
-
constraints: PositionConstraints | None = None,
|
| 318 |
-
) -> PositionalBPResult:
|
| 319 |
-
"""Run exact backward DP with time-indexed positional constraints."""
|
| 320 |
-
|
| 321 |
-
if length < 0:
|
| 322 |
-
raise ValueError("length must be non-negative")
|
| 323 |
-
context0 = model.start_state if start_context is None else _as_context(start_context)
|
| 324 |
-
position_constraints = dict(constraints or {})
|
| 325 |
-
_validate_constraint_positions(position_constraints, length)
|
| 326 |
-
|
| 327 |
-
betas: list[dict[Context, float]] = [dict() for _ in range(length + 1)]
|
| 328 |
-
allowed_edge_counts: list[dict[Context, int]] = [dict() for _ in range(length)]
|
| 329 |
-
|
| 330 |
-
def beta(time_index: int, context: Context) -> float:
|
| 331 |
-
cached = betas[time_index].get(context)
|
| 332 |
-
if cached is not None:
|
| 333 |
-
return cached
|
| 334 |
-
if time_index == length:
|
| 335 |
-
betas[time_index][context] = 1.0
|
| 336 |
-
return 1.0
|
| 337 |
-
|
| 338 |
-
constraint = position_constraints.get(time_index)
|
| 339 |
-
total = 0.0
|
| 340 |
-
allowed_count = 0
|
| 341 |
-
for edge in model.outgoing(context):
|
| 342 |
-
if not _allows(constraint, edge.symbol):
|
| 343 |
-
continue
|
| 344 |
-
allowed_count += 1
|
| 345 |
-
total += edge.probability * beta(time_index + 1, edge.next_state)
|
| 346 |
-
|
| 347 |
-
allowed_edge_counts[time_index][context] = allowed_count
|
| 348 |
-
betas[time_index][context] = total
|
| 349 |
-
return total
|
| 350 |
-
|
| 351 |
-
beta(0, context0)
|
| 352 |
-
return PositionalBPResult(
|
| 353 |
-
model=model,
|
| 354 |
-
length=length,
|
| 355 |
-
start_context=context0,
|
| 356 |
-
constraints=position_constraints,
|
| 357 |
-
betas=betas,
|
| 358 |
-
allowed_edge_counts=allowed_edge_counts,
|
| 359 |
-
)
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
def _normalized_order_weights(
|
| 363 |
-
order_scores: Counter[int],
|
| 364 |
-
symbol_score: float,
|
| 365 |
-
) -> tuple[tuple[int, float], ...]:
|
| 366 |
-
if symbol_score <= 0.0:
|
| 367 |
-
return ()
|
| 368 |
-
return tuple(
|
| 369 |
-
sorted(
|
| 370 |
-
((order, contribution / symbol_score) for order, contribution in order_scores.items()),
|
| 371 |
-
reverse=True,
|
| 372 |
-
)
|
| 373 |
-
)
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
def _allows(constraint: PositionConstraint | None, symbol: Symbol) -> bool:
|
| 377 |
-
if constraint is None:
|
| 378 |
-
return True
|
| 379 |
-
if callable(constraint):
|
| 380 |
-
return bool(constraint(symbol))
|
| 381 |
-
return symbol in constraint
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
def _validate_constraint_positions(constraints: Mapping[int, PositionConstraint], length: int) -> None:
|
| 385 |
-
for position in constraints:
|
| 386 |
-
if position < 0 or position >= length:
|
| 387 |
-
raise IndexError(f"constraint position {position} is outside length {length}")
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
def _coerce_rng(rng: random.Random | int | None) -> random.Random:
|
| 391 |
-
if isinstance(rng, random.Random):
|
| 392 |
-
return rng
|
| 393 |
-
return random.Random(rng)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vendor/vo_regular_bp/vo_regular_bp/product_bp.py
DELETED
|
@@ -1,304 +0,0 @@
|
|
| 1 |
-
"""Backward dynamic programming on reachable context-acceptor products."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from dataclasses import dataclass, field
|
| 6 |
-
import random
|
| 7 |
-
from typing import Hashable, Iterable, Sequence
|
| 8 |
-
|
| 9 |
-
from .acceptors import DFA, transition_weight as regular_transition_weight
|
| 10 |
-
from .context import Context, ContextGraph, Symbol, _as_context
|
| 11 |
-
|
| 12 |
-
ProductState = tuple[Context, Hashable]
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
@dataclass(frozen=True)
|
| 16 |
-
class ProductEdge:
|
| 17 |
-
symbol: Symbol
|
| 18 |
-
probability: float
|
| 19 |
-
transition_weight: float
|
| 20 |
-
next_state: ProductState
|
| 21 |
-
order_weights: tuple[tuple[int, float], ...] = ()
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
@dataclass
|
| 25 |
-
class ProductBPResult:
|
| 26 |
-
graph: ContextGraph
|
| 27 |
-
acceptor: DFA
|
| 28 |
-
length: int
|
| 29 |
-
start_state: ProductState
|
| 30 |
-
layers: list[set[ProductState]]
|
| 31 |
-
edges: list[dict[ProductState, tuple[ProductEdge, ...]]]
|
| 32 |
-
betas: list[dict[ProductState, float]]
|
| 33 |
-
_transition_weight_cache: dict[tuple[int, ProductState], tuple[tuple[ProductEdge, float], ...]] = field(
|
| 34 |
-
default_factory=dict,
|
| 35 |
-
init=False,
|
| 36 |
-
repr=False,
|
| 37 |
-
)
|
| 38 |
-
|
| 39 |
-
@property
|
| 40 |
-
def partition_function(self) -> float:
|
| 41 |
-
return self.betas[0].get(self.start_state, 0.0)
|
| 42 |
-
|
| 43 |
-
@property
|
| 44 |
-
def unique_product_state_count(self) -> int:
|
| 45 |
-
return len(set().union(*self.layers)) if self.layers else 0
|
| 46 |
-
|
| 47 |
-
@property
|
| 48 |
-
def time_indexed_product_state_count(self) -> int:
|
| 49 |
-
return sum(len(layer) for layer in self.layers)
|
| 50 |
-
|
| 51 |
-
@property
|
| 52 |
-
def product_edge_count(self) -> int:
|
| 53 |
-
return sum(len(edges) for layer_edges in self.edges for edges in layer_edges.values())
|
| 54 |
-
|
| 55 |
-
@property
|
| 56 |
-
def reachable_acceptor_state_count(self) -> int:
|
| 57 |
-
return len({state for layer in self.layers for _, state in layer})
|
| 58 |
-
|
| 59 |
-
def transition_weights(self, time: int, state: ProductState) -> tuple[tuple[ProductEdge, float], ...]:
|
| 60 |
-
"""Return outgoing product edges and their conditional sampling weights."""
|
| 61 |
-
|
| 62 |
-
if time < 0 or time >= self.length:
|
| 63 |
-
raise ValueError("time is outside the BP horizon")
|
| 64 |
-
cache_key = (time, state)
|
| 65 |
-
if cache_key in self._transition_weight_cache:
|
| 66 |
-
return self._transition_weight_cache[cache_key]
|
| 67 |
-
|
| 68 |
-
beta_next = self.betas[time + 1]
|
| 69 |
-
weighted = []
|
| 70 |
-
for edge in self.edges[time].get(state, ()):
|
| 71 |
-
weight = (
|
| 72 |
-
edge.probability
|
| 73 |
-
* edge.transition_weight
|
| 74 |
-
* beta_next.get(edge.next_state, 0.0)
|
| 75 |
-
)
|
| 76 |
-
if weight > 0.0:
|
| 77 |
-
weighted.append((edge, weight))
|
| 78 |
-
result = tuple(weighted)
|
| 79 |
-
self._transition_weight_cache[cache_key] = result
|
| 80 |
-
return result
|
| 81 |
-
|
| 82 |
-
def _sample_edge(self, time: int, state: ProductState, generator: random.Random) -> ProductEdge:
|
| 83 |
-
weighted = self.transition_weights(time, state)
|
| 84 |
-
total = sum(weight for _, weight in weighted)
|
| 85 |
-
if total <= 0.0:
|
| 86 |
-
raise RuntimeError("BP table contains no positive continuation for a reachable state")
|
| 87 |
-
threshold = generator.random() * total
|
| 88 |
-
cumulative = 0.0
|
| 89 |
-
chosen = weighted[-1][0]
|
| 90 |
-
for edge, weight in weighted:
|
| 91 |
-
cumulative += weight
|
| 92 |
-
if threshold <= cumulative:
|
| 93 |
-
return edge
|
| 94 |
-
return chosen
|
| 95 |
-
|
| 96 |
-
def sample(self, *, rng: random.Random | int | None = None) -> tuple[Symbol, ...]:
|
| 97 |
-
"""Draw one exact sample from the constrained distribution."""
|
| 98 |
-
|
| 99 |
-
generator = _coerce_rng(rng)
|
| 100 |
-
if self.partition_function <= 0.0:
|
| 101 |
-
raise ValueError("cannot sample because the constrained partition function is zero")
|
| 102 |
-
|
| 103 |
-
state = self.start_state
|
| 104 |
-
output: list[Symbol] = []
|
| 105 |
-
for time in range(self.length):
|
| 106 |
-
chosen = self._sample_edge(time, state, generator)
|
| 107 |
-
output.append(chosen.symbol)
|
| 108 |
-
state = chosen.next_state
|
| 109 |
-
return tuple(output)
|
| 110 |
-
|
| 111 |
-
def sample_many(self, count: int, *, rng: random.Random | int | None = None) -> list[tuple[Symbol, ...]]:
|
| 112 |
-
generator = _coerce_rng(rng)
|
| 113 |
-
return [self.sample(rng=generator) for _ in range(count)]
|
| 114 |
-
|
| 115 |
-
def sample_with_orders(
|
| 116 |
-
self,
|
| 117 |
-
*,
|
| 118 |
-
rng: random.Random | int | None = None,
|
| 119 |
-
) -> tuple[tuple[Symbol, ...], tuple[int, ...]]:
|
| 120 |
-
"""Draw one exact sample and a latent order used at each event.
|
| 121 |
-
|
| 122 |
-
For explicit backoff edges, the order is sampled from the posterior
|
| 123 |
-
contribution of each suffix order to the selected symbol. For ordinary
|
| 124 |
-
MLE edges without order metadata, the current context length is used.
|
| 125 |
-
"""
|
| 126 |
-
|
| 127 |
-
generator = _coerce_rng(rng)
|
| 128 |
-
if self.partition_function <= 0.0:
|
| 129 |
-
raise ValueError("cannot sample because the constrained partition function is zero")
|
| 130 |
-
|
| 131 |
-
state = self.start_state
|
| 132 |
-
output: list[Symbol] = []
|
| 133 |
-
orders: list[int] = []
|
| 134 |
-
for time in range(self.length):
|
| 135 |
-
chosen = self._sample_edge(time, state, generator)
|
| 136 |
-
output.append(chosen.symbol)
|
| 137 |
-
orders.append(_sample_order(chosen.order_weights, generator))
|
| 138 |
-
state = chosen.next_state
|
| 139 |
-
return tuple(output), tuple(orders)
|
| 140 |
-
|
| 141 |
-
def sample_many_with_orders(
|
| 142 |
-
self,
|
| 143 |
-
count: int,
|
| 144 |
-
*,
|
| 145 |
-
rng: random.Random | int | None = None,
|
| 146 |
-
) -> list[tuple[tuple[Symbol, ...], tuple[int, ...]]]:
|
| 147 |
-
generator = _coerce_rng(rng)
|
| 148 |
-
return [self.sample_with_orders(rng=generator) for _ in range(count)]
|
| 149 |
-
|
| 150 |
-
def conditional_probability(self, sequence: Sequence[Symbol]) -> float:
|
| 151 |
-
"""Probability of ``sequence`` under the constrained BP distribution."""
|
| 152 |
-
|
| 153 |
-
if len(sequence) != self.length:
|
| 154 |
-
raise ValueError("sequence length must match the BP horizon")
|
| 155 |
-
if self.partition_function <= 0.0:
|
| 156 |
-
return 0.0
|
| 157 |
-
|
| 158 |
-
state = self.start_state
|
| 159 |
-
probability = 1.0
|
| 160 |
-
for time, symbol in enumerate(sequence):
|
| 161 |
-
beta_now = self.betas[time].get(state, 0.0)
|
| 162 |
-
if beta_now <= 0.0:
|
| 163 |
-
return 0.0
|
| 164 |
-
edge = next(
|
| 165 |
-
(
|
| 166 |
-
edge
|
| 167 |
-
for edge in self.edges[time].get(state, ())
|
| 168 |
-
if edge.symbol == symbol
|
| 169 |
-
),
|
| 170 |
-
None,
|
| 171 |
-
)
|
| 172 |
-
if edge is None:
|
| 173 |
-
return 0.0
|
| 174 |
-
weight = (
|
| 175 |
-
edge.probability
|
| 176 |
-
* edge.transition_weight
|
| 177 |
-
* self.betas[time + 1].get(edge.next_state, 0.0)
|
| 178 |
-
)
|
| 179 |
-
if weight <= 0.0:
|
| 180 |
-
return 0.0
|
| 181 |
-
probability *= weight / beta_now
|
| 182 |
-
state = edge.next_state
|
| 183 |
-
return probability
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
def run_bp(
|
| 187 |
-
graph: ContextGraph,
|
| 188 |
-
acceptor: DFA,
|
| 189 |
-
*,
|
| 190 |
-
length: int,
|
| 191 |
-
start_context: Iterable[Symbol] | Context | None = None,
|
| 192 |
-
start_acceptor_state: Hashable | None = None,
|
| 193 |
-
) -> ProductBPResult:
|
| 194 |
-
"""Run exact backward DP on the reachable product for a fixed horizon."""
|
| 195 |
-
|
| 196 |
-
if length < 0:
|
| 197 |
-
raise ValueError("length must be non-negative")
|
| 198 |
-
|
| 199 |
-
context0 = graph.start_state if start_context is None else _as_context(start_context)
|
| 200 |
-
acceptor0 = acceptor.start_state if start_acceptor_state is None else start_acceptor_state
|
| 201 |
-
start = (context0, acceptor0)
|
| 202 |
-
|
| 203 |
-
layers: list[set[ProductState]] = [set([start])]
|
| 204 |
-
edges_by_time: list[dict[ProductState, tuple[ProductEdge, ...]]] = []
|
| 205 |
-
|
| 206 |
-
for _time in range(length):
|
| 207 |
-
current_layer = layers[-1]
|
| 208 |
-
next_layer: set[ProductState] = set()
|
| 209 |
-
current_edges: dict[ProductState, tuple[ProductEdge, ...]] = {}
|
| 210 |
-
|
| 211 |
-
for context_state, acceptor_state in current_layer:
|
| 212 |
-
product_edges = []
|
| 213 |
-
for edge in graph.outgoing(context_state):
|
| 214 |
-
next_acceptor_state = acceptor.next_state(acceptor_state, edge.symbol)
|
| 215 |
-
if next_acceptor_state is None:
|
| 216 |
-
continue
|
| 217 |
-
dfa_weight = regular_transition_weight(acceptor, acceptor_state, edge.symbol)
|
| 218 |
-
if dfa_weight <= 0.0:
|
| 219 |
-
continue
|
| 220 |
-
next_product_state = (edge.next_state, next_acceptor_state)
|
| 221 |
-
product_edges.append(
|
| 222 |
-
ProductEdge(
|
| 223 |
-
symbol=edge.symbol,
|
| 224 |
-
probability=edge.probability,
|
| 225 |
-
transition_weight=dfa_weight,
|
| 226 |
-
next_state=next_product_state,
|
| 227 |
-
order_weights=edge.order_weights or ((len(context_state), 1.0),),
|
| 228 |
-
)
|
| 229 |
-
)
|
| 230 |
-
next_layer.add(next_product_state)
|
| 231 |
-
current_edges[(context_state, acceptor_state)] = tuple(product_edges)
|
| 232 |
-
|
| 233 |
-
edges_by_time.append(current_edges)
|
| 234 |
-
layers.append(next_layer)
|
| 235 |
-
|
| 236 |
-
betas: list[dict[ProductState, float]] = [dict() for _ in range(length + 1)]
|
| 237 |
-
betas[length] = {
|
| 238 |
-
state: 1.0 if acceptor.is_accepting(state[1]) else 0.0
|
| 239 |
-
for state in layers[length]
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
-
for time in range(length - 1, -1, -1):
|
| 243 |
-
beta_next = betas[time + 1]
|
| 244 |
-
beta_now: dict[ProductState, float] = {}
|
| 245 |
-
for state in layers[time]:
|
| 246 |
-
beta_now[state] = sum(
|
| 247 |
-
edge.probability
|
| 248 |
-
* edge.transition_weight
|
| 249 |
-
* beta_next.get(edge.next_state, 0.0)
|
| 250 |
-
for edge in edges_by_time[time].get(state, ())
|
| 251 |
-
)
|
| 252 |
-
betas[time] = beta_now
|
| 253 |
-
|
| 254 |
-
return ProductBPResult(
|
| 255 |
-
graph=graph,
|
| 256 |
-
acceptor=acceptor,
|
| 257 |
-
length=length,
|
| 258 |
-
start_state=start,
|
| 259 |
-
layers=layers,
|
| 260 |
-
edges=edges_by_time,
|
| 261 |
-
betas=betas,
|
| 262 |
-
)
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
def sample_exact(
|
| 266 |
-
graph: ContextGraph,
|
| 267 |
-
acceptor: DFA,
|
| 268 |
-
*,
|
| 269 |
-
length: int,
|
| 270 |
-
rng: random.Random | int | None = None,
|
| 271 |
-
start_context: Iterable[Symbol] | Context | None = None,
|
| 272 |
-
start_acceptor_state: Hashable | None = None,
|
| 273 |
-
) -> tuple[Symbol, ...]:
|
| 274 |
-
"""Convenience wrapper: run BP and draw one exact sample."""
|
| 275 |
-
|
| 276 |
-
result = run_bp(
|
| 277 |
-
graph,
|
| 278 |
-
acceptor,
|
| 279 |
-
length=length,
|
| 280 |
-
start_context=start_context,
|
| 281 |
-
start_acceptor_state=start_acceptor_state,
|
| 282 |
-
)
|
| 283 |
-
return result.sample(rng=rng)
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
def _coerce_rng(rng: random.Random | int | None) -> random.Random:
|
| 287 |
-
if rng is None:
|
| 288 |
-
return random.Random()
|
| 289 |
-
if isinstance(rng, int):
|
| 290 |
-
return random.Random(rng)
|
| 291 |
-
return rng
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
def _sample_order(order_weights: tuple[tuple[int, float], ...], rng: random.Random) -> int:
|
| 295 |
-
if not order_weights:
|
| 296 |
-
return 0
|
| 297 |
-
threshold = rng.random() * sum(weight for _, weight in order_weights)
|
| 298 |
-
cumulative = 0.0
|
| 299 |
-
chosen = order_weights[-1][0]
|
| 300 |
-
for order, weight in order_weights:
|
| 301 |
-
cumulative += weight
|
| 302 |
-
if threshold <= cumulative:
|
| 303 |
-
return order
|
| 304 |
-
return chosen
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|