Add Rahn normal form, more tests

This commit is contained in:
Jonathan Harker 2024-09-26 00:49:22 +12:00
parent 055026c79b
commit 4fe7f67cb9
3 changed files with 88 additions and 7 deletions

View file

@ -149,6 +149,30 @@ class SetClass(list):
versions.sort(key=lambda x: x.brightness)
return versions
@cache_property
def rahn_normal_form(self):
"""
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 (1987), 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])]
versions = self.versions
n = 1
while len(versions) > 1:
versions = _most_dispersed(versions, n)
n += 1
return versions[0]
@cache_property
def darkest_form(self):
"""
@ -241,10 +265,9 @@ class SetClass(list):
return chain.from_iterable(combinations(seq, r) for r in range(len(seq) + 1))
if tonality < cardinality:
return ValueError("Set classes of cardinality {cardinality} are not possible in a {tonality}-tone octave.")
raise ValueError("Set classes of cardinality {cardinality} are not possible in a {tonality}-tone octave.")
a = set(SetClass(*i, tonality=tonality) for i in _powerset(range(tonality)) if len(i) == cardinality and 0 in i)
return a
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:
"""
@ -253,8 +276,13 @@ class SetClass(list):
Warning: high values of tonality can take a long time to calculate (T=24 takes about a
minute on an Intel i7-13700H CPU).
"""
if tonality < cardinality:
return ValueError("Set classes of cardinality {cardinality} are not possible in a {tonality}-tone octave.")
return set(i.darkest_form for i in SetClass.all_of_cardinality(cardinality, tonality))
a = set(i.darkest_form for i in SetClass.all_of_cardinality(cardinality, tonality))
return a
def normal_of_cardinality(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))

View file

@ -1,3 +1,4 @@
import pytest
from setclass.setclass import SetClass
@ -13,6 +14,30 @@ def test_darkest_of_cardinality_set_lengths():
assert [len(s) for s in sets] == [1, 6, 22, 55, 66, 127, 66, 55, 22, 6, 1, 1]
def test_normal_of_cardinality_set_lengths():
"Confirm expected lengths of the sets of set classes, for cardinality 1 through 12"
sets = [SetClass.normal_of_cardinality(i + 1) for i in range(12)]
assert [len(s) for s in sets] == [1, 6, 19, 43, 66, 80, 66, 43, 19, 6, 1, 1]
def test_all_of_cardinality_error():
"Set cardinality ≤ tonality"
with pytest.raises(ValueError):
SetClass.normal_of_cardinality(13)
def test_darkest_of_cardinality_error():
"Set cardinality ≤ tonality"
with pytest.raises(ValueError):
SetClass.normal_of_cardinality(13)
def test_normal_of_cardinality_error():
"Set cardinality ≤ tonality"
with pytest.raises(ValueError):
SetClass.normal_of_cardinality(13)
def test_all_of_cardinality():
s3 = SetClass.all_of_cardinality(3)
s6 = SetClass.all_of_cardinality(6)

View file

@ -116,6 +116,20 @@ def test_darkest_form():
assert d.leonard_notation == '[0₁1₂3₄7₁8₄]⁽¹⁹⁾'
def test_rahn_normal_form():
r = f520.rahn_normal_form
assert r in f520.versions
assert r == SetClass(0, 1, 5, 6, 8)
def test_inversion():
i = f520.inversion
assert i not in f520.versions
assert i not in f520.complement.versions
assert i not in f520.complement.inversion.versions
assert i == SetClass(0, 4, 6, 7, 11)
def test_leonard_notation():
assert f520.leonard_notation == '[0₁1₄5₁6₂8₄]⁽²⁰⁾'
@ -152,6 +166,20 @@ def test_complement_versions():
assert len(f520.complement.versions) == 7
def test_complement_rahn_normal_form():
r = f520.complement.rahn_normal_form
assert r in f520.complement.versions
assert r == SetClass(0, 2, 3, 4, 7, 8, 9)
def test_complement_inversion():
i = f520.complement.inversion
assert i not in f520.versions
assert i not in f520.inversion.versions
assert i not in f520.complement.versions
assert i == SetClass(0, 3, 4, 5, 7, 10, 11)
def test_complement_brightest_form():
b = f520.complement.brightest_form
assert b in f520.complement.versions