diff --git a/setclass/setclass.py b/setclass/setclass.py index 1ce37b9..746bdb3 100644 --- a/setclass/setclass.py +++ b/setclass/setclass.py @@ -168,3 +168,35 @@ class SetClass(list): pitches += f"{pitch}{sub}" sup = superscript(self.brightness) return f"[{pitches}]{sup}" + + # Class methods ----------------------------------------------------------- + + def all_of_cardinality(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 + minute on an Intel i7-13700H CPU). + """ + def _powerset(seq): + """Set of all possible unique subsets.""" + from itertools import chain, combinations + 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.") + + a = set(SetClass(*i) 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: + """ + 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). + """ + if tonality < cardinality: + return ValueError("Set classes of cardinality {cardinality} are not possible in a {tonality}-tone octave.") + + a = set(i.darkest_form for i in SetClass.all_of_cardinality(cardinality, tonality)) + return a diff --git a/setclass/tests/test_setclass.py b/setclass/tests/test_setclass.py index cf8e158..e3d5452 100644 --- a/setclass/tests/test_setclass.py +++ b/setclass/tests/test_setclass.py @@ -248,3 +248,51 @@ def test_empty_complement_dozenal_notation(): 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