• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

feihoo87 / waveforms / 6534953321

16 Oct 2023 02:19PM UTC coverage: 35.674% (-22.7%) from 58.421%
6534953321

push

github

feihoo87
fix Coveralls

5913 of 16575 relevant lines covered (35.67%)

3.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.06
/waveforms/math/group/permutation_group.py
1
from __future__ import annotations
9✔
2

3
import bisect
9✔
4
import copy
9✔
5
import functools
9✔
6
import logging
9✔
7
import math
9✔
8
import operator
9✔
9
import random
9✔
10
import weakref
9✔
11
from itertools import chain, combinations, product
9✔
12
from typing import Callable, TypeVar
9✔
13

14
import numpy as np
9✔
15

16
logger = logging.getLogger(__name__)
9✔
17

18

19
class _NotContained(Exception):
9✔
20
    pass
9✔
21

22

23
@functools.total_ordering
9✔
24
class Cycles():
9✔
25

26
    __slots__ = ('_cycles', '_support', '_mapping', '_expr', '_order')
9✔
27

28
    def __init__(self, *cycles):
9✔
29
        self._mapping = {}
9✔
30
        self._expr: list[Cycles] = []
9✔
31
        self._order = None
9✔
32
        if len(cycles) == 0:
9✔
33
            self._cycles = ()
9✔
34
            self._support = ()
9✔
35
            return
9✔
36

37
        if not isinstance(cycles[0], (list, tuple)):
9✔
38
            cycles = (cycles, )
9✔
39

40
        support = set()
9✔
41
        ret = []
9✔
42
        for cycle in cycles:
9✔
43
            if len(cycle) <= 1:
9✔
44
                continue
×
45
            support.update(cycle)
9✔
46
            i = cycle.index(min(cycle))
9✔
47
            cycle = cycle[i:] + cycle[:i]
9✔
48
            ret.append(tuple(cycle))
9✔
49
            for i in range(len(cycle) - 1):
9✔
50
                self._mapping[cycle[i]] = cycle[i + 1]
9✔
51
            self._mapping[cycle[-1]] = cycle[0]
9✔
52
        self._cycles = tuple(sorted(ret))
9✔
53
        self._support = tuple(sorted(support))
9✔
54

55
    def __hash__(self):
9✔
56
        return hash(self._cycles)
9✔
57

58
    def is_identity(self):
9✔
59
        return len(self._cycles) == 0
9✔
60

61
    def __eq__(self, value: Cycles) -> bool:
9✔
62
        return self._cycles == value._cycles
9✔
63

64
    def __lt__(self, value: Cycles) -> bool:
9✔
65
        return self._cycles < value._cycles
×
66

67
    def __mul__(self, other: Cycles) -> Cycles:
9✔
68
        """Returns the product of two cycles.
69

70
        The product of permutations a, b is understood to be the permutation
71
        resulting from applying a, then b.
72
        """
73
        support = sorted(set(self.support + other.support), reverse=True)
9✔
74
        mapping = {
9✔
75
            a: b
76
            for a, b in zip(support, other.replace(self.replace(support)))
77
            if a != b
78
        }
79
        return Cycles._from_sorted_mapping(mapping)
9✔
80

81
    def __rmul__(self, other: Cycles) -> Cycles:
9✔
82
        return other.__mul__(self)
9✔
83

84
    @staticmethod
9✔
85
    def _from_sorted_mapping(mapping: dict[int, int]) -> Cycles:
9✔
86
        c = Cycles()
9✔
87
        if not mapping:
9✔
88
            return c
9✔
89

90
        c._support = tuple(reversed(mapping.keys()))
9✔
91
        c._mapping = mapping.copy()
9✔
92
        c._order = 1
9✔
93

94
        cycles = []
9✔
95
        while mapping:
9✔
96
            k, el = mapping.popitem()
9✔
97
            cycle = [k]
9✔
98
            while k != el:
9✔
99
                cycle.append(el)
9✔
100
                el = mapping.pop(el)
9✔
101
            cycles.append(tuple(cycle))
9✔
102
            c._order = math.lcm(c._order, len(cycle))
9✔
103
        c._cycles = tuple(cycles)
9✔
104

105
        return c
9✔
106

107
    def __pow__(self, n: int) -> Cycles:
9✔
108
        if n == 0:
×
109
            return Cycles()
×
110
        elif n > 0:
×
111
            n = n % self.order
×
112
            ret = Cycles()
×
113
            while n > 0:
×
114
                if n % 2 == 1:
×
115
                    ret *= self
×
116
                self *= self
×
117
                n //= 2
×
118
            return ret
×
119
        else:
120
            return self.inv()**(-n)
×
121

122
    def inv(self):
9✔
123
        c = Cycles()
9✔
124
        if len(self._cycles) == 0:
9✔
125
            return c
9✔
126
        c._cycles = tuple([(cycle[0], ) + tuple(reversed(cycle[1:]))
9✔
127
                           for cycle in self._cycles])
128
        c._support = self._support
9✔
129
        c._mapping = {v: k for k, v in self._mapping.items()}
9✔
130
        c._order = self._order
9✔
131
        return c
9✔
132

133
    @property
9✔
134
    def order(self):
9✔
135
        """Returns the order of the permutation.
136

137
        The order of a permutation is the least integer n such that
138
        p**n = e, where e is the identity permutation.
139
        """
140
        if self._order is None:
8✔
141
            self._order = math.lcm(*[len(cycle) for cycle in self._cycles])
×
142
        return self._order
8✔
143

144
    @property
9✔
145
    def support(self):
9✔
146
        """Returns the support of the permutation.
147

148
        The support of a permutation is the set of elements that are moved by
149
        the permutation.
150
        """
