homomorphisms.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. import itertools
  2. from sympy.combinatorics.fp_groups import FpGroup, FpSubgroup, simplify_presentation
  3. from sympy.combinatorics.free_groups import FreeGroup
  4. from sympy.combinatorics.perm_groups import PermutationGroup
  5. from sympy.core.numbers import igcd
  6. from sympy.ntheory.factor_ import totient
  7. from sympy.core.singleton import S
  8. class GroupHomomorphism:
  9. '''
  10. A class representing group homomorphisms. Instantiate using `homomorphism()`.
  11. References
  12. ==========
  13. .. [1] Holt, D., Eick, B. and O'Brien, E. (2005). Handbook of computational group theory.
  14. '''
  15. def __init__(self, domain, codomain, images):
  16. self.domain = domain
  17. self.codomain = codomain
  18. self.images = images
  19. self._inverses = None
  20. self._kernel = None
  21. self._image = None
  22. def _invs(self):
  23. '''
  24. Return a dictionary with `{gen: inverse}` where `gen` is a rewriting
  25. generator of `codomain` (e.g. strong generator for permutation groups)
  26. and `inverse` is an element of its preimage
  27. '''
  28. image = self.image()
  29. inverses = {}
  30. for k in list(self.images.keys()):
  31. v = self.images[k]
  32. if not (v in inverses
  33. or v.is_identity):
  34. inverses[v] = k
  35. if isinstance(self.codomain, PermutationGroup):
  36. gens = image.strong_gens
  37. else:
  38. gens = image.generators
  39. for g in gens:
  40. if g in inverses or g.is_identity:
  41. continue
  42. w = self.domain.identity
  43. if isinstance(self.codomain, PermutationGroup):
  44. parts = image._strong_gens_slp[g][::-1]
  45. else:
  46. parts = g
  47. for s in parts:
  48. if s in inverses:
  49. w = w*inverses[s]
  50. else:
  51. w = w*inverses[s**-1]**-1
  52. inverses[g] = w
  53. return inverses
  54. def invert(self, g):
  55. '''
  56. Return an element of the preimage of ``g`` or of each element
  57. of ``g`` if ``g`` is a list.
  58. Explanation
  59. ===========
  60. If the codomain is an FpGroup, the inverse for equal
  61. elements might not always be the same unless the FpGroup's
  62. rewriting system is confluent. However, making a system
  63. confluent can be time-consuming. If it's important, try
  64. `self.codomain.make_confluent()` first.
  65. '''
  66. from sympy.combinatorics import Permutation
  67. from sympy.combinatorics.free_groups import FreeGroupElement
  68. if isinstance(g, (Permutation, FreeGroupElement)):
  69. if isinstance(self.codomain, FpGroup):
  70. g = self.codomain.reduce(g)
  71. if self._inverses is None:
  72. self._inverses = self._invs()
  73. image = self.image()
  74. w = self.domain.identity
  75. if isinstance(self.codomain, PermutationGroup):
  76. gens = image.generator_product(g)[::-1]
  77. else:
  78. gens = g
  79. # the following can't be "for s in gens:"
  80. # because that would be equivalent to
  81. # "for s in gens.array_form:" when g is
  82. # a FreeGroupElement. On the other hand,
  83. # when you call gens by index, the generator
  84. # (or inverse) at position i is returned.
  85. for i in range(len(gens)):
  86. s = gens[i]
  87. if s.is_identity:
  88. continue
  89. if s in self._inverses:
  90. w = w*self._inverses[s]
  91. else:
  92. w = w*self._inverses[s**-1]**-1
  93. return w
  94. elif isinstance(g, list):
  95. return [self.invert(e) for e in g]
  96. def kernel(self):
  97. '''
  98. Compute the kernel of `self`.
  99. '''
  100. if self._kernel is None:
  101. self._kernel = self._compute_kernel()
  102. return self._kernel
  103. def _compute_kernel(self):
  104. G = self.domain
  105. G_order = G.order()
  106. if G_order is S.Infinity:
  107. raise NotImplementedError(
  108. "Kernel computation is not implemented for infinite groups")
  109. gens = []
  110. if isinstance(G, PermutationGroup):
  111. K = PermutationGroup(G.identity)
  112. else:
  113. K = FpSubgroup(G, gens, normal=True)
  114. i = self.image().order()
  115. while K.order()*i != G_order:
  116. r = G.random()
  117. k = r*self.invert(self(r))**-1
  118. if k not in K:
  119. gens.append(k)
  120. if isinstance(G, PermutationGroup):
  121. K = PermutationGroup(gens)
  122. else:
  123. K = FpSubgroup(G, gens, normal=True)
  124. return K
  125. def image(self):
  126. '''
  127. Compute the image of `self`.
  128. '''
  129. if self._image is None:
  130. values = list(set(self.images.values()))
  131. if isinstance(self.codomain, PermutationGroup):
  132. self._image = self.codomain.subgroup(values)
  133. else:
  134. self._image = FpSubgroup(self.codomain, values)
  135. return self._image
  136. def _apply(self, elem):
  137. '''
  138. Apply `self` to `elem`.
  139. '''
  140. if elem not in self.domain:
  141. if isinstance(elem, (list, tuple)):
  142. return [self._apply(e) for e in elem]
  143. raise ValueError("The supplied element does not belong to the domain")
  144. if elem.is_identity:
  145. return self.codomain.identity
  146. else:
  147. images = self.images
  148. value = self.codomain.identity
  149. if isinstance(self.domain, PermutationGroup):
  150. gens = self.domain.generator_product(elem, original=True)
  151. for g in gens:
  152. if g in self.images:
  153. value = images[g]*value
  154. else:
  155. value = images[g**-1]**-1*value
  156. else:
  157. i = 0
  158. for _, p in elem.array_form:
  159. if p < 0:
  160. g = elem[i]**-1
  161. else:
  162. g = elem[i]
  163. value = value*images[g]**p
  164. i += abs(p)
  165. return value
  166. def __call__(self, elem):
  167. return self._apply(elem)
  168. def is_injective(self):
  169. '''
  170. Check if the homomorphism is injective
  171. '''
  172. return self.kernel().order() == 1
  173. def is_surjective(self):
  174. '''
  175. Check if the homomorphism is surjective
  176. '''
  177. im = self.image().order()
  178. oth = self.codomain.order()
  179. if im is S.Infinity and oth is S.Infinity:
  180. return None
  181. else:
  182. return im == oth
  183. def is_isomorphism(self):
  184. '''
  185. Check if `self` is an isomorphism.
  186. '''
  187. return self.is_injective() and self.is_surjective()
  188. def is_trivial(self):
  189. '''
  190. Check is `self` is a trivial homomorphism, i.e. all elements
  191. are mapped to the identity.
  192. '''
  193. return self.image().order() == 1
  194. def compose(self, other):
  195. '''
  196. Return the composition of `self` and `other`, i.e.
  197. the homomorphism phi such that for all g in the domain
  198. of `other`, phi(g) = self(other(g))
  199. '''
  200. if not other.image().is_subgroup(self.domain):
  201. raise ValueError("The image of `other` must be a subgroup of "
  202. "the domain of `self`")
  203. images = {g: self(other(g)) for g in other.images}
  204. return GroupHomomorphism(other.domain, self.codomain, images)
  205. def restrict_to(self, H):
  206. '''
  207. Return the restriction of the homomorphism to the subgroup `H`
  208. of the domain.
  209. '''
  210. if not isinstance(H, PermutationGroup) or not H.is_subgroup(self.domain):
  211. raise ValueError("Given H is not a subgroup of the domain")
  212. domain = H
  213. images = {g: self(g) for g in H.generators}
  214. return GroupHomomorphism(domain, self.codomain, images)
  215. def invert_subgroup(self, H):
  216. '''
  217. Return the subgroup of the domain that is the inverse image
  218. of the subgroup ``H`` of the homomorphism image
  219. '''
  220. if not H.is_subgroup(self.image()):
  221. raise ValueError("Given H is not a subgroup of the image")
  222. gens = []
  223. P = PermutationGroup(self.image().identity)
  224. for h in H.generators:
  225. h_i = self.invert(h)
  226. if h_i not in P:
  227. gens.append(h_i)
  228. P = PermutationGroup(gens)
  229. for k in self.kernel().generators:
  230. if k*h_i not in P:
  231. gens.append(k*h_i)
  232. P = PermutationGroup(gens)
  233. return P
  234. def homomorphism(domain, codomain, gens, images=(), check=True):
  235. '''
  236. Create (if possible) a group homomorphism from the group ``domain``
  237. to the group ``codomain`` defined by the images of the domain's
  238. generators ``gens``. ``gens`` and ``images`` can be either lists or tuples
  239. of equal sizes. If ``gens`` is a proper subset of the group's generators,
  240. the unspecified generators will be mapped to the identity. If the
  241. images are not specified, a trivial homomorphism will be created.
  242. If the given images of the generators do not define a homomorphism,
  243. an exception is raised.
  244. If ``check`` is ``False``, do not check whether the given images actually
  245. define a homomorphism.
  246. '''
  247. if not isinstance(domain, (PermutationGroup, FpGroup, FreeGroup)):
  248. raise TypeError("The domain must be a group")
  249. if not isinstance(codomain, (PermutationGroup, FpGroup, FreeGroup)):
  250. raise TypeError("The codomain must be a group")
  251. generators = domain.generators
  252. if not all(g in generators for g in gens):
  253. raise ValueError("The supplied generators must be a subset of the domain's generators")
  254. if not all(g in codomain for g in images):
  255. raise ValueError("The images must be elements of the codomain")
  256. if images and len(images) != len(gens):
  257. raise ValueError("The number of images must be equal to the number of generators")
  258. gens = list(gens)
  259. images = list(images)
  260. images.extend([codomain.identity]*(len(generators)-len(images)))
  261. gens.extend([g for g in generators if g not in gens])
  262. images = dict(zip(gens,images))
  263. if check and not _check_homomorphism(domain, codomain, images):
  264. raise ValueError("The given images do not define a homomorphism")
  265. return GroupHomomorphism(domain, codomain, images)
  266. def _check_homomorphism(domain, codomain, images):
  267. """
  268. Check that a given mapping of generators to images defines a homomorphism.
  269. Parameters
  270. ==========
  271. domain : PermutationGroup, FpGroup, FreeGroup
  272. codomain : PermutationGroup, FpGroup, FreeGroup
  273. images : dict
  274. The set of keys must be equal to domain.generators.
  275. The values must be elements of the codomain.
  276. """
  277. pres = domain if hasattr(domain, 'relators') else domain.presentation()
  278. rels = pres.relators
  279. gens = pres.generators
  280. symbols = [g.ext_rep[0] for g in gens]
  281. symbols_to_domain_generators = dict(zip(symbols, domain.generators))
  282. identity = codomain.identity
  283. def _image(r):
  284. w = identity
  285. for symbol, power in r.array_form:
  286. g = symbols_to_domain_generators[symbol]
  287. w *= images[g]**power
  288. return w
  289. for r in rels:
  290. if isinstance(codomain, FpGroup):
  291. s = codomain.equals(_image(r), identity)
  292. if s is None:
  293. # only try to make the rewriting system
  294. # confluent when it can't determine the
  295. # truth of equality otherwise
  296. success = codomain.make_confluent()
  297. s = codomain.equals(_image(r), identity)
  298. if s is None and not success:
  299. raise RuntimeError("Can't determine if the images "
  300. "define a homomorphism. Try increasing "
  301. "the maximum number of rewriting rules "
  302. "(group._rewriting_system.set_max(new_value); "
  303. "the current value is stored in group._rewriting"
  304. "_system.maxeqns)")
  305. else:
  306. s = _image(r).is_identity
  307. if not s:
  308. return False
  309. return True
  310. def orbit_homomorphism(group, omega):
  311. '''
  312. Return the homomorphism induced by the action of the permutation
  313. group ``group`` on the set ``omega`` that is closed under the action.
  314. '''
  315. from sympy.combinatorics import Permutation
  316. from sympy.combinatorics.named_groups import SymmetricGroup
  317. codomain = SymmetricGroup(len(omega))
  318. identity = codomain.identity
  319. omega = list(omega)
  320. images = {g: identity*Permutation([omega.index(o^g) for o in omega]) for g in group.generators}
  321. group._schreier_sims(base=omega)
  322. H = GroupHomomorphism(group, codomain, images)
  323. if len(group.basic_stabilizers) > len(omega):
  324. H._kernel = group.basic_stabilizers[len(omega)]
  325. else:
  326. H._kernel = PermutationGroup([group.identity])
  327. return H
  328. def block_homomorphism(group, blocks):
  329. '''
  330. Return the homomorphism induced by the action of the permutation
  331. group ``group`` on the block system ``blocks``. The latter should be
  332. of the same form as returned by the ``minimal_block`` method for
  333. permutation groups, namely a list of length ``group.degree`` where
  334. the i-th entry is a representative of the block i belongs to.
  335. '''
  336. from sympy.combinatorics import Permutation
  337. from sympy.combinatorics.named_groups import SymmetricGroup
  338. n = len(blocks)
  339. # number the blocks; m is the total number,
  340. # b is such that b[i] is the number of the block i belongs to,
  341. # p is the list of length m such that p[i] is the representative
  342. # of the i-th block
  343. m = 0
  344. p = []
  345. b = [None]*n
  346. for i in range(n):
  347. if blocks[i] == i:
  348. p.append(i)
  349. b[i] = m
  350. m += 1
  351. for i in range(n):
  352. b[i] = b[blocks[i]]
  353. codomain = SymmetricGroup(m)
  354. # the list corresponding to the identity permutation in codomain
  355. identity = range(m)
  356. images = {g: Permutation([b[p[i]^g] for i in identity]) for g in group.generators}
  357. H = GroupHomomorphism(group, codomain, images)
  358. return H
  359. def group_isomorphism(G, H, isomorphism=True):
  360. '''
  361. Compute an isomorphism between 2 given groups.
  362. Parameters
  363. ==========
  364. G : A finite ``FpGroup`` or a ``PermutationGroup``.
  365. First group.
  366. H : A finite ``FpGroup`` or a ``PermutationGroup``
  367. Second group.
  368. isomorphism : bool
  369. This is used to avoid the computation of homomorphism
  370. when the user only wants to check if there exists
  371. an isomorphism between the groups.
  372. Returns
  373. =======
  374. If isomorphism = False -- Returns a boolean.
  375. If isomorphism = True -- Returns a boolean and an isomorphism between `G` and `H`.
  376. Examples
  377. ========
  378. >>> from sympy.combinatorics import free_group, Permutation
  379. >>> from sympy.combinatorics.perm_groups import PermutationGroup
  380. >>> from sympy.combinatorics.fp_groups import FpGroup
  381. >>> from sympy.combinatorics.homomorphisms import group_isomorphism
  382. >>> from sympy.combinatorics.named_groups import DihedralGroup, AlternatingGroup
  383. >>> D = DihedralGroup(8)
  384. >>> p = Permutation(0, 1, 2, 3, 4, 5, 6, 7)
  385. >>> P = PermutationGroup(p)
  386. >>> group_isomorphism(D, P)
  387. (False, None)
  388. >>> F, a, b = free_group("a, b")
  389. >>> G = FpGroup(F, [a**3, b**3, (a*b)**2])
  390. >>> H = AlternatingGroup(4)
  391. >>> (check, T) = group_isomorphism(G, H)
  392. >>> check
  393. True
  394. >>> T(b*a*b**-1*a**-1*b**-1)
  395. (0 2 3)
  396. Notes
  397. =====
  398. Uses the approach suggested by Robert Tarjan to compute the isomorphism between two groups.
  399. First, the generators of ``G`` are mapped to the elements of ``H`` and
  400. we check if the mapping induces an isomorphism.
  401. '''
  402. if not isinstance(G, (PermutationGroup, FpGroup)):
  403. raise TypeError("The group must be a PermutationGroup or an FpGroup")
  404. if not isinstance(H, (PermutationGroup, FpGroup)):
  405. raise TypeError("The group must be a PermutationGroup or an FpGroup")
  406. if isinstance(G, FpGroup) and isinstance(H, FpGroup):
  407. G = simplify_presentation(G)
  408. H = simplify_presentation(H)
  409. # Two infinite FpGroups with the same generators are isomorphic
  410. # when the relators are same but are ordered differently.
  411. if G.generators == H.generators and (G.relators).sort() == (H.relators).sort():
  412. if not isomorphism:
  413. return True
  414. return (True, homomorphism(G, H, G.generators, H.generators))
  415. # `_H` is the permutation group isomorphic to `H`.
  416. _H = H
  417. g_order = G.order()
  418. h_order = H.order()
  419. if g_order is S.Infinity:
  420. raise NotImplementedError("Isomorphism methods are not implemented for infinite groups.")
  421. if isinstance(H, FpGroup):
  422. if h_order is S.Infinity:
  423. raise NotImplementedError("Isomorphism methods are not implemented for infinite groups.")
  424. _H, h_isomorphism = H._to_perm_group()
  425. if (g_order != h_order) or (G.is_abelian != H.is_abelian):
  426. if not isomorphism:
  427. return False
  428. return (False, None)
  429. if not isomorphism:
  430. # Two groups of the same cyclic numbered order
  431. # are isomorphic to each other.
  432. n = g_order
  433. if (igcd(n, totient(n))) == 1:
  434. return True
  435. # Match the generators of `G` with subsets of `_H`
  436. gens = list(G.generators)
  437. for subset in itertools.permutations(_H, len(gens)):
  438. images = list(subset)
  439. images.extend([_H.identity]*(len(G.generators)-len(images)))
  440. _images = dict(zip(gens,images))
  441. if _check_homomorphism(G, _H, _images):
  442. if isinstance(H, FpGroup):
  443. images = h_isomorphism.invert(images)
  444. T = homomorphism(G, H, G.generators, images, check=False)
  445. if T.is_isomorphism():
  446. # It is a valid isomorphism
  447. if not isomorphism:
  448. return True
  449. return (True, T)
  450. if not isomorphism:
  451. return False
  452. return (False, None)
  453. def is_isomorphic(G, H):
  454. '''
  455. Check if the groups are isomorphic to each other
  456. Parameters
  457. ==========
  458. G : A finite ``FpGroup`` or a ``PermutationGroup``
  459. First group.
  460. H : A finite ``FpGroup`` or a ``PermutationGroup``
  461. Second group.
  462. Returns
  463. =======
  464. boolean
  465. '''
  466. return group_isomorphism(G, H, isomorphism=False)