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.
@@ -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
+ [![Launch the tutorial on Binder](https://mybinder.org/badge_logo.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.