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

feihoo87 / waveforms / 6893865468

16 Nov 2023 04:49PM UTC coverage: 43.147% (+0.7%) from 42.467%
6893865468

push

github

feihoo87
update

7 of 17 new or added lines in 4 files covered. (41.18%)

588 existing lines in 10 files now uncovered.

7436 of 17234 relevant lines covered (43.15%)

3.87 hits per line

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

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

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

12
import numpy as np
9✔
13

14
logger = logging.getLogger(__name__)
9✔
15

16

17
class _NotContained(Exception):
9✔
18
    pass
9✔
19

20

21
@functools.total_ordering
9✔
22
class Cycles():
9✔
23

24
    __slots__ = ('_cycles', '_support', '_mapping', '_expr', '_order')
9✔
25

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

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

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

53
    def __hash__(self):
9✔
54
        return hash(self._cycles)
9✔
55

56
    def is_identity(self):
9✔
57
        return len(self._cycles) == 0
9✔
58

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

62
    def __lt__(self, value: Cycles) -> bool:
9✔
63
        return self._cycles < value._cycles
9✔
64

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

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

81
    def __rmul__(self, other: Cycles) -> Cycles:
9✔
UNCOV
82
        return other.__mul__(self)
×
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 __invert__(self):
9✔
UNCOV
123
        return self.inv()
×
124

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

137
    @property
9✔
138
    def order(self):
9✔
139
        """Returns the order of the permutation.
140

141
        The order of a permutation is the least integer n such that
142
        p**n = e, where e is the identity permutation.
143
        """
144
        if self._order is None:
9✔
145
            self._order = math.lcm(*[len(cycle) for cycle in self._cycles])
9✔
146
        return self._order
9✔
147

148
    @property
9✔
149
    def support(self):
9✔
150
        """Returns the support of the permutation.
151

152
        The support of a permutation is the set of elements that are moved by
153
        the permutation.
154
        """
155
        return self._support
9✔
156

157
    @property
9✔
158
    def signature(self):
9✔
159
        """Returns the signature of the permutation.
160

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

167
    def __len__(self):
9✔
UNCOV
168
        return len(self._support)
×
169

170
    def __repr__(self):
9✔
171
        return f'Cycles{tuple(self._cycles)!r}'
×
172

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

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

189
    def _replace(self, x: int) -> int:
9✔
190
        return self._mapping.get(x, x)
9✔
191

192
    def __call__(self, *cycle):
9✔
193
        return self * Cycles(*cycle)
9✔
194

195
    def commutator(self, x: Cycles) -> Cycles:
9✔
196
        """Return the commutator of ``self`` and ``x``: ``self*x*self.inv()*x.inv()``
197

198
        References
199
        ==========
200

201
        .. [1] https://en.wikipedia.org/wiki/Commutator
