zero-sum-sequences 0.1.0__py3-none-any.whl
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/__init__.py +25 -0
- zero_sum_sequences/additive_sequence.py +531 -0
- zero_sum_sequences/atom_catalogue.py +92 -0
- zero_sum_sequences/factorization.py +385 -0
- zero_sum_sequences/orbits.py +533 -0
- zero_sum_sequences/parents.py +264 -0
- zero_sum_sequences-0.1.0.dist-info/METADATA +228 -0
- zero_sum_sequences-0.1.0.dist-info/RECORD +10 -0
- zero_sum_sequences-0.1.0.dist-info/WHEEL +4 -0
- zero_sum_sequences-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Small adapters for defining additive parents without a CAS dependency."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
6
|
+
from itertools import product
|
|
7
|
+
from math import gcd
|
|
8
|
+
from operator import index
|
|
9
|
+
from typing import Generic, TypeVar
|
|
10
|
+
|
|
11
|
+
Element = TypeVar("Element")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _positive_modulus(value: object) -> int:
|
|
15
|
+
if isinstance(value, bool):
|
|
16
|
+
raise ValueError("cyclic moduli must be positive integers")
|
|
17
|
+
try:
|
|
18
|
+
modulus = index(value)
|
|
19
|
+
except TypeError:
|
|
20
|
+
raise ValueError("cyclic moduli must be positive integers") from None
|
|
21
|
+
if modulus < 1:
|
|
22
|
+
raise ValueError("cyclic moduli must be positive integers")
|
|
23
|
+
return modulus
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _scale_coordinate(coordinate: int, unit: int, modulus: int):
|
|
27
|
+
def scaling(element):
|
|
28
|
+
image = list(element)
|
|
29
|
+
image[coordinate] = unit * image[coordinate] % modulus
|
|
30
|
+
return tuple(image)
|
|
31
|
+
|
|
32
|
+
return scaling
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _shear_coordinate(
|
|
36
|
+
target: int,
|
|
37
|
+
source: int,
|
|
38
|
+
coefficient: int,
|
|
39
|
+
modulus: int,
|
|
40
|
+
):
|
|
41
|
+
def shear(element):
|
|
42
|
+
image = list(element)
|
|
43
|
+
image[target] = (
|
|
44
|
+
image[target] + coefficient * image[source]
|
|
45
|
+
) % modulus
|
|
46
|
+
return tuple(image)
|
|
47
|
+
|
|
48
|
+
return shear
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FiniteAdditiveGroup(Generic[Element]):
|
|
52
|
+
"""A finite additive parent defined by elements and an operation.
|
|
53
|
+
|
|
54
|
+
The supplied data is treated as a group presentation; associativity,
|
|
55
|
+
inverses, and closure are not checked exhaustively. Results produced by
|
|
56
|
+
``add`` are coerced and checked for membership when the operation is used.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
__slots__ = (
|
|
60
|
+
"_elements",
|
|
61
|
+
"_element_set",
|
|
62
|
+
"_zero",
|
|
63
|
+
"_operation",
|
|
64
|
+
"_coerce",
|
|
65
|
+
"_additive_generators",
|
|
66
|
+
"_automorphism_generators",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def cyclic_product(cls, *moduli: int) -> FiniteAdditiveGroup[tuple[int, ...]]:
|
|
71
|
+
"""Return the coordinate group ``C_n1 x ... x C_nr``.
|
|
72
|
+
|
|
73
|
+
The parent includes elementary generators of its full automorphism
|
|
74
|
+
group: multiplication of one coordinate by a unit and all admissible
|
|
75
|
+
coordinate shears. Consequently, sequence orbit methods work without
|
|
76
|
+
an explicitly supplied action.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
if not moduli:
|
|
80
|
+
raise ValueError("at least one cyclic modulus is required")
|
|
81
|
+
normalized_moduli = tuple(_positive_modulus(value) for value in moduli)
|
|
82
|
+
|
|
83
|
+
def coerce(value):
|
|
84
|
+
try:
|
|
85
|
+
coordinates = tuple(value)
|
|
86
|
+
except TypeError:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"cyclic-product elements must be coordinate iterables"
|
|
89
|
+
) from None
|
|
90
|
+
if len(coordinates) != len(normalized_moduli):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"cyclic-product elements have the wrong number of coordinates"
|
|
93
|
+
)
|
|
94
|
+
try:
|
|
95
|
+
return tuple(
|
|
96
|
+
index(coordinate) % modulus
|
|
97
|
+
for coordinate, modulus in zip(
|
|
98
|
+
coordinates,
|
|
99
|
+
normalized_moduli,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
except TypeError:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
"cyclic-product coordinates must be integers"
|
|
105
|
+
) from None
|
|
106
|
+
|
|
107
|
+
def add(left, right):
|
|
108
|
+
return tuple(
|
|
109
|
+
(left[position] + right[position]) % modulus
|
|
110
|
+
for position, modulus in enumerate(normalized_moduli)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
generators = []
|
|
114
|
+
for coordinate, modulus in enumerate(normalized_moduli):
|
|
115
|
+
generators.extend(
|
|
116
|
+
_scale_coordinate(coordinate, unit, modulus)
|
|
117
|
+
for unit in range(2, modulus)
|
|
118
|
+
if gcd(unit, modulus) == 1
|
|
119
|
+
)
|
|
120
|
+
for target, target_modulus in enumerate(normalized_moduli):
|
|
121
|
+
for source, source_modulus in enumerate(normalized_moduli):
|
|
122
|
+
if target == source:
|
|
123
|
+
continue
|
|
124
|
+
coefficient = target_modulus // gcd(
|
|
125
|
+
target_modulus,
|
|
126
|
+
source_modulus,
|
|
127
|
+
)
|
|
128
|
+
if coefficient % target_modulus:
|
|
129
|
+
generators.append(
|
|
130
|
+
_shear_coordinate(
|
|
131
|
+
target,
|
|
132
|
+
source,
|
|
133
|
+
coefficient,
|
|
134
|
+
target_modulus,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
additive_generators = tuple(
|
|
139
|
+
tuple(
|
|
140
|
+
1 if position == coordinate else 0
|
|
141
|
+
for position in range(len(normalized_moduli))
|
|
142
|
+
)
|
|
143
|
+
for coordinate, modulus in enumerate(normalized_moduli)
|
|
144
|
+
if modulus > 1
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return cls(
|
|
148
|
+
product(*(range(modulus) for modulus in normalized_moduli)),
|
|
149
|
+
zero=(0,) * len(normalized_moduli),
|
|
150
|
+
add=add,
|
|
151
|
+
coerce=coerce,
|
|
152
|
+
additive_generators=additive_generators,
|
|
153
|
+
automorphism_generators=generators,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
elements: Iterable[Element],
|
|
159
|
+
*,
|
|
160
|
+
zero: Element,
|
|
161
|
+
add: Callable[[Element, Element], Element],
|
|
162
|
+
coerce: Callable[[object], Element] | None = None,
|
|
163
|
+
additive_generators: Iterable[Element] | None = None,
|
|
164
|
+
automorphism_generators: (
|
|
165
|
+
Iterable[Callable[[Element], Element]] | None
|
|
166
|
+
) = None,
|
|
167
|
+
) -> None:
|
|
168
|
+
if not callable(add):
|
|
169
|
+
raise TypeError("the additive operation must be callable")
|
|
170
|
+
if coerce is not None and not callable(coerce):
|
|
171
|
+
raise TypeError("coerce must be callable")
|
|
172
|
+
|
|
173
|
+
self._coerce = coerce if coerce is not None else lambda value: value
|
|
174
|
+
try:
|
|
175
|
+
coerced_elements = tuple(self._coerce(element) for element in elements)
|
|
176
|
+
element_set = frozenset(coerced_elements)
|
|
177
|
+
except TypeError:
|
|
178
|
+
raise TypeError("finite-group elements must be hashable") from None
|
|
179
|
+
if len(element_set) != len(coerced_elements):
|
|
180
|
+
raise ValueError("finite-group elements must be distinct after coercion")
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
ordered_elements = tuple(sorted(coerced_elements))
|
|
184
|
+
except TypeError:
|
|
185
|
+
raise TypeError(
|
|
186
|
+
"finite-group elements must be mutually orderable"
|
|
187
|
+
) from None
|
|
188
|
+
|
|
189
|
+
self._elements = ordered_elements
|
|
190
|
+
self._element_set = element_set
|
|
191
|
+
self._operation = add
|
|
192
|
+
self._zero = self(zero)
|
|
193
|
+
if additive_generators is None:
|
|
194
|
+
self._additive_generators = None
|
|
195
|
+
else:
|
|
196
|
+
try:
|
|
197
|
+
generators = tuple(self(element) for element in additive_generators)
|
|
198
|
+
except TypeError:
|
|
199
|
+
raise TypeError("additive generators must be iterable") from None
|
|
200
|
+
if self._zero in generators:
|
|
201
|
+
raise ValueError("additive generators must be nonzero")
|
|
202
|
+
if len(set(generators)) != len(generators):
|
|
203
|
+
raise ValueError("additive generators must be distinct")
|
|
204
|
+
self._additive_generators = generators
|
|
205
|
+
if automorphism_generators is None:
|
|
206
|
+
self._automorphism_generators = None
|
|
207
|
+
else:
|
|
208
|
+
generators = tuple(automorphism_generators)
|
|
209
|
+
if not all(callable(generator) for generator in generators):
|
|
210
|
+
raise TypeError("automorphism generators must be callable")
|
|
211
|
+
self._automorphism_generators = generators
|
|
212
|
+
|
|
213
|
+
def __call__(self, value: object) -> Element:
|
|
214
|
+
element = self._coerce(value)
|
|
215
|
+
try:
|
|
216
|
+
contained = element in self._element_set
|
|
217
|
+
except TypeError:
|
|
218
|
+
contained = False
|
|
219
|
+
if not contained:
|
|
220
|
+
raise ValueError(f"{element!r} is not an element of this group")
|
|
221
|
+
return element
|
|
222
|
+
|
|
223
|
+
def zero(self) -> Element:
|
|
224
|
+
"""Return the additive identity."""
|
|
225
|
+
|
|
226
|
+
return self._zero
|
|
227
|
+
|
|
228
|
+
def add(self, left: Element, right: Element) -> Element:
|
|
229
|
+
"""Add two elements and return their canonical representative."""
|
|
230
|
+
|
|
231
|
+
left = self(left)
|
|
232
|
+
right = self(right)
|
|
233
|
+
return self(self._operation(left, right))
|
|
234
|
+
|
|
235
|
+
def is_finite(self) -> bool:
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
def additive_generators(self):
|
|
239
|
+
"""Return a configured generating set for the additive group."""
|
|
240
|
+
|
|
241
|
+
if self._additive_generators is None:
|
|
242
|
+
raise NotImplementedError
|
|
243
|
+
return self._additive_generators
|
|
244
|
+
|
|
245
|
+
def automorphism_generators(self):
|
|
246
|
+
"""Return explicitly configured automorphism generators.
|
|
247
|
+
|
|
248
|
+
Generic finite groups do not expose a presentation from which these
|
|
249
|
+
maps could be inferred. Supply them at construction when automatic
|
|
250
|
+
orbit methods are desired.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
if self._automorphism_generators is None:
|
|
254
|
+
raise NotImplementedError
|
|
255
|
+
return self._automorphism_generators
|
|
256
|
+
|
|
257
|
+
def __iter__(self) -> Iterator[Element]:
|
|
258
|
+
return iter(self._elements)
|
|
259
|
+
|
|
260
|
+
def __len__(self) -> int:
|
|
261
|
+
return len(self._elements)
|
|
262
|
+
|
|
263
|
+
def __repr__(self) -> str:
|
|
264
|
+
return f"{type(self).__name__}({list(self._elements)!r}, zero={self._zero!r})"
|
|
@@ -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,10 @@
|
|
|
1
|
+
zero_sum_sequences/__init__.py,sha256=3PomZ1YK7yJWJTAYGgoL-0wWTHJX-29Yy24AWrap3m4,721
|
|
2
|
+
zero_sum_sequences/additive_sequence.py,sha256=KuRvrH3u_7v_5W3GHOY66dCW4gQFyIvkVDFsgDKz0uE,18648
|
|
3
|
+
zero_sum_sequences/atom_catalogue.py,sha256=4l00RjopVORnZ0bl59ZS0LN3sqf6DxyIcl3MTZOyBJM,3449
|
|
4
|
+
zero_sum_sequences/factorization.py,sha256=oMI4NB1sXCbVTl-dEghjrhLkSGHOuveZrovOm6N8c9w,14516
|
|
5
|
+
zero_sum_sequences/orbits.py,sha256=iJRY6KTiqE3wHGQybu_1WgC7m4BHFe5-sHpOorhObIk,18335
|
|
6
|
+
zero_sum_sequences/parents.py,sha256=nbdWJN4j7wCC1kszNUzWbzo3XUrCOI8A51HOY4Z0a6s,9162
|
|
7
|
+
zero_sum_sequences-0.1.0.dist-info/METADATA,sha256=t6uU1gqLp4dIvW1o-fHYknGTOn328ug83KCpgKHfVmU,8413
|
|
8
|
+
zero_sum_sequences-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
9
|
+
zero_sum_sequences-0.1.0.dist-info/licenses/LICENSE,sha256=k6e0VWMPXDbULziLaPVyAIVhYuJdUYEmhCWGP07S8f8,1071
|
|
10
|
+
zero_sum_sequences-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|