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

feihoo87 / waveforms / 6902226398

17 Nov 2023 09:31AM UTC coverage: 43.344% (+0.2%) from 43.147%
6902226398

push

github

feihoo87
upgrade make_clifford_generators

24 of 24 new or added lines in 1 file covered. (100.0%)

110 existing lines in 4 files now uncovered.

7472 of 17239 relevant lines covered (43.34%)

3.89 hits per line

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

67.13
/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✔
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✔
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✔
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
        """
165
        return 1 - 2 * ((len(self._support) - len(self._cycles)) % 2)
×
166

167
    def __len__(self):
9✔
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."""
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):
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✔
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
        """
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
            elif 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✔
254
        return tuple(ret)
×
255
    elif isinstance(expr, str):
9✔
256
        return ''.join(ret)
9✔
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):
9✔
266
    if isinstance(a, np.ndarray):
9✔
267
        if isinstance(b, np.ndarray):
9✔
268
            return not np.allclose(a, b)
9✔
269
        return True
×
270
    else:
271
        return a != b
9✔
272

273

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

286

287
def find_permutation(expr1: list, expr2: list) -> Cycles:
9✔
288
    """find the permutation that transform expr1 to expr2"""
289
    if len(expr1) != len(expr2):
9✔
UNCOV
290
        raise ValueError("expr1 and expr2 must have the same length")
×
291
    codes = {}
9✔
292
    support = []
9✔
293
    perm = []
9✔
294
    for i, (a, b) in enumerate(zip(expr1, expr2)):
9✔
295
        if type(a) != type(b) or _ne(a, b):
9✔
296
            perm.append(b)
9✔
297
            support.append(i)
9✔
298
            codes[i] = a
9✔
299
    if not support:
9✔
300
        return Cycles()
9✔
301
    mapping = {
9✔
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)
9✔
307

308

309
def random_permutation(n: int) -> Cycles:
9✔
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:
×
UNCOV
322
            cycles.append(tuple(cycle))
×
UNCOV
323
    return Cycles(*cycles)
×
324

325

326
class PermutationGroup():
9✔
327

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

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

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

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

349
    def is_trivial(self):
9✔
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:
9✔
355
            self._is_trivial = len(self.generators) == 0
9✔
356
        return self._is_trivial
9✔
357

358
    def is_abelian(self):
9✔
359
        """Test if the group is Abelian.
360
        """
UNCOV
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
×
UNCOV
368
                break
×
UNCOV
369
        return True
×
370

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

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

387
    @staticmethod
9✔
388
    def generate_dimino(generators: list[Cycles]):
9✔
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:
×
UNCOV
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:
×
UNCOV
404
                break
×
UNCOV
405
            elements = new_elements
×
406

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

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

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

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

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

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

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

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

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

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

512
        Explanation
513
        ===========
514

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

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

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

536
        strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
9✔
537

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

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

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

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

564
        Examples
565
        ========
566

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

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

578
    def __len__(self):
9✔
UNCOV
579
        return self.order()
×
580

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

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

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

609
        set_self_gens = set(self.generators)
9✔
610
        set_other_gens = set(other.generators)
9✔
611

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

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

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

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

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

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

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

657
        Explanation
658
        ===========
659

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

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

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

694
        Explanation
695
        ===========
696

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

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

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

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

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

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

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

744
        Explanation
745
        ===========
746

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

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

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

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

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

797

798
class Coset():
9✔
799

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

810
    def is_left_coset(self):
9✔
UNCOV
811
        return self._left
×
812

813
    def is_right_coset(self):
9✔
UNCOV
814
        return not self._left
×
815

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

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

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

838

839
class SymmetricGroup(PermutationGroup):
9✔
840

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

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

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

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

859

860
class CyclicGroup(PermutationGroup):
9✔
861

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

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

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

875

876
class DihedralGroup(PermutationGroup):
9✔
877

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

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

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

901

902
class AbelianGroup(PermutationGroup):
9✔
903

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

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

917
    def __len__(self):
9✔
UNCOV
918
        return max(np.multiply.reduce(self.n), 1)
×
919

920

921
class AlternatingGroup(PermutationGroup):
9✔
922

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

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

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

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

946

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

963

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

971
    Explanation
972
    ===========
973

974
    generators   generators of the group ``G``
975

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

990
    return dict(tr)
9✔
991

992

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

998
    Explanation
999
    ===========
1000

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

1005
    Parameters
1006
    ==========
1007

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

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

1035

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

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

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

1053
    Returns
1054
    =======
1055

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

1153
    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