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

feihoo87 / waveforms / 7087368175

04 Dec 2023 01:33PM UTC coverage: 43.134% (-0.5%) from 43.641%
7087368175

push

github

feihoo87
fix workflow

7413 of 17186 relevant lines covered (43.13%)

2.58 hits per line

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

67.22
/waveforms/math/group/permutation_group.py
1
from __future__ import annotations
6✔
2

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

12
import numpy as np
6✔
13

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

16

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

20

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

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

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

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

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

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

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

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

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

65
    def __mul__(self, other: Cycles) -> Cycles:
6✔
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)
6✔
72
        mapping = {
6✔
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)
6✔
78
        c._expr = (self, other)
6✔
79
        return c
6✔
80

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

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

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

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

105
        return c
6✔
106

107
    def __pow__(self, n: int) -> Cycles:
6✔
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):
6✔
123
        return self.inv()
×
124

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

137
    @property
6✔
138
    def order(self):
6✔
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:
6✔
145
            self._order = math.lcm(*[len(cycle) for cycle in self._cycles])
6✔
146
        return self._order
6✔
147

148
    @property
6✔
149
    def support(self):
6✔
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
6✔
156

157
    @property
6✔
158
    def signature(self):
6✔
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
        """
165
        return 1 - 2 * ((len(self._support) - len(self._cycles)) % 2)
×
166

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

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

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

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

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

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

195
    def commutator(self, x: Cycles) -> Cycles:
6✔
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
        """
203
        return self * x * self.inv() * x.inv()
×
204

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

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

236
        return self
6✔
237

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

243

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

264

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

273

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

286

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

308

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

325

326
class PermutationGroup():
6✔
327

328
    def __init__(self, generators: list[Cycles]):
6✔
329
        self.generators = generators
6✔
330
        self._elements = []
6✔
331
        self._support = None
6✔
332

333
        self._order = None
6✔
334
        self._orbits = None
6✔
335
        self._center = []
6✔
336
        self._is_abelian = None
6✔
337
        self._is_trivial = None
6✔
338

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

346
    def __repr__(self) -> str:
6✔
347
        return f"PermutationGroup({self.generators})"
×
348

349
    def is_trivial(self):
6✔
350
        """Test if the group is the trivial group.
351

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

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

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

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

381
    def generate(self, method: str = "schreier_sims"):
6✔
382
        if method == "schreier_sims":
6✔
383
            yield from self.generate_schreier_sims()
6✔
384
        elif method == "dimino":
×
385
            yield from self.generate_dimino(self.generators)
×
386

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

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

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

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

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

437
    def random(self, N=1, rng: random.Random = None) -> Cycles | list[Cycles]:
6✔
438
        """Return a random element of the group.
439

440
        If N > 1, return a list of N random elements.
441

442
        Parameters
443
        ==========
444
        N : int
445
            Number of random elements to return.
446
        rng : random.Random
447
            Random number generator to use. If None, the default RNG is used.
448
        """
449
        self.schreier_sims()
6✔
450
        transversals = self._transversals
6✔
451
        orbits = self._basic_orbits
6✔
452

453
        if rng is None:
6✔
454
            rng = random.Random()
6✔
455
        ret = []
6✔
456
        for _ in range(N):
6✔
457
            g = Cycles()
6✔
458
            for orbit, coset in zip(orbits, transversals):
6✔
459
                g *= coset[rng.choice(orbit)]
6✔
460
            ret.append(g)
6✔
461
        if N == 1:
6✔
462
            return ret[0]
6✔
463
        return ret
×
464

465
    @property
6✔
466
    def base(self):
6✔
467
        """Return a base from the Schreier-Sims algorithm."""
468
        if self._base == []:
×
469
            self.schreier_sims()
×
470
        return self._base
×
471

472
    def orbit(
6✔
473
        self,
474
        alpha: TypeVar['T'],
475
        action: Callable[[TypeVar['T'], Cycles], TypeVar['T']] | None = None
476
    ) -> list[TypeVar['T']]:
477
        """finds the orbit under the group action given by a function `action`