202
        """
UNCOV
203
        return self * x * self.inv() * x.inv()
×
204

205
    def simplify(self) -> Cycles:
9✔
206
        if isinstance(self._expr, list):
9✔
207
            if self.is_identity():
9✔
208
                pass
9✔
209
            if not self._expr:
9✔
210
                self._expr = [[self, 1]]
9✔
211
            return self
9✔
212

213
        if len(self._expr) == 1:
9✔
214
            # inv
215
            self._expr[0].simplify()
9✔
216
            ret = []
9✔
217
            for g, n in reversed(self._expr[0]._expr):
9✔
218
                if n and not g.is_identity():
9✔
219
                    ret.append([g, g.order - n])
9✔
220
            self._expr = ret
9✔
221
        else:
222
            # mul
223
            self._expr[0].simplify()
9✔
224
            self._expr[1].simplify()
9✔
225
            ret = [[g, n] for g, n in self._expr[0]._expr
9✔
226
                   if n and not g.is_identity()]
227
            for g, n in self._expr[1]._expr:
9✔
228
                if ret and ret[-1][0] == g:
9✔
229
                    ret[-1][1] = (ret[-1][1] + n) % g.order
9✔
230
                    if ret[-1][1] == 0:
9✔
231
                        ret.pop()
9✔
232
                elif n and not g.is_identity():
9✔
233
                    ret.append([g, n])
9✔
234
            self._expr = ret
9✔
235

236
        return self
9✔
237

238
    def expand(self):
9✔
239
        self.simplify()
9✔
240
        for c, n in self._expr:
9✔
241
            yield from repeat(c, n)
9✔
242

243

244
def permute(expr: list | tuple | str | bytes | np.ndarray, perm: Cycles):
9✔
245
    """replaces each part in expr by its image under the permutation."""
246
    ret = list(expr)
9✔
247
    for cycle in perm._cycles:
9✔
248
        i = cycle[0]
9✔
249
        for j in cycle[1:]:
9✔
250
            ret[i], ret[j] = ret[j], ret[i]
9✔
251
    if isinstance(expr, list):
9✔
252
        return ret
×
253
    elif isinstance(expr, tuple):
9✔
UNCOV
254
        return tuple(ret)
×
255
    elif isinstance(expr, str):
9✔
256
        return ''.join(ret)
9✔
UNCOV
257
    elif isinstance(expr, bytes):
×
UNCOV
258
        return b''.join(ret)
×
UNCOV
259
    elif isinstance(expr, np.ndarray):
×
UNCOV
260
        return np.array(ret)
×
261
    else:
UNCOV
262
        return ret
×
263

264

265
def _ne(a, b):
9✔
266
    if isinstance(a, np.ndarray):
9✔
267
        return not np.allclose(a, b)
9✔
268
    else:
UNCOV
269
        return a != b
×
270

271

272
def _encode(perm: list, codes: dict) -> list:
9✔
273
    """encode the permutation"""
274
    ret = []
9✔
275
    for x in perm:
9✔
276
        for k, v in codes.items():
9✔
277
            if _ne(x, v):
9✔
278
                continue
9✔
279
            ret.append(k)
9✔
280
            break
9✔
281
        codes.pop(k)
9✔
282
    return ret
9✔
283

284

285
def find_permutation(expr1: list, expr2: list) -> Cycles:
9✔
286
    """find the permutation that transform expr1 to expr2"""
287
    if len(expr1) != len(expr2):
9✔
UNCOV
288
        raise ValueError("expr1 and expr2 must have the same length")
×
289
    codes = {}
9✔
290
    support = []
9✔
291
    perm = []
9✔
292
    for i, (a, b) in enumerate(zip(expr1, expr2)):
9✔
293
        if type(a) != type(b) or _ne(a, b):
9✔
294
            perm.append(b)
9✔
295
            support.append(i)
9✔
296
            codes[i] = a
9✔
297
    if not support:
9✔
298
        return Cycles()
9✔
299
    mapping = {
9✔
300
        k: v
301
        for k, v in reversed(list(zip(support, _encode(perm, codes))))
302
        if k != v
303
    }
304
    return Cycles._from_sorted_mapping(mapping)
9✔
305

306

307
def random_permutation(n: int) -> Cycles:
9✔
308
    """return a random permutation of n elements"""
UNCOV
309
    cycles = []
×
UNCOV
310
    perm = np.random.permutation(n)
×
UNCOV
311
    rest = list(perm)
×
UNCOV
312
    while len(rest) > 0:
×
UNCOV
313
        cycle = [rest.pop(0)]
×
UNCOV
314
        el = perm[cycle[-1]]
×
315
        while cycle[0] != el:
×
UNCOV
316
            cycle.append(el)
×
UNCOV
317
            rest.remove(el)
×
UNCOV
318
            el = perm[cycle[-1]]
×
UNCOV
319
        if len(cycle) > 1:
×
UNCOV
320
            cycles.append(tuple(cycle))
×
UNCOV
321
    return Cycles(*cycles)
×
322

323

324
class PermutationGroup():
9✔
325

326
    def __init__(self, generators: list[Cycles]):
9✔
327
        self.generators = generators
9✔
328
        self._elements = []
9✔
329
        self._support = None
9✔
330

331
        self._order = None
9✔
332
        self._orbits = None
9✔
333
        self._center = []
9✔
334
        self._is_abelian = None
9✔
335
        self._is_trivial = None
9✔
336

337
        # these attributes are assigned after running schreier_sims
338
        self._base = []
9✔
339
        self._strong_gens = []
9✔
340
        self._basic_orbits = []
9✔
341
        self._transversals: list[dict[int, Cycles]] = []
9✔
342
        self._shape = ()
9✔
343

344
    def __repr__(self) -> str:
9✔
UNCOV
345
        return f"PermutationGroup({self.generators})"
×
346

347
    def is_trivial(self):
9✔
348
        """Test if the group is the trivial group.
349

350
        This is true if the group contains only the identity permutation.
351
        """
352
        if self._is_trivial is None:
9✔
353
            self._is_trivial = len(self.generators) == 0
9✔
354
        return self._is_trivial
9✔
355

356
    def is_abelian(self):
9✔
357
        """Test if the group is Abelian.
358
        """
359
        if self._is_abelian is not None:
×
360
            return self._is_abelian
×
361

362
        self._is_abelian = True
×
363
        for x, y in combinations(self.generators, 2):
×
364
            if not x * y == y * x:
×
365
                self._is_abelian = False
×
366
                break
×
367
        return True
×
368

369
    def is_subgroup(self, G: PermutationGroup):
9✔
370
        """Return ``True`` if all elements of ``self`` belong to ``G``."""
371
        if not isinstance(G, PermutationGroup):
9✔
UNCOV
372
            return False
×
373
        if self == G or self.is_trivial():
9✔
374
            return True
9✔
375
        if G.order() % self.order() != 0:
9✔
UNCOV
376
            return False
×
377
        return all(g in G for g in self.generators)
9✔
378

379
    def generate(self, method: str = "schreier_sims"):
9✔
380
        if method == "schreier_sims":
9✔
381
            yield from self.generate_schreier_sims()
9✔
UNCOV
382
        elif method == "dimino":
×
UNCOV
383
            yield from self.generate_dimino(self.generators)
×
384

385
    @staticmethod
9✔
386
    def generate_dimino(generators: list[Cycles]):
9✔
387
        """Yield group elements using Dimino's algorithm."""
UNCOV
388
        e = Cycles()
×
UNCOV
389
        yield e
×
390
        gens = {e}
×
UNCOV
391
        elements = set(generators) | set(g.inv() for g in generators)
×
UNCOV
392
        while True:
×
UNCOV
393
            new_elements = set()
×
UNCOV
394
            for a, b in chain(product(gens, elements), product(elements, gens),
×
395
                              product(elements, elements)):
UNCOV
396
                c = a * b
×
UNCOV
397
                if c not in gens and c not in elements and c not in new_elements:
×
UNCOV
398
                    new_elements.add(c)
×
UNCOV
399
                    yield c
×
UNCOV
400
            gens.update(elements)
×
UNCOV
401
            if len(new_elements) == 0:
×
UNCOV
402
                break
×
UNCOV
403
            elements = new_elements
×
404

405
    def generate_schreier_sims(self):
9✔
406
        """Yield group elements using the Schreier-Sims representation
407
        in coset_rank order
408
        """
409
        if self.is_trivial():
9✔
410
            yield Cycles()
×
411
            return
×
412

413
        self.schreier_sims()
9✔
414
        for gens in product(
9✔
415
                *
416
            [list(coset.values()) for coset in reversed(self._transversals)]):
417
            yield functools.reduce(operator.mul, gens)
9✔
418

419
    @property
9✔
420
    def support(self):
9✔
421
        if self._support is None:
×
UNCOV
422
            support = set()
×
UNCOV
423
            for g in self.generators:
×
UNCOV
424
                support.update(g.support)
×
425
            self._support = sorted(support)
×
426
        return self._support
×
427

428
    @property
9✔
429
    def elements(self) -> list[Cycles]:
9✔
430
        if self._elements == []:
9✔
431
            for g in self.generate():
9✔
432
                bisect.insort(self._elements, g)
9✔
433
        return self._elements
9✔
434

435
    def random(self, N=1) -> Cycles | list[Cycles]:
9✔
436
        """Return a random element of the group.
437

438
        If N > 1, return a list of N random elements.
439
        """
440
        self.schreier_sims()
9✔
441
        transversals = self._transversals
9✔
442
        orbits = self._basic_orbits
9✔
443
        ret = []
9✔
444
        for _ in range(N):
9✔
445
            g = Cycles()
9✔
446
            for orbit, coset in zip(orbits, transversals):
9✔
447
                g *= coset[random.choice(orbit)]
9✔
448
            ret.append(g)
9✔
449
        if N == 1:
9✔
450
            return ret[0]
9✔
UNCOV
451
        return ret
×
452

453
    @property
9✔
454
    def base(self):
9✔
455
        """Return a base from the Schreier-Sims algorithm."""
456
        if self._base == []:
×
UNCOV
457
            self.schreier_sims()
×
UNCOV
458
        return self._base
×
459

460
    def orbit(
9✔
461
        self,
462
        alpha: TypeVar['T'],
463
        action: Callable[[TypeVar['T'], Cycles], TypeVar['T']] | None = None
464
    ) -> list[TypeVar['T']]:
465
        """finds the orbit under the group action given by a function `action`
466
        """
467
        if isinstance(alpha, int) and action is None:
9✔
468
            for orbit in self.orbits():
×
469
                if alpha in orbit:
×
470
                    return orbit
×
471
            else:
UNCOV
472
                return [alpha]
×
473
        elif isinstance(alpha, Cycles) and action is None:
9✔
UNCOV
474
            action = lambda x, y: y * x
×
475
        elif action is None:
9✔
476
            action = permute
9✔
477
        orbit = [alpha]
9✔
478
        for beta in orbit:
9✔
479
            for g in self.generators:
9✔
480
                beta = action(beta, g)
9✔
481
                if beta not in orbit:
9✔
482
                    orbit.append(beta)
9✔
483
        return orbit
9✔
484

485
    def orbits(self):
9✔
UNCOV
486
        if self._orbits is None:
×
UNCOV
487
            orbit_parts = []
×
UNCOV
488
            for g in self.generators:
×
UNCOV
489
                for cycle in g._cycles:
×
UNCOV
490
                    for orbit in orbit_parts:
×
UNCOV
491
                        if set(cycle) & set(orbit):
×
UNCOV
492
                            orbit.update(cycle)
×
UNCOV
493
                            break
×
494
                    else:
UNCOV
495
                        orbit_parts.append(set(cycle))
×
496
            orbits = []
×
UNCOV
497
            for x in orbit_parts:
×
UNCOV
498
                for y in orbits:
×
UNCOV
499
                    if x & y:
×
UNCOV
500
                        y.update(x)
×
501
                        break
×
502
                else:
503
                    orbits.append(x)
×
UNCOV
504
            self._orbits = orbits
×
UNCOV
505
        return self._orbits
×
506

507
    def schreier_sims(self, base: list[int] | None = None):
9✔
508
        """Schreier-Sims algorithm.
509

510
        Explanation
511
        ===========
512

513
        It computes the generators of the chain of stabilizers
514
        `G > G_{b_1} > .. > G_{b1,..,b_r} > 1`
515
        in which `G_{b_1,..,b_i}` stabilizes `b_1,..,b_i`,
516
        and the corresponding ``s`` cosets.
517
        An element of the group can be written as the product
518
        `h_1*..*h_s`.
519

520
        We use the incremental Schreier-Sims algorithm.
521
        """
