# OPYCLEID: A Python package for transformational music theory

In this post, I introduced a small Python module for transformational theory. It was ok, but I quickly realized there would be scale-up problems, in particular if one wanted to introduce new groups or monoids in transformational music theory.

Recently, I started developping a new Python package for transformational music analysis, which would allow much more flexibility. Hence, I present you today with Opycleid.

Opycleid is on GitHub ! You can clone it here. I would greatly appreciate any feedback on it, whether you use it often, or want to propose some modifications.

But why “Opycleid” ? The ophicleide is a brass instrument of the XIXth century, whose name comes from the Greek and means “a serpent with keys”. Here’s mine :

It is indeed a serpent with keys, as it comes from an older instrument, the serpent, which looks like this:

What a perfect name for a Python package !

EDIT: Opycleid is regularly updated, and some of the code described below might not work anymore. The philosophy behind opycleid is still the same though.

So what can we do with Opycleid ? First a few key concepts…

Monoid Actions

Opycleid is shaped on the base class MonoidAction, which is a lightweight class for doing some computational algebra related to transformational music theory. It is defined in the Monoids.py file, which you can import from opycleid:

from opycleid.monoids import *

This class embarks three fundamental instance variables:

• objects, which is a dictionary of the musical objects the monoid acts on. The keys of this dictionary are the common name of the musical objects (for example “A”,”G”, etc.), and the values are indices in the corresponding operation matrices.
• generators, which is a dictionary of the generators of the operations of the monoid. The keys of this dictionary are the operation names, and the values are boolean 2D Numpy arrays as the operation matrices. The convention here is that, for an array M, M[i][j]=True if j is transformed into i by the operation M.
• operations, which is a dictionary of all operations in the monoid. The structure is the same as in generators.

After having defined the objects of your monoid, you can add a generator by the method add_generator(gen_name,gen_matrix), where gen_name is a string representing the name of your operation, while gen_matrix is the corresponding 2D boolean Numpy array.

When all the generators are added, you can ask for the complete generation of the monoid by calling generate_monoid(). It implements an (inefficient, but sufficient for the generally small number of objects considered in music theory) algorithm for generating all the operations in the monoid (by multiplication on the left), along with the corresponding operation name (by concatenation of strings. Note: there is no string rewriting here, so some operation names may be a bit goofy). The matrix formalism provides more flexibility and allows one to create any group or monoid they want.

Once your monoid action is created, you can call the apply_operation(operation_name,element_name) method to apply a specific operation to an element. Here, operation_name is a key in the dictionary operations, and element_name is a key in the dictionary objects.

You can also determine the possible operation between two objects by calling the get_operation(self,element_name_1,element_name_2) method, where element_name_1, element_name_2, are keys in the objects dictionary. This will return a list of valid operations between them.

The class MonoidAction also implements some basic methods relating to the algebraic structure of the monoid, for example by determining the operations of the same class of Green’s R or L relations (call element_Rclass(operation_name) or element_Lclass(operation_name)), or to determine all the possible R or L classes (call get_Rclasses() or get_Lclasses()).

I’m sure you would like to use it right away with our usual groups, like the T/I group, or the PRL group. Luckily for you, I have already coded these groups in Opycleid. Let’s see how it works. Assume you have a C major chord, and an E minor chord, and you want to find the unique operation relating them in the T/I group. Here is how you would do it:

mygroup.get_operation("C","e")
['I^11']

All right ! Now, what would be the result of applying the $I_4$ operation on a D major chord ?

mygroup.apply_operation("I^4","D")
['g']

Remember, if you don’t know what the operations or objects are, you can always peak into the dictionaries:

mygroup.operations.keys()
['I^10', 'I^11', 'I^8', 'I^9', 'I^4', 'I^5', 'I^6', 'I^7', 'I^0', 'I^1', 'I^2', 'I^3', 'T^5', 'T^4', 'T^7', 'T^6', 'T^1', 'T^3', 'T^2', 'T^9', 'T^8', 'e', 'T^11', 'T^10']
mygroup.objects.keys()
['gs', 'eb', 'B', 'cs', 'Fs', 'bb', 'Bb', 'A', 'C', 'fs', 'E', 'D', 'G', 'F', 'e', 'a', 'c', 'b', 'Gs', 'd', 'g', 'f', 'Eb', 'Cs']

Now, what about the same for the PRL group ?

mygroup = PRL_Group()
mygroup.get_operation("C","e")
['(RL)^11R']

Note that the operations in the PRL group are given with the normal form $(RL)^pR^q$, with $p \in \{0,...,11\}$ and $q \in \{0,1\}$. There are more concise ways to express these operations, in particular with the $P$ operation.

Of course, your group might not be simply transitive. For example, what about the T/I group on pitch classes ?

from opycleid.monoids import TI_Group_PC
mygroup = TI_Group_PC()
mygroup.get_operation("D","F")
['I^7', 'T^3']

The Monoids.py file also includes the Noll monoid, i.e. the monoid acting on pitch classes generated by $f(x)=3x+7$, and $g(x)=8x+4$, which had been discussed here.

from opycleid.monoids import Noll_Monoid
mygroup=Noll_Monoid()
mygroup.operations.keys()
['e', 'g', 'f', 'gg', 'gf', 'ggf', 'ff', 'fg']
mygroup.get_operation("F","C")
['gf']

It finally includes a simply transitive group acting on triads, generated by two operations $T$ and $J$, with presentation $\langle T,J \mid T^12=1, J^2=T^6, J^{-1}TJ=T^{-1} \rangle$, and its action on the left and on the right.

from opycleid.monoids import Left_Z3Q8_Group,Right_Z3Q8_Group
mygroup=Left_Z3Q8_Group()
mygroup.get_operation("C","e")
['J^4']
mygroup=Right_Z3Q8_Group()
mygroup.get_operation("C","e")
['J^8']

K-Net Analysis

If you would like to perform analysis on a certain number of musical objects, then the KNet class in the KNetAnalysis.py file is for you. It is a class for creating a simple labelled graph, having objects of a MonoidAction as vertices, and operations as its labelled arrows. Let’s say we want to create the following (invalid, as we will see below) K-Net:

I do realize I have not defined yet in this blog what a “K-Net” is. I hope it should be clear enough with this simple description. I promise you there will be further posts on this subject. Anyway, here is how you would do it in Opycleid:

from opycleid.monoids import TI_Group_PC
from opycleid.knetanalysis import KNet

my_group = TI_Group_PC()
my_knet = KNet(my_group)
my_knet.print_knet()
T^9
C--->A

T^8
A--->F

I^5
C--->F

When you create a KNet class, you have to specify a corresponding MonoidAction. You then add the vertices, and then specify the edges as a list of tuples, the first element of which indicating the index of the starting vertex, the second element being the index of the ending vertex, and the last element being the operation (Opycleid will tell you if the operation between the two musical objects is a valid one or not). The method print_KNet() prints all the edges in a clear way.

Now, the above K-Net is not a valid one, as it is not commutative. Indeed $T_9T_8$ is not equal to $I_5$. You can check the validity of a K-Net by calling the is_valid() method:

from opycleid.monoids import TI_Group_PC
from opycleid.knetanalysis import KNet

my_group = TI_Group_PC()
my_knet = KNet(my_group)
print my_knet.is_valid()
False

Important: this method will go in an endless loop if you have defined a cyclic graph. There is no current detection of cycles in Opycleid !

The following K-Net is a valid, commutative one:

And indeed:

from opycleid.monoids import TI_Group_PC
from opycleid.knetanalysis import KNet

my_group = TI_Group_PC()
my_knet = KNet(my_group)
print my_knet.is_valid()
True

Now, sometimes it can be boring to specify all edges one by one. We have seen some examples where the K-Net could be a simple path graph, or a complete graph. The KNet class provides methods for automatically creating such graphs.

For example, the Beethoven example seen here, is a typical path graph. Let’s see how we could analyze the first chords with Opycleid:

from opycleid.knetanalysis import KNet

#Create a path K-Net for the famous Beethoven example
my_knet = KNet(my_group)
my_knet.path_knet_from_vertices()
my_knet.print_knet()
I^4
C--->a

I^9
a--->F

I^2
F--->d

I^7
d--->Bb

I^0
Bb--->g

I^5
g--->Eb

I^10
Eb---->c

I^3
c--->Gs

I^8
Gs--->f

I^1
f--->Cs

I^6
Cs--->bb

Not so interesting in this group… but in the PRL group:

from opycleid.knetanalysis import KNet

my_group = PRL_Group()

#Create a path K-Net for the famous Beethoven example
my_knet = KNet(my_group)
my_knet.path_knet_from_vertices()
my_knet.print_knet()

R
C->a

(RL)^11R
a-------->F

R
F->d

(RL)^11R
d-------->Bb

R
Bb->g

(RL)^11R
g-------->Eb

R
Eb->c

(RL)^11R
c-------->Gs

R
Gs->f

(RL)^11R
f-------->Cs

R
Cs->bb

And you can immediately see the regularity in the progression.

If you want to analyze the Herrman’s example presented here in a complete graph, here is how you would do it:

from opycleid.knetanalysis import KNet

my_group = PRL_Group()

#Create a complete K-Net for the Herrmann example
my_knet = KNet(my_group)
my_knet.complete_knet_from_vertices()
my_knet.print_knet()

(RL)^7R
D------->bb

(RL)^4
D------>Fs

(RL)^3R
D------->d

(RL)^11R
bb-------->Fs

(RL)^8
bb------>d

(RL)^7R
Fs------->d