478
        """
479
        if isinstance(alpha, int) and action is None:
6✔
480
            for orbit in self.orbits():
×
481
                if alpha in orbit:
×
482
                    return orbit
×
483
            else:
484
                return [alpha]
×
485
        elif isinstance(alpha, Cycles) and action is None:
6✔
486
            action = lambda x, y: y * x
×
487
        elif action is None:
6✔
488
            action = permute
6✔
489
        orbit = [alpha]
6✔
490
        for beta in orbit:
6✔
491
            for g in self.generators:
6✔
492
                beta = action(beta, g)
6✔
493
                if beta not in orbit:
6✔
494
                    orbit.append(beta)
6✔
495
        return orbit
6✔
496

497
    def orbits(self):
6✔
498
        if self._orbits is None:
×
499
            orbit_parts = []
×
500
            for g in self.generators:
×
501
                for cycle in g._cycles:
×
502
                    for orbit in orbit_parts:
×
503
                        if set(cycle) & set(orbit):
×
504
                            orbit.update(cycle)
×
505
                            break
×
506
                    else:
507
                        orbit_parts.append(set(cycle))
×
508
            orbits = []
×
509
            for x in orbit_parts:
×
510
                for y in orbits:
×
511
                    if x & y:
×
512
                        y.update(x)
×
513
                        break
×
514
                else:
515
                    orbits.append(x)
×
516
            self._orbits = orbits
×
517
        return self._orbits
×
518

519
    def schreier_sims(self, base: list[int] | None = None):
6✔
520
        """Schreier-Sims algorithm.
521

522
        Explanation
523
        ===========
524

525
        It computes the generators of the chain of stabilizers
526
        `G > G_{b_1} > .. > G_{b1,..,b_r} > 1`
527
        in which `G_{b_1,..,b_i}` stabilizes `b_1,..,b_i`,
528
        and the corresponding ``s`` cosets.
529
        An element of the group can be written as the product
530
        `h_1*..*h_s`.
531

532
        We use the incremental Schreier-Sims algorithm.
533
        """
534
        if self._transversals and (base is None or base == self._base):
6✔
535
            return
6✔
536

537
        base, strong_gens = schreier_sims_incremental(self.generators,
6✔
538
                                                      base=base)
539
        self._base = base
6✔
540
        self._strong_gens = strong_gens
6✔
541
        if not base:
6✔
542
            self._transversals = []
×
543
            self._basic_orbits = []
×
544
            return
×
545

546
        strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
6✔
547

548
        # Compute basic orbits and transversals from a base and strong generating set.
549
        transversals = []
6✔
550
        basic_orbits = []
6✔
551
        for alpha, gens in zip(base, strong_gens_distr):
6✔
552
            transversal = _orbit_transversal(gens, alpha)
6✔
553
            basic_orbits.append(list(transversal.keys()))
6✔
554
            transversals.append(transversal)
6✔
555

556
        self._transversals = transversals
6✔
557
        self._basic_orbits = [sorted(x) for x in basic_orbits]
6✔
558
        self._shape = tuple(
6✔
559
            [len(coset.values()) for coset in reversed(self._transversals)])
560

561
    def order(self):
6✔
562
        if self._order is None:
6✔
563
            if self.is_trivial():
6✔
564
                self._order = 1
×
565
            else:
566
                self.schreier_sims()
6✔
567
                self._order = math.prod(len(x) for x in self._transversals)
6✔
568
        return self._order
6✔
569

570
    def index(self, H: PermutationGroup):
6✔
571
        """
572
        Returns the index of a permutation group.
573

574
        Examples
575
        ========
576

577
        >>> a = Permutation(1,2,3)
578
        >>> b =Permutation(3)
579
        >>> G = PermutationGroup([a])
580
        >>> H = PermutationGroup([b])
581
        >>> G.index(H)
582
        3
583

584
        """
585
        if H.is_subgroup(self):
×
586
            return self.order() // H.order()
×
587

588
    def __len__(self):
6✔
589
        return self.order()
×
590

591
    def __getitem__(self, i):
6✔
592
        index = np.unravel_index(i, self._shape)
×
593
        gens = [
×
594
            list(coset.values())[i]
595
            for i, coset in zip(index, reversed(self._transversals))
596
        ]
597
        return functools.reduce(operator.mul, gens)
×
598

599
    def __contains__(self, perm: Cycles):
6✔
600
        if perm in self.generators or perm.is_identity():
6✔
601
            return True
×
602
        if self._elements:
6✔
603
            return perm in self._elements
×
604
        try:
6✔
605
            perm = self.coset_factor(perm)
6✔
606
            return True
6✔
607
        except _NotContained:
6✔
608
            return False
6✔
609

610
    def __eq__(self, other) -> bool:
6✔
611
        """Return ``True`` if PermutationGroup generated by elements in the