151
        return self._support
9✔
152

153
    @property
9✔
154
    def signature(self):
9✔
155
        """Returns the signature of the permutation.
156

157
        The signature of the permutation is (-1)^n, where n is the number of
158
        transpositions of pairs of elements that must be composed to build up
159
        the permutation. 
160
        """
161
        return 1 - 2 * ((len(self._support) - len(self._cycles)) % 2)
×
162

163
    def __len__(self):
9✔
164
        return len(self._support)
×
165

166
    def __repr__(self):
9✔
167
        return f'Cycles{tuple(self._cycles)!r}'
×
168

169
    def to_matrix(self) -> np.ndarray:
9✔
170
        """Returns the matrix representation of the permutation."""
171
        if self._support:
×
172
            return permute(np.eye(max(self._support) + 1, dtype=np.int8), self)
×
173
        else:
174
            return np.eye(0, dtype=np.int8)
×
175

176
    def replace(self, expr):
9✔
177
        """replaces each part in expr by its image under the permutation."""
178
        if isinstance(expr, (tuple, list)):
9✔
179
            return type(expr)(self.replace(e) for e in expr)
9✔
180
        elif isinstance(expr, Cycles):
9✔
181
            return Cycles(*[self.replace(cycle) for cycle in expr._cycles])
×
182
        else:
183
            return self._replace(expr)
9✔
184

185
    def _replace(self, x: int) -> int:
9✔
186
        return self._mapping.get(x, x)
9✔
187

188
    def __call__(self, *cycle):
9✔
189
        return self * Cycles(*cycle)
9✔
190

191
    def commutator(self, x: Cycles) -> Cycles:
9✔
192
        """Return the commutator of ``self`` and ``x``: ``self*x*self.inv()*x.inv()``
193

194
        References
195
        ==========
196

197
        .. [1] https://en.wikipedia.org/wiki/Commutator
198
        """
199
        return self * x * self.inv() * x.inv()
×
200

201
    def simplify(self) -> Cycles:
9✔
202
        return self
9✔
203

204

205
class _ExCycles(Cycles):
9✔
206

207
    def __mul__(self, other: Cycles) -> _ExCycles:
9✔
208
        c = super().__mul__(other)
9✔
209
        ret = _ExCycles()
9✔
210
        if c == Cycles():
9✔
211
            return ret
9✔
212
        ret._cycles = c._cycles
9✔
213
        ret._mapping = c._mapping
9✔
214
        ret._support = c._support
9✔
215
        ret._order = c._order
9✔
216
        ret._expr = (self, other)
9✔
217
        return ret
9✔
218

219
    def __rmul__(self, other: Cycles) -> _ExCycles:
9✔
220
        c = super().__rmul__(other)
9✔
221
        ret = _ExCycles()
9✔
222
        if c == Cycles():
9✔
223
            return ret
7✔
224
        ret._cycles = c._cycles
9✔
225
        ret._mapping = c._mapping
9✔
226
        ret._support = c._support
9✔
227
        ret._order = c._order
9✔
228
        ret._expr = (other, self)
9✔
229
        return ret
9✔
230

231
    def simplify(self) -> Cycles:
9✔
232
        if isinstance(self._expr, tuple):
9✔
233
            self._simplify()
9✔
234
        return self
9✔
235

236
    def _simplify(self):
9✔
237
        if isinstance(self._expr[0], str):
9✔
238
            self._simplify_inv()
9✔
239
        else:
240
            self._simplify_mul()
9✔
241

242
    def _simplify_inv(self):
9✔
243
        self._expr[1].simplify()
9✔
244
        if isinstance(self._expr[1], _ExCycles):
9✔
245
            expr = self._expr[1]._expr
9✔
246
        else:
247
            expr = [[self._expr[1], 1]]
×
248
        ret = []
9✔
249
        for g, n in expr:
9✔
250
            if ret and ret[-1][0] == g:
9✔
251
                ret[-1][1] = (ret[-1][1] + n) % g.order
×
252
                if ret[-1][1] == 0:
×
253
                    ret.pop()
×
254
            else:
255
                ret.append([g, n])
9✔
256
        self._expr = ret
9✔
257

258
    def _simplify_mul(self):
9✔
259
        self._expr[0].simplify()
9✔
260
        self._expr[1].simplify()
9✔
261
        if isinstance(self._expr[0], _ExCycles):
9✔
262
            ret = self._expr[0]._expr.copy()
9✔
263
        else:
264
            ret = [[self._expr[0], 1]]
×
265
        if isinstance(self._expr[1], _ExCycles):
9✔
266
            expr = self._expr[1]._expr
9✔
267
        else:
268
            expr = [[self._expr[1], 1]]
9✔
269

270
        for g, n in expr:
9✔
271
            if ret and ret[-1][0] == g:
9✔
272
                ret[-1][1] = (ret[-1][1] + n) % g.order
8✔
273
                if ret[-1][1] == 0:
8✔
274
                    ret.pop()
8✔
275
            else:
276
                ret.append([g, n])
9✔
277
        self._expr = ret
9✔
278

279
    def inv(self) -> _ExCycles:
9✔
280
        c = super().inv()
9✔
281
        ret = _ExCycles()
9✔
282
        ret._cycles = c._cycles
9✔
283
        ret._mapping = c._mapping
9✔
284
        ret._support = c._support
9✔
285
        ret._order = c._order
9✔
286
        ret._expr = ('-', self)
9✔
287
        return ret
9✔
288

289

290
def permute(expr: list | tuple | str | bytes | np.ndarray, perm: Cycles):
9✔
291
    """replaces each part in expr by its image under the permutation."""
292
    ret = list(expr)
9✔
293
    for cycle in perm._cycles:
9✔
294
        i = cycle[0]
9✔
295
        for j in cycle[1:]:
9✔
296
            ret[i], ret[j] = ret[j], ret[i]
9✔
297
    if isinstance(expr, list):
9✔
298
        return ret
×
299
    elif isinstance(expr, tuple):
9✔
300
        return tuple(ret)
×
301
    elif isinstance(expr, str):
9✔
302
        return ''.join(ret)
9✔
303
    elif isinstance(expr, bytes):
×
304
        return b''.join(ret)
×
305
    elif isinstance(expr, np.ndarray):
×
306
        return np.array(ret)
×
307
    else:
308
        return ret
×
309

310

311
def _ne(a, b):
9✔
312
    if isinstance(a, np.ndarray):
9✔
313
        return not np.allclose(a, b)
9✔
314
    else:
315
        return a != b
×
316

317

318
def _encode(perm: list, codes: dict) -> list:
9✔
319
    """encode the permutation"""
320
    ret = []
9✔
321
    for x in perm:
9✔
322
        for k, v in codes.items():
9✔
323
            if _ne(x, v):
9✔
324
                continue
9✔
325
            ret.append(k)
9✔
326
            break
9✔
327
        codes.pop(k)
9✔
328
    return ret
9✔
329

330

331
def find_permutation(expr1: list, expr2: list) -> Cycles:
9✔
332
    """find the permutation that transform expr1 to expr2"""
333
    if len(expr1) != len(expr2):
9✔
334
        raise ValueError("expr1 and expr2 must have the same length")
×
335
    codes = {}
9✔
336
    support = []
9✔
337
    perm = []
9✔
338
    for i, (a, b) in enumerate(zip(expr1, expr2)):
9✔
339
        if type(a) != type(b) or _ne(a, b):
9✔
340
            perm.append(b)
9✔
341
            support.append(i)
9✔
342
            codes[i] = a
9✔
343
    if not support:
9✔
344
        return Cycles()
×
345
    mapping = {
9✔
346
        k: v
347
        for k, v in reversed(list(zip(support, _encode(perm, codes))))
348
        if k != v
349
    }
350
    return Cycles._from_sorted_mapping(mapping)
9✔
351

352

353
def random_permutation(n: int) -> Cycles:
9✔
354
    """return a random permutation of n elements"""
355
    cycles = []
×
356
    perm = np.random.permutation(n)
×
357
    rest = list(perm)
×
358
    while len(rest) > 0:
×
359
        cycle = [rest.pop(0)]
×
360
        el = perm[cycle[-1]]
×
361
        while cycle[0] != el:
×
362
            cycle.append(el)
×
363
            rest.remove(el)
×
364
            el = perm[cycle[-1]]
×
365
        if len(cycle) > 1:
×
366
            cycles.append(tuple(cycle))
×
367
    return Cycles(*cycles)
×
368

369

370
class PermutationGroup():
9✔
371

372
    def __init__(self, generators: list[Cycles]):
9✔
373
        self.generators = generators
9✔
374
        self._elements = []
9✔
375
        self._support = None
9✔
376

377
        self._order = None
9✔
378
        self._orbits = None
9✔
379
        self._center = []
9✔
380
        self._is_abelian = None
9✔
381
        self._is_trivial = None
9✔
382

383
        # these attributes are assigned after running schreier_sims
384
        self._base = []
9✔
385
        self._strong_gens = []
9✔
386
        self._basic_orbits = []
9✔
387
        self._transversals: list[dict[int, Cycles]] = []
9✔
388

389
    def __repr__(self) -> str:
9✔
390
        return f"PermutationGroup({self.generators})"
×
391

392
    def is_trivial(self):
9✔
393
        """Test if the group is the trivial group.
394

395
        This is true if the group contains only the identity permutation.
396
        """
397
        if self._is_trivial is None:
9✔
398
            self._is_trivial = len(self.generators) == 0
9✔
399
        return self._is_trivial
9✔
400

401
    def is_abelian(self):
9✔
402
        """Test if the group is Abelian.
403
        """
404
        if self._is_abelian is not None:
×
405
            return self._is_abelian
×
406

407
        self._is_abelian = True
×
408
        for x, y in combinations(self.generators, 2):
×
409
            if not x * y == y * x:
×
410
                self._is_abelian = False
×
411
                break
×
412
        return True
×
413

414
    def is_subgroup(self, G: PermutationGroup):
9✔
415
        """Return ``True`` if all elements of ``self`` belong to ``G``."""
416
        if not isinstance(G, PermutationGroup):
9✔
417
            return False
×
418
        if self == G or self.is_trivial():
9✔
419
            return True
9✔
420
        if G.order() % self.order() != 0:
9✔
421
            return False
×
422
        return all(g in G for g in self.generators)
9✔
423

424
    def generate(self, method: str = "schreier_sims"):
9✔
425
        if method == "schreier_sims":
×
426
            yield from self.generate_schreier_sims()
×
427
        elif method == "dimino":
×
428
            yield from self.generate_dimino(self.generators)
×
429

430
    @staticmethod
9✔
431
    def generate_dimino(generators: list[Cycles]):
9✔
432
        """Yield group elements using Dimino's algorithm."""
433
        e = Cycles()
×
434
        yield e
×
435
        gens = {e}
×
436
        elements = set(generators) | set(g.inv() for g in generators)
×
437
        while True:
×
438
            new_elements = set()
×
439
            for a, b in chain(product(gens, elements), product(elements, gens),
×
440
                              product(elements, elements)):
441
                c = a * b
×
442
                if c not in gens and c not in elements and c not in new_elements:
×
443
                    new_elements.add(c)
×
444
                    yield c
×
445
            gens.update(elements)
×
446
            if len(new_elements) == 0:
×
447
                break
×
448
            elements = new_elements
×
449

450
    def generate_schreier_sims(self):
9✔
451
        """Yield group elements using the Schreier-Sims representation
452
        in coset_rank order
453
        """
454
        if self.is_trivial():
9✔
455
            yield Cycles()
×
456
            return
×
457

458
        self.schreier_sims()
9✔
459
        for gens in product(
9✔
460
                *
461
            [list(coset.values()) for coset in reversed(self._transversals)]):
462
            yield functools.reduce(operator.mul, gens)
9✔
463

464
    @property
9✔
465
    def support(self):
9✔
466
        if self._support is None:
×
467
            support = set()
×
468
            for g in self.generators:
×
469
                support.update(g.support)
×
470
            self._support = sorted(support)
×
471
        return self._support
×
472

473
    @property
9✔
474
    def elements(self):
9✔
475
        if self._elements == []:
×
476
            for g in self.generate():
×
477
                bisect.insort(self._elements, g)
×
478
        return self._elements
×
479

480
    def random(self, N=1):
9✔
481
        """Return a random element of the group.
482

483
        If N > 1, return a list of N random elements.
484
        """
485
        self.schreier_sims()
9✔
486
        transversals = self._transversals
9✔
487
        orbits = self._basic_orbits
9✔
488
        ret = []
9✔
489
        for _ in range(N):
9✔
490
            g = Cycles()
9✔
491
            for orbit, coset in zip(orbits, transversals):
9✔
492
                g *= coset[random.choice(orbit)]
9✔
493
            ret.append(g)
9✔
494
        if N == 1:
9✔
495
            return ret[0]
9✔
496
        return ret
×
497

498
    @property
9✔
499
    def base(self):
9✔
500
        """Return a base from the Schreier-Sims algorithm."""
501
        if self._base == []:
×
502
            self.schreier_sims()
×
503
        return self._base
×
504

505
    def orbit(
9✔
506
        self,
507
        alpha: TypeVar['T'],
508
        action: Callable[[TypeVar['T'], Cycles], TypeVar['T']] | None = None
509
    ) -> list[TypeVar['T']]:
510
        """finds the orbit under the group action given by a function `action`
511
        """
512
        if isinstance(alpha, int) and action is None:
9✔
513
            for orbit in self.orbits():
×
514
                if alpha in orbit:
×
515
                    return orbit
×
516
            else:
517
                return [alpha]
×
518
        elif isinstance(alpha, Cycles) and action is None:
9✔
519
            action = lambda x, y: y * x
×
520
        elif action is None:
9✔
521
            action = permute
9✔
522
        orbit = [alpha]
9✔
523
        for beta in orbit:
9✔
524
            for g in self.generators:
9✔
525
                beta = action(beta, g)
9✔
526
                if beta not in orbit:
9✔
527
                    orbit.append(beta)
9✔
528
        return orbit
9✔
529

530
    def orbits(self):
9✔
531
        if self._orbits is None:
×
532
            orbit_parts = []
×
533
            for g in self.generators:
×
534
                for cycle in g._cycles:
×
535
                    for orbit in orbit_parts:
×
536
                        if set(cycle) & set(orbit):
×
537
                            orbit.update(cycle)
×
538
                            break
×
539
                    else:
540
                        orbit_parts.append(set(cycle))
×
541
            orbits = []
×
542
            for x in orbit_parts:
×
543
                for y in orbits:
×
544
                    if x & y:
×
545
                        y.update(x)
×
546
                        break
×
547
                else:
548
                    orbits.append(x)
×
549
            self._orbits = orbits
×
550
        return self._orbits
×
551

552
    def schreier_sims(self, base: list[int] | None = None):
9✔
553
        """Schreier-Sims algorithm.
554

555
        Explanation
556
        ===========
557

558
        It computes the generators of the chain of stabilizers
559
        `G > G_{b_1} > .. > G_{b1,..,b_r} > 1`
560
        in which `G_{b_1,..,b_i}` stabilizes `b_1,..,b_i`,
561
        and the corresponding ``s`` cosets.
562
        An element of the group can be written as the product
563
        `h_1*..*h_s`.
564

565
        We use the incremental Schreier-Sims algorithm.
566
        """
567
        if self._transversals and (base is None or base == self._base):
9✔
568
            return
9✔
569

570
        base, strong_gens = schreier_sims_incremental(self.generators,
9✔
571
                                                      base=base)
572
        self._base = base
9✔
573
        self._strong_gens = strong_gens
9✔
574
        if not base:
9✔
575
            self._transversals = []
×
576
            self._basic_orbits = []
×
577
            return
×
578

579
        strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
9✔
580

581
        # Compute basic orbits and transversals from a base and strong generating set.
582
        transversals = []
9✔
583
        basic_orbits = []
9✔
584
        for alpha, gens in zip(base, strong_gens_distr):
9✔
585
            transversal = _orbit_transversal(gens, alpha)
9✔
586
            basic_orbits.append(list(transversal.keys()))
9✔
587
            transversals.append(transversal)
9✔
588

589
        self._transversals = transversals
9✔
590
        self._basic_orbits = [sorted(x) for x in basic_orbits]
9✔
591

592
    def order(self):
9✔
593
        if self._order is None:
9✔
594
            if self.is_trivial():
9✔
595
                self._order = 1
×
596
            else:
597
                self.schreier_sims()
9✔
598
                self._order = math.prod(len(x) for x in self._transversals)
9✔
599
        return self._order
9✔
600

601
    def index(self, H: PermutationGroup):
