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,533 @@
1
+ """Automorphism actions on additive sequences.
2
+
3
+ The core orbit routines are deliberately independent of Sage or any other
4
+ computer algebra system. An :class:`AutomorphismAction` consists of a finite
5
+ list of generators and, optionally, a callback describing how a generator
6
+ acts on one base-parent element. For the common case in which generators are
7
+ callables, the callback is inferred automatically.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections import deque
13
+ from collections.abc import Callable, Iterable, Iterator
14
+ from dataclasses import dataclass, field
15
+ from operator import index
16
+ from typing import Generic, TypeVar
17
+
18
+ from .additive_sequence import AdditiveSequence
19
+
20
+ Element = TypeVar("Element")
21
+ Generator = TypeVar("Generator")
22
+
23
+
24
+ class AutomorphismActionUnavailable(RuntimeError):
25
+ """Raised when an additive parent cannot supply an automorphism action."""
26
+
27
+
28
+ def _non_negative_index(value: object, *, name: str) -> int:
29
+ """Return a non-negative indexable integer, rejecting booleans."""
30
+
31
+ if isinstance(value, bool):
32
+ raise ValueError(f"{name} must be a non-negative integer")
33
+ try:
34
+ integer = index(value)
35
+ except TypeError:
36
+ raise ValueError(f"{name} must be a non-negative integer") from None
37
+ if integer < 0:
38
+ raise ValueError(f"{name} must be a non-negative integer")
39
+ return integer
40
+
41
+
42
+ class AutomorphismAction(Generic[Element, Generator]):
43
+ """A finite generating set acting on terms of an additive parent.
44
+
45
+ Parameters
46
+ ----------
47
+ generators:
48
+ The generators, in the order used by the orbit search. If no
49
+ ``apply_term`` callback is supplied, every generator must be callable
50
+ on one base-parent element.
51
+ apply_term:
52
+ Optional callback ``apply_term(generator, term)``. This is useful for
53
+ matrix or CAS objects that represent maps without being callable
54
+ themselves.
55
+ materialize_word:
56
+ Optional callback turning an :class:`OrbitWitness` into a concrete
57
+ automorphism. Orbit traversal itself only needs generator words;
58
+ adapters can use this hook when an explicit map or matrix is needed.
59
+
60
+ The class intentionally does not inspect or enumerate the generated group.
61
+ It only provides the term-wise action needed by the orbit routines.
62
+ """
63
+
64
+ __slots__ = ("generators", "_apply_term", "_materialize_word")
65
+
66
+ def __init__(
67
+ self,
68
+ generators: Iterable[Generator],
69
+ *,
70
+ apply_term: Callable[[Generator, Element], Element] | None = None,
71
+ materialize_word: Callable[[OrbitWitness], object] | None = None,
72
+ ) -> None:
73
+ try:
74
+ normalized_generators = tuple(generators)
75
+ except TypeError:
76
+ raise TypeError("automorphism generators must be iterable") from None
77
+ if apply_term is not None and not callable(apply_term):
78
+ raise TypeError("apply_term must be callable")
79
+ if materialize_word is not None and not callable(materialize_word):
80
+ raise TypeError("materialize_word must be callable")
81
+ if apply_term is None:
82
+ for generator in normalized_generators:
83
+ if not callable(generator):
84
+ raise TypeError(
85
+ "generators must be callable when apply_term is omitted"
86
+ )
87
+ apply_term = _call_generator
88
+ self.generators = normalized_generators
89
+ self._apply_term = apply_term
90
+ self._materialize_word = materialize_word
91
+
92
+ def __iter__(self) -> Iterator[Generator]:
93
+ return iter(self.generators)
94
+
95
+ def __len__(self) -> int:
96
+ return len(self.generators)
97
+
98
+ def __repr__(self) -> str:
99
+ return f"{type(self).__name__}({self.generators!r})"
100
+
101
+ def apply_term(self, generator_index: int, term: Element) -> Element:
102
+ """Apply one indexed generator to a base-parent term."""
103
+
104
+ generator_index = _non_negative_index(
105
+ generator_index,
106
+ name="generator index",
107
+ )
108
+ try:
109
+ generator = self.generators[generator_index]
110
+ except IndexError:
111
+ raise IndexError("automorphism generator index out of range") from None
112
+ return self._apply_term(generator, term)
113
+
114
+ def apply_sequence(
115
+ self,
116
+ sequence: AdditiveSequence[Element],
117
+ generator_index: int,
118
+ ) -> AdditiveSequence[Element]:
119
+ """Apply one indexed generator term by term to ``sequence``."""
120
+
121
+ if not isinstance(sequence, AdditiveSequence):
122
+ raise TypeError("expected an additive sequence")
123
+ return sequence.map_terms(
124
+ lambda term: self.apply_term(generator_index, term)
125
+ )
126
+
127
+ def apply_word(
128
+ self,
129
+ sequence: AdditiveSequence[Element],
130
+ word: OrbitWitness | Iterable[int],
131
+ ) -> AdditiveSequence[Element]:
132
+ """Apply a generator word from left to right to ``sequence``."""
133
+
134
+ if not isinstance(word, OrbitWitness):
135
+ word = OrbitWitness(word)
136
+ image = sequence
137
+ for generator_index in word.generator_indices:
138
+ image = self.apply_sequence(image, generator_index)
139
+ return image
140
+
141
+ def materialize_word(
142
+ self,
143
+ word: OrbitWitness | Iterable[int],
144
+ ):
145
+ """Return the concrete automorphism represented by ``word``.
146
+
147
+ Not every action has a useful concrete representation. Such actions
148
+ still support orbit traversal, but this method raises ``TypeError``.
149
+ """
150
+
151
+ if not isinstance(word, OrbitWitness):
152
+ word = OrbitWitness(word)
153
+ if self._materialize_word is None:
154
+ raise TypeError("this action cannot materialize generator words")
155
+ return self._materialize_word(word)
156
+
157
+
158
+ def _call_generator(generator, term):
159
+ return generator(term)
160
+
161
+
162
+ @dataclass(frozen=True, slots=True, init=False)
163
+ class OrbitWitness:
164
+ """A word in the indexed generators of an :class:`AutomorphismAction`.
165
+
166
+ ``generator_indices`` records the order in which generators are applied.
167
+ The empty tuple is the identity word.
168
+ """
169
+
170
+ generator_indices: tuple[int, ...]
171
+ _sequence_space: object | None = field(
172
+ default=None,
173
+ repr=False,
174
+ compare=False,
175
+ hash=False,
176
+ )
177
+ _action: AutomorphismAction | None = field(
178
+ default=None,
179
+ repr=False,
180
+ compare=False,
181
+ hash=False,
182
+ )
183
+
184
+ def __init__(
185
+ self,
186
+ generator_indices: Iterable[int] = (),
187
+ *,
188
+ _sequence_space=None,
189
+ _action: AutomorphismAction | None = None,
190
+ ) -> None:
191
+ try:
192
+ normalized = tuple(
193
+ _non_negative_index(value, name="generator indices")
194
+ for value in generator_indices
195
+ )
196
+ except TypeError:
197
+ raise TypeError("generator indices must be iterable") from None
198
+ object.__setattr__(self, "generator_indices", normalized)
199
+ object.__setattr__(self, "_sequence_space", _sequence_space)
200
+ object.__setattr__(self, "_action", _action)
201
+
202
+ def __iter__(self) -> Iterator[int]:
203
+ return iter(self.generator_indices)
204
+
205
+ def __len__(self) -> int:
206
+ return len(self.generator_indices)
207
+
208
+ def __repr__(self) -> str:
209
+ return (
210
+ f"{type(self).__name__}("
211
+ f"generator_indices={self.generator_indices!r})"
212
+ )
213
+
214
+ def _with_context(self, sequence_space, action) -> OrbitWitness:
215
+ return type(self)(
216
+ self.generator_indices,
217
+ _sequence_space=sequence_space,
218
+ _action=action,
219
+ )
220
+
221
+ def additive_generator_images(self) -> tuple[tuple[object, object], ...]:
222
+ """Return the induced images of distinguished additive generators.
223
+
224
+ Witnesses returned by :func:`orbit_witness` remember the sequence
225
+ space and action used to construct them. The base parent must expose
226
+ an additive generating set through ``additive_generators()``,
227
+ ``basis()``, or ``gens()``.
228
+ """
229
+
230
+ if self._sequence_space is None or self._action is None:
231
+ raise ValueError(
232
+ "term images require a witness returned by orbit_witness()"
233
+ )
234
+ parent = self._sequence_space.base_parent
235
+ generators = _additive_generators(parent)
236
+ images = []
237
+ for generator in generators:
238
+ image = generator
239
+ for generator_index in self.generator_indices:
240
+ image = self._action.apply_term(generator_index, image)
241
+ images.append((generator, image))
242
+ return tuple(images)
243
+
244
+ def show(self, *, file=None) -> None:
245
+ """Print the induced map on distinguished additive generators."""
246
+
247
+ for generator, image in self.additive_generator_images():
248
+ print(f"{generator} ↦ {image}", file=file)
249
+
250
+
251
+ def _additive_generators(parent) -> tuple:
252
+ for provider_name in ("additive_generators", "basis", "gens"):
253
+ provider = getattr(parent, provider_name, None)
254
+ if not callable(provider):
255
+ continue
256
+ try:
257
+ return tuple(provider())
258
+ except (AttributeError, NotImplementedError, TypeError, ValueError):
259
+ continue
260
+ raise ValueError(
261
+ "the additive parent does not expose a basis or generating set"
262
+ )
263
+
264
+
265
+ def _action_from_parent_provider(parent) -> AutomorphismAction | None:
266
+ """Return an action explicitly exposed by ``parent``, if available."""
267
+
268
+ provider = getattr(parent, "automorphism_action", None)
269
+ if isinstance(provider, AutomorphismAction):
270
+ return provider
271
+ if callable(provider):
272
+ try:
273
+ action = provider()
274
+ except (AttributeError, NotImplementedError):
275
+ action = None
276
+ if action is not None:
277
+ if not isinstance(action, AutomorphismAction):
278
+ raise TypeError(
279
+ "automorphism_action() must return an AutomorphismAction"
280
+ )
281
+ return action
282
+
283
+ provider = getattr(parent, "automorphism_generators", None)
284
+ if provider is not None:
285
+ try:
286
+ generators = provider() if callable(provider) else provider
287
+ except (AttributeError, NotImplementedError):
288
+ generators = None
289
+ if generators is not None:
290
+ return AutomorphismAction(generators)
291
+ return None
292
+
293
+
294
+ def _sage_vector_space_action(parent) -> AutomorphismAction | None:
295
+ """Construct the natural matrix action for a finite Sage vector space."""
296
+
297
+ try:
298
+ from sage.all import FiniteFields, GL, VectorSpaces, identity_matrix, matrix
299
+ except ImportError:
300
+ return None
301
+
302
+ try:
303
+ field = parent.base_ring()
304
+ if field not in FiniteFields():
305
+ return None
306
+ if parent not in VectorSpaces(field).FiniteDimensional():
307
+ return None
308
+ dimension = int(parent.dimension())
309
+ except (AttributeError, TypeError, ValueError):
310
+ return None
311
+
312
+ generators = []
313
+ for generator in GL(dimension, field).gens() if dimension else ():
314
+ generator = matrix(generator)
315
+ generator.set_immutable()
316
+ generators.append(generator)
317
+ generators = tuple(generators)
318
+
319
+ def apply_term(generator, term):
320
+ return parent(generator * parent(term))
321
+
322
+ def materialize_word(word):
323
+ result = identity_matrix(field, dimension)
324
+ for generator_index in word.generator_indices:
325
+ # Generator words record maps in application order. With column
326
+ # vectors, the matrix of g_0 followed by g_1 is g_1 * g_0.
327
+ result = generators[generator_index] * result
328
+ result = matrix(result)
329
+ result.set_immutable()
330
+ return result
331
+
332
+ return AutomorphismAction(
333
+ generators,
334
+ apply_term=apply_term,
335
+ materialize_word=materialize_word,
336
+ )
337
+
338
+
339
+ def _cas_automorphism_action(parent) -> AutomorphismAction | None:
340
+ """Try conservative automorphism-group conventions used by CAS parents."""
341
+
342
+ is_finite = getattr(parent, "is_finite", None)
343
+ try:
344
+ if not callable(is_finite) or not is_finite():
345
+ return None
346
+ except (NotImplementedError, TypeError, ValueError):
347
+ return None
348
+
349
+ for method_name in ("automorphism_group", "aut"):
350
+ method = getattr(parent, method_name, None)
351
+ if not callable(method):
352
+ continue
353
+ try:
354
+ group = method()
355
+ generators = tuple(group.gens())
356
+ except (AttributeError, NotImplementedError, TypeError, ValueError):
357
+ continue
358
+ if not all(callable(generator) for generator in generators):
359
+ continue
360
+
361
+ def apply_term(generator, term):
362
+ return parent(generator(term))
363
+
364
+ action = AutomorphismAction(generators, apply_term=apply_term)
365
+ try:
366
+ if any(
367
+ action.apply_term(index, parent.zero()) != parent.zero()
368
+ for index in range(len(action))
369
+ ):
370
+ continue
371
+ except (TypeError, ValueError):
372
+ continue
373
+ return action
374
+ return None
375
+
376
+
377
+ def automorphism_action(sequence_space) -> AutomorphismAction:
378
+ """Resolve and cache an automorphism action for a sequence space.
379
+
380
+ Resolution first honors an explicit provider on the additive parent, then
381
+ recognizes finite-dimensional Sage vector spaces over finite fields, and
382
+ finally tries conventional CAS automorphism-group methods.
383
+ """
384
+
385
+ cached = sequence_space._automorphism_action
386
+ if cached is not None:
387
+ return cached
388
+
389
+ parent = sequence_space.base_parent
390
+ action = _action_from_parent_provider(parent)
391
+ if action is None:
392
+ action = _sage_vector_space_action(parent)
393
+ if action is None:
394
+ action = _cas_automorphism_action(parent)
395
+ if action is None:
396
+ raise AutomorphismActionUnavailable(
397
+ "could not determine automorphism generators for the additive "
398
+ "parent; pass action=AutomorphismAction(...) explicitly"
399
+ )
400
+ sequence_space._automorphism_action = action
401
+ return action
402
+
403
+
404
+ def _normalize_action(action, sequence) -> AutomorphismAction:
405
+ if isinstance(action, AutomorphismAction):
406
+ return action
407
+ if action is None:
408
+ return automorphism_action(sequence.parent())
409
+ return AutomorphismAction(action)
410
+
411
+
412
+ def _require_sequence_pair(
413
+ source: AdditiveSequence[Element],
414
+ target: AdditiveSequence[Element],
415
+ ) -> None:
416
+ if not isinstance(source, AdditiveSequence):
417
+ raise TypeError("expected an additive sequence")
418
+ source._require_same_space(target)
419
+
420
+
421
+ def _orbit_with_witnesses(
422
+ source: AdditiveSequence[Element],
423
+ action: AutomorphismAction,
424
+ *,
425
+ target: AdditiveSequence[Element] | None = None,
426
+ ) -> tuple[dict[AdditiveSequence[Element], OrbitWitness], bool]:
427
+ """Traverse an orbit and optionally stop when ``target`` is found."""
428
+
429
+ witnesses: dict[AdditiveSequence[Element], OrbitWitness] = {
430
+ source: OrbitWitness()
431
+ }
432
+ pending = deque((source,))
433
+ while pending:
434
+ current = pending.popleft()
435
+ if target is not None and current == target:
436
+ return witnesses, True
437
+ current_word = witnesses[current]
438
+ for generator_index in range(len(action)):
439
+ image = action.apply_sequence(current, generator_index)
440
+ if image in witnesses:
441
+ continue
442
+ image_word = OrbitWitness(
443
+ (*current_word.generator_indices, generator_index)
444
+ )
445
+ witnesses[image] = image_word
446
+ if target is not None and image == target:
447
+ return witnesses, True
448
+ pending.append(image)
449
+ return witnesses, target is None
450
+
451
+
452
+ def orbit(
453
+ sequence: AdditiveSequence[Element],
454
+ *,
455
+ action: AutomorphismAction[Element, Generator] | Iterable[Generator] | None = None,
456
+ ) -> tuple[AdditiveSequence[Element], ...]:
457
+ """Return all distinct automorphism images of ``sequence``.
458
+
459
+ Images are returned in deterministic breadth-first order, beginning with
460
+ ``sequence`` itself. The action must have a finite orbit for this
461
+ materialized traversal to terminate.
462
+ """
463
+
464
+ if not isinstance(sequence, AdditiveSequence):
465
+ raise TypeError("expected an additive sequence")
466
+ normalized_action = _normalize_action(action, sequence)
467
+ witnesses, _ = _orbit_with_witnesses(sequence, normalized_action)
468
+ return tuple(witnesses)
469
+
470
+
471
+ def is_in_same_orbit(
472
+ source: AdditiveSequence[Element],
473
+ target: AdditiveSequence[Element],
474
+ *,
475
+ action: AutomorphismAction[Element, Generator] | Iterable[Generator] | None = None,
476
+ ) -> bool:
477
+ """Return whether two sequences lie in one automorphism orbit.
478
+
479
+ When ``action`` is omitted, equality is recognized without resolving an
480
+ action from the additive parent.
481
+ """
482
+
483
+ _require_sequence_pair(source, target)
484
+ if source == target and action is None:
485
+ return True
486
+ normalized_action = _normalize_action(action, source)
487
+ _, found = _orbit_with_witnesses(
488
+ source,
489
+ normalized_action,
490
+ target=target,
491
+ )
492
+ return found
493
+
494
+
495
+ def orbit_witness(
496
+ source: AdditiveSequence[Element],
497
+ target: AdditiveSequence[Element],
498
+ *,
499
+ action: AutomorphismAction[Element, Generator] | Iterable[Generator] | None = None,
500
+ ) -> OrbitWitness | None:
501
+ """Return a shortest deterministic generator word to ``target``.
502
+
503
+ The empty word is returned for equal sequences. If ``action`` is omitted,
504
+ this equality case does not require the additive parent to expose an
505
+ action. If ``target`` is not in the orbit, return ``None``.
506
+ """
507
+
508
+ _require_sequence_pair(source, target)
509
+ if source == target and action is None:
510
+ return OrbitWitness()._with_context(
511
+ source.parent(),
512
+ AutomorphismAction(()),
513
+ )
514
+ normalized_action = _normalize_action(action, source)
515
+ witnesses, found = _orbit_with_witnesses(
516
+ source,
517
+ normalized_action,
518
+ target=target,
519
+ )
520
+ if not found:
521
+ return None
522
+ return witnesses[target]._with_context(source.parent(), normalized_action)
523
+
524
+
525
+ __all__ = [
526
+ "AutomorphismAction",
527
+ "AutomorphismActionUnavailable",
528
+ "OrbitWitness",
529
+ "automorphism_action",
530
+ "is_in_same_orbit",
531
+ "orbit",
532
+ "orbit_witness",
533
+ ]