612
        group are same i.e they represent the same PermutationGroup.
613
        """
614
        if not isinstance(other, PermutationGroup):
6✔
615
            raise TypeError(
×
616
                f"'==' not supported between instances of '{type(self)}' and '{type(other)}'"
617
            )
618

619
        set_self_gens = set(self.generators)
6✔
620
        set_other_gens = set(other.generators)
6✔
621

622
        # before reaching the general case there are also certain
623
        # optimisation and obvious cases requiring less or no actual
624
        # computation.
625
        if set_self_gens == set_other_gens:
6✔
626
            return True
6✔
627

628
        # in the most general case it will check that each generator of
629
        # one group belongs to the other PermutationGroup and vice-versa
630
        for gen1 in set_self_gens:
6✔
631
            if gen1 not in other:
6✔
632
                return False
6✔
633
        for gen2 in set_other_gens:
×
634
            if gen2 not in self:
×
635
                return False
×
636
        return True
×
637

638
    def __lt__(self, other) -> bool:
6✔
639
        if isinstance(other, PermutationGroup):
6✔
640
            return self.is_subgroup(other) and self.order() < other.order()
6✔
641
        else:
642
            raise TypeError(
×
643
                f"'<' not supported between instances of '{type(self)}' and '{type(other)}'"
644
            )
645

646
    def __le__(self, other) -> bool:
6✔
647
        if isinstance(other, PermutationGroup):
6✔
648
            return self.is_subgroup(other)
6✔
649
        else:
650
            raise TypeError(
×
651
                f"'<=' not supported between instances of '{type(self)}' and '{type(other)}'"
652
            )
653

654
    def __mul__(self, other: Cycles):
6✔
655
        if other in self:
×
656
            return self
×
657
        return Coset(self, other, left=False)
×
658

659
    def __rmul__(self, other: Cycles):
6✔
660
        if other in self:
×
661
            return self
×
662
        return Coset(self, other, left=True)
×
663

664
    def coset_factor(self, g: Cycles, index=False):
6✔
665
        """Return ``G``'s (self's) coset factorization of ``g``
666

667
        Explanation
668
        ===========
669

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

673
        The permutations returned in ``f`` are those for which
674
        the product gives ``g``: ``g = f[n]*...f[1]*f[0]`` where ``n = len(B)``
675
        and ``B = G.base``. f[i] is one of the permutations in
676
        ``self._basic_orbits[i]``.
677
        """
678
        self.schreier_sims()
6✔
679
        factors = []
6✔
680
        for alpha, coset, orbit in zip(self._base, self._transversals,
6✔
681
                                       self._basic_orbits):
682
            beta = g._replace(alpha)
6✔
683
            if beta == alpha:
6✔
684
                if index:
6✔
685
                    factors.append(0)
6✔
686
                continue
6✔
687
            if beta not in coset:
6✔
688
                raise _NotContained
6✔
689
            u = coset[beta]
6✔
690
            if index:
6✔
691
                factors.append(orbit.index(beta))
6✔
692
            else:
693
                factors.append(u)
6✔
694
            g = g * u.inv()
6✔
695
            if g.is_identity():
6✔
696
                break
6✔
697
        if not g.is_identity():
6✔
698
            raise _NotContained
6✔
699
        return factors
6✔
700

701
    def coset_rank(self, g):
6✔
702
        """rank using Schreier-Sims representation.
703

704
        Explanation
705
        ===========
706

707
        The coset rank of ``g`` is the ordering number in which
708
        it appears in the lexicographic listing according to the
709
        coset decomposition
710

711
        The ordering is the same as in G.generate(method='coset').
712
        If ``g`` does not belong to the group it returns None.
