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,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
|
+
]
|