Skip to content

Commit 98d45df

Browse files
committed
Added Group.find_isomorphism, Group.generators, Group.Dn, and fixed Function hashing bug.
1 parent 864bd26 commit 98d45df

File tree

3 files changed

+153
-10
lines changed

3 files changed

+153
-10
lines changed

absalg/Function.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,28 @@ def __call__(self, elem):
3030
return self.function(elem)
3131

3232
def __hash__(self):
33-
return hash(self.domain) ^ hash(self.codomain) ^ hash(self.function)
33+
"""Returns the hash of self"""
34+
35+
# Need to be a little careful, since self.domain and self.codomain are
36+
# often the same, and we don't want to cancel out their hashes by xoring
37+
# them against each other.
38+
#
39+
# Also, functions we consider equal, like lambda x: x + 1, and
40+
# def jim(x): return x + 1, have different hashes, so we can't include
41+
# the hash of self.function.
42+
#
43+
# Finally, we should make sure that if you switch the domain and
44+
# codomain, the hash will (usually) change, so you can't just add or
45+
# multiply the hashes together.
46+
47+
return hash(self.domain) + 2 * hash(self.codomain)
3448

3549
def __eq__(self, other):
3650
if not isinstance(other, Function):
3751
return False
3852

3953
return id(self) == id(other) or ( \
40-
isinstance(other, Function) and self.domain == other.domain and \
54+
self.domain == other.domain and \
4155
self.codomain == other.codomain and \
4256
all(self(elem) == other(elem) for elem in self.domain) )
4357

absalg/Group.py

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ def __iter__(self):
160160
for g in self.group_elems:
161161
if g != self.e: yield g
162162

163+
def __contains__(self, item):
164+
return item in self.group_elems
165+
163166
def __hash__(self):
164167
return hash(self.Set) ^ hash(self.bin_op)
165168

@@ -258,15 +261,22 @@ def __mul__(self, other):
258261
def generate(self, elems):
259262
"""
260263
Returns the subgroup of self generated by GroupElems elems
264+
265+
If any of the items aren't already GroupElems, we will try to convert
266+
them to GroupElems before continuing.
261267
262268
elems must be iterable
263269
"""
264-
if not Set(elems) <= self.group_elems:
270+
271+
elems = Set(g if isinstance(g, GroupElem) else GroupElem(g, self) \
272+
for g in elems)
273+
274+
if not elems <= self.group_elems:
265275
raise ValueError("elems must be a subset of self.group_elems")
266276
if len(elems) == 0:
267277
raise ValueError("elems must have at least one element")
268278

269-
oldG = Set(elems)
279+
oldG = elems
270280
while True:
271281
newG = oldG | Set(a * b for a in oldG for b in oldG)
272282
if oldG == newG: break
@@ -288,6 +298,80 @@ def subgroups(self):
288298

289299
return old_sgs
290300

301+
def generators(self):
302+
"""
303+
Returns a list of GroupElems that generate self, with length
304+
at most log_2(len(self)) + 1
305+
"""
306+
307+
result = [self.e.elem]
308+
H = self.generate(result)
309+
310+
while len(H) < len(self):
311+
result.append((self.Set - H.Set).pick())
312+
H = self.generate(result)
313+
314+
# The identity is always a redundant generator in nontrivial Groups
315+
if len(self) != 1:
316+
result = result[1:]
317+
318+
return [GroupElem(g, self) for g in result]
319+
320+
def find_isomorphism(self, other):
321+
"""
322+
Returns an isomorphic GroupHomomorphism between self and other,
323+
or None if self and other are not isomorphic
324+
325+
Uses Tarjan's algorithm, running in O(n^(log n + O(1))) time, but
326+
runs a lot faster than that if the group has a small generating set.
327+
"""
328+
if not isinstance(other, Group):
329+
raise TypeError("other must be a Group")
330+
331+
if len(self) != len(other) or self.is_abelian() != other.is_abelian():
332+
return None
333+
334+
# Try to match the generators of self with some subset of other
335+
A = self.generators()
336+
for B in itertools.permutations(other, len(A)):
337+
338+
func = dict(itertools.izip(A, B)) # the mapping
339+
counterexample = False
340+
while not counterexample:
341+
342+
# Loop through the mapped elements so far, trying to extend the
343+
# mapping or else find a counterexample
344+
noobs = {}
345+
for g, h in itertools.product(func, func):
346+
if g * h in func:
347+
if func[g] * func[h] != func[g * h]:
348+
counterexample = True
349+
break
350+
else:
351+
noobs[g * h] = func[g] * func[h]
352+
353+
# If we've mapped all the elements of self, then it's a
354+
# homomorphism provided we haven't seen any counterexamples.
355+
if len(func) == len(self):
356+
break
357+
358+
# Make sure there aren't any collisions before updating
359+
imagelen = len(set(noobs.values()) | set(func.values()))
360+
if imagelen != len(noobs) + len(func):
361+
counterexample = True
362+
func.update(noobs)
363+
364+
if not counterexample:
365+
return GroupHomomorphism(self.group_elems, other.group_elems, \
366+
lambda x: func[x])
367+
368+
return None
369+
370+
def is_isomorphic(self, other):
371+
"""Checks if self and other are isomorphic"""
372+
return bool(self.find_isomorphism(other))
373+
374+
291375
class GroupHomomorphism(Function):
292376
"""
293377
The definition of a Group Homomorphism
@@ -342,13 +426,32 @@ def is_isomorphism(self):
342426

343427

344428
def Zn(n):
345-
""" Returns the cylic group of order n"""
429+
"""Returns the cylic group of order n"""
346430
G = Set(range(n))
347431
bin_op = Function(G * G, G, lambda x: (x[0] + x[1]) % n)
348432
return Group(G, bin_op)
349433

350434
def Sn(n):
351-
""" Returns the symmetric group of order n! """
435+
"""Returns the symmetric group of order n! """
352436
G = Set(g for g in itertools.permutations(range(n)))
353437
bin_op = Function(G * G, G, lambda x: tuple(x[0][j] for j in x[1]))
354438
return Group(G, bin_op)
439+
440+
def Dn(n):
441+
"""Returns the dihedral group of order 2n """
442+
G = Set("%s%d" % (l, x) for l in "RS" for x in xrange(n))
443+
def multiply_symmetries(x):
444+
l1, l2 = x[0][0], x[1][0]
445+
x1, x2 = int(x[0][1:]), int(x[1][1:])
446+
if l1 == "R":
447+
if l2 == "R":
448+
return "R%d" % ((x1 + x2) % n)
449+
else:
450+
return "S%d" % ((x1 + x2) % n)
451+
else:
452+
if l2 == "R":
453+
return "S%d" % ((x1 - x2) % n)
454+
else:
455+
return "R%d" % ((x1 - x2) % n)
456+
return Group(G, Function(G * G, G, multiply_symmetries))
457+

test/group_test.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def test_group_elem(self):
7070
self.assertEquals(b + c, a)
7171
self.assertEquals(a + c, b)
7272
for g in V:
73+
self.assertTrue(g in V)
7374
self.assertEquals(g, g)
7475
self.assertEquals(e * g, g)
7576
self.assertEquals(g * e, g)
@@ -107,10 +108,35 @@ def test_group_elem(self):
107108
self.assertEquals(h * g, GroupElem(h.elem, G) * g)
108109
self.assertEquals(g * h, g * GroupElem(h.elem, G))
109110

110-
def test_group_homomorphism(self):
111-
Z2, Z3 = Zn(2), Zn(3)
112-
Z32 = Z3 * Z2
113-
111+
def test_generators(self):
112+
for G in [Zn(1), Zn(2), Zn(5), Zn(8), Sn(1), Sn(2), Sn(3), \
113+
Dn(1), Dn(2), Dn(3), Dn(4)]:
114+
self.assertEquals(G, G.generate(G.generators()))
115+
self.assertEquals(G, G.generate(g.elem for g in G.generators()))
116+
117+
def test_find_isomorphism(self):
118+
f = Dn(2).find_isomorphism(Zn(2) * Zn(2))
119+
self.assertTrue(f is not None)
120+
self.assertTrue(f.is_isomorphism())
121+
self.assertEquals(f.kernel(), Dn(2).generate([Dn(2).e]))
122+
self.assertEquals(f.image(), Zn(2) * Zn(2))
123+
124+
self.assertFalse(Dn(12).is_isomorphic(Sn(4)))
125+
self.assertFalse(Sn(3).is_isomorphic(Zn(6)))
126+
self.assertFalse(Sn(3).is_isomorphic(Zn(4)))
127+
128+
for G in [Zn(1), Zn(2), Zn(5), Sn(1), Sn(3), Dn(1), Dn(4)]:
129+
self.assertTrue(G.is_isomorphic(G))
130+
f = G.find_isomorphism(G)
131+
self.assertTrue(f is not None)
132+
self.assertTrue(f.is_isomorphism())
133+
self.assertEquals(f.kernel(), G.generate([G.e]))
134+
self.assertEquals(f.image(), G)
135+
136+
self.assertTrue(Zn(1).is_isomorphic(Sn(1)))
137+
self.assertTrue(Zn(2).is_isomorphic(Sn(2)))
138+
self.assertTrue(Zn(2).is_isomorphic(Dn(1)))
139+
114140

115141
if __name__ == "__main__":
116142
unittest.main()

0 commit comments

Comments
 (0)