713
        """
714
        try:
6✔
715
            index = self.coset_factor(g, index=True)
6✔
716
            index = index + [0] * (len(self._transversals) - len(index))
6✔
717
        except _NotContained:
×
718
            raise IndexError(f"Permutation {g} not contained in group.")
×
719
        rank = 0
6✔
720
        b = 1
6✔
721
        for i, coset in zip(index, self._transversals):
6✔
722
            rank += b * i
6✔
723
            b = b * len(coset)
6✔
724
        return rank
6✔
725

726
    def coset_unrank(self, rank):
6✔
727
        """unrank using Schreier-Sims representation
728

729
        coset_unrank is the inverse operation of coset_rank
730
        if 0 <= rank < order; otherwise it returns None.
731

732
        """
733
        if rank < 0 or rank >= self.order():
6✔
734
            return None
×
735
        transversals = self._transversals
6✔
736
        orbits = self._basic_orbits
6✔
737
        ret = Cycles()
6✔
738
        for orbit, coset in zip(orbits, transversals):
6✔
739
            rank, c = divmod(rank, len(coset))
6✔
740
            ret = coset[orbit[c]] * ret
6✔
741
        return ret
6✔
742

743
    def express(self, perm: Cycles):
6✔
744
        if perm.is_identity():
6✔
745
            return Cycles()
6✔
746
        self.schreier_sims()
6✔
747
        return functools.reduce(operator.mul, self.coset_factor(perm)[::-1])
6✔
748

749
    def stabilizer_chain(self) -> list[tuple[tuple[int], PermutationGroup]]:
6✔
750
        r"""
751
        Return a chain of stabilizers relative to a base and strong generating
752
        set.
753

754
        Explanation
755
        ===========
756

757
        The ``i``-th basic stabilizer `G^{(i)}` relative to a base
758
        `(b_1, b_2, \dots, b_k)` is `G_{b_1, b_2, \dots, b_{i-1}}`.
