diff --git a/setclass/setclass.py b/setclass/setclass.py index dc62532..36946ce 100644 --- a/setclass/setclass.py +++ b/setclass/setclass.py @@ -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)) diff --git a/setclass/tests/test_cardinalities.py b/setclass/tests/test_cardinalities.py index c9c5e79..1c2a751 100644 --- a/setclass/tests/test_cardinalities.py +++ b/setclass/tests/test_cardinalities.py @@ -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) diff --git a/setclass/tests/test_setclass.py b/setclass/tests/test_setclass.py index 8f8b4f7..f16c957 100644 --- a/setclass/tests/test_setclass.py +++ b/setclass/tests/test_setclass.py @@ -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