9✔
602
        """
603
        Returns the index of a permutation group.
604

605
        Examples
606
        ========
607

608
        >>> a = Permutation(1,2,3)
609
        >>> b =Permutation(3)
610
        >>> G = PermutationGroup([a])
611
        >>> H = PermutationGroup([b])
612
        >>> G.index(H)
613
        3
614

615
        """
616
        if H.is_subgroup(self):
×
617
            return self.order() // H.order()
×
618

619
    def __len__(self):
9✔
620
        return self.order()
×
621

622
    def __getitem__(self, i):
9✔
623
        return self.elements[i]
×
624

625
    def __contains__(self, perm: Cycles):
9✔
626
        if perm in self.generators or perm.is_identity():
9✔
627
            return True
×
628
        if self._elements:
9✔
629
            return perm in self._elements
×
630
        try:
9✔
631
            perm = self.coset_factor(perm)
9✔
632
            return True
9✔
633
        except _NotContained:
9✔
634
            return False
9✔
635

636
    def __eq__(self, other) -> bool:
9✔
637
        """Return ``True`` if PermutationGroup generated by elements in the
638
        group are same i.e they represent the same PermutationGroup.
639
        """
640
        if not isinstance(other, PermutationGroup):
9✔
641
            raise TypeError(
×
642
                f"'==' not supported between instances of '{type(self)}' and '{type(other)}'"
643
            )
644

645
        set_self_gens = set(self.generators)
9✔
646
        set_other_gens = set(other.generators)
9✔
647

648
        # before reaching the general case there are also certain
649
        # optimisation and obvious cases requiring less or no actual
650
        # computation.
651
        if set_self_gens == set_other_gens:
9✔
652
            return True
9✔
653

654
        # in the most general case it will check that each generator of
655
        # one group belongs to the other PermutationGroup and vice-versa
656
        for gen1 in set_self_gens:
9✔
657
            if gen1 not in other:
9✔
658
                return False
9✔
659
        for gen2 in set_other_gens:
×
660
            if gen2 not in self:
×
661
                return False
×
662
        return True
×
663

664
    def __lt__(self, other) -> bool:
9✔
665
        if isinstance(other, PermutationGroup):
9✔
666
            return self.is_subgroup(other) and self.order() < other.order()
9✔
667
        else:
668
            raise TypeError(
×
669
                f"'<' not supported between instances of '{type(self)}' and '{type(other)}'"
670
            )
671

672
    def __le__(self, other) -> bool:
9✔
673
        if isinstance(other, PermutationGroup):
9✔
674
            return self.is_subgroup(other)
9✔
675
        else:
676
            raise TypeError(
×
677
                f"'<=' not supported between instances of '{type(self)}' and '{type(other)}'"
678
            )
679

680
    def __mul__(self, other: Cycles):
9✔
681
        if other in self:
×
682
            return self
×
683
        return Coset(self, other, left=False)
×
684

685
    def __rmul__(self, other: Cycles):
9✔
686
        if other in self:
×
687
            return self
×
688
        return Coset(self, other, left=True)
×
689

690
    def coset_factor(self, g: Cycles, index=False):
9✔
691
        """Return ``G``'s (self's) coset factorization of ``g``
692

693
        Explanation
694
        ===========
695

696
        If ``g`` is an element of ``G`` then it can be written as the product
697
        of permutations drawn from the Schreier-Sims coset decomposition,
698

699
        The permutations returned in ``f`` are those for which
700
        the product gives ``g``: ``g = f[n]*...f[1]*f[0]`` where ``n = len(B)``
701
        and ``B = G.base``. f[i] is one of the permutations in
702
        ``self._basic_orbits[i]``.
703
        """
704
        self.schreier_sims()
9✔
705
        factors = []
9✔
706
        for alpha, coset, orbit in zip(self._base, self._transversals,
9✔
707
                                       self._basic_orbits):
708
            beta = g._replace(alpha)
9✔
709
            if beta == alpha:
9✔
710
                if index:
9✔
711
                    factors.append(0)
9✔
712
                continue
9✔
713
            if beta not in coset:
9✔
714
                raise _NotContained
9✔
715
            u = coset[beta]
9✔
716
            if index:
9✔
717
                factors.append(orbit.index(beta))
9✔
718
            else:
719
                factors.append(u)
9✔
720
            g = g * u.inv()
9✔
721
            if g.is_identity():
9✔
722
                break
9✔
723
        if not g.is_identity():
9✔
724
            raise _NotContained
9✔
725
        return factors
9✔
726

727
    def coset_rank(self, g):
9✔
728
        """rank using Schreier-Sims representation.
729

730
        Explanation
731
        ===========
732

733
        The coset rank of ``g`` is the ordering number in which
734
        it appears in the lexicographic listing according to the
735
        coset decomposition
736

737
        The ordering is the same as in G.generate(method='coset').
738
        If ``g`` does not belong to the group it returns None.
739
        """
740
        try:
9✔
741
            index = self.coset_factor(g, index=True)
9✔
742
            index = index + [0] * (len(self._transversals) - len(index))
9✔
743
        except _NotContained:
×
744
            raise IndexError(f"Permutation {g} not contained in group.")
×
745
        rank = 0
9✔
746
        b = 1
9✔
747
        for i, coset in zip(index, self._transversals):
9✔
748
            rank += b * i
9✔
749
            b = b * len(coset)
9✔
750
        return rank
9✔
751

752
    def coset_unrank(self, rank):
9✔
753
        """unrank using Schreier-Sims representation
754

755
        coset_unrank is the inverse operation of coset_rank
756
        if 0 <= rank < order; otherwise it returns None.
757

758
        """
759
        if rank < 0 or rank >= self.order():
9✔
760
            return None
×
761
        transversals = self._transversals
9✔
762
        orbits = self._basic_orbits
9✔
763
        ret = Cycles()