759
        """
760
        self.schreier_sims()
×
761
        strong_gens = self._strong_gens
×
762
        base = self._base
×
763
        if not base:  # e.g. if self is trivial
×
764
            return []
×
765
        strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
×
766
        basic_stabilizers = []
×
767
        for i, gens in enumerate(strong_gens_distr):
×
768
            basic_stabilizers.append((tuple(base[:i]), PermutationGroup(gens)))
×
769
        basic_stabilizers.append((tuple(base), PermutationGroup([])))
×
770
        return basic_stabilizers
×
771

772
    def stabilizer(self, alpha) -> PermutationGroup:
6✔
773
        """Return the stabilizer subgroup of ``alpha``."""
774
        orb = [alpha]
×
775
        table = {alpha: Cycles()}
×
776
        table_inv = {alpha: Cycles()}
×
777
        used = {}
×
778
        used[alpha] = True
×
779
        stab_gens = []
×
780
        for b in orb:
×
781
            for gen in self.generators:
×
782
                temp = gen[b]
×
783
                if temp not in used:
×
784
                    gen_temp = table[b] * gen
×
785
                    orb.append(temp)
×
786
                    table[temp] = gen_temp
×
787
                    table_inv[temp] = gen_temp.inv()
×
788
                    used[temp] = True
×
789
                else:
790
                    schreier_gen = table[b] * gen * table_inv[temp]
×
791
                    if schreier_gen not in stab_gens:
×
792
                        stab_gens.append(schreier_gen)
×
793
        return PermutationGroup(stab_gens)
×
794

795
    def centralizer(self, H: PermutationGroup) -> PermutationGroup:
6✔
796
        """Return the centralizer of ``H`` in ``self``."""
797
        raise NotImplementedError
×
798

799
    def normalizer(self, H: PermutationGroup) -> PermutationGroup:
6✔
800
        """Return the normalizer of ``H`` in ``self``."""
801
        raise NotImplementedError
×
802

803
    def center(self) -> PermutationGroup:
6✔
804
        """Return the center of group."""
805
        return self.centralizer(self)
×
806

807

808
class Coset():
6✔
809

810
    def __init__(self, H: PermutationGroup, g: Cycles, left: bool = True):
6✔
811
        self._left = left
×
812
        self._norm = True
×
813
        self.H = H
×
814
        self.g = g
×
815
        for gen in self.H.generators:
×
816
            if gen * self.g not in self.g * self.H:
×
817
                self._norm = False
×
818
                break
×
819

820
    def is_left_coset(self):
6✔
821
        return self._left
×
822

823
    def is_right_coset(self):
6✔
824
        return not self._left
×
825

826
    def __contains__(self, perm: Cycles):
6✔
827
        if self._left:
×
828
            return self.g * perm in self.H
×
829
        else:
830
            return perm * self.g in self.H
×
831

832
    def generate(self):
6✔
833
        if self._left:
×
834
            for perm in self.H.generate():
×
835
                yield self.g * perm
×
836
        else:
837
            for perm in self.H.generate():
×
838
                yield perm * self.g
×
839

840
    def __mul__(self, other: PermutationGroup | Coset) -> Coset:
6✔
841
        if isinstance(other, PermutationGroup) and other == self.H:
×
842
            return self
×
843
        elif isinstance(other, Coset) and other.H == self.H:
×
844
            return Coset(self.H, self.g * other.g, self._left)
×
845
        else:
846
            raise TypeError(f"Cannot multiply {self} by {other}")
×
847

848

849
class SymmetricGroup(PermutationGroup):
6✔
850

851
    def __init__(self, N: int):
6✔
852
        if N < 2:
6✔
853
            super().__init__([])
×
854
        elif N == 2:
6✔
855
            super().__init__([Cycles((0, 1))])
×
856
        else:
857
            super().__init__([Cycles((0, 1)), Cycles(tuple(range(N)))])
6✔
858
        self.N = N
6✔
859

860
    def __repr__(self) -> str:
6✔
861
        return f"SymmetricGroup({self.N})"
×
862

863
    def __len__(self):
6✔
864
        return np.math.factorial(self.N)
×
865

866
    def __contains__(self, perm: Cycles):
6✔
867
        return set(perm.support) <= set(range(self.N))
6✔
868

869

870
class CyclicGroup(PermutationGroup):
6✔
871

872
    def __init__(self, N: int):
6✔
873
        if N < 2:
×
874
            super().__init__([])
×
875
        else:
876
            super().__init__([Cycles(tuple(range(N)))])
×
877
        self.N = N
×
878

879
    def __repr__(self) -> str:
6✔
880
        return f"CyclicGroup({self.N})"
×
881

882
    def __len__(self):
6✔
883
        return max(self.N, 1)
×
884

885

886
class DihedralGroup(PermutationGroup):
6✔
887

888
    def __init__(self, N: int):
6✔
889
        if N < 2:
×
890
            generators = []
×
891
        elif N == 2:
×
892
            generators = [Cycles((0, 1))]
×
893
        else:
894
            generators = [
×
895
                Cycles(tuple(range(N))),
896
                Cycles(*[(i + N % 2, N - 1 - i) for i in range(N // 2)])
897
            ]
898
        super().__init__(generators)
×
899
        self.N = N
×
900

901
    def __repr__(self) -> str:
6✔
902
        return f"DihedralGroup({self.N})"
×
903

904
    def __len__(self):
6✔
905
        if self.N == 1:
×
906
            return 1
×
907
        elif self.N == 2:
×
908
            return 2
×
909
        return max(2 * self.N, 1)
×
910

911

912
class AbelianGroup(PermutationGroup):
6✔
913

914
    def __init__(self, *n: int):
6✔
915
        self.n = tuple(sorted(n))
×
916
        generators = []
×
917
        start = 0
×
918
        for ni in self.n:
×
919
            if ni >= 2:
×
920
                generators.append(Cycles(tuple(range(start, start + ni))))
×
921
                start += ni
×
922
        super().__init__(generators)
×
923

924
    def __repr__(self) -> str:
6✔
925
        return f"AbelianGroup{self.n}"
×
926

927
    def __len__(self):
6✔
928
        return max(np.multiply.reduce(self.n), 1)
×
929

930

931
class AlternatingGroup(PermutationGroup):
6✔
932

933
    def __init__(self, N: int):
6✔
934
        if N <= 2:
×
935
            generators = []
×
936
        elif N == 3:
×
937
            generators = [Cycles((0, 1, 2))]
×
938
        else:
939
            generators = [
×
940
                Cycles((0, 1, 2)),
941
                Cycles(tuple(range(N))) if N %
942
                2 else Cycles(tuple(range(1, N)))
943
            ]
944
        super().__init__(generators)
×
945
        self.N = N
×
946

947
    def __repr__(self) -> str:
6✔
948
        return f"AlternatingGroup({self.N})"
×
949

950
    def __len__(self):
6✔
951
        return max(np.math.factorial(self.N) // 2, 1)
×
952

953
    def __contains__(self, perm: Cycles):
6✔
954
        return perm in SymmetricGroup(self.N) and perm.signature == 1
×
955

956

957
def _strip(h: Cycles, base, orbits, transversals, j):
6✔
958
    """
959
    """
960
    base_len = len(base)
6✔
961
    for i in range(j + 1, base_len):
6✔
962
        beta = h._replace(base[i])
6✔
963
        if beta == base[i]:
6✔
964
            continue
6✔
965
        if beta not in orbits[i]:
6✔
966
            return h, i + 1
6✔
967
        u = transversals[i][beta]
6✔
968
        if h == u:
6✔
969
            return None, base_len + 1
6✔
970
        h = h * u.inv()
6✔
971
    return h, base_len + 1
6✔
972

973

974
def _orbit_transversal(
6✔
975
    generators: list[Cycles],
976
    alpha: int,
977
    Identity: Cycles = Cycles(),
978
) -> tuple[list[tuple[int, Cycles]], dict[int, list[int]]]:
979
    r"""Computes a transversal for the orbit of ``alpha`` as a set.
980

981
    Explanation
982
    ===========
983

984
    generators   generators of the group ``G``
985

986
    For a permutation group ``G``, a transversal for the orbit
987
    `Orb = \{g(\alpha) | g \in G\}` is a set
988
    `\{g_\beta | g_\beta(\alpha) = \beta\}` for `\beta \in Orb`.
989
    Note that there may be more than one possible transversal.
990
    """
991
    tr = [(alpha, Identity)]
6✔
992
    db = {alpha}
6✔
993
    for x, px in tr:
6✔
994
        for i, gen in enumerate(generators):
6✔
995
            temp = gen._replace(x)
6✔
996
            if temp not in db:
6✔
997
                db.add(temp)
6✔
998
                tr.append((temp, px * gen))
6✔
999

1000
    return dict(tr)
6✔
1001

1002

1003
def _distribute_gens_by_base(base: list,
6✔
1004
                             gens: list[Cycles]) -> list[list[Cycles]]:
1005
    r"""
1006
    Distribute the group elements ``gens`` by membership in basic stabilizers.
1007

1008
    Explanation
1009
    ===========
1010

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

1015
    Parameters
1016
    ==========
1017

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

1021
    Returns
1022
    =======
1023
    list
1024
        List of length `k`, where `k` is the length of *base*. The `i`-th entry
1025
        contains those elements in *gens* which fix the first `i` elements of
1026
        *base* (so that the `0`-th entry is equal to *gens* itself). If no
1027
        element fixes the first `i` elements of *base*, the `i`-th element is
1028
        set to a list containing the identity element.
1029
    """
1030
    base_len = len(base)
6✔
1031
    stabs = [[] for _ in range(base_len)]
6✔
1032
    max_stab_index = 0
6✔
1033
    for gen in gens:
6✔
1034
        j = 0
6✔
1035
        while j < base_len - 1 and gen._replace(base[j]) == base[j]:
6✔
1036
            j += 1
6✔
1037
        if j > max_stab_index:
6✔
1038
            max_stab_index = j
6✔
1039
        for k in range(j + 1):
6✔
1040
            stabs[k].append(gen)
6✔
1041
    for i in range(max_stab_index + 1, base_len):
6✔
1042
        stabs[i].append(Cycles())
×
1043
    return stabs
6✔
1044

1045

1046
def schreier_sims_incremental(
6✔
1047
    gens: list[Cycles],
1048
    base: list[int] | None = None
1049
) -> tuple[list[int], list[Cycles], dict[int, list[int]]]:
1050
    """Extend a sequence of points and generating set to a base and strong
1051
    generating set.
1052

1053
    Parameters
1054
    ==========
1055
    gens
1056
        The generating set to be extended to a strong generating set
1057
        relative to the base obtained.
1058

1059
    base
1060
        The sequence of points to be extended to a base. Optional
1061
        parameter with default value ``[]``.
1062

1063
    Returns
1064
    =======
1065

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

1163
    return (base, gens + new_strong_gens)
6✔
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