522
        if self._transversals and (base is None or base == self._base):
9✔
523
            return
9✔
524

525
        base, strong_gens = schreier_sims_incremental(self.generators,
9✔
526
                                                      base=base)
527
        self._base = base
9✔
528
        self._strong_gens = strong_gens
9✔
529
        if not base:
9✔
UNCOV
530
            self._transversals = []
×
531
            self._basic_orbits = []
×
532
            return
×
533

534
        strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
9✔
535

536
        # Compute basic orbits and transversals from a base and strong generating set.
537
        transversals = []
9✔
538
        basic_orbits = []
9✔
539
        for alpha, gens in zip(base, strong_gens_distr):
9✔
540
            transversal = _orbit_transversal(gens, alpha)
9✔
541
            basic_orbits.append(list(transversal.keys()))
9✔
542
            transversals.append(transversal)
9✔
543

544
        self._transversals = transversals
9✔
545
        self._basic_orbits = [sorted(x) for x in basic_orbits]
9✔
546
        self._shape = tuple(
9✔
547
            [len(coset.values()) for coset in reversed(self._transversals)])
548

549
    def order(self):
9✔
550
        if self._order is None:
9✔
551
            if self.is_trivial():
9✔
UNCOV
552
                self._order = 1
×
553
            else:
554
                self.schreier_sims()
9✔
555
                self._order = math.prod(len(x) for x in self._transversals)
9✔
556
        return self._order
9✔
557

558
    def index(self, H: PermutationGroup):
9✔
559
        """
560
        Returns the index of a permutation group.
561

562
        Examples
563
        ========
564

565
        >>> a = Permutation(1,2,3)
566
        >>> b =Permutation(3)
567
        >>> G = PermutationGroup([a])
568
        >>> H = PermutationGroup([b])
569
        >>> G.index(H)
570
        3
571

572
        """
UNCOV
573
        if H.is_subgroup(self):
×
UNCOV
574
            return self.order() // H.order()
×
575

576
    def __len__(self):
9✔
577
        return self.order()
×
578

579
    def __getitem__(self, i):
9✔
UNCOV
580
        index = np.unravel_index(i, self._shape)
×
UNCOV
581
        gens = [
×
582
            list(coset.values())[i]
583
            for i, coset in zip(index, reversed(self._transversals))
584
        ]
UNCOV
585
        return functools.reduce(operator.mul, gens)
×
586

587
    def __contains__(self, perm: Cycles):
9✔
588
        if perm in self.generators or perm.is_identity():
9✔
UNCOV
589
            return True
×
590
        if self._elements:
9✔
UNCOV
591
            return perm in self._elements
×
592
        try:
9✔
593
            perm = self.coset_factor(perm)
9✔
594
            return True
9✔
595
        except _NotContained:
9✔
596
            return False
9✔
597

598
    def __eq__(self, other) -> bool:
9✔
599
        """Return ``True`` if PermutationGroup generated by elements in the
600
        group are same i.e they represent the same PermutationGroup.
601
        """
602
        if not isinstance(other, PermutationGroup):
9✔
UNCOV
603
            raise TypeError(
×
604
                f"'==' not supported between instances of '{type(self)}' and '{type(other)}'"
605
            )
606

607
        set_self_gens = set(self.generators)
9✔
608
        set_other_gens = set(other.generators)
9✔
609

610
        # before reaching the general case there are also certain
611
        # optimisation and obvious cases requiring less or no actual
612
        # computation.
613
        if set_self_gens == set_other_gens:
9✔
614
            return True
9✔
615

616
        # in the most general case it will check that each generator of
617
        # one group belongs to the other PermutationGroup and vice-versa
618
        for gen1 in set_self_gens:
9✔
619
            if gen1 not in other:
9✔
620
                return False
9✔
UNCOV
621
        for gen2 in set_other_gens:
×
UNCOV
622
            if gen2 not in self:
×
623
                return False
×
UNCOV
624
        return True
×
625

626
    def __lt__(self, other) -> bool:
9✔
627
        if isinstance(other, PermutationGroup):
9✔
628
            return self.is_subgroup(other) and self.order() < other.order()
9✔
629
        else:
UNCOV
630
            raise TypeError(
×
631
                f"'<' not supported between instances of '{type(self)}' and '{type(other)}'"
632
            )
633

634
    def __le__(self, other) -> bool:
9✔
635
        if isinstance(other, PermutationGroup):
9✔
636
            return self.is_subgroup(other)
9✔
637
        else:
UNCOV
638
            raise TypeError(
×
639
                f"'<=' not supported between instances of '{type(self)}' and '{type(other)}'"
640
            )
641

642
    def __mul__(self, other: Cycles):
9✔
UNCOV
643
        if other in self:
×
UNCOV
644
            return self
×
UNCOV
645
        return Coset(self, other, left=False)
×
646

647
    def __rmul__(self, other: Cycles):
