diff --git a/setclass/setclass.py b/setclass/setclass.py index 61fd3a1..5aabdcf 100644 --- a/setclass/setclass.py +++ b/setclass/setclass.py @@ -48,7 +48,7 @@ class SetClass(list): def __repr__(self) -> str: """Return the Python instance string representation.""" - return f"SetClass{self.pitch_classes}".replace(' ', '') + return f"SetClass{set(self.pitch_classes) or '{}'}".replace(' ', '') def __hash__(self) -> int: return sum([hash(i) for i in self.pitch_classes]) @@ -89,6 +89,38 @@ class SetClass(list): intervals.append(self.tonality - prev) return intervals + @cache_property + def interval_vector(self): + """ + An ordered tuple containing the multiplicities of each interval class in the set class. + Denoted in angle-brackets, e.g. + The interval vector of {0,2,4,5,7,9,11} is ⟨2,5,4,3,6,1⟩ + — Rahn (1987), p. 100 + """ + from itertools import combinations + iv = [0 for i in range(1, int(self.tonality / 2) + 1)] + for (a, b) in combinations(self.pitch_classes, 2): + ic = SetClass.unordered_interval(a, b) + iv[ic - 1] += 1 + return iv + + def ordered_interval(a: int, b: int) -> int: + """ + The ordered interval or "directed interval" (Babbitt) of two pitch classes is determined by + the difference of the pitch class values, modulo 12: + i⟨a,b⟩ = b-a mod 12 — Rahn (1987), p. 25 + """ + return (b % 12 - a % 12) % 12 + + def unordered_interval(a: int, b: int) -> int: + """ + The unordered interval (also "interval distance", "interval class", "ic", or "undirected + interval") of two pitch classes is the smaller of the two possible ordered intervals + (differences in pitch class value): + i(a,b) = min: i⟨a,b⟩, i⟨b,a⟩ — Rahn (1987), p. 28 + """ + return min(SetClass.ordered_interval(a, b), SetClass.ordered_interval(b, a)) + @cache_property def versions(self): """ diff --git a/setclass/tests/test_setclass.py b/setclass/tests/test_setclass.py index fd1b75d..ad3cbac 100644 --- a/setclass/tests/test_setclass.py +++ b/setclass/tests/test_setclass.py @@ -38,6 +38,31 @@ def test_repeat_intervals(): assert a == SetClass(12, 1, -11, 15, -9) +def test_interval_vector(): + test = [ + # pairs 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]), + ] + for (sc, iv) in test: + assert sc.interval_vector == iv + + +def test_interval_vector_invarance(): + 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), + ] + iv = [2, 5, 4, 3, 6, 1] + for sc in test: + assert sc.interval_vector == iv + + # ---------------------------------------------------------------------------- # Example Forte 5-20 set class f520 = SetClass(0, 1, 5, 6, 8) @@ -180,11 +205,11 @@ def test_empty_bright_dark_forms_equal(): def test_empty_duodecimal_notation(): - assert empty.duodecimal_notation == 'SetClass[]' + assert empty.duodecimal_notation == 'SetClass{}' def test_empty_dozenal_notation(): - assert empty.dozenal_notation == 'SetClass[]' + assert empty.dozenal_notation == 'SetClass{}' def test_empty_leonard_notation(): @@ -192,7 +217,7 @@ def test_empty_leonard_notation(): # ---------------------------------------------------------------------------- -# Forte 12-1, the complement of empty (full set class) +# Forte 12-1, the complement of empty is the "aggregate" set class f121 = empty.complement @@ -239,11 +264,11 @@ def test_empty_complement_bright_dark_forms_equal(): def test_empty_complement_duodecimal_notation(): - assert f121.duodecimal_notation == 'SetClass[0,1,2,3,4,5,6,7,8,9,T,E]' + 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,↊,↋]' + assert f121.dozenal_notation == 'SetClass{0,1,2,3,4,5,6,7,8,9,↊,↋}' def test_empty_complement_leonard_notation():