Group Theory is usually presented as a branch of exploration that started with Galois Theory, which in turn traces to searching for all roots of a polynomial, or at least knowing in principle how many one has.
The concept of a Group only makes sense in connection with other concepts: Ring and Field.
The School of Tomorrow does not aim to provide exhaustive treatment of topics touched on. You're in a web of windows, frames of reference. The School of Tomorrow is a hub of hyperlinks, floating in cyberspace (Cyberia).
One of the most abstract ways to understand Finite Groups, in a way that exercises coding skills, is to use permutations. A permutation is a reordering of elements. Think of pairing every lowercase letter with every other lowercase letter, throwing in a space.
from string import ascii_lowercase as letters
from random import shuffle
members = list(letters + " ")
mem_copy = members[:]
shuffle(mem_copy)
p = {key:value for key, value in zip(members, mem_copy)}
p
{'a': 's', 'b': 'f', 'c': 'l', 'd': 'n', 'e': 'x', 'f': 'a', 'g': 'b', 'h': 'i', 'i': 't', 'j': 'j', 'k': ' ', 'l': 'r', 'm': 'y', 'n': 'z', 'o': 'w', 'p': 'k', 'q': 'u', 'r': 'g', 's': 'c', 't': 'p', 'u': 'd', 'v': 'm', 'w': 'e', 'x': 'q', 'y': 'o', 'z': 'v', ' ': 'h'}
For a more complete implementation of a Permutation type, you might want to check these other resources:
In Group Theory, we'll visit the idea of totatives, as we do in Number Theory as well. The totatives of N, multiplied modulo N, form a group. The totatives of N modulo N, when N is prime, form a field.
The properties of a Group are often summarized with the acronym CAIN, which is somewhat a worplay on Abel, one of the founders of Group Theory.
CAIN:
Here's a simple integer-like class that builds in the modulus, so we can use the standard operators yet have the operation carried out modulo N.
We could flesh this out more. Subtracting is adding the additive inverse. Division is multiplying by the multiplicative inverse. That's why these two operations are, in a sense, "syntactic sugar" once we've defined an inverse vis-a-vis each of these operations.
class Mod:
_modulus = 12
def __init__(self, n):
self.value = n % self._modulus
def __eq__(self, other):
return self.value == other.value % self._modulus
def __repr__(self):
return "({} mod {})".format(self.value, self._modulus)
def __add__(self, other):
return type(self)(self.value + other.value)
def __mul__(self, other):
return type(self)(self.value * other.value)
def __pow__(self, n):
return type(self)(pow(self.value, n, self._modulus))
def __hash__(self):
return self.value
You'll find more details on the code below in Number Theory.
def gcd(a, b):
while b:
b, a = a % b, b
return a
def totatives(n):
return [totative for totative in range(1, n)
if gcd(totative, n) == 1]
totatives(12)
[1, 5, 7, 11]
group_12 = map(Mod, totatives(12))
list(group_12)
[(1 mod 12), (5 mod 12), (7 mod 12), (11 mod 12)]
The itertools
module (Standard Library) gives us a Cartesian Product, meaning we may quickly construct a Cayley Table for our group.
from itertools import product
group_12 = map(Mod, totatives(12))
n = list(group_12)
n
[(1 mod 12), (5 mod 12), (7 mod 12), (11 mod 12)]
Iterators get "used up" in Python, meaning once you've traversed an iterator, you need to recreate it from scratch. The name table
points to a listing of every member paired both with itself, and every other member of the group.
The list comprehension makes multiplication the operator for this Cayley Table, and shows Closure, meaning every outcome, every product, is a member of the Group.
table = product(n, n)
[a * b for a, b in table] # closure
[(1 mod 12), (5 mod 12), (7 mod 12), (11 mod 12), (5 mod 12), (1 mod 12), (11 mod 12), (7 mod 12), (7 mod 12), (11 mod 12), (1 mod 12), (5 mod 12), (11 mod 12), (7 mod 12), (5 mod 12), (1 mod 12)]
table
<itertools.product at 0x10d7a8c80>
list(table)
[]
What happens if the modulus N is prime? Then by definition, every number from 1 up to N-1 is a totative of N. The totient of N, where N is prime, is N-1.
In this case, addition joins multiplication as an operator with group properties, while the distributive property connects the two operations thusly:
$$ A * (B + C) = (A * B) + (A * C) $$That means we have a Field.
A Ring is between a Group and a Field in that it features two operations, but without inverses and/or closure for one of them.
Mod._modulus = 13
group_13 = map(Mod, totatives(13))
n = list(group_13) + [Mod(0)] # 0 is added yet has no multiplicative inverse
table = product(n, n) # Cayley Table
sums = [a + b for a, b in table] # check addition this time
all([the_sum in n for the_sum in sums]) # closure
True
Before being too quick to dive into some definition, lets start with etymology and talk about Al Jabr (الجبر) or bone-setting (metaphorically). Al Jabr is perhaps a root for both jabber and gibberish, which is what an algebra looks like to many, especially with algorithms added, from al-Khwarizmi (محمد بن موسی خوارزمی).
An algebra provides a way of operating with elements according to familiar rules of addition, multiplication and so on. If group, ring and field properties emerge, and/or some kind of "vector space", then you have an algebra on your hands.
import numpy as np
Lets jump right in to the complex numbers and write a generator for matrices of this form, where a, b are any complex numbers:
\begin{bmatrix} a & -b \\ b^{*} & a^{*} \end{bmatrix}The cryptic little asterisk superscript means "conjugate of". Matrices of this form will be closed under matrix multiplication, giving us the germ of an algebra.
Lets do some Python:
a = complex(1, 2)
a_star = a.conjugate()
from numpy.random import randint
def M():
"""Generates complex number matrices of a specific form.
Note Quaternions at this web page:
http://www.zipcon.net/~swhite/docs/math/quaternions/matrices.html
Expects numpy imported as np
"""
while True:
# using arbitrary ranges, these could be argument-driven
a = complex(randint(100, 199), randint(200, 299))
b = complex(randint(100, 199), randint(200, 299))
a_star = a.conjugate()
b_star = b.conjugate()
M = np.zeros((2,2), dtype=complex) # initialize to all complex zeros
# being explicit about what's going in each cell of a 2x2 matrix
M[0,0] = a
M[0,1] = -b
M[1,0] = b_star
M[1,1] = a_star
yield M
ms = M()
next(ms)
array([[ 150.+241.j, -195.-239.j], [ 195.-239.j, 150.-241.j]])
Matrix_A = next(ms)
Now we can start testing the claim of Closure (see above). Any random matrix of this form, times another of the same form, including itself, should give another matrix in that same form. That's what Closure means (you don't escape the group by means of its internally defined operation -- matrix multiplication in this case).
Matrix_A @ Matrix_A
array([[-117104.+105840.j, -46872. -91854.j], [ 46872. -91854.j, -117104.-105840.j]])
So far so good. One may prove closure in this case, by means of Algebra.
Answering a Matrix-related question on Quora, with this code