9✔
UNCOV
648
        if other in self:
×
UNCOV
649
            return self
×
UNCOV
650
        return Coset(self, other, left=True)
×
651

652
    def coset_factor(self, g: Cycles, index=False):
9✔
653
        """Return ``G``'s (self's) coset factorization of ``g``
654

655
        Explanation
656
        ===========
657

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

661
        The permutations returned in ``f`` are those for which
662
        the product gives ``g``: ``g = f[n]*...f[1]*f[0]`` where ``n = len(B)``
663
        and ``B = G.base``. f[i] is one of the permutations in
664
        ``self._basic_orbits[i]``.
665
        """
666
        self.schreier_sims()
9✔
667
        factors = []
9✔
668
        for alpha, coset, orbit in zip(self._base, self._transversals,
9✔
669
                                       self._basic_orbits):
670
            beta = g._replace(alpha)
9✔
671
            if beta == alpha:
9✔
672
                if index:
9✔
673
                    factors.append(0)
9✔
674
                continue
9✔
675
            if beta not in coset:
9✔
676
                raise _NotContained
9✔
677
            u = coset[beta]
9✔
678
            if index:
9✔
679
                factors.append(orbit.index(beta))
9✔
680
            else:
681
                factors.append(u)
9✔
682
            g = g * u.inv()
9✔
683
            if g.is_identity():
9✔
684
                break
9✔
685
        if not g.is_identity():
9✔
686
            raise _NotContained
9✔
687
        return factors
9✔
688

689
    def coset_rank(self, g):
9✔
690
        """rank using Schreier-Sims representation.
691

692
        Explanation
693
        ===========
694

695
        The coset rank of ``g`` is the ordering number in which
696
        it appears in the lexicographic listing according to the
697
        coset decomposition
698

699
        The ordering is the same as in G.generate(method='coset').
700
        If ``g`` does not belong to the group it returns None.
701
        """
702
        try:
9✔
703
            index = self.coset_factor(g, index=True)
9✔
704
            index = index + [0] * (len(self._transversals) - len(index))
9✔
UNCOV
705
        except _NotContained:
×
UNCOV
706
            raise IndexError(f"Permutation {g} not contained in group.")
×
707
        rank = 0
9✔
708
        b = 1
9✔
709
        for i, coset in zip(index, self._transversals):
9✔
710
            rank += b * i
9✔
711
            b = b * len(coset)
9✔
712
        return rank
9✔
713

714
    def coset_unrank(self, rank):
9✔
715
        """unrank using Schreier-Sims representation
716

717
        coset_unrank is the inverse operation of coset_rank
718
        if 0 <= rank < order; otherwise it returns None.
719

720
        """
721
        if rank < 0 or rank >= self.order():
9✔
UNCOV
722
            return None
×
723
        transversals = self._transversals
9✔
724
        orbits = self._basic_orbits
9✔
725
        ret = Cycles()
9✔
726
        for orbit, coset in zip(orbits, transversals):
9✔
727
            rank, c = divmod(rank, len(coset))
9✔
728
            ret = coset[orbit[c]] * ret
9✔
729
        return ret
9✔
730

731
    def express(self, perm: Cycles):
9✔
732
        if perm.is_identity():
9✔
733
            return Cycles()
9✔
734
        self.schreier_sims()
9✔
735
        return functools.reduce(operator.mul, self.coset_factor(perm)[::-1])
9✔
736

737
    def stabilizer_chain(self) -> list[tuple[tuple[int], PermutationGroup]]:
9✔
738
        r"""
739
        Return a chain of stabilizers relative to a base and strong generating
740
        set.
741

742
        Explanation
743
        ===========
744

745
        The ``i``-th basic stabilizer `G^{(i)}` relative to a base
746
        `(b_1, b_2, \dots, b_k)` is `G_{b_1, b_2, \dots, b_{i-1}}`.
747
        """
UNCOV
748
        self.schreier_sims()
×
UNCOV
749
        strong_gens = self._strong_gens
×
UNCOV
750
        base = self._base
×
UNCOV
751
        if not base:  # e.g. if self is trivial
×
UNCOV
752
            return []
×
UNCOV
753
        strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
×
UNCOV
754
        basic_stabilizers = []
×
UNCOV
755
        for i, gens in enumerate(strong_gens_distr):
×
UNCOV
756
            basic_stabilizers.append((tuple(base[:i]), PermutationGroup(gens)))
×
UNCOV
757
        basic_stabilizers.append((tuple(base), PermutationGroup([])))
×
UNCOV
758
        return basic_stabilizers
×
759

760
    def stabilizer(self, alpha) -> PermutationGroup:
9✔
761
        """Return the stabilizer subgroup of ``alpha``."""
UNCOV
762
        orb = [alpha]
×
UNCOV
763
        table = {alpha: Cycles()}
×
UNCOV
764
        table_inv = {alpha: Cycles()}
×
UNCOV
765
        used = {}
×
UNCOV
766
        used[alpha] = True
×
UNCOV
767
        stab_gens = []
×
UNCOV
768
        for b in orb:
×
UNCOV
769
            for gen in self.generators:
×
UNCOV
770
                temp = gen[b]
×
771
                if temp not in used:
×
UNCOV
772
                    gen_temp = table[b] * gen
×
UNCOV
773
                    orb.append(temp)
×
UNCOV
774
                    table[temp] = gen_temp
×
UNCOV
775
                    table_inv[temp] = gen_temp.inv()
×
UNCOV
776
                    used[temp] = True
×
777
                else:
UNCOV
778
                    schreier_gen = table[b] * gen * table_inv[temp]
×
UNCOV
779
                    if schreier_gen not in stab_gens:
×
UNCOV
780
                        stab_gens.append(schreier_gen)
×
UNCOV
781
        return PermutationGroup(stab_gens)
×
782

783
    def centralizer(self, H: PermutationGroup) -> PermutationGroup:
9✔
784
        """Return the centralizer of ``H`` in ``self``."""
UNCOV
785
        raise NotImplementedError
×
786

787
    def normalizer(self, H: PermutationGroup) -> PermutationGroup:
9✔
788
        """Return the normalizer of ``H`` in ``self``."""
789
        raise NotImplementedError
×
790

791
    def center(self) -> PermutationGroup:
9✔
792
        """Return the center of group."""
793
        return self.centralizer(self)
×
794

795

796
class Coset():
9✔
797

798
    def __init__(self, H: PermutationGroup, g: Cycles, left: bool = True):
9✔
UNCOV
799
        self._left = left
×
800
        self._norm = True
×
801
        self.H = H
×
802
        self.g = g
×
803
        for gen in self.H.generators:
×
804
            if gen * self.g not in self.g * self.H:
×
805
                self._norm = False
×
806
                break
×
807

808
    def is_left_coset(self):
9✔
809
        return self._left
×
810

811
    def is_right_coset(self):
9✔
812
        return not self._left
×
813

814
    def __contains__(self, perm: Cycles):
9✔
UNCOV
815
        if self._left:
×
816
            return self.g * perm in self.H
×
817
        else:
818
            return perm * self.g in self.H
×
819

820
    def generate(self):
9✔
UNCOV
821
        if self._left:
×
UNCOV
822
            for perm in self.H.generate():
×
823
                yield self.g * perm
×
824
        else:
UNCOV
825
            for perm in self.H.generate():
×
UNCOV
826
                yield perm * self.g
×
827

828
    def __mul__(self, other: PermutationGroup | Coset) -> Coset:
9✔
UNCOV
829
        if isinstance(other, PermutationGroup) and other == self.H:
×
UNCOV
830
            return self
×
831
        elif isinstance(other, Coset) and other.H == self.H:
×
UNCOV
832
            return Coset(self.H, self.g * other.g, self._left)
×
833
        else:
UNCOV
834
            raise TypeError(f"Cannot multiply {self} by {other}")
×
835

836

837
class SymmetricGroup(PermutationGroup):
9✔
838

839
    def __init__(self, N: int):
9✔
840
        if N < 2:
9✔
841
            super().__init__([])
×
842
        elif N == 2:
9✔
843
            super().__init__([Cycles((0, 1))])
×
844
        else:
845
            super().__init__([Cycles((0, 1)), Cycles(tuple(range(N)))])
9✔
846
        self.N = N
9✔
847

848
    def __repr__(self) -> str:
9✔
UNCOV
849
        return f"SymmetricGroup({self.N})"
×
850

851
    def __len__(self):
9✔
UNCOV
852
        return np.math.factorial(self.N)
×
853

854
    def __contains__(self, perm: Cycles):
9✔
855
        return set(perm.support) <= set(range(self.N))
9✔
856

857

858
class CyclicGroup(PermutationGroup):
9✔
859

860
    def __init__(self, N: int):
9✔
861
        if N < 2:
×
UNCOV
862
            super().__init__([])
×
863
        else:
864
            super().__init__([Cycles(tuple(range(N)))])
×
UNCOV
865
        self.N = N
×
866

867
    def __repr__(self) -> str:
9✔
868
        return f"CyclicGroup({self.N})"
×
869

870
    def __len__(self):
9✔
UNCOV
871
        return max(self.N, 1)
×
872

873

874
class DihedralGroup(PermutationGroup):
9✔
875

876
    def __init__(self, N: int):
9✔
UNCOV
877
        if N < 2:
×
UNCOV
878
            generators = []
×
879
        elif N == 2:
×
UNCOV
880
            generators = [Cycles((0, 1))]
×
881
        else:
