Add inversions, is_symmetrical, z_relations, split test files

This commit is contained in:
Jonathan Harker 2024-09-24 19:06:20 +12:00
parent 0b968d8e48
commit 6f9cfb62b5
5 changed files with 221 additions and 185 deletions

View file

@ -48,7 +48,8 @@ class SetClass(list):
def __repr__(self) -> str:
"""Return the Python instance string representation."""
return f"SetClass{set(self.pitch_classes) or '{}'}".replace(' ', '')
s = f"SetClass{{{','.join(str(i) for i in self.pitch_classes)}}}"
return s if self.tonality == 12 else f"{s} (T={self.tonality})"
def __hash__(self) -> int:
return sum([hash(i) for i in self.pitch_classes])
@ -90,7 +91,16 @@ class SetClass(list):
return intervals
@cache_property
def interval_vector(self):
def z_relations(self):
"""
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 iv1,1,1,1,1,1 but
are not inversions, complements, or transpositions (rotations) of each other.
"""
return [i for i in SetClass.darkest_of_cardinality(self.cardinality) if i.interval_vector == self.interval_vector]
@cache_property
def interval_vector(self) -> list:
"""
An ordered tuple containing the multiplicities of each interval class in the set class.
Denoted in angle-brackets, e.g.
@ -125,7 +135,7 @@ class SetClass(list):
def versions(self):
"""
Returns all possible zero-normalised versions (clock rotations) of this set class,
sorted by brightness.
sorted by brightness. See Rahn (1987) Set types, Tₙ
"""
# The empty set class has one version, itself
if not self.pitch_classes:
@ -133,8 +143,8 @@ class SetClass(list):
versions = set()
for i in self.pitch_classes:
translate = self.tonality - i
versions.add(SetClass(*[j + translate for j in self.pitch_classes], tonality=self.tonality))
transpose = self.tonality - i
versions.add(SetClass(*[j + transpose for j in self.pitch_classes], tonality=self.tonality))
versions = list(versions)
versions.sort(key=lambda x: x.brightness)
return versions
@ -155,6 +165,22 @@ class SetClass(list):
"""
return self.versions[-1] if self.versions else self
@cache_property
def inversion(self):
"""
Returns the inversion of this set class, equivalent of reflection through the 0 axis on a
clock diagram.
"""
return SetClass(*[self.tonality - i for i in self.pitch_classes], tonality=self.tonality)
@cache_property
def is_symmetrical(self):
"""
Returns whether this set class is symmetrical upon inversion, for example Forte 5-Z17:
{0,1,2,5,9} {0,4,8,9,11}
"""
return self.darkest_form == self.inversion.darkest_form
@cache_property
def complement(self):
"""
@ -217,7 +243,7 @@ class SetClass(list):
if tonality < cardinality:
return ValueError("Set classes of cardinality {cardinality} are not possible in a {tonality}-tone octave.")
a = set(SetClass(*i) for i in _powerset(range(tonality)) if len(i) == cardinality and 0 in i)
a = set(SetClass(*i, tonality=tonality) for i in _powerset(range(tonality)) if len(i) == cardinality and 0 in i)
return a
def darkest_of_cardinality(cardinality: int, tonality: int = 12, prime: bool = False) -> set:

View file

@ -0,0 +1,60 @@
from setclass.setclass import SetClass
# ----------------------------------------------------------------------------
# Forte 12-1, the complement of empty is the "aggregate" set class
f121 = SetClass().complement
def test_empty_complement():
assert f121 == SetClass(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
def test_empty_complement_tonality():
assert f121.tonality == 12
def test_empty_complement_cardinality():
assert f121.cardinality == 12
def test_empty_complement_brightness():
assert f121.brightness == 66
def test_empty_complement_pitch_classes():
assert len(f121) == 12
assert len(f121.pitch_classes) == 12
def test_empty_complement_adjacency_intervals():
assert len(f121.adjacency_intervals) == 12
def test_empty_complement_versions():
assert len(f121.versions) == 1
assert f121.versions[0] == f121
def test_empty_complement_brightest_form():
assert f121.brightest_form == f121
def test_empty_complement_darkest_form():
assert f121.darkest_form == f121
def test_empty_complement_bright_dark_forms_equal():
assert f121.brightest_form == f121.darkest_form
def test_empty_complement_duodecimal_notation():
assert f121.duodecimal_notation == 'SetClass{0,1,2,3,4,5,6,7,8,9,T,E}'
def test_empty_complement_dozenal_notation():
assert f121.dozenal_notation == 'SetClass{0,1,2,3,4,5,6,7,8,9,↊,↋}'
def test_empty_complement_leonard_notation():
assert f121.leonard_notation == '[0₁1₁2₁3₁4₁5₁6₁7₁8₁9₁10₁11₁]⁽⁶⁶⁾'

View file

@ -0,0 +1,47 @@
from setclass.setclass import SetClass
def test_all_of_cardinality_set_lengths():
"Confirm expected lengths of the sets of set classes, for cardinality 1 through 12"
sets = [SetClass.all_of_cardinality(i + 1) for i in range(12)]
assert [len(s) for s in sets] == [1, 11, 55, 165, 330, 462, 462, 330, 165, 55, 11, 1]
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]
def test_all_of_cardinality():
s3 = SetClass.all_of_cardinality(3)
s6 = SetClass.all_of_cardinality(6)
assert len(s3) == 55
assert len(s6) == 462
# all versions (rotations) of each set class are present
assert SetClass(0, 1, 2) in s3
assert SetClass(0, 10, 11) in s3
assert SetClass(0, 4, 6) in s3
assert SetClass(0, 6, 10) in s3
assert SetClass(0, 1, 3, 4, 8, 10) in s6
assert SetClass(0, 4, 6, 8, 9, 11) in s6
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
# 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, 6, 10) not in s3
assert SetClass(0, 1, 3, 4, 8, 10) in s6
assert SetClass(0, 4, 6, 8, 9, 11) not in s6

View file

@ -0,0 +1,56 @@
from setclass.setclass import SetClass
# ----------------------------------------------------------------------------
# Forte 0-1, the empty set class
empty = SetClass()
def test_empty_tonality():
assert empty.tonality == 12
def test_empty_cardinality():
assert empty.cardinality == 0
def test_empty_brightness():
assert empty.brightness == 0
def test_empty_pitch_classes():
assert len(empty) == 0
assert len(empty.pitch_classes) == 0
def test_empty_ajdacency_intervals():
assert len(empty.adjacency_intervals) == 0
def test_empty_versions():
assert len(empty.versions) == 1
assert empty.versions[0] == empty
def test_empty_brightest_form():
assert empty.brightest_form == empty
def test_empty_darkest_form():
assert empty.darkest_form == empty
def test_empty_bright_dark_forms_equal():
assert empty.brightest_form == empty.darkest_form
def test_empty_duodecimal_notation():
assert empty.duodecimal_notation == 'SetClass{}'
def test_empty_dozenal_notation():
assert empty.dozenal_notation == 'SetClass{}'
def test_empty_leonard_notation():
assert empty.leonard_notation == '[]⁽⁰⁾'

View file

@ -5,24 +5,28 @@ from setclass.setclass import SetClass
# Functional tests
def test_zero_normalised_intervals():
def test_zero_normalised_pc():
"Pitch classes are normalised to start at zero"
a = SetClass(0, 1, 3)
assert a == SetClass(1, 2, 4)
assert a == SetClass(4, 5, 7)
def test_modulo_intervals():
def test_modulo_pc():
"Pitch classes are normalised to modulo tonality (12)"
a = SetClass(0, 1, 3)
assert a == SetClass(12, 13, 15) # modulo down
assert a == SetClass(-11, -9, -12) # modulo up
def test_negative_intervals():
def test_negative_pc():
"Negative pitch classes are modulo tonality (12)"
a = SetClass(0, 9, 10)
assert a == SetClass(-3, -2, 0)
def test_immutable_intervals():
def test_unordered_pc():
"Set classes are unordered; normalised to incremental sort"
a = SetClass(0, 1, 3)
assert a == SetClass(0, 3, 1)
assert a == SetClass(3, 0, 1)
@ -31,7 +35,8 @@ def test_immutable_intervals():
assert a == SetClass(3, 1, 0)
def test_repeat_intervals():
def test_resolve_duplicate_pc():
"Set classes are sets, so duplicates (including after modulo) are eliminated"
a = SetClass(0, 1, 3)
assert a == SetClass(0, 1, 3, 3, 1)
assert a == SetClass(0, 12, 1, -11, 15)
@ -39,8 +44,9 @@ def test_repeat_intervals():
def test_interval_vector():
"Set classes have an interval vector"
test = [
# pairs of set class, expected interval vector
# 2-tuple of set class, expected interval vector
(SetClass(0, 3, 4, 7, 8, 11), [3, 0, 3, 6, 3, 0]),
(SetClass(0, 1, 3, 5, 6, 8, 10), [2, 5, 4, 3, 6, 1]),
]
@ -48,19 +54,16 @@ def test_interval_vector():
assert sc.interval_vector == iv
def test_interval_vector_invarance():
def test_interval_vector_invarant():
"The interval vector is invariant under inversion and transposition (rotation)"
test = [
SetClass(0, 1, 3, 5, 6, 8, 10),
SetClass(0, 1, 3, 5, 7, 8, 10),
SetClass(0, 2, 3, 5, 7, 8, 10),
SetClass(0, 2, 3, 5, 7, 9, 10),
SetClass(0, 2, 4, 5, 7, 9, 10),
SetClass(0, 2, 4, 5, 7, 9, 11),
SetClass(0, 2, 4, 6, 7, 9, 11),
# 2-tuple of set class, expected interval vector
(SetClass(0, 3, 4, 7, 8, 11), [3, 0, 3, 6, 3, 0]),
(SetClass(0, 1, 3, 5, 6, 8, 10), [2, 5, 4, 3, 6, 1]),
]
iv = [2, 5, 4, 3, 6, 1]
for sc in test:
assert sc.interval_vector == iv
for (sc, iv) in test:
for version in (sc.versions + sc.inversion.versions):
assert version.interval_vector == iv
# ----------------------------------------------------------------------------
@ -69,27 +72,33 @@ f520 = SetClass(0, 1, 5, 6, 8)
def test_tonality():
"Set classes has a default tonality of 12"
assert f520.tonality == 12
def test_cardinality():
"Set classes has a set cardinality"
assert f520.cardinality == 5
def test_brightness():
"Set classes have a brightness (sum of normalised pitch class values)"
assert f520.brightness == 20
def test_pitch_classes():
"Set classes have pitch classes"
assert len(f520) == 5
assert len(f520.pitch_classes) == 5
def test_adjacency_intervals():
"Set classes have a set of adjacency intervals"
assert len(f520.adjacency_intervals) == 5
def test_versions():
"Set classes have a set of transpositions (rotations)"
assert len(f520.versions) == 5
@ -159,165 +168,3 @@ def test_complement_darkest_form():
def test_complement_leonard_notation():
assert f520.complement.leonard_notation == '[0₁1₁2₃5₂7₁8₁9₃]⁽³²⁾'
# ----------------------------------------------------------------------------
# Forte 0-1, the empty set class
empty = SetClass()
def test_empty_tonality():
assert empty.tonality == 12
def test_empty_cardinality():
assert empty.cardinality == 0
def test_empty_brightness():
assert empty.brightness == 0
def test_empty_pitch_classes():
assert len(empty) == 0
assert len(empty.pitch_classes) == 0
def test_empty_ajdacency_intervals():
assert len(empty.adjacency_intervals) == 0
def test_empty_versions():
assert len(empty.versions) == 1
assert empty.versions[0] == empty
def test_empty_brightest_form():
assert empty.brightest_form == empty
def test_empty_darkest_form():
assert empty.darkest_form == empty
def test_empty_bright_dark_forms_equal():
assert empty.brightest_form == empty.darkest_form
def test_empty_duodecimal_notation():
assert empty.duodecimal_notation == 'SetClass{}'
def test_empty_dozenal_notation():
assert empty.dozenal_notation == 'SetClass{}'
def test_empty_leonard_notation():
assert empty.leonard_notation == '[]⁽⁰⁾'
# ----------------------------------------------------------------------------
# Forte 12-1, the complement of empty is the "aggregate" set class
f121 = empty.complement
def test_empty_complement():
assert f121 == SetClass(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
def test_empty_complement_tonality():
assert f121.tonality == 12
def test_empty_complement_cardinality():
assert f121.cardinality == 12
def test_empty_complement_brightness():
assert f121.brightness == 66
def test_empty_complement_pitch_classes():
assert len(f121) == 12
assert len(f121.pitch_classes) == 12
def test_empty_complement_adjacency_intervals():
assert len(f121.adjacency_intervals) == 12
def test_empty_complement_versions():
assert len(f121.versions) == 1
assert f121.versions[0] == f121
def test_empty_complement_brightest_form():
assert f121.brightest_form == f121
def test_empty_complement_darkest_form():
assert f121.darkest_form == f121
def test_empty_complement_bright_dark_forms_equal():
assert f121.brightest_form == f121.darkest_form
def test_empty_complement_duodecimal_notation():
assert f121.duodecimal_notation == 'SetClass{0,1,2,3,4,5,6,7,8,9,T,E}'
def test_empty_complement_dozenal_notation():
assert f121.dozenal_notation == 'SetClass{0,1,2,3,4,5,6,7,8,9,↊,↋}'
def test_empty_complement_leonard_notation():
assert f121.leonard_notation == '[0₁1₁2₁3₁4₁5₁6₁7₁8₁9₁10₁11₁]⁽⁶⁶⁾'
# ----------------------------------------------------------------------------
def test_all_of_cardinality_set_lengths():
"Confirm expected lengths of the sets of set classes, for cardinality 1 through 12"
sets = [SetClass.all_of_cardinality(i + 1) for i in range(12)]
assert [len(s) for s in sets] == [1, 11, 55, 165, 330, 462, 462, 330, 165, 55, 11, 1]
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]
def test_all_of_cardinality():
s3 = SetClass.all_of_cardinality(3)
s6 = SetClass.all_of_cardinality(6)
assert len(s3) == 55
assert len(s6) == 462
# all versions (rotations) of each set class are present
assert SetClass(0, 1, 2) in s3
assert SetClass(0, 10, 11) in s3
assert SetClass(0, 4, 6) in s3
assert SetClass(0, 6, 10) in s3
assert SetClass(0, 1, 3, 4, 8, 10) in s6
assert SetClass(0, 4, 6, 8, 9, 11) in s6
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
# 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, 6, 10) not in s3
assert SetClass(0, 1, 3, 4, 8, 10) in s6
assert SetClass(0, 4, 6, 8, 9, 11) not in s6