05.31.07
Pitch class set operations: pcops.py
Previously, I wrote a Python module to implement Pitch Class Sets, pcset.py. I have now extended that module with another: pcops.py, a module of operations for pitch class sets. These are some of the functions:
- copy(A) — returns a new pitch class set, a copy of A.
- complement(A) — returns a new pitch class set, the complement of A.
- Comparison functions: exact_equality, set_equality, same_normal, is_transposition, is_inverse, same_prime.
- Relationship functions: is_complement, is_prime_complement, is_subset, is_prime_subset.
- Similarity relations: Rp, Rp_prime, R0, R1, R2, Zpair.
Basically, the term “prime” in the relationship or similarity functions extends the meaning of the base function. For example:
>>> from pcset import PitchClassSet as Pcs
>>> from pcops import *
>>> majorscale = Pcs("C D E F G A B C")
>>> g7 = Pcs("G B D F")
>>> b7 = Pcs("B D# F# A")
>>> is_subset(majorscale,g7)
True
>>> is_subset(majorscale,b7)
False
>>> is_prime_subset(majorscale,b7)
True
The notes in the G7 chord are a strict subset of the C major scale; the notes in B7 are not. However, the “prime” version of the function checks if any transposition, inversion, or combination of the two operations on B7 will make it a subset of the C major scale. This is useful in analyzing the relationships between sets in prime form.
Here are the listings for two new modules. In order to get this to work, you need version 1.1 of pcset.py (and, optionally, of the test suite, test_pcset.py). Below, I’ve included not only pcops.py, but its unit testing suite as well (test_pcops.py):
pcops.py:
# prototype
from pcset import PitchClassSet as Pcs
# ----------------------------------------------------------------- utility
def copy(a):
"""
Returns a copy of an existing pitch class set as a new object.
"""
return Pcs(a.toList())
def complement(a):
"""
Returns a new pitch class set which is the complement of the
original set -- it contains all the elements which the original
does not.
"""
anti = []
for n in range(11):
if n not in a.toList():
anti.append(n)
return Pcs(anti)
def _setform(a,b):
return set(a.toList()),set(b.toList())
# ---------------------------------------------------------------- equality
def exact_equality(a,b):
"""
Test if two pitch class sets are *exactly* equal,
including the ordering of the elements.
"""
return a.toList() == b.toList()
def set_equality(a,b):
"""
Test if two pitch class sets are equal. The order
of the elements is irrelevant, but the elements must
be the same in both sets.
"""
return set(a.toList()) == set(b.toList())
def same_normal(a,b):
"""
Test if two pitch class sets have the same normal form.
"""
a_norm, b_norm = copy(a), copy(b)
a_norm.normalize()
b_norm.normalize()
return a_norm.toList() == b_norm.toList()
def is_transposition(a,b):
"""
Tests if two pitch class sets differ only by transposition.
"""
if a.card() != b.card():
return False
if a.card() < 2:
return True
differences = []
for x, y in zip(a.toList(),b.toList()):
differences.append((x-y) % 12)
# The corresponding element differences must all be equal.
diffset = set(differences)
return len(diffset) == 1
def is_inverse(a,b):
"""
Tests if two pitch class sets are inverses.
"""
if a.card() != b.card():
return False
if a.card() < 2:
return True
sums = []
for x, y in zip(a.toList(),b.toList()):
sums.append((x+y) % 12)
sumset = set(sums)
return len(sumset) == 1
def same_prime(a,b):
"""
Test if two pitch class sets have the same prime form.
"""
a_prime, b_prime = copy(a), copy(b)
a_prime.prime()
b_prime.prime()
return a_prime.toList() == b_prime.toList()
# ----------------------------------------------------------- relationships
def is_complement(a,b):
"""
Determines if set a and set b are complementary.
"""
a_set, b_set = _setform(a,b)
union = a_set.union(b_set)
return len(union) == 12
def is_prime_complement(a,b):
"""
Determines if b, or any transposition / inversion of b, is
complementary to set a. Note that b may not be a direct
complement of a.
"""
not_a = complement(a)
return same_prime(not_a,b)
def is_subset(a,b):
"""
Determines if b is a subset of a. The first set must contain all
of the elements of the second one.
"""
if a.card() < b.card():
return False
a_set, b_set = _setform(a,b)
return b_set.issubset(a_set)
def is_prime_subset(a,b):
"""
Determines if b, or any transposition / inversion of b, is
a subset of a. Note that b may not be a direct subset of a.
"""
if a.card() < b.card():
return False
# Brute force algorithm. I'm hoping to find a better one.
# Systematically tests if any of the transpositions and inversions
# of b are subsets of a.
b_copy = copy(b)
for t in range(12):
b_copy.transpose(t)
if is_subset(a,b_copy):
return True
b_copy.invert()
if is_subset(a,b_copy):
return True
return False
# -------------------------------------------------------------- similarity
def Rp(a,b):
"""
Determines if two sets of the same cardinality differ by only one tone.
The remaining elements must match up exactly.
"""
if a.card() != b.card():
return False
a_set, b_set = _setform(a,b)
simset = a_set.intersection(b_set)
return len(simset) + 1 == a.card()
def Rp_prime(a,b):
"""
Determines if it is possible, through inversion, transposition, or a
combination of the two, to match all the elements of sets a and b
-- except for one element in each set.
"""
if a.card() != b.card():
return False
# brute force algorithm again. See is_prime_subset.
b_copy = copy(b)
for t in range(12):
b_copy.transpose(t)
if Rp(a,b_copy):
return True
b_copy.invert()
if Rp(a,b_copy):
return True
return False
def R0(a,b):
"""
Checks for R0, or 'minimum similarity': when the interval vectors
of two sets have no digits in common.
"""
if a.card() != b.card():
return False
for x, y in zip(a.ivec(),b.ivec()):
if x == y:
return False
return True
def R1(a,b):
"""
Checks for R1, one type of 'maximum similarity': the interval vectors
should differ only by interchange of two values.
"""
if a.card() != b.card():
return False
misfits = []
for x, y in zip(a.ivec(),b.ivec()):
if x != y:
misfits.append((x,y))
if len(misfits) == 2:
first, second = misfits[0], misfits[1]
if first[0] == second[1] and first[1] == second[0]:
return True
return False
def R2(a,b):
"""
Checks for R2, another type of 'maximum similarity': the interval vectors
differ only by two values, but relation R1 does not hold -- the two misfit
values are not related by interchange.
"""
if a.card() != b.card():
return False
misfits = []
for x, y in zip(a.ivec(),b.ivec()):
if x != y:
misfits.append((x,y))
if len(misfits) == 2:
first, second = misfits[0], misfits[1]
if first[0] == second[1] and first[1] == second[0]:
return False
else:
return True
return False
def Zpair(a,b):
"""
Checks if two sets are a Z pair, that is, if they have the same
interval vector. Note this does not discriminate against testing
a set against itself -- Zpair(a,a) will return True.
"""
return a.ivec() == b.ivec()
test_pcops.py:
#!/usr/bin/env python
from pcops import *
import unittest
class UtilityTests(unittest.TestCase):
def setUp(self):
self.allint1 = Pcs([0,1,3,7])
self.allint2 = Pcs([0,1,4,6])
self.majorscale = Pcs("C D E F G A B C")
self.pentatonic = Pcs("A C D E G")
def test_copy(self):
a = copy(self.allint1)
self.assertEqual(a.toList(),self.allint1.toList())
def test_complement(self):
a = complement(self.majorscale)
self.assert_(same_prime(a,self.pentatonic))
class EqualityTests(unittest.TestCase):
def setUp(self):
self.cscale = Pcs("C D E F G A B C")
self.ionian = Pcs("C D E F G A B C")
self.phrygian = Pcs("E F G A B C D E")
self.jazzminor = Pcs("C D Eb F G A B C")
self.ebscale = Pcs("Eb F G Ab Bb C D Eb")
self.majortriad = Pcs("Eb G Bb")
self.minortriad = Pcs("C Eb G")
self.majorcopy = copy(self.majortriad)
self.diminished = Pcs("B D F")
def test_exact_equality_yes(self):
self.assert_(exact_equality(self.cscale,self.ionian))
def test_exact_equality_no(self):
self.failIf(exact_equality(self.ionian,self.phrygian))
def test_set_equality_yes(self):
self.assert_(set_equality(self.cscale,self.phrygian))
def test_set_equality_no(self):
self.failIf(set_equality(self.cscale,self.jazzminor))
def test_same_normal_yes(self):
self.assert_(same_normal(self.cscale,self.phrygian))
def test_same_normal_no(self):
self.failIf(same_normal(self.cscale,self.jazzminor))
def test_is_transposition_yes(self):
self.assert_(is_transposition(self.cscale,self.ebscale))
def test_is_transposition_no(self):
self.failIf(is_transposition(self.cscale,self.phrygian))
def test_is_inverse_yes_easy(self):
self.majorcopy.invert()
self.assert_(is_inverse(self.majortriad,self.majorcopy))
def test_is_inverse_yes_tricky_TnI(self):
self.majorcopy.TnI(3)
self.assert_(is_inverse(self.majortriad,self.majorcopy))
def test_is_inverse_yes_tricky_Ixy(self):
self.majorcopy.Ixy('A','B')
self.assert_(is_inverse(self.majortriad,self.majorcopy))
def test_is_inverse_no(self):
self.failIf(is_inverse(self.majortriad,self.minortriad))
def test_same_prime_yes(self):
self.assert_(same_prime(self.majortriad,self.minortriad))
def test_same_prime_no(self):
self.failIf(same_prime(self.majortriad,self.diminished))
class SetRelationTests(unittest.TestCase):
def setUp(self):
self.a = Pcs("C D E F G A B C")
self.b = Pcs("E G B D A")
self.c = Pcs("F Ab C Eb Bb")
self.d = Pcs("C Eb G B D")
self.not_a = complement(self.a)
def test_is_complement_yes(self):
self.assert_(is_complement(self.a,self.not_a))
def test_is_complement_no(self):
self.failIf(is_complement(self.a,self.b))
def test_is_prime_complement_yes(self):
self.assert_(is_prime_complement(self.a,self.b))
def test_is_prime_complement_no(self):
self.failIf(is_prime_complement(self.a,self.d))
def test_is_subset_yes(self):
self.assert_(is_subset(self.a,self.b))
def test_is_subset_no(self):
self.failIf(is_subset(self.a,self.c))
def test_is_prime_subset_yes(self):
self.assert_(is_prime_subset(self.a,self.c))
def test_is_prime_subset_no(self):
self.failIf(is_prime_subset(self.a,self.d))
class SimilarityTests(unittest.TestCase):
def setUp(self):
self.a = Pcs("A C E G B D")
self.b = Pcs("C E G B D F")
self.c = copy(self.b)
self.c.TnI(9)
self.d = Pcs("C Eb G B D F")
self.forte42 = Pcs([0,1,2,4])
self.forte413 = Pcs([0,1,3,6])
self.forte43 = Pcs([0,1,3,4])
self.forte510 = Pcs([0,1,3,4,6])
self.forte5Z12 = Pcs([0,1,3,5,6])
self.forte5Z36 = Pcs([0,1,2,4,7])
def test_Rp_yes(self):
self.assert_(Rp(self.a,self.b))
def test_Rp_no(self):
self.failIf(Rp(self.a,self.d))
def test_Rp_prime_yes(self):
self.assert_(Rp_prime(self.a,self.c))
def test_Rp_prime_no(self):
self.failIf(Rp(self.a,self.d))
def test_R0_yes(self):
self.assert_(R0(self.forte42,self.forte413))
def test_R0_no(self):
self.failIf(R0(self.forte42,self.forte43))
def test_R1_yes(self):
self.assert_(R1(self.forte42,self.forte43))
def test_R1_no(self):
self.failIf(R1(self.forte42,self.forte413))
def test_R2_yes(self):
self.assert_(R2(self.forte510,self.forte5Z12))
def test_R2_no_actually_R0(self):
self.failIf(R2(self.forte42,self.forte413))
def test_R2_no_actually_R1(self):
self.failIf(R2(self.forte42,self.forte43))
def test_Zpair_yes(self):
self.assert_(Zpair(self.forte5Z12,self.forte5Z36))
def test_Zpair_no(self):
self.failIf(Zpair(self.forte5Z12,self.forte510))
class EmptyOperationTests(unittest.TestCase):
def setUp(self):
self.a = Pcs([])
self.b = Pcs([])
self.chromo = Pcs(range(11))
def test_copy(self):
a1 = copy(self.a)
self.assertEqual(self.a.toList(),a1.toList())
def test_complement_empty(self):
c = complement(self.a)
self.assert_(exact_equality(c,self.chromo))
def test_complement_chromatic(self):
c = complement(self.chromo)
self.assert_(exact_equality(c,self.a))
def test_exact_equality(self):
self.assert_(exact_equality(self.a,self.b))
def test_set_equality(self):
self.assert_(set_equality(self.a,self.b))
def test_same_normal(self):
self.assert_(same_normal(self.a,self.b))
def test_is_transposition(self):
self.assert_(is_transposition(self.a,self.b))
def test_is_inverse(self):
self.assert_(is_inverse(self.a,self.b))
def test_same_prime(self):
self.assert_(same_prime(self.a,self.b))
def test_is_complement(self):
self.failIf(is_complement(self.a,self.b))
def test_is_prime_complement(self):
self.failIf(is_prime_complement(self.a,self.b))
def test_is_subset(self):
self.assert_(is_subset(self.a,self.b))
def test_is_prime_subset(self):
self.assert_(is_prime_subset(self.a,self.b))
def test_Rp(self):
self.failIf(Rp(self.a,self.b))
def test_Rp_prime(self):
self.failIf(Rp_prime(self.a,self.b))
def test_R0(self):
self.failIf(R0(self.a,self.b))
def test_R1(self):
self.failIf(R1(self.a,self.b))
def test_R2(self):
self.failIf(R2(self.a,self.b))
def test_Zpair(self):
self.assert_(Zpair(self.a,self.b))
class SubtleAlterationTests(unittest.TestCase):
"""
Tests for unintentional alteration of original
values during binary operations. Also, any function
that is 'touched' by the normal and prime functions
is checked for accidental alteration of the originals.
"""
def setUp(self):
self.a = Pcs("D F# A")
self.b = Pcs("A C E")
def test_copy_change_original(self):
b = copy(self.a)
self.a.prime()
self.assertNotEqual(b.toList(),self.a.toList())
def test_copy_change_copy(self):
b = copy(self.a)
b.prime()
self.assertNotEqual(b.toList(),self.a.toList())
def test_same_normal(self):
throwaway = same_normal(self.a,self.b)
self.assert_(exact_equality(self.a,Pcs("D F# A")))
self.assert_(exact_equality(self.b,Pcs("A C E")))
def test_same_prime(self):
throwaway = same_prime(self.a,self.b)
self.assert_(exact_equality(self.a,Pcs("D F# A")))
self.assert_(exact_equality(self.b,Pcs("A C E")))
def test_is_prime_subset(self):
throwaway = is_prime_subset(self.a,self.b)
self.assert_(exact_equality(self.a,Pcs("D F# A")))
self.assert_(exact_equality(self.b,Pcs("A C E")))
if __name__ == '__main__':
unittest.main()
Software examples on this page are licensed under the CC-GNU GPL.