9✔
764
        for orbit, coset in zip(orbits, transversals):
9✔
765
            rank, c = divmod(rank, len(coset))
9✔
766
            ret = coset[orbit[c]] * ret
9✔
767
        return ret
9✔
768

769
    def express(self, perm: Cycles):
9✔
770
        if perm.is_identity():
9✔
771
            return Cycles()
×
772
        self.schreier_sims()
9✔
773
        return functools.reduce(operator.mul, self.coset_factor(perm)[::-1])
9✔
774

775
    def stabilizer_chain(self) -> list[tuple[tuple[int], PermutationGroup]]:
9✔
776
        r"""
777
        Return a chain of stabilizers relative to a base and strong generating
778
        set.
779

780
        Explanation
781
        ===========
782

783
        The ``i``-th basic stabilizer `G^{(i)}` relative to a base
784
        `(b_1, b_2, \dots, b_k)` is `G_{b_1, b_2, \dots, b_{i-1}}`.
785
        """
786
        self.schreier_sims()
×
787
        strong_gens = self._strong_gens
×
788
        base = self._base
×
789
        if not base:  # e.g. if self is trivial
×
790
            return []
×
791
        strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
×
792
        basic_stabilizers = []
×
793
        for i, gens in enumerate(strong_gens_distr):
×
794
            basic_stabilizers.append((tuple(base[:i]), PermutationGroup(gens)))
×
795
        basic_stabilizers.append((tuple(base), PermutationGroup([])))
×
796
        return basic_stabilizers
×
797

798
    def stabilizer(self, alpha) -> PermutationGroup:
9✔
799
        """Return the stabilizer subgroup of ``alpha``."""
800
        orb = [alpha]
×
801
        table = {alpha: Cycles()}
×
802
        table_inv = {alpha: Cycles()}
×
803
        used = {}
×
804
        used[alpha] = True
×
805
        stab_gens = []
×
806
        for b in orb:
×
807
            for gen in self.generators:
×
808
                temp = gen[b]
×
809
                if temp not in used:
×
810
                    gen_temp = table[b] * gen
×
811
                    orb.append(temp)
×
812
                    table[temp] = gen_temp
×
813
                    table_inv[temp] = gen_temp.inv()
×
814
                    used[temp] = True
×
815
                else:
816
                    schreier_gen = table[b] * gen * table_inv[temp]
×
817
                    if schreier_gen not in stab_gens:
×
818
                        stab_gens.append(schreier_gen)
×
819
        return PermutationGroup(stab_gens)
×
820

821
    def centralizer(self, H: PermutationGroup) -> PermutationGroup:
9✔
822
        """Return the centralizer of ``H`` in ``self``."""
823
        raise NotImplementedError
×
824

825
    def normalizer(self, H: PermutationGroup) -> PermutationGroup:
9✔
826
        """Return the normalizer of ``H`` in ``self``."""
827
        raise NotImplementedError
×
828

829
    def center(self) -> PermutationGroup:
9✔
830
        """Return the center of group."""
831
        return self.centralizer(self)
×
832

833

834
class Coset():
9✔
835

836
    def __init__(self, H: PermutationGroup, g: Cycles, left: bool = True):
9✔
837
        self._left = left
×
838
        self._norm = True
×
839
        self.H = H
×
840
        self.g = g
×
841
        for gen in self.H.generators:
×
842
            if gen * self.g not in self.g * self.H:
×
843
                self._norm = False
×
844
                break
×
845

846
    def is_left_coset(self):
9✔
847
        return self._left
×
848

849
    def is_right_coset(self):
9✔
850
        return not self._left
×
851

852
    def __contains__(self, perm: Cycles):
9✔
853
        if self._left:
×
854
            return self.g * perm in self.H
×
855
        else:
856
            return perm * self.g in self.H
×
857

858
    def generate(self):
9✔
859
        if self._left:
×
860
            for perm in self.H.generate():
×
861
                yield self.g * perm
×
862
        else:
863
            for perm in self.H.generate():
×
864
                yield perm * self.g
×
865

866
    def __mul__(self, other: PermutationGroup | Coset) -> Coset:
9✔
867
        if isinstance(other, PermutationGroup) and other == self.H:
×
868
            return self
×
869
        elif isinstance(other, Coset) and other.H == self.H:
×
870
            return Coset(self.H, self.g * other.g, self._left)
×
871
        else:
872
            raise TypeError(f"Cannot multiply {self} by {other}")
×
873

874

875
class SymmetricGroup(PermutationGroup):
9✔
876

877
    def __init__(self, N: int):
9✔
878
        if N < 2:
9✔
879
            super().__init__([])
×
880
        elif N == 2:
9✔
881
            super().__init__([Cycles((0, 1))])
×
882
        else:
883
            super().__init__([Cycles((0, 1)), Cycles(tuple(range(N)))])
9✔
884
        self.N = N
9✔
885

886
    def __repr__(self) -> str:
9✔
887
        return f"SymmetricGroup({self.N})"
×
888

889
    def __len__(self):
9✔
890
        return np.math.factorial(self.N)
×
891

892
    def __contains__(self, perm: Cycles):
9✔
893
        return set(perm.support) <= set(range(self.N))
9✔
894

895

896
class CyclicGroup(PermutationGroup):
9✔
897

898
    def __init__(self, N: int):
9✔
899
        if N < 2:
×
900
            super().__init__([])
×
901
        else:
902
            super().__init__([Cycles(tuple(range(N)))])
×
903
        self.N = N
×
904

905
    def __repr__(self) -> str:
9✔
906
        return f"CyclicGroup({self.N})"
×
907

908
    def __len__(self):
9✔
909
        return max(self.N, 1)
×
910

911