UNCOV
882
            generators = [
×
883
                Cycles(tuple(range(N))),
884
                Cycles(*[(i + N % 2, N - 1 - i) for i in range(N // 2)])
885
            ]
UNCOV
886
        super().__init__(generators)
×
887
        self.N = N
×
888

889
    def __repr__(self) -> str:
9✔
890
        return f"DihedralGroup({self.N})"
×
891

892
    def __len__(self):
9✔
UNCOV
893
        if self.N == 1:
×
UNCOV
894
            return 1
×
UNCOV
895
        elif self.N == 2:
×
UNCOV
896
            return 2
×
UNCOV
897
        return max(2 * self.N, 1)
×
898

899

900
class AbelianGroup(PermutationGroup):
9✔
901

902
    def __init__(self, *n: int):
9✔
903
        self.n = tuple(sorted(n))
×
UNCOV
904
        generators = []
×
UNCOV
905
        start = 0
×
906
        for ni in self.n:
×
UNCOV
907
            if ni >= 2:
×
UNCOV
908
                generators.append(Cycles(tuple(range(start, start + ni))))
×
909
                start += ni
×
UNCOV
910
        super().__init__(generators)
×
911

912
    def __repr__(self) -> str:
9✔
UNCOV
913
        return f"AbelianGroup{self.n}"
×
914

915
    def __len__(self):
9✔
916
        return max(np.multiply.reduce(self.n), 1)
×
917

918

919
class AlternatingGroup(PermutationGroup):
9✔
920

921
    def __init__(self, N: int):
9✔
UNCOV
922
        if N <= 2:
×
UNCOV
923
            generators = []
×
924
        elif N == 3:
×
925
            generators = [Cycles((0, 1, 2))]
×
926
        else:
UNCOV
927
            generators = [
×
928
                Cycles((0, 1, 2)),
929
                Cycles(tuple(range(N))) if N %
930
                2 else Cycles(tuple(range(1, N)))
931
            ]
932
        super().__init__(generators)
×
933
        self.N = N
×
934

935
    def __repr__(self) -> str:
9✔
UNCOV
936
        return f"AlternatingGroup({self.N})"
×
937

938
    def __len__(self):
9✔
UNCOV
939
        return max(np.math.factorial(self.N) // 2, 1)
×
940

941
    def __contains__(self, perm: Cycles):
9✔
942
        return perm in SymmetricGroup(self.N) and perm.signature == 1
×
943

944

945
def _strip(h: Cycles, base, orbits, transversals, j):
9✔
946
    """
947
    """
948
    base_len = len(base)
9✔
949
    for i in range(j + 1, base_len):
9✔
950
        beta = h._replace(base[i])
9✔
951
        if beta == base[i]:
9✔
952
            continue
9✔
953
        if beta not in orbits[i]:
9✔
954
            return h, i + 1
9✔
955
        u = transversals[i][beta]
9✔
956
        if h == u:
9✔
957
            return None, base_len + 1
9✔
958
        h = h * u.inv()
9✔
959
    return h, base_len + 1
9✔
960

961

962
def _orbit_transversal(
9✔
963
    generators: list[Cycles],
964
    alpha: int,
965
    Identity: Cycles = Cycles(),
966
) -> tuple[list[tuple[int, Cycles]], dict[int, list[int]]]:
967
    r"""Computes a transversal for the orbit of ``alpha`` as a set.
968

969
    Explanation
970
    ===========
971

972
    generators   generators of the group ``G``
973

974
    For a permutation group ``G``, a transversal for the orbit
975
    `Orb = \{g(\alpha) | g \in G\}` is a set
976
    `\{g_\beta | g_\beta(\alpha) = \beta\}` for `\beta \in Orb`.
977
    Note that there may be more than one possible transversal.
978
    """
979
    tr = [(alpha, Identity)]
9✔
980
    db = {alpha}
9✔
981
    for x, px in tr:
9✔
982
        for i, gen in enumerate(generators):
9✔
983
            temp = gen._replace(x)
9✔
984
            if temp not in db:
9✔
985
                db.add(temp)
9✔
986
                tr.append((temp, px * gen))
9✔
987

988
    return dict(tr)
9✔
989

990

991
def _distribute_gens_by_base(base: list,
9✔
992
                             gens: list[Cycles]) -> list[list[Cycles]]:
993
    r"""
994
    Distribute the group elements ``gens`` by membership in basic stabilizers.
995

996
    Explanation
997
    ===========
998

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

1003
    Parameters
1004
    ==========
1005

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

1009
    Returns
1010
    =======
1011
    list
1012
        List of length `k`, where `k` is the length of *base*. The `i`-th entry
1013
        contains those elements in *gens* which fix the first `i` elements of
1014
        *base* (so that the `0`-th entry is equal to *gens* itself). If no
1015
        element fixes the first `i` elements of *base*, the `i`-th element is
1016
        set to a list containing the identity element.
1017
    """
1018
    base_len = len(base)
9✔
1019
    stabs = [[] for _ in range(base_len)]
9✔
1020
    max_stab_index = 0
9✔
1021
    for gen in gens:
9✔
1022
        j = 0
9✔
1023
        while j < base_len - 1 and gen._replace(base[j]) == base[j]:
9✔
1024
            j += 1
9✔
1025
        if j > max_stab_index:
9✔
1026
            max_stab_index = j
9✔
1027
        for k in range(j + 1):
9✔
1028
            stabs[k].append(gen)
9✔
1029
    for i in range(max_stab_index + 1, base_len):
9✔
UNCOV
1030
        stabs[i].append(Cycles())
×
1031
    return stabs
9✔
1032

1033

1034
def schreier_sims_incremental(
9✔
1035
    gens: list[Cycles],
1036
    base: list[int] | None = None
1037
) -> tuple[list[int], list[Cycles], dict[int, list[int]]]:
1038
    """Extend a sequence of points and generating set to a base and strong
1039
    generating set.
1040

1041
    Parameters
1042
    ==========
1043
    gens
1044
        The generating set to be extended to a strong generating set
1045
        relative to the base obtained.
1046

1047
    base
1048
        The sequence of points to be extended to a base. Optional
1049
        parameter with default value ``[]``.
1050

1051
    Returns
1052
    =======
1053

1054
    (base, strong_gens)
1055
        ``base`` is the base obtained, and ``strong_gens`` is the strong
1056
        generating set relative to it. The original parameters ``base``,
1057
        ``gens`` remain unchanged.
1058
    """
1059
    if base is None:
9✔
1060
        base = []
9✔
1061
    else:
UNCOV
1062
        base = base.copy()
×
1063
    support = set()
9✔
1064
    for g in gens:
9✔
1065
        support.update(g.support)
9✔
1066
    # handle the trivial group
1067
    if len(gens) == 1 and gens[0].is_identity():
9✔
1068
        return base, gens, {gens[0]: [gens[0]]}
×
1069
    # remove the identity as a generator
1070
    gens = [x for x in gens if not x.is_identity()]
9✔
1071
    # make sure no generator fixes all base points
1072
    for gen in gens:
9✔
1073
        if all(x == gen._replace(x) for x in base):
9✔
1074
            for new in support:
9✔
1075
                if gen._replace(new) != new:
9✔
1076
                    break
9✔
1077
            else:
UNCOV
1078
                assert None  # can this ever happen?
×
1079
            base.append(new)
9✔
1080
    #logger.debug("Schreier-Sims: base = %s, gens = %s", _base, _gens)
1081
    # distribute generators according to basic stabilizers
1082
    strong_gens_distr = _distribute_gens_by_base(base, gens)
9✔
1083
    new_strong_gens = []
9✔
1084
    # initialize the basic stabilizers, basic orbits and basic transversals
1085
    orbs = {}
9✔
1086
    transversals = {}
9✔
1087
    for i, alpha in enumerate(base):
9✔
1088
        transversals[i] = _orbit_transversal(strong_gens_distr[i], alpha)
9✔
1089
        orbs[i] = list(transversals[i].keys())
9✔
1090
    # main loop: amend the stabilizer chain until we have generators
1091
    # for all stabilizers
1092
    base_len = len(base)
9✔
1093
    i = base_len - 1
9✔
1094
    while i >= 0:
9✔
1095
        # this flag is used to continue with the main loop from inside
1096
        # a nested loop
1097
        continue_i = False
9✔
1098
        # test the generators for being a strong generating set
1099
        db = {}
9✔
1100
        for beta, u_beta in list(transversals[i].items()):
9✔
1101
            for j, gen in enumerate(strong_gens_distr[i]):
9✔
1102
                gb = gen._replace(beta)
9✔
1103
                u1 = transversals[i][gb]
9✔
1104
                g1 = u_beta * gen
9✔
1105
                if g1 != u1:
9✔
1106
                    # test if the schreier generator is in the i+1-th
1107
                    # would-be basic stabilizer
1108
                    new_strong_generator_found = False
9✔
1109
                    try:
9✔
1110
                        u1_inv = db[gb]
9✔
1111
                    except KeyError:
9✔
1112
                        u1_inv = db[gb] = u1.inv()
9✔
1113
                    schreier_gen = g1 * u1_inv
9✔
1114
                    h, j = _strip(schreier_gen, base, orbs, transversals, i)
9✔
1115
                    if j <= base_len:
9✔
1116
                        # new strong generator h at level j
1117
                        new_strong_generator_found = True
9✔
1118
                    elif h is not None:
9✔
1119
                        # h fixes all base points
1120
                        new_strong_generator_found = True
9✔
1121
                        for moved in support:
9✔
1122
                            if h._replace(moved) != moved:
9✔
1123
                                break
9✔
1124
                        base.append(moved)
9✔
1125
                        base_len += 1
9✔
1126
                        strong_gens_distr.append([])
9✔
1127
                    if new_strong_generator_found:
9✔
1128
                        # if a new strong generator is found, update the
1129
                        # data structures and start over
1130
                        new_strong_gens.append(h)
9✔
1131
                        for l in range(i + 1, j):
9✔
1132
                            strong_gens_distr[l].append(h)
9✔
1133
                            transversals[l] =\
9✔
1134
                            _orbit_transversal(strong_gens_distr[l],
1135
                                base[l])
1136
                            orbs[l] = list(transversals[l].keys())
9✔
1137
                        i = j - 1
9✔
1138
                        # continue main loop using the flag
1139
                        continue_i = True
9✔
1140
                if continue_i is True:
9✔
1141
                    break
9✔
1142
            if continue_i is True:
9✔
1143
                break
9✔
1144
        logger.debug(
9✔
1145
            "Schreier-Sims: i = %s, continue_i = %s, len(transversals[i]) = %s",
1146
            i, continue_i, len(transversals[i]))
1147
        if continue_i is True:
9✔
1148
            continue
9✔
1149
        i -= 1
9✔
1150

1151
    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