zero-sum-sequences 0.1.2__tar.gz → 0.2.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.2 → zero_sum_sequences-0.2.0}/PKG-INFO +101 -1
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/README.md +100 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/pyproject.toml +1 -1
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/src/zero_sum_sequences/__init__.py +15 -1
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/src/zero_sum_sequences/additive_sequence.py +89 -1
- zero_sum_sequences-0.2.0/src/zero_sum_sequences/atom_catalogue.py +353 -0
- zero_sum_sequences-0.2.0/src/zero_sum_sequences/automorphisms.py +534 -0
- zero_sum_sequences-0.2.0/src/zero_sum_sequences/factorization_cache.py +350 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/src/zero_sum_sequences/parents.py +75 -0
- zero_sum_sequences-0.2.0/tests/test_atom_catalogue_index.py +207 -0
- zero_sum_sequences-0.2.0/tests/test_automorphisms.py +406 -0
- zero_sum_sequences-0.2.0/tests/test_factorization_cache.py +250 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/tests/test_orbits_sage.py +29 -0
- zero_sum_sequences-0.1.2/src/zero_sum_sequences/atom_catalogue.py +0 -92
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/.gitignore +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/LICENSE +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/benchmarks/README.md +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/benchmarks/__init__.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/benchmarks/benchmark_factorization.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/benchmarks/factorization_cases.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/notebooks/tutorial.ipynb +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/src/zero_sum_sequences/factorization.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/src/zero_sum_sequences/orbits.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/src/zero_sum_sequences/relations.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/tests/groups.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/tests/test_additive_sequence.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/tests/test_atom_catalogue.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/tests/test_factorization.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/tests/test_factorization_benchmarks.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/tests/test_factorization_enumeration.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/tests/test_factorization_relations.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/tests/test_generic_parents.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/tests/test_orbits.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/tests/test_package_metadata.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/tests/test_parents.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/tests/test_support_catalogue.py +0 -0
- {zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/tests/test_targeted_witness.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: zero-sum-sequences
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Computations with additive and zero-sum sequences
|
|
5
5
|
Project-URL: Repository, https://github.com/behackl/zero-sum-sequences
|
|
6
6
|
Author: Benjamin Hackl
|
|
@@ -312,6 +312,106 @@ Automatic discovery raises `AutomorphismActionUnavailable` when the parent
|
|
|
312
312
|
does not expose suitable generators, in which case an explicit action is
|
|
313
313
|
required.
|
|
314
314
|
|
|
315
|
+
## Materialized automorphism groups
|
|
316
|
+
|
|
317
|
+
For a finite parent the whole automorphism group can be materialized, which
|
|
318
|
+
gives stabilizers, canonical orbit representatives, transporters and
|
|
319
|
+
classification of many objects at once:
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
G = FiniteAdditiveGroup.cyclic_product(2, 4)
|
|
323
|
+
C2xC4 = AdditiveSequenceSpace(G, davenport_bound=5)
|
|
324
|
+
group = C2xC4.automorphism_group() # AutomorphismGroup(order=8)
|
|
325
|
+
|
|
326
|
+
x = C2xC4([(1, 1), (1, 1), (0, 2)])
|
|
327
|
+
group.orbit(x) # the four images, sorted
|
|
328
|
+
len(group.stabilizer(x)) # 2
|
|
329
|
+
form = group.canonical_form(x) # orbit minimum + smallest automorphism reaching it
|
|
330
|
+
form.automorphism.generator_images() # ((1, 0), (1, 1)): the "matrix", column by column
|
|
331
|
+
group.transporter(x, group.apply(form.automorphism, x))
|
|
332
|
+
|
|
333
|
+
atoms = tuple(C2xC4.enumerate_atom_catalogue())
|
|
334
|
+
group.orbit_representatives(atoms) # 11 orbits: representative -> members
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
The group acts on sequences and on tuples of sequences; tuples are treated as
|
|
338
|
+
multisets unless `ordered=True` (the canonical form of a tuple also records
|
|
339
|
+
the permutation that sorts the image). Canonical forms are minima with
|
|
340
|
+
respect to the total order on sequences (length first, then the sorted term
|
|
341
|
+
list, exposed through `<`), so representatives do not depend on how the group
|
|
342
|
+
was found.
|
|
343
|
+
|
|
344
|
+
The parent decides how the group is obtained: a `FiniteAdditiveGroup` may
|
|
345
|
+
receive a callable `automorphism_group=` returning an `AutomorphismGroup`
|
|
346
|
+
built from any element type with `apply_term`, `compose` and `inverse`
|
|
347
|
+
callbacks (for example units acting on a cyclic group, or CAS matrices);
|
|
348
|
+
otherwise its `automorphism_generators` are closed under composition.
|
|
349
|
+
Orbit traversals cost proportionally to the orbit size; stabilizers and
|
|
350
|
+
smallest transporters scan the whole group once, so they are cheap for
|
|
351
|
+
groups of a few thousand elements and expensive beyond that.
|
|
352
|
+
`AutomorphismGroupUnavailable` is raised when neither a provider nor
|
|
353
|
+
generators exist.
|
|
354
|
+
|
|
355
|
+
## Indexed and serialized catalogues
|
|
356
|
+
|
|
357
|
+
An `AtomCatalogue` stores its atoms in the sequence order (length first,
|
|
358
|
+
then terms), so `catalogue.index(atom)` and `catalogue[i]` form a stable
|
|
359
|
+
identifier namespace. It can be filtered, classified into automorphism
|
|
360
|
+
orbits, and written to or read from JSON lines:
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
catalogue = C2xC4.enumerate_atom_catalogue()
|
|
364
|
+
catalogue.index(atom), catalogue[7], atom in catalogue
|
|
365
|
+
short = catalogue.restrict(max_length=3) # also restrict(support=...)
|
|
366
|
+
for orbit in catalogue.orbits(): # AtomOrbit(representative, indices, stabilizer_order)
|
|
367
|
+
...
|
|
368
|
+
catalogue.representative(atom) # orbit minimum of an atom
|
|
369
|
+
catalogue.transporter(atom) # an automorphism mapping atom to it
|
|
370
|
+
catalogue.permutation(group.elements[1]) # an automorphism as an index permutation
|
|
371
|
+
|
|
372
|
+
catalogue.to_jsonl("atoms.jsonl", annotate=lambda atom: {"label": ...})
|
|
373
|
+
same = AtomCatalogue.from_jsonl("atoms.jsonl", C2xC4)
|
|
374
|
+
same.annotations[atom]["label"]
|
|
375
|
+
catalogue.digest() # sha256 of the atoms, annotation-independent
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
`from_jsonl` checks the order, the indices and the digest, and re-verifies
|
|
379
|
+
every record as an atom unless `verify=False`. Terms are serialized through
|
|
380
|
+
the parent: `FiniteAdditiveGroup` accepts `encode_term=`/`decode_term=`,
|
|
381
|
+
coordinate groups and Sage vector spaces use lists of integers by default,
|
|
382
|
+
and `sequence.encode()` / `space.decode(data)` apply the codec to whole
|
|
383
|
+
sequences.
|
|
384
|
+
|
|
385
|
+
## Memoized factorization queries
|
|
386
|
+
|
|
387
|
+
A `FactorizationCache` answers length-set questions over one catalogue with
|
|
388
|
+
caching, and can exploit that length sets are automorphism invariants:
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
cache = C2xC4.factorization_cache(catalogue)
|
|
392
|
+
cache.length_set(x), cache.minimum(x), cache.maximum(x)
|
|
393
|
+
cache.has_length(x, 3), cache.witness(x, 3), cache.witnesses(x)
|
|
394
|
+
cache.statistics() # hits, misses, solver builds
|
|
395
|
+
|
|
396
|
+
aware = C2xC4.factorization_cache(catalogue, group=group)
|
|
397
|
+
aware.length_set(group.apply(a, x)) == aware.length_set(x) # one solve per orbit
|
|
398
|
+
aware.witness(group.apply(a, x), 3) # transported back through a^-1
|
|
399
|
+
|
|
400
|
+
cache.to_jsonl("lengths.jsonl") # bound to catalogue.digest()
|
|
401
|
+
FactorizationCache.from_jsonl("lengths.jsonl", catalogue)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Length sets are kept without bound; solvers are kept in a bounded LRU
|
|
405
|
+
(`maxsize=`) so that follow-up questions about a recent sequence reuse its
|
|
406
|
+
remainder graph. With `group=` every query is keyed by a canonical form.
|
|
407
|
+
The default is the full canonical form, whose orbit traversal is worthwhile
|
|
408
|
+
when solving is the expensive part; for large groups pass
|
|
409
|
+
`canonicalize=AnchoredCanonicalizer(catalogue, group)`, which only
|
|
410
|
+
transports the largest atom dividing the sequence to its orbit
|
|
411
|
+
representative and reduces under that representative's stabilizer. Any
|
|
412
|
+
callable returning an automorphic image of its argument (optionally with the
|
|
413
|
+
automorphism) is accepted as `canonicalize=`.
|
|
414
|
+
|
|
315
415
|
## Benchmarks
|
|
316
416
|
|
|
317
417
|
Run the short-to-very-long performance corpus with:
|
|
@@ -292,6 +292,106 @@ Automatic discovery raises `AutomorphismActionUnavailable` when the parent
|
|
|
292
292
|
does not expose suitable generators, in which case an explicit action is
|
|
293
293
|
required.
|
|
294
294
|
|
|
295
|
+
## Materialized automorphism groups
|
|
296
|
+
|
|
297
|
+
For a finite parent the whole automorphism group can be materialized, which
|
|
298
|
+
gives stabilizers, canonical orbit representatives, transporters and
|
|
299
|
+
classification of many objects at once:
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
G = FiniteAdditiveGroup.cyclic_product(2, 4)
|
|
303
|
+
C2xC4 = AdditiveSequenceSpace(G, davenport_bound=5)
|
|
304
|
+
group = C2xC4.automorphism_group() # AutomorphismGroup(order=8)
|
|
305
|
+
|
|
306
|
+
x = C2xC4([(1, 1), (1, 1), (0, 2)])
|
|
307
|
+
group.orbit(x) # the four images, sorted
|
|
308
|
+
len(group.stabilizer(x)) # 2
|
|
309
|
+
form = group.canonical_form(x) # orbit minimum + smallest automorphism reaching it
|
|
310
|
+
form.automorphism.generator_images() # ((1, 0), (1, 1)): the "matrix", column by column
|
|
311
|
+
group.transporter(x, group.apply(form.automorphism, x))
|
|
312
|
+
|
|
313
|
+
atoms = tuple(C2xC4.enumerate_atom_catalogue())
|
|
314
|
+
group.orbit_representatives(atoms) # 11 orbits: representative -> members
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The group acts on sequences and on tuples of sequences; tuples are treated as
|
|
318
|
+
multisets unless `ordered=True` (the canonical form of a tuple also records
|
|
319
|
+
the permutation that sorts the image). Canonical forms are minima with
|
|
320
|
+
respect to the total order on sequences (length first, then the sorted term
|
|
321
|
+
list, exposed through `<`), so representatives do not depend on how the group
|
|
322
|
+
was found.
|
|
323
|
+
|
|
324
|
+
The parent decides how the group is obtained: a `FiniteAdditiveGroup` may
|
|
325
|
+
receive a callable `automorphism_group=` returning an `AutomorphismGroup`
|
|
326
|
+
built from any element type with `apply_term`, `compose` and `inverse`
|
|
327
|
+
callbacks (for example units acting on a cyclic group, or CAS matrices);
|
|
328
|
+
otherwise its `automorphism_generators` are closed under composition.
|
|
329
|
+
Orbit traversals cost proportionally to the orbit size; stabilizers and
|
|
330
|
+
smallest transporters scan the whole group once, so they are cheap for
|
|
331
|
+
groups of a few thousand elements and expensive beyond that.
|
|
332
|
+
`AutomorphismGroupUnavailable` is raised when neither a provider nor
|
|
333
|
+
generators exist.
|
|
334
|
+
|
|
335
|
+
## Indexed and serialized catalogues
|
|
336
|
+
|
|
337
|
+
An `AtomCatalogue` stores its atoms in the sequence order (length first,
|
|
338
|
+
then terms), so `catalogue.index(atom)` and `catalogue[i]` form a stable
|
|
339
|
+
identifier namespace. It can be filtered, classified into automorphism
|
|
340
|
+
orbits, and written to or read from JSON lines:
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
catalogue = C2xC4.enumerate_atom_catalogue()
|
|
344
|
+
catalogue.index(atom), catalogue[7], atom in catalogue
|
|
345
|
+
short = catalogue.restrict(max_length=3) # also restrict(support=...)
|
|
346
|
+
for orbit in catalogue.orbits(): # AtomOrbit(representative, indices, stabilizer_order)
|
|
347
|
+
...
|
|
348
|
+
catalogue.representative(atom) # orbit minimum of an atom
|
|
349
|
+
catalogue.transporter(atom) # an automorphism mapping atom to it
|
|
350
|
+
catalogue.permutation(group.elements[1]) # an automorphism as an index permutation
|
|
351
|
+
|
|
352
|
+
catalogue.to_jsonl("atoms.jsonl", annotate=lambda atom: {"label": ...})
|
|
353
|
+
same = AtomCatalogue.from_jsonl("atoms.jsonl", C2xC4)
|
|
354
|
+
same.annotations[atom]["label"]
|
|
355
|
+
catalogue.digest() # sha256 of the atoms, annotation-independent
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
`from_jsonl` checks the order, the indices and the digest, and re-verifies
|
|
359
|
+
every record as an atom unless `verify=False`. Terms are serialized through
|
|
360
|
+
the parent: `FiniteAdditiveGroup` accepts `encode_term=`/`decode_term=`,
|
|
361
|
+
coordinate groups and Sage vector spaces use lists of integers by default,
|
|
362
|
+
and `sequence.encode()` / `space.decode(data)` apply the codec to whole
|
|
363
|
+
sequences.
|
|
364
|
+
|
|
365
|
+
## Memoized factorization queries
|
|
366
|
+
|
|
367
|
+
A `FactorizationCache` answers length-set questions over one catalogue with
|
|
368
|
+
caching, and can exploit that length sets are automorphism invariants:
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
cache = C2xC4.factorization_cache(catalogue)
|
|
372
|
+
cache.length_set(x), cache.minimum(x), cache.maximum(x)
|
|
373
|
+
cache.has_length(x, 3), cache.witness(x, 3), cache.witnesses(x)
|
|
374
|
+
cache.statistics() # hits, misses, solver builds
|
|
375
|
+
|
|
376
|
+
aware = C2xC4.factorization_cache(catalogue, group=group)
|
|
377
|
+
aware.length_set(group.apply(a, x)) == aware.length_set(x) # one solve per orbit
|
|
378
|
+
aware.witness(group.apply(a, x), 3) # transported back through a^-1
|
|
379
|
+
|
|
380
|
+
cache.to_jsonl("lengths.jsonl") # bound to catalogue.digest()
|
|
381
|
+
FactorizationCache.from_jsonl("lengths.jsonl", catalogue)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Length sets are kept without bound; solvers are kept in a bounded LRU
|
|
385
|
+
(`maxsize=`) so that follow-up questions about a recent sequence reuse its
|
|
386
|
+
remainder graph. With `group=` every query is keyed by a canonical form.
|
|
387
|
+
The default is the full canonical form, whose orbit traversal is worthwhile
|
|
388
|
+
when solving is the expensive part; for large groups pass
|
|
389
|
+
`canonicalize=AnchoredCanonicalizer(catalogue, group)`, which only
|
|
390
|
+
transports the largest atom dividing the sequence to its orbit
|
|
391
|
+
representative and reduces under that representative's stabilizer. Any
|
|
392
|
+
callable returning an automorphic image of its argument (optionally with the
|
|
393
|
+
automorphism) is accepted as `canonicalize=`.
|
|
394
|
+
|
|
295
395
|
## Benchmarks
|
|
296
396
|
|
|
297
397
|
Run the short-to-very-long performance corpus with:
|
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
from importlib.metadata import version as _distribution_version
|
|
4
4
|
|
|
5
5
|
from .additive_sequence import AdditiveSequence, AdditiveSequenceSpace
|
|
6
|
-
from .atom_catalogue import AtomCatalogue
|
|
6
|
+
from .atom_catalogue import AtomCatalogue, AtomOrbit
|
|
7
|
+
from .automorphisms import (
|
|
8
|
+
Automorphism,
|
|
9
|
+
AutomorphismGroup,
|
|
10
|
+
AutomorphismGroupUnavailable,
|
|
11
|
+
CanonicalForm,
|
|
12
|
+
)
|
|
7
13
|
from .factorization import Factorization, FactorizationSolver
|
|
14
|
+
from .factorization_cache import AnchoredCanonicalizer, FactorizationCache
|
|
8
15
|
from .orbits import (
|
|
9
16
|
AutomorphismAction,
|
|
10
17
|
AutomorphismActionUnavailable,
|
|
@@ -17,12 +24,19 @@ __all__ = [
|
|
|
17
24
|
"AdditiveSequence",
|
|
18
25
|
"AdditiveSequenceSpace",
|
|
19
26
|
"AtomCatalogue",
|
|
27
|
+
"AtomOrbit",
|
|
20
28
|
"Factorization",
|
|
21
29
|
"FactorizationRelation",
|
|
30
|
+
"AnchoredCanonicalizer",
|
|
31
|
+
"FactorizationCache",
|
|
22
32
|
"FactorizationSolver",
|
|
23
33
|
"FiniteAdditiveGroup",
|
|
34
|
+
"Automorphism",
|
|
24
35
|
"AutomorphismAction",
|
|
25
36
|
"AutomorphismActionUnavailable",
|
|
37
|
+
"AutomorphismGroup",
|
|
38
|
+
"AutomorphismGroupUnavailable",
|
|
39
|
+
"CanonicalForm",
|
|
26
40
|
"OrbitWitness",
|
|
27
41
|
]
|
|
28
42
|
__version__ = _distribution_version("zero-sum-sequences")
|
{zero_sum_sequences-0.1.2 → zero_sum_sequences-0.2.0}/src/zero_sum_sequences/additive_sequence.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import functools
|
|
5
6
|
import itertools
|
|
6
7
|
from collections import Counter
|
|
7
8
|
from collections.abc import Callable, Iterable, Iterator, Mapping
|
|
@@ -74,7 +75,12 @@ class AdditiveSequenceSpace(Generic[Element]):
|
|
|
74
75
|
A positive upper bound for the parent's Davenport constant.
|
|
75
76
|
"""
|
|
76
77
|
|
|
77
|
-
__slots__ = (
|
|
78
|
+
__slots__ = (
|
|
79
|
+
"_parent",
|
|
80
|
+
"_davenport_bound",
|
|
81
|
+
"_automorphism_action",
|
|
82
|
+
"_automorphism_group",
|
|
83
|
+
)
|
|
78
84
|
|
|
79
85
|
def __init__(self, parent, *, davenport_bound: int) -> None:
|
|
80
86
|
if not callable(parent):
|
|
@@ -88,6 +94,52 @@ class AdditiveSequenceSpace(Generic[Element]):
|
|
|
88
94
|
name="Davenport bound",
|
|
89
95
|
)
|
|
90
96
|
self._automorphism_action = None
|
|
97
|
+
self._automorphism_group = None
|
|
98
|
+
|
|
99
|
+
def factorization_cache(self, catalogue, *, group=None, canonicalize=None, maxsize: int | None = 1024):
|
|
100
|
+
"""A :class:`FactorizationCache` over ``catalogue`` for this space."""
|
|
101
|
+
|
|
102
|
+
from .factorization_cache import FactorizationCache
|
|
103
|
+
|
|
104
|
+
if catalogue.space is not self:
|
|
105
|
+
raise TypeError("the catalogue belongs to a different space")
|
|
106
|
+
return FactorizationCache(
|
|
107
|
+
catalogue, group=group, canonicalize=canonicalize, maxsize=maxsize
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def encode_term(self, term: Element) -> object:
|
|
111
|
+
"""JSON-compatible encoding of one term, delegated to the parent."""
|
|
112
|
+
|
|
113
|
+
encoder = getattr(self._parent, "encode_term", None)
|
|
114
|
+
if callable(encoder):
|
|
115
|
+
return encoder(term)
|
|
116
|
+
from .parents import default_encode_term
|
|
117
|
+
|
|
118
|
+
return default_encode_term(term)
|
|
119
|
+
|
|
120
|
+
def decode_term(self, data: object) -> Element:
|
|
121
|
+
"""Inverse of :meth:`encode_term`, delegated to the parent."""
|
|
122
|
+
|
|
123
|
+
decoder = getattr(self._parent, "decode_term", None)
|
|
124
|
+
if callable(decoder):
|
|
125
|
+
return _immutable_term(decoder(data))
|
|
126
|
+
return _immutable_term(self._parent(data))
|
|
127
|
+
|
|
128
|
+
def decode(self, data: Iterable[object]) -> AdditiveSequence[Element]:
|
|
129
|
+
"""Construct a sequence from a list of encoded terms."""
|
|
130
|
+
|
|
131
|
+
return self(self.decode_term(item) for item in data)
|
|
132
|
+
|
|
133
|
+
def automorphism_group(self):
|
|
134
|
+
"""Return the materialized automorphism group of the additive parent.
|
|
135
|
+
|
|
136
|
+
The parent may supply the group itself; otherwise it is the closure
|
|
137
|
+
of the parent's automorphism generators. The result is cached.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
from .automorphisms import automorphism_group
|
|
141
|
+
|
|
142
|
+
return automorphism_group(self)
|
|
91
143
|
|
|
92
144
|
@property
|
|
93
145
|
def base_parent(self):
|
|
@@ -226,6 +278,7 @@ class AdditiveSequenceSpace(Generic[Element]):
|
|
|
226
278
|
)
|
|
227
279
|
|
|
228
280
|
|
|
281
|
+
@functools.total_ordering
|
|
229
282
|
class AdditiveSequence(Generic[Element]):
|
|
230
283
|
"""An immutable finite multiset in an :class:`AdditiveSequenceSpace`.
|
|
231
284
|
|
|
@@ -258,6 +311,28 @@ class AdditiveSequence(Generic[Element]):
|
|
|
258
311
|
self._length = sum(count for _, count in items)
|
|
259
312
|
self._hash = hash((space, items))
|
|
260
313
|
|
|
314
|
+
@classmethod
|
|
315
|
+
def _from_items(
|
|
316
|
+
cls, space: AdditiveSequenceSpace[Element], items: tuple[tuple[Element, int], ...]
|
|
317
|
+
) -> AdditiveSequence[Element]:
|
|
318
|
+
"""Fast constructor from sorted ``(term, count)`` items of canonical terms."""
|
|
319
|
+
|
|
320
|
+
sequence = cls.__new__(cls)
|
|
321
|
+
sequence._space = space
|
|
322
|
+
sequence._items = items
|
|
323
|
+
sequence._length = sum(count for _, count in items)
|
|
324
|
+
sequence._hash = hash((space, items))
|
|
325
|
+
return sequence
|
|
326
|
+
|
|
327
|
+
def _map_canonical(self, mapping: Callable[[Element], Element]) -> AdditiveSequence[Element]:
|
|
328
|
+
"""Image under a term map whose values are canonical parent elements."""
|
|
329
|
+
|
|
330
|
+
counts: dict[Element, int] = {}
|
|
331
|
+
for term, count in self._items:
|
|
332
|
+
image = mapping(term)
|
|
333
|
+
counts[image] = counts.get(image, 0) + count
|
|
334
|
+
return AdditiveSequence._from_items(self._space, tuple(sorted(counts.items())))
|
|
335
|
+
|
|
261
336
|
def parent(self) -> AdditiveSequenceSpace[Element]:
|
|
262
337
|
"""Return the configured sequence space."""
|
|
263
338
|
|
|
@@ -558,6 +633,19 @@ class AdditiveSequence(Generic[Element]):
|
|
|
558
633
|
def __rmul__(self, repetitions: object) -> Self:
|
|
559
634
|
return self * repetitions
|
|
560
635
|
|
|
636
|
+
def encode(self) -> list[object]:
|
|
637
|
+
"""The sorted term list with multiplicities, terms encoded by the space."""
|
|
638
|
+
|
|
639
|
+
return [self._space.encode_term(term) for term in self]
|
|
640
|
+
|
|
641
|
+
def __lt__(self, other: object) -> bool:
|
|
642
|
+
"""Total order: shorter sequences first, then the sorted term lists."""
|
|
643
|
+
|
|
644
|
+
if not isinstance(other, AdditiveSequence):
|
|
645
|
+
return NotImplemented
|
|
646
|
+
self._require_same_space(other)
|
|
647
|
+
return (self._length, tuple(self)) < (other._length, tuple(other))
|
|
648
|
+
|
|
561
649
|
def __eq__(self, other: object) -> bool:
|
|
562
650
|
if not isinstance(other, AdditiveSequence):
|
|
563
651
|
return NotImplemented
|