zero-sum-sequences 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zero_sum_sequences-0.1.0/.gitignore +9 -0
- zero_sum_sequences-0.1.0/LICENSE +21 -0
- zero_sum_sequences-0.1.0/PKG-INFO +228 -0
- zero_sum_sequences-0.1.0/README.md +208 -0
- zero_sum_sequences-0.1.0/benchmarks/README.md +32 -0
- zero_sum_sequences-0.1.0/benchmarks/__init__.py +1 -0
- zero_sum_sequences-0.1.0/benchmarks/benchmark_factorization.py +136 -0
- zero_sum_sequences-0.1.0/benchmarks/factorization_cases.py +120 -0
- zero_sum_sequences-0.1.0/notebooks/tutorial.ipynb +424 -0
- zero_sum_sequences-0.1.0/pyproject.toml +53 -0
- zero_sum_sequences-0.1.0/src/zero_sum_sequences/__init__.py +25 -0
- zero_sum_sequences-0.1.0/src/zero_sum_sequences/additive_sequence.py +531 -0
- zero_sum_sequences-0.1.0/src/zero_sum_sequences/atom_catalogue.py +92 -0
- zero_sum_sequences-0.1.0/src/zero_sum_sequences/factorization.py +385 -0
- zero_sum_sequences-0.1.0/src/zero_sum_sequences/orbits.py +533 -0
- zero_sum_sequences-0.1.0/src/zero_sum_sequences/parents.py +264 -0
- zero_sum_sequences-0.1.0/tests/groups.py +24 -0
- zero_sum_sequences-0.1.0/tests/test_additive_sequence.py +145 -0
- zero_sum_sequences-0.1.0/tests/test_atom_catalogue.py +151 -0
- zero_sum_sequences-0.1.0/tests/test_factorization.py +116 -0
- zero_sum_sequences-0.1.0/tests/test_factorization_benchmarks.py +40 -0
- zero_sum_sequences-0.1.0/tests/test_factorization_enumeration.py +176 -0
- zero_sum_sequences-0.1.0/tests/test_generic_parents.py +116 -0
- zero_sum_sequences-0.1.0/tests/test_orbits.py +202 -0
- zero_sum_sequences-0.1.0/tests/test_orbits_sage.py +81 -0
- zero_sum_sequences-0.1.0/tests/test_package_metadata.py +7 -0
- zero_sum_sequences-0.1.0/tests/test_parents.py +147 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benjamin Hackl
|
|
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.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: zero-sum-sequences
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Computations with additive and zero-sum sequences
|
|
5
|
+
Project-URL: Repository, https://github.com/behackl/zero-sum-sequences
|
|
6
|
+
Author: Benjamin Hackl
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: networkx>=3.2
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
13
|
+
Provides-Extra: sage
|
|
14
|
+
Requires-Dist: sagelite[gap]==10.9.post1; extra == 'sage'
|
|
15
|
+
Provides-Extra: tutorial
|
|
16
|
+
Requires-Dist: ipykernel>=6.29; extra == 'tutorial'
|
|
17
|
+
Requires-Dist: matplotlib>=3.8; extra == 'tutorial'
|
|
18
|
+
Requires-Dist: nbconvert>=7.16; extra == 'tutorial'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Zero-sum sequences
|
|
22
|
+
|
|
23
|
+
`zero-sum-sequences` provides immutable finite additive sequences and tools for
|
|
24
|
+
enumerating their factorizations into minimal zero-sum sequences. The runtime
|
|
25
|
+
is ordinary Python with NetworkX; SageMath is supported as an optional source
|
|
26
|
+
of additive parents, but is not required.
|
|
27
|
+
|
|
28
|
+
The package keeps group-specific mathematics explicit. Callers configure the
|
|
29
|
+
ambient parent and provide an upper bound for its Davenport constant; the
|
|
30
|
+
package does not infer structural invariants from the parent.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Install the latest release from PyPI into Python 3.12 or newer:
|
|
35
|
+
|
|
36
|
+
```console
|
|
37
|
+
python -m pip install zero-sum-sequences
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The optional `sage` extra installs the `sagelite` runtime on supported
|
|
41
|
+
platforms:
|
|
42
|
+
|
|
43
|
+
```console
|
|
44
|
+
python -m pip install "zero-sum-sequences[sage]"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For a reproducible development environment using the committed `uv.lock`,
|
|
48
|
+
clone the repository and run:
|
|
49
|
+
|
|
50
|
+
```console
|
|
51
|
+
uv sync --extra dev
|
|
52
|
+
uv run python -m pytest
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Alternatively, install the package and its test tools with `pip`:
|
|
56
|
+
|
|
57
|
+
```console
|
|
58
|
+
python -m pip install -e '.[dev]'
|
|
59
|
+
python -m pytest
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
To include the Sage integration tests, use `uv sync --extra dev --extra sage`
|
|
63
|
+
or install the editable `.[dev,sage]` extra.
|
|
64
|
+
|
|
65
|
+
## Tutorial
|
|
66
|
+
|
|
67
|
+
[](https://mybinder.org/v2/gh/behackl/zero-sum-sequences/main?urlpath=lab/tree/notebooks/tutorial.ipynb)
|
|
68
|
+
|
|
69
|
+
The executable [tutorial](https://github.com/behackl/zero-sum-sequences/blob/main/notebooks/tutorial.ipynb) introduces the public API
|
|
70
|
+
with small hand-checkable examples in ordinary Python.
|
|
71
|
+
Its execution dependencies are available through
|
|
72
|
+
`uv sync --extra tutorial` or the corresponding `pip` extra.
|
|
73
|
+
|
|
74
|
+
## Additive sequences
|
|
75
|
+
|
|
76
|
+
Configure an ambient parent and a Davenport upper bound once, then use the
|
|
77
|
+
resulting callable space to construct sequences. `FiniteAdditiveGroup` is a
|
|
78
|
+
small convenience adapter for groups represented by ordinary Python values:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from zero_sum_sequences import AdditiveSequenceSpace, FiniteAdditiveGroup
|
|
82
|
+
|
|
83
|
+
group = FiniteAdditiveGroup(
|
|
84
|
+
range(3),
|
|
85
|
+
zero=0,
|
|
86
|
+
add=lambda left, right: (left + right) % 3,
|
|
87
|
+
coerce=lambda value: int(value) % 3,
|
|
88
|
+
)
|
|
89
|
+
Sequences = AdditiveSequenceSpace(group, davenport_bound=3)
|
|
90
|
+
|
|
91
|
+
sequence = Sequences([1, 1, 2, 2])
|
|
92
|
+
sequence.is_zero_sum() # True
|
|
93
|
+
sequence.is_atom() # False: 1 * 2 is a proper zero-sum subsequence
|
|
94
|
+
sequence.multiplicities
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Existing algebra systems can be used directly. A compatible parent is
|
|
98
|
+
callable for coercion and provides `zero()`; its elements must be hashable and
|
|
99
|
+
mutually orderable. Elements may implement `+`, or the parent may provide
|
|
100
|
+
`add(left, right)`. Exhaustive catalogue enumeration additionally requires the
|
|
101
|
+
parent to be finite, iterable, and to provide `is_finite()`.
|
|
102
|
+
|
|
103
|
+
A sequence is immutable and hashable. Addition combines multisets,
|
|
104
|
+
subtraction removes a subsequence, and multiplication by a non-negative
|
|
105
|
+
integer repeats a sequence. Arithmetic preserves the sequence space, and the
|
|
106
|
+
empty sequence retains the base parent and its zero element.
|
|
107
|
+
`sequence.map_terms(mapping)` applies a map to every term and reconstructs the
|
|
108
|
+
result as a canonical multiset; pass `target_space=` when the image belongs to
|
|
109
|
+
a different sequence space.
|
|
110
|
+
|
|
111
|
+
The configured bound must not be smaller than the actual Davenport constant
|
|
112
|
+
when complete atom or factorization results are required.
|
|
113
|
+
|
|
114
|
+
## Factorizations
|
|
115
|
+
|
|
116
|
+
The factorization engine indexes relevant atom divisors as sparse
|
|
117
|
+
multiplicity vectors and merges equal remainders in a directed acyclic graph.
|
|
118
|
+
Attained lengths are represented internally as integer bitsets.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
lengths = sequence.length_set()
|
|
122
|
+
witnesses = sequence.factorization_witnesses()
|
|
123
|
+
factorizations = list(sequence.factorizations())
|
|
124
|
+
graph = sequence.factorization_digraph()
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`factorization_witnesses()` retains one factorization for every attained
|
|
128
|
+
length. Exhaustive `factorizations()` is necessarily output-sensitive, but it
|
|
129
|
+
emits each unordered factorization once. `factorization_digraph()` returns a
|
|
130
|
+
NetworkX `DiGraph` whose vertices are remainder sequences and whose edges
|
|
131
|
+
store the removed atom in their `"atom"` attribute.
|
|
132
|
+
|
|
133
|
+
For several queries against the same remainder DAG, use the public solver:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from zero_sum_sequences import FactorizationSolver
|
|
137
|
+
|
|
138
|
+
solver = FactorizationSolver(sequence)
|
|
139
|
+
solver.length_set()
|
|
140
|
+
solver.factorization_witnesses()
|
|
141
|
+
solver.statistics
|
|
142
|
+
solver.digraph()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A complete precomputed catalogue can avoid rediscovering atoms:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from zero_sum_sequences import AtomCatalogue, FactorizationSolver
|
|
149
|
+
|
|
150
|
+
catalogue = AtomCatalogue(Sequences, atoms)
|
|
151
|
+
solver = FactorizationSolver(sequence, atom_catalogue=catalogue)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
For a small finite parent, a complete reduced catalogue can instead be
|
|
155
|
+
generated exhaustively through the configured Davenport bound:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
catalogue = Sequences.enumerate_atom_catalogue()
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The parent must be a finite iterable additive group. Enumeration completes
|
|
162
|
+
each sorted prefix with its uniquely determined final term, rather than
|
|
163
|
+
testing multisets whose sum is nonzero. Completeness depends on the configured
|
|
164
|
+
Davenport bound being valid.
|
|
165
|
+
|
|
166
|
+
The caller is responsible for catalogue completeness. A catalogue used for a
|
|
167
|
+
complete result must contain every atom divisor relevant to the input.
|
|
168
|
+
|
|
169
|
+
Factorizations are computed in the reduced block monoid: identity terms are
|
|
170
|
+
not accepted by the solver and are not stored in an `AtomCatalogue`.
|
|
171
|
+
|
|
172
|
+
## Automorphism orbits
|
|
173
|
+
|
|
174
|
+
Group automorphisms act on sequences term by term. Because an
|
|
175
|
+
`AdditiveSequence` is a multiset, the induced action automatically disregards
|
|
176
|
+
the order of its terms. The package can materialize an orbit, test orbit
|
|
177
|
+
membership, and return a shortest deterministic word in the configured
|
|
178
|
+
automorphism generators.
|
|
179
|
+
|
|
180
|
+
The convenience constructor for a product of cyclic groups configures the
|
|
181
|
+
coordinate group and generators of its full automorphism group together. For
|
|
182
|
+
example, the eight maximal-length atoms over $C_2\oplus C_4$ form one orbit:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
G = FiniteAdditiveGroup.cyclic_product(2, 4)
|
|
186
|
+
C2xC4 = AdditiveSequenceSpace(G, davenport_bound=5)
|
|
187
|
+
|
|
188
|
+
atom = C2xC4([(0, 1)] * 3 + [(1, 0), (1, 1)])
|
|
189
|
+
other = C2xC4([(0, 1), (1, 0)] + [(1, 1)] * 3)
|
|
190
|
+
|
|
191
|
+
len(atom.orbit()) # 8
|
|
192
|
+
atom.is_in_same_orbit(other) # True
|
|
193
|
+
witness = atom.orbit_witness(other)
|
|
194
|
+
witness.show()
|
|
195
|
+
# (1, 0) ↦ (1, 0)
|
|
196
|
+
# (0, 1) ↦ (1, 1)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Automorphism data is resolved only on the first orbit query and then cached on
|
|
200
|
+
the sequence space. A returned witness retains this context, so `show()` can
|
|
201
|
+
display the induced homomorphism on the base parent's distinguished additive
|
|
202
|
+
generators. `FiniteAdditiveGroup.cyclic_product(...)` supplies both the
|
|
203
|
+
standard additive generators and elementary coordinate scalings and shears.
|
|
204
|
+
Finite-dimensional Sage vector spaces over finite fields are recognized
|
|
205
|
+
automatically and use their basis and generators of their general linear
|
|
206
|
+
group. A custom `FiniteAdditiveGroup` can receive `additive_generators=` and
|
|
207
|
+
callable `automorphism_generators=` at construction; callers can also pass an
|
|
208
|
+
`AutomorphismAction` explicitly through the `action=` keyword.
|
|
209
|
+
|
|
210
|
+
Orbit traversal is breadth-first and therefore requires a finite orbit.
|
|
211
|
+
Automatic discovery raises `AutomorphismActionUnavailable` when the parent
|
|
212
|
+
does not expose suitable generators, in which case an explicit action is
|
|
213
|
+
required.
|
|
214
|
+
|
|
215
|
+
## Benchmarks
|
|
216
|
+
|
|
217
|
+
Run the short-to-very-long performance corpus with:
|
|
218
|
+
|
|
219
|
+
```console
|
|
220
|
+
python -m benchmarks.benchmark_factorization --enumerate
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The cases and their mathematical expectations are documented in
|
|
224
|
+
[`benchmarks/README.md`](https://github.com/behackl/zero-sum-sequences/blob/main/benchmarks/README.md).
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
The source code is available under the MIT License.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Zero-sum sequences
|
|
2
|
+
|
|
3
|
+
`zero-sum-sequences` provides immutable finite additive sequences and tools for
|
|
4
|
+
enumerating their factorizations into minimal zero-sum sequences. The runtime
|
|
5
|
+
is ordinary Python with NetworkX; SageMath is supported as an optional source
|
|
6
|
+
of additive parents, but is not required.
|
|
7
|
+
|
|
8
|
+
The package keeps group-specific mathematics explicit. Callers configure the
|
|
9
|
+
ambient parent and provide an upper bound for its Davenport constant; the
|
|
10
|
+
package does not infer structural invariants from the parent.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Install the latest release from PyPI into Python 3.12 or newer:
|
|
15
|
+
|
|
16
|
+
```console
|
|
17
|
+
python -m pip install zero-sum-sequences
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The optional `sage` extra installs the `sagelite` runtime on supported
|
|
21
|
+
platforms:
|
|
22
|
+
|
|
23
|
+
```console
|
|
24
|
+
python -m pip install "zero-sum-sequences[sage]"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For a reproducible development environment using the committed `uv.lock`,
|
|
28
|
+
clone the repository and run:
|
|
29
|
+
|
|
30
|
+
```console
|
|
31
|
+
uv sync --extra dev
|
|
32
|
+
uv run python -m pytest
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Alternatively, install the package and its test tools with `pip`:
|
|
36
|
+
|
|
37
|
+
```console
|
|
38
|
+
python -m pip install -e '.[dev]'
|
|
39
|
+
python -m pytest
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
To include the Sage integration tests, use `uv sync --extra dev --extra sage`
|
|
43
|
+
or install the editable `.[dev,sage]` extra.
|
|
44
|
+
|
|
45
|
+
## Tutorial
|
|
46
|
+
|
|
47
|
+
[](https://mybinder.org/v2/gh/behackl/zero-sum-sequences/main?urlpath=lab/tree/notebooks/tutorial.ipynb)
|
|
48
|
+
|
|
49
|
+
The executable [tutorial](https://github.com/behackl/zero-sum-sequences/blob/main/notebooks/tutorial.ipynb) introduces the public API
|
|
50
|
+
with small hand-checkable examples in ordinary Python.
|
|
51
|
+
Its execution dependencies are available through
|
|
52
|
+
`uv sync --extra tutorial` or the corresponding `pip` extra.
|
|
53
|
+
|
|
54
|
+
## Additive sequences
|
|
55
|
+
|
|
56
|
+
Configure an ambient parent and a Davenport upper bound once, then use the
|
|
57
|
+
resulting callable space to construct sequences. `FiniteAdditiveGroup` is a
|
|
58
|
+
small convenience adapter for groups represented by ordinary Python values:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from zero_sum_sequences import AdditiveSequenceSpace, FiniteAdditiveGroup
|
|
62
|
+
|
|
63
|
+
group = FiniteAdditiveGroup(
|
|
64
|
+
range(3),
|
|
65
|
+
zero=0,
|
|
66
|
+
add=lambda left, right: (left + right) % 3,
|
|
67
|
+
coerce=lambda value: int(value) % 3,
|
|
68
|
+
)
|
|
69
|
+
Sequences = AdditiveSequenceSpace(group, davenport_bound=3)
|
|
70
|
+
|
|
71
|
+
sequence = Sequences([1, 1, 2, 2])
|
|
72
|
+
sequence.is_zero_sum() # True
|
|
73
|
+
sequence.is_atom() # False: 1 * 2 is a proper zero-sum subsequence
|
|
74
|
+
sequence.multiplicities
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Existing algebra systems can be used directly. A compatible parent is
|
|
78
|
+
callable for coercion and provides `zero()`; its elements must be hashable and
|
|
79
|
+
mutually orderable. Elements may implement `+`, or the parent may provide
|
|
80
|
+
`add(left, right)`. Exhaustive catalogue enumeration additionally requires the
|
|
81
|
+
parent to be finite, iterable, and to provide `is_finite()`.
|
|
82
|
+
|
|
83
|
+
A sequence is immutable and hashable. Addition combines multisets,
|
|
84
|
+
subtraction removes a subsequence, and multiplication by a non-negative
|
|
85
|
+
integer repeats a sequence. Arithmetic preserves the sequence space, and the
|
|
86
|
+
empty sequence retains the base parent and its zero element.
|
|
87
|
+
`sequence.map_terms(mapping)` applies a map to every term and reconstructs the
|
|
88
|
+
result as a canonical multiset; pass `target_space=` when the image belongs to
|
|
89
|
+
a different sequence space.
|
|
90
|
+
|
|
91
|
+
The configured bound must not be smaller than the actual Davenport constant
|
|
92
|
+
when complete atom or factorization results are required.
|
|
93
|
+
|
|
94
|
+
## Factorizations
|
|
95
|
+
|
|
96
|
+
The factorization engine indexes relevant atom divisors as sparse
|
|
97
|
+
multiplicity vectors and merges equal remainders in a directed acyclic graph.
|
|
98
|
+
Attained lengths are represented internally as integer bitsets.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
lengths = sequence.length_set()
|
|
102
|
+
witnesses = sequence.factorization_witnesses()
|
|
103
|
+
factorizations = list(sequence.factorizations())
|
|
104
|
+
graph = sequence.factorization_digraph()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`factorization_witnesses()` retains one factorization for every attained
|
|
108
|
+
length. Exhaustive `factorizations()` is necessarily output-sensitive, but it
|
|
109
|
+
emits each unordered factorization once. `factorization_digraph()` returns a
|
|
110
|
+
NetworkX `DiGraph` whose vertices are remainder sequences and whose edges
|
|
111
|
+
store the removed atom in their `"atom"` attribute.
|
|
112
|
+
|
|
113
|
+
For several queries against the same remainder DAG, use the public solver:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from zero_sum_sequences import FactorizationSolver
|
|
117
|
+
|
|
118
|
+
solver = FactorizationSolver(sequence)
|
|
119
|
+
solver.length_set()
|
|
120
|
+
solver.factorization_witnesses()
|
|
121
|
+
solver.statistics
|
|
122
|
+
solver.digraph()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
A complete precomputed catalogue can avoid rediscovering atoms:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from zero_sum_sequences import AtomCatalogue, FactorizationSolver
|
|
129
|
+
|
|
130
|
+
catalogue = AtomCatalogue(Sequences, atoms)
|
|
131
|
+
solver = FactorizationSolver(sequence, atom_catalogue=catalogue)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
For a small finite parent, a complete reduced catalogue can instead be
|
|
135
|
+
generated exhaustively through the configured Davenport bound:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
catalogue = Sequences.enumerate_atom_catalogue()
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The parent must be a finite iterable additive group. Enumeration completes
|
|
142
|
+
each sorted prefix with its uniquely determined final term, rather than
|
|
143
|
+
testing multisets whose sum is nonzero. Completeness depends on the configured
|
|
144
|
+
Davenport bound being valid.
|
|
145
|
+
|
|
146
|
+
The caller is responsible for catalogue completeness. A catalogue used for a
|
|
147
|
+
complete result must contain every atom divisor relevant to the input.
|
|
148
|
+
|
|
149
|
+
Factorizations are computed in the reduced block monoid: identity terms are
|
|
150
|
+
not accepted by the solver and are not stored in an `AtomCatalogue`.
|
|
151
|
+
|
|
152
|
+
## Automorphism orbits
|
|
153
|
+
|
|
154
|
+
Group automorphisms act on sequences term by term. Because an
|
|
155
|
+
`AdditiveSequence` is a multiset, the induced action automatically disregards
|
|
156
|
+
the order of its terms. The package can materialize an orbit, test orbit
|
|
157
|
+
membership, and return a shortest deterministic word in the configured
|
|
158
|
+
automorphism generators.
|
|
159
|
+
|
|
160
|
+
The convenience constructor for a product of cyclic groups configures the
|
|
161
|
+
coordinate group and generators of its full automorphism group together. For
|
|
162
|
+
example, the eight maximal-length atoms over $C_2\oplus C_4$ form one orbit:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
G = FiniteAdditiveGroup.cyclic_product(2, 4)
|
|
166
|
+
C2xC4 = AdditiveSequenceSpace(G, davenport_bound=5)
|
|
167
|
+
|
|
168
|
+
atom = C2xC4([(0, 1)] * 3 + [(1, 0), (1, 1)])
|
|
169
|
+
other = C2xC4([(0, 1), (1, 0)] + [(1, 1)] * 3)
|
|
170
|
+
|
|
171
|
+
len(atom.orbit()) # 8
|
|
172
|
+
atom.is_in_same_orbit(other) # True
|
|
173
|
+
witness = atom.orbit_witness(other)
|
|
174
|
+
witness.show()
|
|
175
|
+
# (1, 0) ↦ (1, 0)
|
|
176
|
+
# (0, 1) ↦ (1, 1)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Automorphism data is resolved only on the first orbit query and then cached on
|
|
180
|
+
the sequence space. A returned witness retains this context, so `show()` can
|
|
181
|
+
display the induced homomorphism on the base parent's distinguished additive
|
|
182
|
+
generators. `FiniteAdditiveGroup.cyclic_product(...)` supplies both the
|
|
183
|
+
standard additive generators and elementary coordinate scalings and shears.
|
|
184
|
+
Finite-dimensional Sage vector spaces over finite fields are recognized
|
|
185
|
+
automatically and use their basis and generators of their general linear
|
|
186
|
+
group. A custom `FiniteAdditiveGroup` can receive `additive_generators=` and
|
|
187
|
+
callable `automorphism_generators=` at construction; callers can also pass an
|
|
188
|
+
`AutomorphismAction` explicitly through the `action=` keyword.
|
|
189
|
+
|
|
190
|
+
Orbit traversal is breadth-first and therefore requires a finite orbit.
|
|
191
|
+
Automatic discovery raises `AutomorphismActionUnavailable` when the parent
|
|
192
|
+
does not expose suitable generators, in which case an explicit action is
|
|
193
|
+
required.
|
|
194
|
+
|
|
195
|
+
## Benchmarks
|
|
196
|
+
|
|
197
|
+
Run the short-to-very-long performance corpus with:
|
|
198
|
+
|
|
199
|
+
```console
|
|
200
|
+
python -m benchmarks.benchmark_factorization --enumerate
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The cases and their mathematical expectations are documented in
|
|
204
|
+
[`benchmarks/README.md`](https://github.com/behackl/zero-sum-sequences/blob/main/benchmarks/README.md).
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
The source code is available under the MIT License.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Factorization benchmark corpus
|
|
2
|
+
|
|
3
|
+
The benchmark corpus is shared by the test suite and timing runner. It
|
|
4
|
+
separates input size from combinatorial difficulty and checks the sole
|
|
5
|
+
supported memoized factorization implementation.
|
|
6
|
+
|
|
7
|
+
| Case | Tier | Terms | Expected length set |
|
|
8
|
+
|---|---|---:|---|
|
|
9
|
+
| `c3-atom` | short | 3 | `{1}` |
|
|
10
|
+
| `c3-balanced-block` | short | 6 | `{2,3}` |
|
|
11
|
+
| `rank-three-inverse-pair` | short | 14 | `{2,3,4,5,7}` |
|
|
12
|
+
| `c3-balanced-power-7` | long | 42 | `[14,21]` |
|
|
13
|
+
| `c3-balanced-power-40` | long | 240 | `[80,120]` |
|
|
14
|
+
| `rank-three-inverse-power-3` | long | 42 | `[6,21]` |
|
|
15
|
+
| `c3-balanced-power-250` | very long | 1,500 | `[500,750]` |
|
|
16
|
+
| `c3-pure-power-5000` | very long | 15,000 | `{5000}` |
|
|
17
|
+
|
|
18
|
+
For the balanced `C_3` family, write `A = 1^3`, `B = 2^3`, and
|
|
19
|
+
`C = 1 * 2`. A factorization of `(A * B)^n` has `x` copies each of `A`
|
|
20
|
+
and `B`, and `3(n-x)` copies of `C`, for `0 <= x <= n`. Its length set is
|
|
21
|
+
therefore exactly `[2n,3n]`, and it has `n+1` unordered factorizations. The
|
|
22
|
+
pure-power case has one factorization of length 5,000.
|
|
23
|
+
|
|
24
|
+
The rank-three cases exercise a less artificial support and a larger
|
|
25
|
+
candidate-atom set. The runner reports candidate indexing, state-graph solve,
|
|
26
|
+
and safe exhaustive-enumeration timings while validating all expected results.
|
|
27
|
+
|
|
28
|
+
Run:
|
|
29
|
+
|
|
30
|
+
```console
|
|
31
|
+
python -m benchmarks.benchmark_factorization --enumerate
|
|
32
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Reproducible performance benchmarks for the computational package."""
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Benchmark the memoized factorization implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import platform
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from time import perf_counter
|
|
9
|
+
|
|
10
|
+
from zero_sum_sequences import FactorizationSolver
|
|
11
|
+
|
|
12
|
+
from .factorization_cases import factorization_benchmark_cases
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def elapsed(callable_):
|
|
16
|
+
start = perf_counter()
|
|
17
|
+
result = callable_()
|
|
18
|
+
return result, perf_counter() - start
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def benchmark_case(
|
|
22
|
+
case, *, enumerate_factorizations: bool, repeats: int
|
|
23
|
+
) -> dict[str, object]:
|
|
24
|
+
best_run = None
|
|
25
|
+
for _ in range(repeats):
|
|
26
|
+
solver, preparation_seconds = elapsed(
|
|
27
|
+
lambda: FactorizationSolver(case.sequence)
|
|
28
|
+
)
|
|
29
|
+
lengths, solve_seconds = elapsed(solver.length_set)
|
|
30
|
+
if best_run is None or preparation_seconds + solve_seconds < sum(best_run[1:]):
|
|
31
|
+
best_run = (solver, preparation_seconds, solve_seconds)
|
|
32
|
+
assert best_run is not None
|
|
33
|
+
solver, preparation_seconds, solve_seconds = best_run
|
|
34
|
+
lengths = solver.length_set()
|
|
35
|
+
if lengths != set(case.expected_lengths):
|
|
36
|
+
raise AssertionError(
|
|
37
|
+
f"{case.name}: expected {sorted(case.expected_lengths)}, "
|
|
38
|
+
f"got {sorted(lengths)}"
|
|
39
|
+
)
|
|
40
|
+
statistics = solver.statistics
|
|
41
|
+
|
|
42
|
+
factorization_count: int | None = None
|
|
43
|
+
enumeration_seconds: float | None = None
|
|
44
|
+
if enumerate_factorizations and case.enumerate_factorizations:
|
|
45
|
+
factorizations, enumeration_seconds = elapsed(
|
|
46
|
+
lambda: list(solver.factorizations())
|
|
47
|
+
)
|
|
48
|
+
factorization_count = len(factorizations)
|
|
49
|
+
if factorization_count != case.expected_factorizations:
|
|
50
|
+
raise AssertionError(
|
|
51
|
+
f"{case.name}: expected {case.expected_factorizations} "
|
|
52
|
+
f"factorizations, got {factorization_count}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"name": case.name,
|
|
57
|
+
"tier": case.tier,
|
|
58
|
+
"terms": len(case.sequence),
|
|
59
|
+
"lengths": len(lengths),
|
|
60
|
+
"atoms": statistics.candidate_atoms,
|
|
61
|
+
"states": statistics.states,
|
|
62
|
+
"transitions": statistics.transitions,
|
|
63
|
+
"prepare_ms": preparation_seconds * 1000,
|
|
64
|
+
"solve_ms": solve_seconds * 1000,
|
|
65
|
+
"factorizations": factorization_count,
|
|
66
|
+
"enumerate_ms": (
|
|
67
|
+
None if enumeration_seconds is None else enumeration_seconds * 1000
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def format_value(value: object) -> str:
|
|
73
|
+
if value is None:
|
|
74
|
+
return "—"
|
|
75
|
+
if isinstance(value, float):
|
|
76
|
+
return f"{value:.3f}"
|
|
77
|
+
return str(value)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def print_markdown(rows: list[dict[str, object]]) -> None:
|
|
81
|
+
columns = (
|
|
82
|
+
"name",
|
|
83
|
+
"tier",
|
|
84
|
+
"terms",
|
|
85
|
+
"lengths",
|
|
86
|
+
"atoms",
|
|
87
|
+
"states",
|
|
88
|
+
"transitions",
|
|
89
|
+
"prepare_ms",
|
|
90
|
+
"solve_ms",
|
|
91
|
+
"factorizations",
|
|
92
|
+
"enumerate_ms",
|
|
93
|
+
)
|
|
94
|
+
print("| " + " | ".join(columns) + " |")
|
|
95
|
+
print("| " + " | ".join("---" for _ in columns) + " |")
|
|
96
|
+
for row in rows:
|
|
97
|
+
print("| " + " | ".join(format_value(row[column]) for column in columns) + " |")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
101
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"--enumerate",
|
|
104
|
+
action="store_true",
|
|
105
|
+
help="also enumerate unique factorizations for cases marked as safe",
|
|
106
|
+
)
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--repeats",
|
|
109
|
+
type=int,
|
|
110
|
+
default=3,
|
|
111
|
+
help="number of timing repetitions; the minimum is reported",
|
|
112
|
+
)
|
|
113
|
+
return parser
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
117
|
+
arguments = build_parser().parse_args(argv)
|
|
118
|
+
if arguments.repeats < 1:
|
|
119
|
+
raise SystemExit("--repeats must be positive")
|
|
120
|
+
print(f"Python {platform.python_version()}")
|
|
121
|
+
print()
|
|
122
|
+
cases = factorization_benchmark_cases()
|
|
123
|
+
rows = [
|
|
124
|
+
benchmark_case(
|
|
125
|
+
case,
|
|
126
|
+
enumerate_factorizations=arguments.enumerate,
|
|
127
|
+
repeats=arguments.repeats,
|
|
128
|
+
)
|
|
129
|
+
for case in cases
|
|
130
|
+
]
|
|
131
|
+
print_markdown(rows)
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
raise SystemExit(main())
|