GroupsMath 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
groupsmath/core.py ADDED
@@ -0,0 +1,1176 @@
1
+ # ———————————————————————————————————————————————————————————— #
2
+ # #
3
+ # GroupsMath #
4
+ # A Python library for finite group theory #
5
+ # #
6
+ # by: Mario Sultan Romero #
7
+ # #
8
+ # ———————————————————————————————————————————————————————————— #
9
+
10
+
11
+ #################### IMPORTS ####################
12
+
13
+ import numpy as np
14
+ import matplotlib.pyplot as plt
15
+ import matplotlib.colors as mcolors
16
+ from itertools import permutations, combinations, product
17
+ from collections import Counter
18
+ from math import gcd
19
+ from abc import abstractmethod, ABC
20
+
21
+
22
+ #################### DEFINITIONS ####################
23
+
24
+ __version__ = "0.3.0"
25
+
26
+ tgl_color = "#2A646E"
27
+ white = mcolors.LinearSegmentedColormap.from_list("white", ["white", "white"])
28
+ tgl = mcolors.LinearSegmentedColormap.from_list("tgl", ["white", tgl_color])
29
+ rainbow = mcolors.LinearSegmentedColormap.from_list("rainbow", ["#FF0000","#FF9100","#F2DA00","#2CDB00","#00DAE9","#1869FF"])#,"#7648FF","#D12BFF"])
30
+ rainbow8 = mcolors.LinearSegmentedColormap.from_list("rainbow8", ["#FF0000","#FF9100","#F2DA00","#2CDB00","#00DAE9","#1869FF","#7648FF","#FF1FF4"])
31
+
32
+ def info():
33
+ print("GroupsMath v"+__version__)
34
+
35
+
36
+ #################### GROUP CLASS ####################
37
+
38
+ class Group(ABC):
39
+ @classmethod
40
+ @abstractmethod
41
+ def operation(self, a, b):
42
+ pass
43
+
44
+ @classmethod
45
+ @abstractmethod
46
+ def identity(self):
47
+ pass
48
+
49
+ @classmethod
50
+ @abstractmethod
51
+ def inverse(self, a):
52
+ pass
53
+
54
+ class CayleyGroup(Group):
55
+
56
+ def __init__(self, cayley, names=None, _skip_validation=False):
57
+
58
+ if not _skip_validation:
59
+ valid, message = _is_group(cayley)
60
+ if not valid:
61
+ raise ValueError(message)
62
+
63
+ if names is None:
64
+ names = list(range(len(cayley)))
65
+
66
+ if len(names) != len(cayley):
67
+ raise ValueError("The number of names must match the group order")
68
+
69
+ self.cayley = cayley
70
+ self.names = names
71
+
72
+ #–> DUNDERS
73
+
74
+ def __str__(self):
75
+ G = []
76
+ for i in range(self.order()):
77
+ g = []
78
+ for j in range(self.order()):
79
+ g.append(self.names[self.cayley[i][j]])
80
+ G.append(g)
81
+ s = str(G)
82
+ return f"{G}"
83
+
84
+ def __len__(self):
85
+ return self.order()
86
+
87
+ def __contains__(self, element):
88
+ return element in self.names
89
+
90
+ def __mul__(self, other):
91
+ return direct_product(self,other)
92
+
93
+ def __pow__(self, n):
94
+ return direct_power(self,n)
95
+
96
+ def __truediv__(self, subgroup):
97
+ return self.quotient(subgroup)
98
+
99
+ #–> GROUP METHODS
100
+
101
+ def operation(self, a, b):
102
+ return self.cayley[a][b]
103
+
104
+ def identity(self):
105
+ neutro = None
106
+ for e in range(self.order()):
107
+ ok = True
108
+ for a in range(self.order()):
109
+ if self.cayley[e][a] != a:
110
+ ok = False
111
+ break
112
+ if self.cayley[a][e] != a:
113
+ ok = False
114
+ break
115
+ if ok:
116
+ neutro = e
117
+ break
118
+ return neutro
119
+
120
+ def inverse(self, a):
121
+ e = self.identity()
122
+ inv = None
123
+ for i in range(len(self.cayley)):
124
+ if self.cayley[a][i]==e:
125
+ inv = i
126
+ break
127
+ return inv
128
+
129
+ #–> BASIC METHODS
130
+
131
+ def _print_group(self):
132
+ print(f"CayleyGroup({self.cayley},{self.names})")
133
+
134
+ def order(self):
135
+ return len(self.cayley)
136
+
137
+ def element_orders(self):
138
+ O = []
139
+ for i in range(len(self.cayley)):
140
+ ik = self.cayley[i][i]
141
+ o = 1
142
+ while i!=ik:
143
+ ik = self.cayley[ik][i]
144
+ o+=1
145
+ O.append(o)
146
+ return O
147
+
148
+ def order_distribution(self):
149
+ l = sorted(self.element_orders())
150
+ return dict(Counter(l))
151
+
152
+ def is_cyclic(self):
153
+ return self.order() in self.element_orders()
154
+
155
+ def center(self):
156
+ G = self.cayley
157
+ Z = []
158
+ for i in range(len(G)):
159
+ r = True
160
+ for j in range(len(G)):
161
+ if G[i][j]!=G[j][i]:
162
+ r = False
163
+ break
164
+ if r:
165
+ Z.append(i)
166
+ return(Z)
167
+
168
+ def is_abelian(self):
169
+ return self.order()==len(self.center())
170
+
171
+ def cayley_table(self, title="", colormap=rainbow, names=None):
172
+
173
+ if names==None:
174
+ if len(self.cayley)<=20:
175
+ names=True
176
+ else:
177
+ names=False
178
+
179
+
180
+ elements = range(len(self.cayley))
181
+ fig, ax = plt.subplots(figsize=(6, 5))
182
+ im = ax.imshow(self.cayley, cmap=colormap)
183
+ ax.xaxis.tick_top()
184
+
185
+ if names:
186
+ ax.set_xticks(np.arange(len(elements)))
187
+ ax.set_yticks(np.arange(len(elements)))
188
+ ax.set_xticklabels(self.names)
189
+ ax.set_yticklabels(self.names)
190
+ else:
191
+ ax.set_xticks([])
192
+ ax.set_yticks([])
193
+ ax.set_xticklabels([])
194
+ ax.set_yticklabels([])
195
+
196
+ if names:
197
+ for i in range(len(elements)):
198
+ for j in range(len(elements)):
199
+ color_texto = "black"
200
+ ax.text(
201
+ i,
202
+ j,
203
+ f"{self.names[self.cayley[j][i]]}",
204
+ ha="center",
205
+ va="center",
206
+ color=color_texto,
207
+ fontsize=12,
208
+ )
209
+
210
+ plt.title(title)
211
+ plt.tight_layout()
212
+ plt.show()
213
+
214
+ def delete_names(self):
215
+ self.names = [i for i in range(len(self.cayley))]
216
+
217
+ #–> SUBGROUPS
218
+
219
+ def proper_subgroups(self):
220
+ G = self.cayley
221
+ E = self.names
222
+ L = []
223
+
224
+ for idxs in _graded_power_set_with_id(G):
225
+ # 1. Comprobación rápida de clausura
226
+ if _is_closed_subset(G, idxs):
227
+ # 2. Construcción rápida omitiendo axiomas heredados
228
+ sub_names = [E[j] for j in idxs]
229
+ sub_matrix = _reset_renaming(_subset(G, idxs))
230
+
231
+ sub_grp = CayleyGroup(sub_matrix, sub_names, _skip_validation=True)
232
+ L.append(CayleySubgroup(sub_grp, self))
233
+
234
+ return L
235
+
236
+ def subgroups(self):
237
+ # 1. Subgrupo trivial {e}
238
+ e_name = self.names[0]
239
+ trivial_grp = CayleyGroup([[0]], [e_name])
240
+ trivial_subgroup = CayleySubgroup(trivial_grp, self)
241
+
242
+ # 2. Subgrupos propios
243
+ subs = self.proper_subgroups()
244
+
245
+ # 3. Subgrupo total (el propio grupo G)
246
+ total_grp = CayleyGroup(self.cayley, self.names)
247
+ total_subgroup = CayleySubgroup(total_grp, self)
248
+
249
+ return [trivial_subgroup] + subs + [total_subgroup]
250
+
251
+ def normal_subgroups(self):
252
+ return [sub for sub in self.subgroups() if sub.is_normal()]
253
+
254
+ def is_simple(self):
255
+ normals = self.normal_subgroups()
256
+ return len(normals) == 2
257
+
258
+ def quotient(self, subgroup):
259
+ if not isinstance(subgroup, CayleySubgroup):
260
+ raise TypeError("Argument must be an instance of CayleySubgroup")
261
+ if subgroup.group is not self:
262
+ raise ValueError("The subgroup does not belong to this group")
263
+ return subgroup.quotient()
264
+
265
+ #–> AUTOMORPHISMS
266
+
267
+ def is_automorphism(self, phi):
268
+
269
+ if not isinstance(phi, (tuple, list)):
270
+ return False
271
+
272
+ n = self.order()
273
+ if len(phi) != n:
274
+ return False
275
+
276
+ # 1. Biyectividad (debe ser una permutación completa de los índices 0..n-1)
277
+ if set(phi) != set(range(n)):
278
+ return False
279
+
280
+ # 2. Conservación del elemento neutro (el índice 0)
281
+ if phi[0] != 0:
282
+ return False
283
+
284
+ # 3. Preservación de la operación: phi(a * b) == phi(a) * phi(b)
285
+ G = self.cayley
286
+ for i in range(n):
287
+ for j in range(n):
288
+ if phi[G[i][j]] != G[phi[i]][phi[j]]:
289
+ return False
290
+
291
+ return True
292
+
293
+ def automorphisms(self):
294
+ n = self.order()
295
+ auts = []
296
+ orders = self.element_orders()[:]
297
+ for p in _order_preserving_permutations(orders):
298
+ try:
299
+ auts.append(Automorphism(p,self))
300
+ except:
301
+ pass
302
+
303
+ return auts
304
+
305
+ def automorphism_group(self):
306
+ auts = [i.phi for i in self.automorphisms()]
307
+ n = len(auts)
308
+ dict_auts = {a: i for i, a in enumerate(auts)}
309
+
310
+ # Tabla de Cayley de Aut(G) mediante composición de permutaciones: (a o b)[x] = a[b[x]]
311
+ cayley_aut = []
312
+ for i in range(n):
313
+ row = []
314
+ a = auts[i]
315
+ for j in range(n):
316
+ b = auts[j]
317
+ comp = tuple(a[b[k]] for k in range(len(b)))
318
+ row.append(dict_auts[comp])
319
+ cayley_aut.append(row)
320
+
321
+ names_aut = [str(auts[i]) for i in range(n)]
322
+ return CayleyGroup(cayley_aut, names_aut, _skip_validation=True)
323
+
324
+ class Subgroup(ABC):
325
+ pass
326
+
327
+ class CayleySubgroup(CayleyGroup, Subgroup):
328
+
329
+ def __init__(self, subgroup:CayleyGroup, group:CayleyGroup):
330
+
331
+ if not isinstance(subgroup, CayleyGroup) or not isinstance(group, CayleyGroup):
332
+ raise ValueError("Both arguments must be instances of CayleyGroup")
333
+
334
+ try:
335
+ subgroup_indices = [group.names.index(name) for name in subgroup.names]
336
+ except ValueError:
337
+ raise ValueError("All elements of subgroup must belong to group")
338
+
339
+ if not _form_subgroup(group.cayley, subgroup_indices):
340
+ raise ValueError("The provided group is not a valid subgroup of the main group")
341
+
342
+ # Todo lo que se puede hacer con grupos ahora también con subgrupos.
343
+ super().__init__(subgroup.cayley, subgroup.names)
344
+
345
+ self.subgroup = subgroup
346
+ self.group = group
347
+ self.cayley = subgroup.cayley
348
+ self.names = subgroup.names
349
+ self.gcayley = group.cayley
350
+ self.gnames = group.names
351
+ self._indices = subgroup_indices
352
+
353
+ #–> DUNDERS
354
+
355
+ def __truediv__(self, other):
356
+ """Permite usar la sintaxis natural G / H o H / H."""
357
+ return self.quotient()
358
+
359
+ def __le__(self, group):
360
+ return group==self.group
361
+
362
+ def __lt__(self, group):
363
+ return group==self.group and self.subgroup.order()<self.group.order()
364
+
365
+ #–> QUOTIENTS
366
+
367
+ def coset(self, element, side="left", return_names=True):
368
+ if side not in ("left", "right"):
369
+ raise ValueError("side must be either 'left' or 'right'")
370
+
371
+ # Buscar directamente la primera aparición del elemento en los nombres del grupo padre
372
+ try:
373
+ elem_idx = self.gnames.index(element)
374
+ except ValueError:
375
+ raise ValueError(f"Element '{element}' is not present in parent group names.")
376
+
377
+ coset_indices = []
378
+ for h in self._indices:
379
+ ah = self.gcayley[elem_idx][h] if side == "left" else self.gcayley[h][elem_idx]
380
+ if ah not in coset_indices:
381
+ coset_indices.append(ah)
382
+
383
+ coset_indices.sort()
384
+
385
+ if return_names:
386
+ return [self.gnames[i] for i in coset_indices]
387
+ return coset_indices
388
+
389
+ def is_normal(self):
390
+ for name in self.gnames:
391
+ left = self.coset(name, side="left", return_names=False)
392
+ right = self.coset(name, side="right", return_names=False)
393
+ if left != right:
394
+ return False
395
+ return True
396
+
397
+ def quotient(self):
398
+ """Calcula el grupo cociente G/H devolviendo una instancia de CayleyGroup."""
399
+ if not self.is_normal():
400
+ raise ValueError("The subgroup must be normal to construct a quotient group.")
401
+
402
+ # 1. Obtener todas las clases laterales (cosets) únicas expresadas como listas de nombres
403
+ cosets = []
404
+ for name in self.gnames:
405
+ c = self.coset(name, side="left", return_names=True)
406
+ if c not in cosets:
407
+ cosets.append(c)
408
+
409
+ n_cosets = len(cosets)
410
+
411
+ # 2. Mapear cada elemento de G al índice de su clase lateral en 'cosets'
412
+ elem_to_coset = {}
413
+ for idx, c in enumerate(cosets):
414
+ for name in c:
415
+ elem_to_coset[name] = idx
416
+
417
+ # 3. Construir la tabla de Cayley del grupo cociente
418
+ quotient_cayley = []
419
+ for i, c1 in enumerate(cosets):
420
+ row = []
421
+ rep1_idx = self.gnames.index(c1[0])
422
+ for j, c2 in enumerate(cosets):
423
+ rep2_idx = self.gnames.index(c2[0])
424
+ # Producto en el grupo padre: g1 * g2
425
+ prod_idx = self.gcayley[rep1_idx][rep2_idx]
426
+ prod_name = self.gnames[prod_idx]
427
+ row.append(elem_to_coset[prod_name])
428
+ quotient_cayley.append(row)
429
+
430
+ # 4. Asignar nombres representativos a los cosets, p. ej. "{e, r}" o "gH"
431
+ quotient_names = [f"{{{','.join(str(e) for e in c)}}}" for c in cosets]
432
+
433
+ return CayleyGroup(quotient_cayley, quotient_names, _skip_validation=True)
434
+
435
+ class Element:
436
+
437
+ def __init__(self,element,group):
438
+
439
+ if not isinstance(group, CayleyGroup):
440
+ raise TypeError("Argument must be an instance of CayleyGroup")
441
+
442
+ if not element in group:
443
+ raise ValueError("The element must belong to the group.")
444
+
445
+ self.element = element
446
+ self.group = group
447
+ self.index = group.names.index(element)
448
+
449
+ #–> DUNDERS
450
+
451
+ def __str__(self):
452
+ return self.element
453
+
454
+ def __mul__(self, other):
455
+ return Element(self.group.names[self.group.cayley[self.index][other.index]],self.group)
456
+
457
+ def __pow__(self,k):
458
+ if type(k)!=int:
459
+ raise ValueError("n must be an integer")
460
+ if k==1:
461
+ return self
462
+ elif k>1:
463
+ p = self
464
+ for i in range(k-1):
465
+ p = p * self
466
+ return p
467
+ elif k==0:
468
+ return Element(self.group.identity(),self.group)
469
+ elif k==-1:
470
+ return self.inverse()
471
+ elif k<-1:
472
+ p = self.inverse()
473
+ for i in range(k-1):
474
+ p = p * self
475
+ return p
476
+
477
+ def __eq__(self,other):
478
+ return self.element==other.element and self.group==other.group
479
+
480
+ #–> OTHER FUNCTIONS
481
+
482
+ def inverse(self):
483
+ inv = None
484
+ for i in self.group.names:
485
+ if self*Element(i,self.group)==Element(self.group.names[self.group.identity()],self.group):
486
+ inv = i
487
+ break
488
+ return Element(inv,self.group)
489
+
490
+ class Automorphism:
491
+
492
+ def __init__(self,phi:tuple,group:CayleyGroup):
493
+
494
+ if not group.is_automorphism(phi):
495
+ raise ValueError("The tuple phi has to be an automorphism of G.")
496
+
497
+ self.phi = phi
498
+ self.group = group
499
+
500
+ def __len__(self):
501
+ return len(self.phi)
502
+
503
+ def __str__(self):
504
+ return str(self.phi)
505
+
506
+ class AutomorphismFunction:
507
+
508
+ def __init__(self,function:list,group:CayleyGroup):
509
+ f = []
510
+ for i in function:
511
+ if type(i)==Automorphism:
512
+ f.append(i.phi)
513
+ elif type(i)==tuple:
514
+ if not group.is_automorphism(i):
515
+ raise ValueError("The transformation is not a valid automorfism for this group.")
516
+ f.append(i)
517
+ else:
518
+ raise TypeError("The argument must be a list of a Automorphisms or tuples.")
519
+
520
+ self.phi = f
521
+ self.group = group
522
+
523
+ def __str__(self):
524
+ return str(self.phi)
525
+
526
+
527
+ #################### HIDDEN COMMANDS ####################
528
+
529
+ def _obtener_nombre(var_obj):
530
+ for nombre, valor in globals().items():
531
+ if valor is var_obj:
532
+ return nombre
533
+ return None
534
+
535
+ def _is_closed(tabla):
536
+ n = len(tabla)
537
+ E = _get_elements(tabla)
538
+ if len(E)!=n:
539
+ return False
540
+ for fila in tabla:
541
+ if len(fila) != n:
542
+ return False
543
+ return True
544
+
545
+ def _is_group(tabla):
546
+ n = len(tabla)
547
+
548
+ # 1. Clausura
549
+ for fila in tabla:
550
+ if len(fila) != n:
551
+ return (False,"ClosingError")
552
+ for x in fila:
553
+ if not (0 <= x < n):
554
+ return (False,"ClosingError")
555
+
556
+ # 2. Cada fila y columna debe ser una permutación
557
+ conjunto = set(range(n))
558
+
559
+ for fila in tabla:
560
+ if set(fila) != conjunto:
561
+ return (False,f"UniquenessError – row {tabla.index(fila)}")
562
+
563
+ for j in range(n):
564
+ columna = {tabla[i][j] for i in range(n)}
565
+ if columna != conjunto:
566
+ return (False,f"UniquenessError – column {j}")
567
+
568
+ # 3. Buscar neutro
569
+ neutro = None
570
+
571
+ for e in range(n):
572
+ ok = True
573
+
574
+ for a in range(n):
575
+ if tabla[e][a] != a:
576
+ ok = False
577
+ break
578
+ if tabla[a][e] != a:
579
+ ok = False
580
+ break
581
+
582
+ if ok:
583
+ neutro = e
584
+ break
585
+
586
+ if neutro is None:
587
+ return (False,"IndentityError – no identity element found")
588
+
589
+ # 4. Inversos
590
+ for a in range(n):
591
+ existe = False
592
+
593
+ for b in range(n):
594
+ if tabla[a][b] == neutro and tabla[b][a] == neutro:
595
+ existe = True
596
+ break
597
+
598
+ if not existe:
599
+ return (False,f"InverseError – ({a},{b})")
600
+
601
+ # 5. Asociatividad
602
+ for a in range(n):
603
+ for b in range(n):
604
+ for c in range(n):
605
+
606
+ izquierda = tabla[tabla[a][b]][c]
607
+ derecha = tabla[a][tabla[b][c]]
608
+
609
+ if izquierda != derecha:
610
+ return (False,f"AssociativityError – ({a},{b},{c})")
611
+
612
+ return (True,"G is a group")
613
+
614
+ def _identity(G):
615
+ neutro = None
616
+ for e in range(len(G)):
617
+ ok = True
618
+ for a in range(len(G)):
619
+ if G[e][a] != a:
620
+ ok = False
621
+ break
622
+ if G[a][e] != a:
623
+ ok = False
624
+ break
625
+ if ok:
626
+ neutro = e
627
+ break
628
+ if neutro is None:
629
+ return (False,"IndentityError – no identity element found")
630
+ return neutro
631
+
632
+ def _subset(G, elements):
633
+ return [[G[r][c] for c in elements] for r in elements]
634
+
635
+ def _form_subgroup(G,elements):
636
+ # Por propiedades de los subgrupos, solo hace falta comprobar la clausura, el resto se heredan del grupo principal.
637
+ return _is_closed(_reset_renaming(_subset(G,elements)))
638
+
639
+ def _sign(p):
640
+ """
641
+ Devuelve:
642
+ 1 -> permutación par
643
+ -1 -> permutación impar
644
+ """
645
+ inv = 0
646
+ n = len(p)
647
+
648
+ for i in range(n):
649
+ for j in range(i+1, n):
650
+ if p[i] > p[j]:
651
+ inv += 1
652
+
653
+ return 1 if inv % 2 == 0 else -1
654
+
655
+ def _renamed_elements(G,L):
656
+ renamed_G = []
657
+ for I in G:
658
+ H = []
659
+ for i in I:
660
+ H.append(L[i])
661
+ renamed_G.append(H)
662
+ return renamed_G
663
+
664
+ def _get_elements(G):
665
+ """
666
+ Devuelve una lista con todos los elementos/nombres únicos que aparecen
667
+ dentro de la tabla de Cayley G, conservando el orden de primera aparición.
668
+
669
+ Parámetros:
670
+ G: Lista de listas que representa la tabla de Cayley.
671
+
672
+ Devuelve:
673
+ Lista con los elementos únicos del grupo.
674
+ """
675
+ # dict.fromkeys() elimina duplicados preservando el orden de aparición
676
+ return list(dict.fromkeys(e for fila in G for e in fila))
677
+
678
+ def _min_div(n):
679
+ for i in range(2, int(n**0.5) + 1):
680
+ if n % i == 0:
681
+ return i
682
+ return n
683
+
684
+ def _graded_power_set_with_id(G):
685
+
686
+ # Principios
687
+ n = len(G)
688
+ m = _min_div(n)
689
+ e = _identity(G)
690
+
691
+ def compute_orders(G):
692
+
693
+ if not _is_group(G)[0]:
694
+ raise TypeError
695
+
696
+ O = []
697
+ for i in range(len(G)):
698
+ ik = G[i][i]
699
+ o = 1
700
+ while i!=ik:
701
+ ik = G[ik][i]
702
+ o+=1
703
+ O.append(o)
704
+
705
+ return O
706
+
707
+ orders = compute_orders(G)
708
+
709
+ # Posibles órdenes de subgrupos (Lagrange)
710
+ R = []
711
+ for r in range(int(n/m)):
712
+ if n%(r+1)==0:
713
+ R.append(r+1)
714
+ #R.append(n)
715
+
716
+ Rm = []
717
+ for r in R[1:]:
718
+ Rm.append(r-1)
719
+
720
+ P = []
721
+
722
+ # Bucle principal
723
+ for rm in Rm:
724
+ E = []
725
+ for el in range(1,n):
726
+ if (rm+1)%orders[el]==0:
727
+ E.append(el)
728
+
729
+ for c in list(combinations(E, rm)):
730
+ P.append(sorted([e]+list(c)))
731
+
732
+ return P
733
+
734
+ def _is_closed_subset(G_cayley, indices):
735
+ """Comprueba clausura en O(|H|^2) usando un conjunto de índices."""
736
+ indices_set = set(indices)
737
+ for i in indices:
738
+ for j in indices:
739
+ if G_cayley[i][j] not in indices_set:
740
+ return False
741
+ return True
742
+
743
+ def _operate_cosets(G,C,A,B):
744
+ a = A[0]
745
+ b = B[0]
746
+ g = G[a][b]
747
+ for i in C:
748
+ if g in i:
749
+ r = i
750
+ break
751
+ return r
752
+
753
+ def _order_preserving_permutations(orders):
754
+
755
+ classes = {}
756
+
757
+ for i, order in enumerate(orders):
758
+ classes.setdefault(order, []).append(i)
759
+
760
+ classes = list(classes.values())
761
+
762
+ for perms in product(*(permutations(c) for c in classes)):
763
+ result = list(range(len(orders)))
764
+
765
+ for domain, image in zip(classes, perms):
766
+ for x, y in zip(domain, image):
767
+ result[x] = y
768
+
769
+ yield tuple(result)
770
+
771
+
772
+ #################### PRODUCTS ####################
773
+
774
+ def direct_product(A:CayleyGroup,B:CayleyGroup):
775
+ G = []
776
+ n, m = A.order(), B.order()
777
+
778
+ for a1 in range(n):
779
+ for b1 in range(m):
780
+ g = []
781
+ for a2 in range(n):
782
+ for b2 in range(m):
783
+ x2 = (a1,b1)
784
+ g.append( (A.cayley[a1][a2])*m+(B.cayley[b1][b2]) )
785
+ G.append(g)
786
+
787
+ names = []
788
+ for a in range(n):
789
+ for b in range(m):
790
+ names.append(str(A.names[a])+","+str(B.names[b]))
791
+
792
+ return CayleyGroup(G,names,_skip_validation=True)
793
+
794
+ def direct_power(G:CayleyGroup,n):
795
+ if n<2:
796
+ raise ValueError("n must be at least 2")
797
+ H = G
798
+ for i in range(n-1):
799
+ H = direct_product(H,G)
800
+ return H
801
+
802
+ def semidirect_product(A: CayleyGroup, B: CayleyGroup, f: AutomorphismFunction):
803
+ G = []
804
+ n, m = A.order(), B.order()
805
+
806
+ phi_list = f.phi if hasattr(f, "phi") else f
807
+
808
+ for a1 in range(n):
809
+ for b1 in range(m):
810
+ g = []
811
+ phi_b1 = phi_list[b1]
812
+ for a2 in range(n):
813
+ for b2 in range(m):
814
+ a2_trans = phi_b1[a2]
815
+ a_prod = A.cayley[a1][a2_trans]
816
+ b_prod = B.cayley[b1][b2]
817
+ g.append(a_prod * m + b_prod)
818
+ G.append(g)
819
+
820
+ names = []
821
+ for a in range(n):
822
+ for b in range(m):
823
+ names.append(str(A.names[a]) + "," + str(B.names[b]))
824
+
825
+ return CayleyGroup(G, names, _skip_validation=True)
826
+
827
+
828
+ #################### GENERATORS ####################
829
+
830
+ def cyclic_group(n):
831
+ elements = range(n)
832
+ G = []
833
+ for i in elements:
834
+ g = []
835
+ for j in elements:
836
+ g.append((i+j)%n)
837
+ G.append(g)
838
+ return CayleyGroup(G,_renaming_C(n))
839
+
840
+ def symmetric_group(n):
841
+
842
+ # Lista de todas las permutaciones
843
+ perms = list(permutations(range(n)))
844
+
845
+ # Diccionario permutación -> índice
846
+ index = {p: i for i, p in enumerate(perms)}
847
+
848
+ # Tabla
849
+ G = []
850
+
851
+ for p in perms:
852
+ fila = []
853
+ for q in perms:
854
+ fila.append(index[tuple(p[i] for i in q)])
855
+ G.append(fila)
856
+
857
+ return CayleyGroup(G,_renaming_S(n))
858
+
859
+ def alternating_group(n):
860
+
861
+ perms = [p for p in permutations(range(n)) if _sign(p) == 1]
862
+
863
+ index = {p: i for i, p in enumerate(perms)}
864
+
865
+ G = []
866
+
867
+ for p in perms:
868
+ fila = []
869
+ for q in perms:
870
+ fila.append(index[tuple(p[i] for i in q)])
871
+ G.append(fila)
872
+
873
+ return CayleyGroup(G,_renaming_A(n))
874
+
875
+ def dihedric_group(n):
876
+
877
+ elems = [(k,0) for k in range(n)] + [(k,1) for k in range(n)]
878
+
879
+ index = {g:i for i,g in enumerate(elems)}
880
+
881
+ G = []
882
+
883
+ for (k,a) in elems:
884
+ fila = []
885
+
886
+ for (l,b) in elems:
887
+
888
+ if a == 0:
889
+ m = (k + l) % n
890
+ else:
891
+ m = (k - l) % n
892
+
893
+ fila.append(index[(m, a ^ b)])
894
+
895
+ G.append(fila)
896
+
897
+ return CayleyGroup(G,_renaming_D(n))
898
+
899
+ def quaternion_group():
900
+
901
+ # Multiplicación para la parte positiva
902
+ base = {
903
+ (0,0):( 1,0),
904
+ (0,1):( 1,1),
905
+ (0,2):( 1,2),
906
+ (0,3):( 1,3),
907
+
908
+ (1,0):( 1,1),
909
+ (2,0):( 1,2),
910
+ (3,0):( 1,3),
911
+
912
+ (1,1):(-1,0),
913
+ (2,2):(-1,0),
914
+ (3,3):(-1,0),
915
+
916
+ (1,2):( 1,3),
917
+ (2,3):( 1,1),
918
+ (3,1):( 1,2),
919
+
920
+ (2,1):(-1,3),
921
+ (3,2):(-1,1),
922
+ (1,3):(-1,2),
923
+ }
924
+
925
+ def multiply(a, b):
926
+ sa, xa = a
927
+ sb, xb = b
928
+
929
+ s, x = base[(xa, xb)]
930
+
931
+ return (sa * sb * s, x)
932
+
933
+ elements = [
934
+ ( 1,0), (-1,0),
935
+ ( 1,1), (-1,1),
936
+ ( 1,2), (-1,2),
937
+ ( 1,3), (-1,3)
938
+ ]
939
+
940
+ index = {g: i for i, g in enumerate(elements)}
941
+
942
+ G = []
943
+
944
+ for a in elements:
945
+ fila = []
946
+
947
+ for b in elements:
948
+ fila.append(index[multiply(a, b)])
949
+
950
+ G.append(fila)
951
+
952
+ return CayleyGroup(G,_renaming_Q8())
953
+
954
+ def units_group(n):
955
+
956
+ if n <= 1:
957
+ raise ValueError("n debe ser mayor que 1")
958
+
959
+ elems = [x for x in range(1, n) if gcd(x, n) == 1]
960
+ index = {g: i for i, g in enumerate(elems)}
961
+
962
+ G = []
963
+ for a in elems:
964
+ fila = []
965
+ for b in elems:
966
+ fila.append(index[(a * b) % n])
967
+ G.append(fila)
968
+
969
+ return CayleyGroup(G,_renaming_U(n))
970
+
971
+ def dicyclic_group(n):
972
+ """
973
+ Genera la tabla de Cayley del grupo dicíclico Dic_n (de orden 4n).
974
+ Presentación: <a, x | a^(2n) = 1, x^2 = a^n, x^-1 a x = a^-1>
975
+ """
976
+ if n < 1:
977
+ raise ValueError("n debe ser un entero positivo")
978
+
979
+ # Elementos representados como pares (k, e) donde 0 <= k < 2n y e en {0, 1}
980
+ # (k, 0) -> a^k
981
+ # (k, 1) -> a^k * x
982
+ elems = [(k, 0) for k in range(2 * n)] + [(k, 1) for k in range(2 * n)]
983
+ index = {g: i for i, g in enumerate(elems)}
984
+
985
+ G = []
986
+ for (k, a) in elems:
987
+ fila = []
988
+ for (l, b) in elems:
989
+ if a == 0:
990
+ m = (k + l) % (2 * n)
991
+ c = b
992
+ else:
993
+ if b == 0:
994
+ m = (k - l) % (2 * n)
995
+ c = 1
996
+ else:
997
+ m = (k - l + n) % (2 * n)
998
+ c = 0
999
+ fila.append(index[(m, c)])
1000
+ G.append(fila)
1001
+
1002
+ return CayleyGroup(G,_renaming_Dic(n))
1003
+
1004
+ def tetrahedral_group():
1005
+ """
1006
+ Full symmetry group of the tetrahedron. Isomorphic to S₄.
1007
+ """
1008
+ return CayleyGroup(symmetric_group(4).cayley)
1009
+
1010
+ def octahedral_group():
1011
+ """
1012
+ Full symmetry group of the cube/octahedron. Isomorphic to S₄ × C₂.
1013
+ """
1014
+ return CayleyGroup((symmetric_group(4) * cyclic_group(2)).cayley)
1015
+
1016
+ def icosahedral_group():
1017
+ """
1018
+ Full symmetry group of the icosahedron/dodecahedron. Isomorphic to A₅ × C₂.
1019
+ """
1020
+ return CayleyGroup((alternating_group(5) * cyclic_group(2)).cayley)
1021
+
1022
+
1023
+ #################### VISUALIZATION AND RENAMING HELPERS ####################
1024
+
1025
+ def cayley_table(G, title="", colormap=rainbow, names="", renaming=[]):
1026
+ if title=="":
1027
+ if _obtener_nombre(G)==None:
1028
+ title = f"Cayley table"
1029
+ else:
1030
+ title = f"Cayley table of {_obtener_nombre(G)}"
1031
+ if names=="":
1032
+ if len(G)<=20:
1033
+ names=True
1034
+ else:
1035
+ names=False
1036
+ if renaming==[]:
1037
+ renaming = [rf"${e}$" for e in range(len(G))]
1038
+
1039
+ elements = range(len(G))
1040
+ fig, ax = plt.subplots(figsize=(6, 5))
1041
+ im = ax.imshow(G, cmap=colormap)
1042
+ ax.set_xticks(np.arange(len(elements)))
1043
+ ax.set_yticks(np.arange(len(elements)))
1044
+ ax.set_xticklabels(renaming)
1045
+ ax.set_yticklabels(renaming)
1046
+
1047
+ ax.xaxis.tick_top()
1048
+
1049
+ if names:
1050
+ for i in range(len(elements)):
1051
+ for j in range(len(elements)):
1052
+ color_texto = "black"
1053
+ ax.text(
1054
+ i,
1055
+ j,
1056
+ f"{renaming[G[j][i]]}",
1057
+ ha="center",
1058
+ va="center",
1059
+ color=color_texto,
1060
+ fontsize=12,
1061
+ )
1062
+
1063
+ plt.title(title)
1064
+ plt.tight_layout()
1065
+ plt.show()
1066
+
1067
+ def _renaming_C(n):
1068
+ r = []
1069
+ if n >= 1:
1070
+ r.append(r"$e$")
1071
+ if n > 1:
1072
+ r.append(r"$r$")
1073
+ if n > 2:
1074
+ for i in range(2, n):
1075
+ r.append(rf"$r^{{{i}}}$")
1076
+ return r
1077
+
1078
+ def _renaming_S(n):
1079
+ """
1080
+ Renombrado para S_n en notación de ciclos.
1081
+ Coincide con el orden de elementos de generate_group_S(n).
1082
+ """
1083
+ def tuple_to_cycle(p):
1084
+ visited = [False] * len(p)
1085
+ cycles = []
1086
+ for i in range(len(p)):
1087
+ if not visited[i]:
1088
+ curr = i
1089
+ cycle = []
1090
+ while not visited[curr]:
1091
+ visited[curr] = True
1092
+ cycle.append(curr + 1) # Usamos representación 1-based (1..n)
1093
+ curr = p[curr]
1094
+ if len(cycle) > 1:
1095
+ cycles.append("(" + "".join(map(str, cycle)) + ")")
1096
+ return "".join(cycles) if cycles else "e"
1097
+
1098
+ perms = list(permutations(range(n)))
1099
+ return [tuple_to_cycle(p) for p in perms]
1100
+
1101
+ def _renaming_A(n):
1102
+ """
1103
+ Renombrado para A_n en notación de ciclos.
1104
+ Coincide con el orden de elementos de generate_group_A(n).
1105
+ """
1106
+ def tuple_to_cycle(p):
1107
+ visited = [False] * len(p)
1108
+ cycles = []
1109
+ for i in range(len(p)):
1110
+ if not visited[i]:
1111
+ curr = i
1112
+ cycle = []
1113
+ while not visited[curr]:
1114
+ visited[curr] = True
1115
+ cycle.append(curr + 1)
1116
+ curr = p[curr]
1117
+ if len(cycle) > 1:
1118
+ cycles.append("(" + "".join(map(str, cycle)) + ")")
1119
+ return "".join(cycles) if cycles else "e"
1120
+
1121
+ perms = [p for p in permutations(range(n)) if _sign(p) == 1]
1122
+ return [tuple_to_cycle(p) for p in perms]
1123
+
1124
+ def _renaming_D(n):
1125
+ r = []
1126
+ # Rotaciones
1127
+ for k in range(n):
1128
+ if k == 0:
1129
+ r.append(r"$e$")
1130
+ elif k == 1:
1131
+ r.append(r"$r$")
1132
+ else:
1133
+ r.append(rf"$r^{{{k}}}$")
1134
+
1135
+ # Reflexiones
1136
+ for k in range(n):
1137
+ if k == 0:
1138
+ r.append(r"$s$")
1139
+ elif k == 1:
1140
+ r.append(r"$rs$")
1141
+ else:
1142
+ r.append(rf"$r^{{{k}}}s$")
1143
+
1144
+ return r
1145
+
1146
+ def _renaming_Q8():
1147
+ return ["1","-1","i","-i","j","-j","k","-k"]
1148
+
1149
+ def _renaming_U(n):
1150
+ return [str(x) for x in range(1, n) if gcd(x, n) == 1]
1151
+
1152
+ def _renaming_Dic(n):
1153
+ names = []
1154
+ for k in range(2 * n):
1155
+ if k == 0:
1156
+ names.append(r"$e$")
1157
+ elif k == 1:
1158
+ names.append(r"$a$")
1159
+ else:
1160
+ names.append(rf"$a^{{{k}}}$")
1161
+ for k in range(2 * n):
1162
+ if k == 0:
1163
+ names.append(r"$x$")
1164
+ elif k == 1:
1165
+ names.append(r"$ax$")
1166
+ else:
1167
+ names.append(rf"$a^{{{k}}}x$")
1168
+ return names
1169
+
1170
+ def _reset_renaming(G):
1171
+ l = _get_elements(G)
1172
+ D = {}
1173
+ for i in range(len(l)):
1174
+ D[l[i]]=i
1175
+ return _renamed_elements(G,D)
1176
+