912
class DihedralGroup(PermutationGroup):
9✔
913

914
    def __init__(self, N: int):
9✔
915
        if N < 2:
×
916
            generators = []
×
917
        elif N == 2:
×
918
            generators = [Cycles((0, 1))]
×
919
        else:
920
            generators = [
×
921
                Cycles(tuple(range(N))),
922
                Cycles(*[(i + N % 2, N - 1 - i) for i in range(N // 2)])
923
            ]
924
        super().__init__(generators)
×
925
        self.N = N
×
926

927
    def __repr__(self) -> str:
9✔
928
        return f"DihedralGroup({self.N})"
×
929

930
    def __len__(self):
9✔
931
        if self.N == 1:
×
932
            return 1
×
933
        elif self.N == 2:
×
934
            return 2
×
935
        return max(2 * self.N, 1)
×
936

937

938
class AbelianGroup(PermutationGroup):
9✔
939

940
    def __init__(self, *n: int):
9✔
941
        self.n = tuple(sorted(n))
×
942
        generators = []
×
943
        start = 0
×
944
        for ni in self.n:
×
945
            if ni >= 2:
×
946
                generators.append(Cycles(tuple(range(start, start + ni))))
×
947
                start += ni
×
948
        super().__init__(generators)
×
949

950
    def __repr__(self) -> str:
9✔
951
        return f"AbelianGroup{self.n}"
×
952

953
    def __len__(self):
9✔
954
        return max(np.multiply.reduce(self.n), 1)
×
955

956

957
class AlternatingGroup(PermutationGroup):
9✔
958

959
    def __init__(self, N: int):
9✔
960
        if N <= 2:
×
961
            generators = []
×
962
        elif N == 3:
×
963
            generators = [Cycles((0, 1, 2))]
×
964
        else:
965
            generators = [
×
966
                Cycles((0, 1, 2)),
967
                Cycles(tuple(range(N))) if N %
968
                2 else Cycles(tuple(range(1, N)))
969
            ]
970
        super().__init__(generators)
×
971
        self.N = N
×
972

973
    def __repr__(self) -> str:
9✔
974
        return f"AlternatingGroup({self.N})"
×
975

976
    def __len__(self):
9✔
977
        return max(np.math.factorial(self.N) // 2, 1)
×
978

979
    def __contains__(self, perm: Cycles):
9✔
980
        return perm in SymmetricGroup(self.N) and perm.signature == 1
×
981

982

983
def _strip(h: Cycles, base, orbits, transversals, j):
9✔
984
    """
985
    """
986
    base_len = len(base)
9✔
987
    for i in range(j + 1, base_len):
9✔
988
        beta = h._replace(base[i])
9✔
989
        if beta == base[i]:
9✔
990
            continue
9✔
991
        if beta not in orbits[i]:
9✔
992
            return h, i + 1
9✔
993
        u = transversals[i][beta]
9✔
994
        if h == u:
9✔
995
            return None, base_len + 1
9✔
996
        h = h * u.inv()
9✔
997
    return h, base_len + 1
9✔
998

999

1000
def _orbit_transversal(
9✔
1001
    generators: list[Cycles],
1002
    alpha: int,
1003
    Identity: Cycles = _ExCycles(),
1004
) -> tuple[list[tuple[int, Cycles]], dict[int, list[int]]]:
1005
    r"""Computes a transversal for the orbit of ``alpha`` as a set.
1006

1007
    Explanation
1008
    ===========
1009

1010
    generators   generators of the group ``G``
1011

1012
    For a permutation group ``G``, a transversal for the orbit
1013
    `Orb = \{g(\alpha) | g \in G\}` is a set
1014
    `\{g_\beta | g_\beta(\alpha) = \beta\}` for `\beta \in Orb`.
1015
    Note that there may be more than one possible transversal.
1016
    """
1017
    tr = [(alpha, Identity)]
9✔
1018
    db = {alpha}
9✔
1019
    for x, px in tr:
9✔
1020
        for i, gen in enumerate(generators):
9✔
1021
            temp = gen._replace(x)
9✔
1022
            if temp not in db:
9✔
1023
                db.add(temp)
9✔
1024
                tr.append((temp, px * gen))
9✔
1025

1026
    return dict(tr)
9✔
1027

1028

1029
def _distribute_gens_by_base(base: list,
9✔
1030
                             gens: list[Cycles]) -> list[list[Cycles]]:
1031
    r"""
1032
    Distribute the group elements ``gens`` by membership in basic stabilizers.
1033

1034
    Explanation
1035
    ===========
1036

1037
    Notice that for a base `(b_1, b_2, \dots, b_k)`, the basic stabilizers
1038
    are defined as `G^{(i)} = G_{b_1, \dots, b_{i-1}}` for
1039
    `i \in\{1, 2, \dots, k\}`.
1040

1041
    Parameters
1042
    ==========
1043

1044
    base : a sequence of points in `\{0, 1, \dots, n-1\}`
1045
    gens : a list of elements of a permutation group of degree `n`.
1046

1047
    Returns
1048
    =======
1049
    list
1050
        List of length `k`, where `k` is the length of *base*. The `i`-th entry
1051
        contains those elements in *gens* which fix the first `i` elements of
1052
        *base* (so that the `0`-th entry is equal to *gens* itself). If no
1053
        element fixes the first `i` elements of *base*, the `i`-th element is
1054
        set to a list containing the identity element.
1055
    """
1056
    base_len = len(base)
9✔
1057
    stabs = [[] for _ in range(base_len)]
9✔
1058
    max_stab_index = 0
9✔
1059
    for gen in gens:
9✔
1060
        j = 0
9✔
1061
        while j < base_len - 1 and gen._replace(base[j]) == base[j]:
9✔
1062
            j += 1
9✔
1063
        if j > max_stab_index:
9✔
1064
            max_stab_index = j
9✔
1065
        for k in range(j + 1):
9✔
1066
            stabs[k].append(gen)
9✔
1067
    for i in range(max_stab_index + 1, base_len):
9✔
1068
        stabs[i].append(Cycles())
×
1069
    return stabs
9✔
1070

1071

1072
def schreier_sims_incremental(
9✔
1073
    gens: list[Cycles],
1074
    base: list[int] | None = None
1075
) -> tuple[list[int], list[Cycles], dict[int, list[int]]]:
1076
    """Extend a sequence of points and generating set to a base and strong
1077
    generating set.
1078

1079
    Parameters
1080
    ==========
1081
    gens
1082
        The generating set to be extended to a strong generating set
1083
        relative to the base obtained.
1084

1085
    base
1086
        The sequence of points to be extended to a base. Optional
1087
        parameter with default value ``[]``.
1088

1089
    Returns
1090
    =======
1091

1092
    (base, strong_gens)
1093
        ``base`` is the base obtained, and ``strong_gens`` is the strong
1094
        generating set relative to it. The original parameters ``base``,
1095
        ``gens`` remain unchanged.
1096
    """
1097
    if base is None:
9✔
1098
        base = []
9✔
1099
    else:
1100
        base = base.copy()
×
1101
    support = set()
9✔
1102
    for g in gens:
9✔
1103
        support.update(g.support)
9✔
1104
    # handle the trivial group
1105
    if len(gens) == 1 and gens[0].is_identity():
9✔
1106
        return base, gens, {gens[0]: [gens[0]]}
×
1107
    # remove the identity as a generator
1108
    gens = [x for x in gens if not x.is_identity()]
9✔
1109
    # make sure no generator fixes all base points
1110
    for gen in gens:
9✔
1111
        if all(x == gen._replace(x) for x in base):
9✔
1112
            for new in support:
9✔
1113
                if gen._replace(new) != new:
9✔
1114
                    break
9✔
1115
            else:
1116
                assert None  # can this ever happen?
×
1117
            base.append(new)
9✔
1118
    #logger.debug("Schreier-Sims: base = %s, gens = %s", _base, _gens)
1119
    # distribute generators according to basic stabilizers
1120
    strong_gens_distr = _distribute_gens_by_base(base, gens)
9✔
1121
    new_strong_gens = []
9✔
1122
    # initialize the basic stabilizers, basic orbits and basic transversals
1123
    orbs = {}
9✔
1124
    transversals = {}
9✔
1125
    for i, alpha in enumerate(base):
9✔
1126
        transversals[i] = _orbit_transversal(strong_gens_distr[i], alpha)
9✔
1127
        orbs[i] = list(transversals[i].keys())
9✔
1128
    # main loop: amend the stabilizer chain until we have generators
1129
    # for all stabilizers
1130
    base_len = len(base)
9✔
1131
    i = base_len - 1
9✔
1132
    while i >= 0:
9✔
1133
        # this flag is used to continue with the main loop from inside
1134
        # a nested loop
1135
        continue_i = False
9✔
1136
        # test the generators for being a strong generating set
1137
        db = {}
9✔
1138
        for beta, u_beta in list(transversals[i].items()):
9✔
1139
            for j, gen in enumerate(strong_gens_distr[i]):
9✔
1140
                gb = gen._replace(beta)
9✔
1141
                u1 = transversals[i][gb]
9✔
1142
                g1 = u_beta * gen
9✔
1143
                if g1 != u1:
9✔
1144
                    # test if the schreier generator is in the i+1-th
1145
                    # would-be basic stabilizer
1146
                    new_strong_generator_found = False
9✔
1147
                    try:
9✔
1148
                        u1_inv = db[gb]
9✔
1149
                    except KeyError:
9✔
1150
                        u1_inv = db[gb] = u1.inv()
9✔
1151
                    schreier_gen = g1 * u1_inv
9✔
1152
                    h, j = _strip(schreier_gen, base, orbs, transversals, i)
9✔
1153
                    if j <= base_len:
9✔
1154
                        # new strong generator h at level j
1155
                        new_strong_generator_found = True
9✔
1156
                    elif h is not None:
9✔
1157
                        # h fixes all base points
1158
                        new_strong_generator_found = True
9✔
1159
                        for moved in support:
9✔
1160
                            if h._replace(moved) != moved:
9✔
1161
                                break
9✔
1162
                        base.append(moved)
9✔
1163
                        base_len += 1
9✔
1164
                        strong_gens_distr.append([])
9✔
1165
                    if new_strong_generator_found:
9✔
1166
                        # if a new strong generator is found, update the
1167
                        # data structures and start over
1168
                        new_strong_gens.append(h)
9✔
1169
                        for l in range(i + 1, j):
9✔
1170
                            strong_gens_distr[l].append(h)
9✔
1171
                            transversals[l] =\
9✔
1172
                            _orbit_transversal(strong_gens_distr[l],
1173
                                base[l])
1174
                            orbs[l] = list(transversals[l].keys())
9✔
1175
                        i = j - 1
9✔
1176
                        # continue main loop using the flag
1177
                        continue_i = True
9✔
1178
                if continue_i is True:
9✔
1179
                    break
9✔
1180
            if continue_i is True:
9✔
1181
                break
9✔
1182
        logger.debug(
9✔
1183
            "Schreier-Sims: i = %s, continue_i = %s, len(transversals[i]) = %s",
1184
            i, continue_i, len(transversals[i]))
1185
        if continue_i is True:
9✔
1186
            continue
9✔
1187
        i -= 1
9✔
1188

1189
    return (base, gens + new_strong_gens)
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc