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:

from opycleid.monoids import TI_Group_Triads,PRL_Group mygroup = TI_Group_Triads() mygroup.get_operation("C","e") ['I^11']

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

from opycleid.monoids import TI_Group_Triads,PRL_Group mygroup = TI_Group_Triads() 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 ?

from opycleid.monoids import TI_Group_Triads,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 , with and . There are more concise ways to express these operations, in particular with the 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 , and , 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 and , with presentation , 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.add_vertices(["C","A","F"]) my_knet.add_edges([(0,1,"T^9"),(1,2,"I^2"),(0,2,"I^5")]) 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 is not equal to . 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) my_knet.add_vertices(["C","A","F"]) my_knet.add_edges([(0,1,"T^9"),(1,2,"I^2"),(0,2,"I^5")]) 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) my_knet.add_vertices(["C","A","F"]) my_knet.add_edges([(0,1,"T^9"),(1,2,"I^2"),(0,2,"I^5")]) 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.monoids import TI_Group_Triads,PRL_Group from opycleid.knetanalysis import KNet my_group = TI_Group_Triads() #Create a path K-Net for the famous Beethoven example my_knet = KNet(my_group) my_knet.add_vertices(["C","a","F","d","Bb","g","Eb","c","Gs","f","Cs","bb"]) 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.monoids import TI_Group_Triads,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.add_vertices(["C","a","F","d","Bb","g","Eb","c","Gs","f","Cs","bb"]) 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.monoids import TI_Group_Triads,PRL_Group 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.add_vertices(["D","bb","Fs","d"]) 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

Please enjoy Opycleid and don’t hesitate to share your thoughts about it !

I’m very intrigued by this & hope to look into TMT & Opycleid. I’m venturing into applying spatial data analysis tools to neo-Riemannian topologies, as part of an idea I’ve had about fast approximate motif discovery & mining large sets of musical data.

For spatial data analysis, you should check the work of Mattia Bergomi (Here and here among other examples… check out his PhD at IRCAM as well), Louis Bigo (his website here), and Dimitri Tymoczko.

Meanwhile, I’ll be glad to help with Opycleid.