Add Forte prime form, fix darkest form and Rahn errors, use classmethods
- add the packed_left and prime_form property for Allen Forte prime forms - add type annotations - add from_string convenience class factory method - fix darkest_form to always produce the same result using left-packing - amend tests to correct values (eliminated duplicates in bright lists) - produce lists of cases where Rahn and Forte primes are not the "darkest" forms (smallest sum of pitch classes)
This commit is contained in:
parent
d6b4b07d86
commit
0bf0531fd6
3 changed files with 152 additions and 67 deletions
|
|
@ -1,4 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import re
|
||||
|
||||
|
||||
class cache_property:
|
||||
"""
|
||||
|
|
@ -19,7 +22,7 @@ class SetClass(list):
|
|||
Musical set class, containing zero or more pitch classes.
|
||||
"""
|
||||
|
||||
def __init__(self, *args: int, tonality: int = 12):
|
||||
def __init__(self, *args: int, tonality: int = 12) -> None:
|
||||
"""
|
||||
Instantiate a Class Set with a series of integers. Each pitch class is "normalised" by
|
||||
modulo the tonality; the set is sorted in ascending order; and values "rotated" until the
|
||||
|
|
@ -57,7 +60,7 @@ class SetClass(list):
|
|||
return sum([hash(i) for i in self.pitch_classes])
|
||||
|
||||
@property
|
||||
def pitch_classes(self):
|
||||
def pitch_classes(self) -> list:
|
||||
return list(self)
|
||||
|
||||
@cache_property
|
||||
|
|
@ -79,7 +82,7 @@ class SetClass(list):
|
|||
return sum(self.pitch_classes)
|
||||
|
||||
@cache_property
|
||||
def adjacency_intervals(self):
|
||||
def adjacency_intervals(self) -> list:
|
||||
"""Adjacency intervals between the pitch classes, used for Leonard notation subscripts."""
|
||||
if not self.pitch_classes:
|
||||
return list()
|
||||
|
|
@ -93,7 +96,7 @@ class SetClass(list):
|
|||
return intervals
|
||||
|
||||
@cache_property
|
||||
def z_relations(self):
|
||||
def z_relations(self) -> list:
|
||||
"""
|
||||
Return all distinct set classes with the same interval vector (Allan Forte: "Z-related").
|
||||
For example, Forte 4-Z15 {0,1,4,6} and Forte 4-Z29 {0,1,3,7} both have iv⟨1,1,1,1,1,1⟩ but
|
||||
|
|
@ -134,7 +137,7 @@ class SetClass(list):
|
|||
return min(SetClass.ordered_interval(a, b), SetClass.ordered_interval(b, a))
|
||||
|
||||
@cache_property
|
||||
def versions(self):
|
||||
def versions(self) -> list:
|
||||
"""
|
||||
Returns all possible zero-normalised versions (clock rotations) of this set class,
|
||||
sorted by brightness. See Rahn (1980) Set types, Tₙ
|
||||
|
|
@ -152,19 +155,12 @@ class SetClass(list):
|
|||
return versions
|
||||
|
||||
@cache_property
|
||||
def rahn_normal_form(self):
|
||||
def rahn_normal_form(self) -> SetClass:
|
||||
"""
|
||||
Return the Rahn normal form of the set class; Leonard describes this as "most dispersed from
|
||||
the right". Find the smallest outside interval, and proceed inwards from the right until one
|
||||
result remains. See Rahn (1980), p. 33
|
||||
"""
|
||||
# Set class {0,2,4,5,8,9} has four darkest versions:
|
||||
# {0,2,4,5,8,9} B = 28
|
||||
# {0,1,4,6,8,9} B = 28
|
||||
# {0,2,3,6,7,10} B = 28
|
||||
# {0,1,4,5,8,10} B = 28
|
||||
# {0,3,5,7,8,11} B = 34
|
||||
# {0,3,4,7,9,11} B = 34
|
||||
def _most_dispersed(versions, n):
|
||||
return [i for i in versions if i.pitch_classes[-n] == min([i.pitch_classes[-n] for i in versions])]
|
||||
|
||||
|
|
@ -176,15 +172,69 @@ class SetClass(list):
|
|||
return versions[0]
|
||||
|
||||
@cache_property
|
||||
def darkest_form(self):
|
||||
def packed_left(self) -> SetClass:
|
||||
"""
|
||||
Returns the version with the smallest brightness value.
|
||||
TODO: How to break a tie? Or return all matches in a tuple? Sorted by what?
|
||||
Return the form of the set class that is most packed to the left (smallest adjacency
|
||||
intervals to the left). Find the smallest adjacency interval, and proceed towards the right
|
||||
until one result remains.
|
||||
"""
|
||||
return self.versions[0] if self.versions else self
|
||||
def _most_packed(versions, n):
|
||||
return [i for i in versions if i.pitch_classes[n] == min([i.pitch_classes[n] for i in versions])]
|
||||
|
||||
versions = self.versions
|
||||
n = 0
|
||||
while len(versions) > 1:
|
||||
versions = _most_packed(versions, n)
|
||||
n += 1
|
||||
return versions[0]
|
||||
|
||||
@cache_property
|
||||
def brightest_form(self):
|
||||
def prime_form(self) -> SetClass:
|
||||
"""
|
||||
Return the prime form of the set class. Find the forms with the smallest outside interval,
|
||||
and if necessary chose the form most packed to the left (the smallest adjacency intervals
|
||||
working from left to right).
|
||||
Allen Forte describes the algorithm in his book *The Structure of Atonal Music* (1973) in
|
||||
section 1.2 (pp. 3-5), citing Milton Babbitt (1961).
|
||||
"""
|
||||
def _most_packed(versions, n):
|
||||
return [i for i in versions if i.pitch_classes[n] == min([i.pitch_classes[n] for i in versions])]
|
||||
|
||||
# Requirement 1: select forms where the largest pitch class has the smallest value
|
||||
smallest_span = min([i.pitch_classes[-1] for i in self.versions])
|
||||
versions = [i for i in self.versions if i.pitch_classes[-1] == smallest_span]
|
||||
|
||||
# Requirement 2: select forms most packed to the left
|
||||
n = 0
|
||||
while len(versions) > 1:
|
||||
versions = _most_packed(versions, n)
|
||||
n += 1
|
||||
return versions[0]
|
||||
|
||||
@cache_property
|
||||
def darkest_form(self) -> SetClass:
|
||||
"""
|
||||
Returns the version with the smallest brightness value, most packed to the left.
|
||||
"""
|
||||
if not self.versions:
|
||||
return self
|
||||
|
||||
B = min(i.brightness for i in self.versions)
|
||||
versions = [i for i in self.versions if i.brightness == B]
|
||||
if len(versions) == 1:
|
||||
return versions[0]
|
||||
|
||||
def _most_packed(versions, n):
|
||||
return [i for i in versions if i.pitch_classes[n] == min([i.pitch_classes[n] for i in versions])]
|
||||
|
||||
n = 0
|
||||
while len(versions) > 1:
|
||||
versions = _most_packed(versions, n)
|
||||
n += 1
|
||||
return versions[0]
|
||||
|
||||
@cache_property
|
||||
def brightest_form(self) -> SetClass:
|
||||
"""
|
||||
Returns the version with the largest brightness value.
|
||||
TODO: How to break a tie? Or return all matches in a tuple? Sorted by what?
|
||||
|
|
@ -192,7 +242,7 @@ class SetClass(list):
|
|||
return self.versions[-1] if self.versions else self
|
||||
|
||||
@cache_property
|
||||
def inversion(self):
|
||||
def inversion(self) -> SetClass:
|
||||
"""
|
||||
Returns the inversion of this set class, equivalent of reflection through the 0 axis on a
|
||||
clock diagram.
|
||||
|
|
@ -200,7 +250,7 @@ class SetClass(list):
|
|||
return SetClass(*[self.tonality - i for i in self.pitch_classes], tonality=self.tonality)
|
||||
|
||||
@cache_property
|
||||
def is_symmetrical(self):
|
||||
def is_symmetrical(self) -> bool:
|
||||
"""
|
||||
Returns whether this set class is symmetrical upon inversion, for example Forte 5-Z17:
|
||||
{0,1,2,5,9} → {0,4,8,9,11}
|
||||
|
|
@ -208,7 +258,7 @@ class SetClass(list):
|
|||
return self.darkest_form == self.inversion.darkest_form
|
||||
|
||||
@cache_property
|
||||
def complement(self):
|
||||
def complement(self) -> SetClass:
|
||||
"""
|
||||
Returns the set class containing all pitch classes absent in this one (rotated so the
|
||||
smallest is 0).
|
||||
|
|
@ -257,7 +307,16 @@ class SetClass(list):
|
|||
|
||||
# Class methods -----------------------------------------------------------
|
||||
|
||||
def all_of_cardinality(cardinality: int, tonality: int = 12) -> set:
|
||||
@classmethod
|
||||
def from_string(this, string: str) -> SetClass:
|
||||
"""
|
||||
Attempt to create a SetClass from any string containing a sequence of zero or more integers.
|
||||
A useful convenience function, e.g. SetClass.from_string('{0,3,7,9}')
|
||||
"""
|
||||
return SetClass(*re.findall(r'\d+', string))
|
||||
|
||||
@classmethod
|
||||
def all_of_cardinality(cls, cardinality: int, tonality: int = 12) -> set:
|
||||
"""
|
||||
Returns all set classes of a given cardinality. Tonality can be specified, default is 12.
|
||||
Warning: high values of tonality can take a long time to calculate (T=24 takes about a
|
||||
|
|
@ -273,35 +332,53 @@ class SetClass(list):
|
|||
|
||||
return set(SetClass(*i, tonality=tonality) for i in _powerset(range(tonality)) if len(i) == cardinality and 0 in i)
|
||||
|
||||
def darkest_of_cardinality(cardinality: int, tonality: int = 12, prime: bool = False) -> set:
|
||||
@classmethod
|
||||
def darkest_of_cardinality(this, cardinality: int, tonality: int = 12, prime: bool = False) -> set:
|
||||
"""
|
||||
Returns all set classes of a given cardinality, in their darkest forms (ignore rotations).
|
||||
Tonality can be specified, default is 12.
|
||||
Warning: high values of tonality can take a long time to calculate (T=24 takes about a
|
||||
minute on an Intel i7-13700H CPU).
|
||||
"""
|
||||
return set(i.darkest_form for i in SetClass.all_of_cardinality(cardinality, tonality))
|
||||
return set(i.darkest_form for i in this.all_of_cardinality(cardinality, tonality))
|
||||
|
||||
def normal_of_cardinality(cardinality: int, tonality: int = 12, prime: bool = False) -> set:
|
||||
@classmethod
|
||||
def normal_of_cardinality(this, cardinality: int, tonality: int = 12, prime: bool = False) -> set:
|
||||
"""
|
||||
Returns all set classes of a given cardinality, in their (darkest) Rahn normal forms (ignore
|
||||
rotations). Tonality can be specified, default is 12.
|
||||
Warning: high values of tonality can take a long time to calculate (T=24 takes about a
|
||||
minute on an Intel i7-13700H CPU).
|
||||
"""
|
||||
return set(i.rahn_normal_form for i in SetClass.all_of_cardinality(cardinality, tonality))
|
||||
return set(i.rahn_normal_form for i in this.all_of_cardinality(cardinality, tonality))
|
||||
|
||||
@classmethod
|
||||
def bright_rahn_normal_forms(this) -> set:
|
||||
"""
|
||||
John Rahn's normal form from *Basic Atonal Theory* (1980) is an algorithm to produce a
|
||||
unique form for each set class. Most of the time it is also the "darkest" (smallest
|
||||
brightness value), except for all the times when that is not the case; this function returns
|
||||
that list, as a set of (cardinality, darkest, rahn) tuples.
|
||||
"""
|
||||
cases = set()
|
||||
for C in range(13): # 0 to 12
|
||||
for sc in SetClass.all_of_cardinality(C):
|
||||
if sc.darkest_form.brightness < sc.rahn_normal_form.brightness:
|
||||
cases.add((C, sc.darkest_form, sc.rahn_normal_form))
|
||||
return cases
|
||||
|
||||
def bright_rahn_normal_forms():
|
||||
"""
|
||||
John Rahn's normal form from _Basic Atonal Theory_ (1980) is an algorithm to produce a unique
|
||||
form for each set class. Most of the time it is also the "darkest" (smallest brightness value),
|
||||
except for all the times when that is not the case; this function returns that list, as a set of
|
||||
(cardinality, darkest, rahn) tuples.
|
||||
"""
|
||||
cases = set()
|
||||
for C in range(13): # 0 to 12
|
||||
for sc in SetClass.all_of_cardinality(C):
|
||||
if sc.darkest_form.brightness < sc.rahn_normal_form.brightness:
|
||||
cases.add((C, sc.darkest_form, sc.rahn_normal_form))
|
||||
return cases
|
||||
@classmethod
|
||||
def bright_prime_forms(this) -> set:
|
||||
"""
|
||||
Allen Forte describes the algorithm for deriving the "prime" or zeroed normal form, in his
|
||||
book *The Structure of Atonal Music* (1973) in section 1.2 (pp. 3-5), citing Milton Babbitt
|
||||
(1961). It produces a unique form for each set class, which most of the time is also the
|
||||
"darkest" (smallest brightness value), except for all the times when that is not the case;
|
||||
this function returns that list, as a set of (cardinality, darkest, prime) tuples.
|
||||
"""
|
||||
cases = set()
|
||||
for C in range(13): # 0 to 12
|
||||
for sc in SetClass.all_of_cardinality(C):
|
||||
if sc.darkest_form.brightness < sc.prime_form.brightness:
|
||||
cases.add((C, sc.darkest_form, sc.prime_form))
|
||||
return cases
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ def test_all_of_cardinality_set_lengths():
|
|||
def test_darkest_of_cardinality_set_lengths():
|
||||
"Confirm expected lengths of the sets of set classes, for cardinality 1 through 12"
|
||||
sets = [SetClass.darkest_of_cardinality(i + 1) for i in range(12)]
|
||||
assert [len(s) for s in sets] == [1, 6, 22, 55, 66, 127, 66, 55, 22, 6, 1, 1]
|
||||
assert [len(s) for s in sets] == [1, 6, 19, 43, 66, 80, 66, 43, 19, 6, 1, 1]
|
||||
|
||||
|
||||
def test_normal_of_cardinality_set_lengths():
|
||||
|
|
@ -48,7 +48,7 @@ def test_all_of_cardinality():
|
|||
assert SetClass(0, 1, 2) in s3
|
||||
assert SetClass(0, 10, 11) in s3
|
||||
|
||||
assert SetClass(0, 4, 6) in s3
|
||||
assert SetClass(0, 2, 8) in s3
|
||||
assert SetClass(0, 6, 10) in s3
|
||||
|
||||
assert SetClass(0, 1, 3, 4, 8, 10) in s6
|
||||
|
|
@ -58,14 +58,14 @@ def test_all_of_cardinality():
|
|||
def test_darkest_of_cardinality():
|
||||
s3 = SetClass.darkest_of_cardinality(3)
|
||||
s6 = SetClass.darkest_of_cardinality(6)
|
||||
assert len(s3) == 22
|
||||
assert len(s6) == 127
|
||||
assert len(s3) == 19
|
||||
assert len(s6) == 80
|
||||
|
||||
# only one version (rotation) of each set class is present
|
||||
assert SetClass(0, 1, 2) in s3
|
||||
assert SetClass(0, 10, 11) not in s3
|
||||
|
||||
assert SetClass(0, 4, 6) in s3
|
||||
assert SetClass(0, 2, 8) in s3
|
||||
assert SetClass(0, 6, 10) not in s3
|
||||
|
||||
assert SetClass(0, 1, 3, 4, 8, 10) in s6
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from setclass.setclass import SetClass, bright_rahn_normal_forms
|
||||
from setclass.setclass import SetClass
|
||||
|
||||
|
||||
def test_brightness_conjecture():
|
||||
|
|
@ -23,29 +23,24 @@ def test_brightness_conjecture():
|
|||
|
||||
def test_bright_rahn():
|
||||
"""
|
||||
There are 63 instances where the Rahn normal form is not also the "darkest" (smallest sum of
|
||||
There are 61 instances where the Rahn normal form is not also the "darkest" (smallest sum of
|
||||
pitch class values).
|
||||
"""
|
||||
R = bright_rahn_normal_forms()
|
||||
assert len(R) == 63
|
||||
counts = {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 1,
|
||||
4: 3,
|
||||
5: 17,
|
||||
6: 7,
|
||||
7: 23,
|
||||
8: 9,
|
||||
9: 3,
|
||||
10: 0,
|
||||
11: 0,
|
||||
12: 0
|
||||
}
|
||||
for C in range(13): # 0 to 12
|
||||
bright_rahns = set(i for i in R if i[0] == C)
|
||||
assert len(bright_rahns) == counts[C]
|
||||
R = SetClass.bright_rahn_normal_forms()
|
||||
assert len(R) == 61
|
||||
sets = [[i for i in R if i[0] == C] for C in range(13)]
|
||||
assert [len(s) for s in sets] == [0, 0, 0, 1, 3, 17, 6, 23, 8, 3, 0, 0, 0]
|
||||
|
||||
|
||||
def test_bright_prime_forms():
|
||||
"""
|
||||
There are 59 instances where the prime form is not also the "darkest" (smallest sum of pitch
|
||||
class values).
|
||||
"""
|
||||
P = SetClass.bright_prime_forms()
|
||||
assert len(P) == 59
|
||||
sets = [[i for i in P if i[0] == C] for C in range(13)]
|
||||
assert [len(s) for s in sets] == [0, 0, 0, 1, 3, 16, 6, 23, 8, 2, 0, 0, 0]
|
||||
|
||||
|
||||
def print_bright_rahn():
|
||||
|
|
@ -56,6 +51,19 @@ def print_bright_rahn():
|
|||
values).
|
||||
"""
|
||||
print("Smallest brightness, Rahn normal form")
|
||||
set_classes = sorted(bright_rahn_normal_forms(), key=lambda x: x[0] * 1000 + x[1].brightness)
|
||||
set_classes = sorted(SetClass.bright_rahn_normal_forms(), key=lambda x: x[0] * 1000 + x[1].brightness)
|
||||
for (C, d, r) in set_classes:
|
||||
print(f"{d.leonard_notation}, {r.leonard_notation}")
|
||||
|
||||
|
||||
def print_bright_prime():
|
||||
"""
|
||||
Convenience function, to print the list of set classes where the Forte prime form is not the
|
||||
"darkest" form (that with the smallest brightness value), as pairs of "darkest" and Forte prime
|
||||
form pairs, using Leonard notation, sorted by cardinality then brightness (sum of pitch class
|
||||
values).
|
||||
"""
|
||||
print("Smallest brightness, Forte prime form")
|
||||
set_classes = sorted(SetClass.bright_prime_forms(), key=lambda x: x[0] * 1000 + x[1].brightness)
|
||||
for (C, d, r) in set_classes:
|
||||
print(f"{d.leonard_notation}, {r.leonard_notation}")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue