Compare commits

..

No commits in common. "main" and "docs" have entirely different histories.
main ... docs

8 changed files with 143 additions and 892 deletions

View file

@ -25,20 +25,17 @@ In Python:
from setclass import SetClass from setclass import SetClass
sc = SetClass(0, 3, 5, 6, 7, 10, 11) # Forte 7-20; pitch classes as integers 0-11 sc = SetClass(0, 3, 5, 6, 7, 10, 11) # Forte 7-20; pitch classes as integers 0-11
sc.versions sc.versions
sc.rahn_normal_form sc.brightest_form
sc.forte_name sc.darkest_form
sc.duodecimal_notation # SetClass[0,3,5,6,7,T,E] sc.duodecimal_notation # SetClass[0,3,5,6,7,T,E]
``` ```
Documentation is generated from the doc comments with Sphinx, and in the meantime is available [here](https://git.jon.geek.nz/docs/public/setclass/setclass.html): Proper library documentation to come soon with Sphinx.
```
make -C docs html
```
## TODO ## TODO
- <s>Documentation (Sphinx)</s> - Documentation (Sphinx)
- Interoperate with music21 objects - Interoperate with music21 objects
- Generate MIDI files - Generate MIDI files
- Generate LilyPond files for set pitches - Generate LilyPond files for set pitches

View file

@ -3,9 +3,8 @@
# You can set these variables from the command line, and also # You can set these variables from the command line, and also
# from the environment for the first two. # from the environment for the first two.
PROJROOT := $(abspath $(MAKEFILE_LIST)/../..)
SPHINXOPTS ?= SPHINXOPTS ?=
SPHINXBUILD ?= PYTHONPATH="$(PROJROOT)" sphinx-build SPHINXBUILD ?= sphinx-build
SOURCEDIR = source SOURCEDIR = source
BUILDDIR = build BUILDDIR = build

View file

@ -28,7 +28,6 @@ release = '0.1'
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [ extensions = [
'sphinx.ext.napoleon',
'sphinx.ext.autodoc', 'sphinx.ext.autodoc',
'sphinx.ext.todo', 'sphinx.ext.todo',
'sphinx.ext.viewcode', 'sphinx.ext.viewcode',
@ -62,7 +61,9 @@ source_suffix = {
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'sphinx_rtd_theme' html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
html_theme_options = { html_theme_options = {
'display_version': False,
'navigation_depth': 2, 'navigation_depth': 2,
'prev_next_buttons_location': 'None' 'prev_next_buttons_location': 'None'
} }

View file

@ -6,8 +6,6 @@ setclass module
.. automodule:: setclass.setclass .. automodule:: setclass.setclass
:members: :members:
:special-members: __init__
:member-order: bysource
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View file

@ -1,4 +1,3 @@
# Main requirements
-r requirements.txt -r requirements.txt
# Code lint # Code lint
@ -15,6 +14,5 @@ tox
# Documentation # Documentation
sphinx sphinx
sphinx-rtd-theme sphinx_rtd_theme
sphinxcontrib-napoleon
myst-parser myst-parser

View file

@ -1,353 +0,0 @@
FORTE_PRIMES = {
1: '1-1',
3: '2-1',
5: '2-2',
9: '2-3',
17: '2-4',
33: '2-5',
65: '2-6',
7: '3-1',
11: '3-2',
13: '3-2',
19: '3-3',
25: '3-3',
35: '3-4',
49: '3-4',
67: '3-5',
97: '3-5',
21: '3-6',
37: '3-7',
41: '3-7',
69: '3-8',
81: '3-8',
133: '3-9',
73: '3-10',
137: '3-11',
145: '3-11',
273: '3-12',
15: '4-1',
23: '4-2',
29: '4-2',
27: '4-3',
39: '4-4',
57: '4-4',
71: '4-5',
113: '4-5',
135: '4-6',
51: '4-7',
99: '4-8',
195: '4-9',
45: '4-10',
43: '4-11',
53: '4-11',
77: '4-12',
89: '4-12',
75: '4-13',
105: '4-13',
141: '4-14',
177: '4-14',
83: '4-Z15',
101: '4-Z15',
163: '4-16',
197: '4-16',
153: '4-17',
147: '4-18',
201: '4-18',
275: '4-19',
281: '4-19',
291: '4-20',
85: '4-21',
149: '4-22',
169: '4-22',
165: '4-23',
277: '4-24',
325: '4-25',
297: '4-26',
293: '4-27',
329: '4-27',
585: '4-28',
139: '4-Z29',
209: '4-Z29',
31: '5-1',
47: '5-2',
61: '5-2',
55: '5-3',
59: '5-3',
79: '5-4',
121: '5-4',
143: '5-5',
241: '5-5',
103: '5-6',
115: '5-6',
199: '5-7',
227: '5-7',
93: '5-8',
87: '5-9',
117: '5-9',
91: '5-10',
109: '5-10',
157: '5-11',
185: '5-11',
107: '5-Z12',
279: '5-13',
285: '5-13',
167: '5-14',
229: '5-14',
327: '5-15',
155: '5-16',
217: '5-16',
283: '5-Z17',
179: '5-Z18',
205: '5-Z18',
203: '5-19',
211: '5-19',
355: '5-20',
397: '5-20',
307: '5-21',
409: '5-21',
403: '5-22',
173: '5-23',
181: '5-23',
171: '5-24',
213: '5-24',
301: '5-25',
361: '5-25',
309: '5-26',
345: '5-26',
299: '5-27',
425: '5-27',
333: '5-28',
357: '5-28',
331: '5-29',
421: '5-29',
339: '5-30',
405: '5-30',
587: '5-31',
589: '5-31',
595: '5-32',
613: '5-32',
341: '5-33',
597: '5-34',
661: '5-35',
151: '5-Z36',
233: '5-Z36',
313: '5-Z37',
295: '5-Z38',
457: '5-Z38',
63: '6-1',
95: '6-2',
125: '6-2',
111: '6-Z3',
123: '6-Z3',
119: '6-Z4',
207: '6-5',
243: '6-5',
231: '6-Z6',
455: '6-7',
189: '6-8',
175: '6-9',
245: '6-9',
187: '6-Z10',
221: '6-Z10',
183: '6-Z11',
237: '6-Z11',
215: '6-Z12',
235: '6-Z12',
219: '6-Z13',
315: '6-14',
441: '6-14',
311: '6-15',
473: '6-15',
371: '6-16',
413: '6-16',
407: '6-Z17',
467: '6-Z17',
423: '6-18',
459: '6-18',
411: '6-Z19',
435: '6-Z19',
819: '6-20',
349: '6-21',
373: '6-21',
343: '6-22',
469: '6-22',
365: '6-Z23',
347: '6-Z24',
437: '6-Z24',
363: '6-Z25',
429: '6-Z25',
427: '6-Z26',
603: '6-27',
621: '6-27',
619: '6-Z28',
717: '6-Z29',
715: '6-30',
845: '6-30',
691: '6-31',
821: '6-31',
693: '6-32',
685: '6-33',
725: '6-33',
683: '6-34',
853: '6-34',
1365: '6-35',
159: '6-Z36',
249: '6-Z36',
287: '6-Z37',
399: '6-Z38',
317: '6-Z39',
377: '6-Z39',
303: '6-Z40',
489: '6-Z40',
335: '6-Z41',
485: '6-Z41',
591: '6-Z42',
359: '6-Z43',
461: '6-Z43',
615: '6-Z44',
627: '6-Z44',
605: '6-Z45',
599: '6-Z46',
629: '6-Z46',
663: '6-Z47',
669: '6-Z47',
679: '6-Z48',
667: '6-Z49',
723: '6-Z50',
127: '7-1',
191: '7-2',
253: '7-2',
319: '7-3',
505: '7-3',
223: '7-4',
251: '7-4',
239: '7-5',
247: '7-5',
415: '7-6',
499: '7-6',
463: '7-7',
487: '7-7',
381: '7-8',
351: '7-9',
501: '7-9',
607: '7-10',
637: '7-10',
379: '7-11',
445: '7-11',
671: '7-Z12',
375: '7-13',
477: '7-13',
431: '7-14',
491: '7-14',
471: '7-15',
623: '7-16',
635: '7-16',
631: '7-Z17',
755: '7-Z18',
829: '7-Z18',
719: '7-19',
847: '7-19',
743: '7-20',
925: '7-20',
823: '7-21',
827: '7-21',
871: '7-22',
701: '7-23',
757: '7-23',
687: '7-24',
981: '7-24',
733: '7-25',
749: '7-25',
699: '7-26',
885: '7-26',
695: '7-27',
949: '7-27',
747: '7-28',
861: '7-28',
727: '7-29',
941: '7-29',
855: '7-30',
939: '7-30',
731: '7-31',
877: '7-31',
859: '7-32',
875: '7-32',
1367: '7-33',
1371: '7-34',
1387: '7-35',
367: '7-Z36',
493: '7-Z36',
443: '7-Z37',
439: '7-Z38',
475: '7-Z38',
255: '8-1',
383: '8-2',
509: '8-2',
639: '8-3',
447: '8-4',
507: '8-4',
479: '8-5',
503: '8-5',
495: '8-6',
831: '8-7',
927: '8-8',
975: '8-9',
765: '8-10',
703: '8-11',
1013: '8-11',
763: '8-12',
893: '8-12',
735: '8-13',
1005: '8-13',
759: '8-14',
957: '8-14',
863: '8-Z15',
1003: '8-Z15',
943: '8-16',
983: '8-16',
891: '8-17',
879: '8-18',
987: '8-18',
887: '8-19',
955: '8-19',
951: '8-20',
1375: '8-21',
1391: '8-22',
1403: '8-22',
1455: '8-23',
1399: '8-24',
1495: '8-25',
1467: '8-26',
1463: '8-27',
1499: '8-27',
1755: '8-28',
751: '8-Z29',
989: '8-Z29',
511: '9-1',
767: '9-2',
1021: '9-2',
895: '9-3',
1019: '9-3',
959: '9-4',
1015: '9-4',
991: '9-5',
1007: '9-5',
1407: '9-6',
1471: '9-7',
1531: '9-7',
1503: '9-8',
1527: '9-8',
1519: '9-9',
1759: '9-10',
1775: '9-11',
1783: '9-11',
1911: '9-12',
1023: '10-1',
1535: '10-2',
1791: '10-3',
1919: '10-4',
1983: '10-5',
2015: '10-6',
2047: '11-1',
4095: '12-1',
}

View file

@ -1,37 +1,37 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
from functools import cached_property
import re import re
class cache_property:
"""
Property that only computes its value once when first accessed, and caches the result.
"""
def __init__(self, function):
self.function = function
self.name = function.__name__
self.__doc__ = function.__doc__
def __get__(self, obj, type=None) -> object:
obj.__dict__[self.name] = self.function(obj)
return obj.__dict__[self.name]
class SetClass(list): class SetClass(list):
""" """
Musical set class, containing zero or more pitch classes. This implementation can handle set Musical set class, containing zero or more pitch classes.
classes of any arbitrary :attr:`tonality`, or number of uniform divisions of the octave.
""" """
def __init__(self, *args: int, tonality: int = 12) -> None: def __init__(self, *args: int, tonality: int = 12) -> None:
""" """
Instantiate a :class:`ClassSet` object with a series of integers. Instantiate a Class Set with a series of integers. Each pitch class is "normalised" by
modulo the tonality; the set is sorted in ascending order; and values "rotated" until the
lowest value is 0. For a number of divisions of the octave other than the default (Western
harmony) value of 12, supply an integer value using the 'tonality' keyword. Example:
Each supplied pitch class is "normalised" by modulo the :attr:`tonality`, the set is sc = SetClass(0, 7, 9)
sorted in ascending order, and values are transposed until the lowest pitch class value is sc = SetClass(0, 2, 3, tonality=7)
0. For a number of divisions of the octave other than the default (Western harmony) value of
12, supply an integer value using the 'tonality' keyword. Example:
>>> sc1 = SetClass(0, 7, 9)
>>> sc1
SetClass{0,7,9}
>>> sc2 = SetClass(0, 2, 3, tonality=7)
>>> sc2
SetClass{0,2,3} (T=7)
Args:
*args (int):
The pitch class values.
tonality (int):
The :attr:`tonality`, or modulus, is the number of uniform divisions of the octave.
If unspecified, the default value of 12 is assumed (Western harmony).
""" """
self._tonality = tonality self._tonality = tonality
@ -41,7 +41,7 @@ class SetClass(list):
try: try:
pitches.add(int(i) % tonality) pitches.add(int(i) % tonality)
except ValueError: except ValueError:
raise TypeError("Requires integer arguments (use 10 and 11 for 'T' and 'E')") raise ValueError("Requires integer arguments (use 10 and 11 for 'T' and 'E')")
# if necessary "rotate" so that the lowest non-zero interval is zero # if necessary "rotate" so that the lowest non-zero interval is zero
if pitches: if pitches:
@ -53,60 +53,38 @@ class SetClass(list):
super().append(i) super().append(i)
def __repr__(self) -> str: def __repr__(self) -> str:
"""Return a string representation of this instance.""" """Return the Python instance string representation."""
s = f"SetClass{{{','.join(str(i) for i in self.pitch_classes)}}}" s = f"SetClass{{{','.join(str(i) for i in self.pitch_classes)}}}"
return s if self.tonality == 12 else f"{s} (T={self.tonality})" return s if self.tonality == 12 else f"{s} (T={self.tonality})"
def __hash__(self) -> int: def __hash__(self) -> int:
"""Return a unique identifier for the instance, based on the :attr:`pitch_classes` used."""
return sum([hash(i) for i in self.pitch_classes]) return sum([hash(i) for i in self.pitch_classes])
@property @property
def pitch_classes(self) -> list(int): def pitch_classes(self) -> list:
"""The pitch classes as an ordered :class:`list` of integers."""
return list(self) return list(self)
@cached_property @cache_property
def tonality(self) -> int: def tonality(self) -> int:
""" """
The number of uniform divisions of the octave. The default value is 12, which Returns the number of (equal) divisions of the octave. The default value is 12, which
represents traditional Western chromatic harmony, the octave divided into twelve semitones. represents traditional Western chromatic harmony, the octave divided into twelve semitones.
""" """
return self._tonality return self._tonality
@cached_property @cache_property
def cardinality(self) -> int: def cardinality(self) -> int:
""" """Returns the cardinality of the set class, i.e. the number of pitch classes."""
The cardinality of the set class, i.e. the number of :attr:`pitch_classes`.
"""
return len(self.pitch_classes) return len(self.pitch_classes)
@cached_property @cache_property
def brightness(self) -> int: def brightness(self) -> int:
""" """Returns the brightness of the set class, defined as the sum of the pitch class values."""
The brightness of the set class.
Brightness (B) is a property proposed by Brian Leonard, defined as the sum of the values of
the :attr:`pitch_classes` in the set class.
"""
return sum(self.pitch_classes) return sum(self.pitch_classes)
@cached_property @cache_property
def decimal(self) -> int: def adjacency_intervals(self) -> list:
""" """Adjacency intervals between the pitch classes, used for Leonard notation subscripts."""
The decimal value of the binary representation of the :attr:`pitch_classes`.
Returns the decimal value of the pitch classes expressed as a binary bit mask, i.e. the sum
of 2 where i is each pitch class value in ascending order. For example, set class {0,1,4,6}
has a binary value of 000001010011, which is the decimal value 83.
Further reading: Goyette (2012) p. 25, citing Brinkman (1986).
"""
return sum([2**i for i in self.pitch_classes])
@cached_property
def adjacency_intervals(self) -> list(int):
"""The ordered :class:`list` of adjacency intervals between the pitch classes."""
if not self.pitch_classes: if not self.pitch_classes:
return list() return list()
intervals = list() intervals = list()
@ -118,85 +96,52 @@ class SetClass(list):
intervals.append(self.tonality - prev) intervals.append(self.tonality - prev)
return intervals return intervals
@cached_property @cache_property
def z_relations(self) -> list:
"""
Return all distinct set classes with the same interval vector (Allen 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: def interval_vector(self) -> list:
""" """
An ordered :class:`list` containing the multiplicities of each interval class in the set An ordered tuple containing the multiplicities of each interval class in the set class.
class. Denoted in angle-brackets, e.g. the interval vector of {0,2,4,5,7,9,11} is Denoted in angle-brackets, e.g.
2,5,4,3,6,1. Each element in the vector is the frequency of occurrence of the interval The interval vector of {0,2,4,5,7,9,11} is 2,5,4,3,6,1
represented by its ordinal position, i.e. 2,5,4,3,6,1 means two semitones, five major Rahn (1980), p. 100
seconds, four minor thirds, and so on. Rahn (1980), p. 100.
""" """
from itertools import combinations from itertools import combinations
iv = [0 for i in range(1, int(self.tonality / 2) + 1)] iv = [0 for i in range(1, int(self.tonality / 2) + 1)]
for (a, b) in combinations(self.pitch_classes, 2): for (a, b) in combinations(self.pitch_classes, 2):
ic = self.unordered_interval(a, b) ic = SetClass.unordered_interval(a, b)
iv[ic - 1] += 1 iv[ic - 1] += 1
return iv return iv
@cached_property def ordered_interval(a: int, b: int) -> int:
def z_relations(self) -> list:
""" """
A :class:`list` of the Z-relations of this set class. The ordered interval or "directed interval" (Babbitt) of two pitch classes is determined by
the difference of the pitch class values, modulo 12:
Allen Forte in his book *The Structure of Atonal Music* (1973) described the relationship ia,b = b-a mod 12 Rahn (1980), p. 25
between twins of set classes that share the same :attr:`interval_vector`, but are not
related by :attr:`inversion`, :attr:`complement`, or transposition, as Z-related ('Z' for
*zygote* from Greek: ζυγωτός, 'joined' or 'paired'). This property returns all distinct set
classes with the same interval vector.
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 of each other.
""" """
return [i for i in SetClass.darkest_of_cardinality(self.cardinality) if i.interval_vector == self.interval_vector] return (b % 12 - a % 12) % 12
def ordered_interval(self, a: int, b: int) -> int: def unordered_interval(a: int, b: int) -> int:
""" """
Return the ordered interval of two pitch classes.
The ordered interval, or "directed interval" (Babbitt) of two :attr:`pitch_classes` is
determined by the difference of the pitch class values, modulo :attr:`tonality`. For example
with tonality 12:
ia,b = b-a mod 12
Rahn (1980), p. 25
Args:
a (int): the first pitch class value.
b (int): the second pitch class value.
Returns:
The ordered interval as ann integer.
"""
return (b % self.tonality - a % self.tonality) % self.tonality
def unordered_interval(self, a: int, b: int) -> int:
"""
Return the unordered interval of two pitch classes.
The unordered interval (also "interval distance", "interval class", "ic", or "undirected The unordered interval (also "interval distance", "interval class", "ic", or "undirected
interval") of two :attr:`pitch_classes` is the smaller of the possible values of interval") of two pitch classes is the smaller of the two possible ordered intervals
:attr:`ordered_interval` (differences in pitch class value): (differences in pitch class value):
i(a,b) = min: ia,b, ib,a Rahn (1980), p. 28
i(a,b) = min: ia,b, ib,a
Rahn (1980), p. 28
Args:
a (int): the first pitch class value.
b (int): the second pitch class value.
Returns:
The ordered interval as ann integer.
""" """
return min(self.ordered_interval(a, b), self.ordered_interval(b, a)) return min(SetClass.ordered_interval(a, b), SetClass.ordered_interval(b, a))
@cached_property @cache_property
def versions(self) -> list(SetClass): def versions(self) -> list:
""" """
All possible zero-normalised transpositions of this set class, sorted by :attr:`brightness`. Returns all possible zero-normalised versions (clock rotations) of this set class,
See Rahn (1980), Tₙ set types. sorted by brightness. See Rahn (1980) Set types, Tₙ
""" """
# The empty set class has one version, itself # The empty set class has one version, itself
if not self.pitch_classes: if not self.pitch_classes:
@ -210,16 +155,12 @@ class SetClass(list):
versions.sort(key=lambda x: x.brightness) versions.sort(key=lambda x: x.brightness)
return versions return versions
@cached_property @cache_property
def rahn_normal_form(self) -> SetClass: def rahn_normal_form(self) -> SetClass:
""" """
The Rahn normal form of the set class. 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
John Rahn's normal form described in his book *Basic Atonal Theory* (1980) is an algorithm result remains. See Rahn (1980), p. 33
to produce a unique form for each set class. Often wrongly described as "most packed to the
left", Leonard describes it as "most dispersed from the right". Find the smallest outside
interval, and if necessary proceed inwards from the right finding the smallest next interval
until one result remains. See Rahn (1980), p. 33.
""" """
def _most_dispersed(versions, n): 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])] return [i for i in versions if i.pitch_classes[-n] == min([i.pitch_classes[-n] for i in versions])]
@ -231,20 +172,11 @@ class SetClass(list):
n += 1 n += 1
return versions[0] return versions[0]
@cached_property @cache_property
def rahn_prime_form(self) -> SetClass:
"""
The Rahn prime is the most dispersed from the right of the Rahn normal forms of a set class
and its inversion.
"""
prime = min(self.rahn_normal_form.decimal, self.inversion.rahn_normal_form.decimal)
return self.inversion.rahn_normal_form if self.inversion.rahn_normal_form.decimal == prime else self.inversion.rahn_normal_form
@cached_property
def packed_left(self) -> SetClass: def packed_left(self) -> SetClass:
""" """
The form of the set class that is most packed to the left (the smallest adjacency intervals Return the form of the set class that is most packed to the left (smallest adjacency
to the left). Find the smallest first adjacency interval, and proceed towards the right intervals to the left). Find the smallest adjacency interval, and proceed towards the right
until one result remains. until one result remains.
""" """
def _most_packed(versions, n): def _most_packed(versions, n):
@ -257,15 +189,14 @@ class SetClass(list):
n += 1 n += 1
return versions[0] return versions[0]
@cached_property @cache_property
def prime_form(self) -> SetClass: def prime_form(self) -> SetClass:
""" """
Return the prime form of the set class. Return the prime form of the set class. Find the forms with the smallest outside interval,
and if necessary chose the form most packed to the left (the smallest adjacency intervals
Allen Forte describes the algorithm for finding this normal form in his book *The Structure working from left to right).
of Atonal Music* (1973) in section 1.2 (pp. 3-5), citing Milton Babbitt (1961). Find the Allen Forte describes the algorithm in his book *The Structure of Atonal Music* (1973) in
forms with the smallest outside interval, and if necessary chose the form most packed to the section 1.2 (pp. 3-5), citing Milton Babbitt (1961).
left (the smallest adjacency intervals working from left to right).
""" """
def _most_packed(versions, n): def _most_packed(versions, n):
return [i for i in versions if i.pitch_classes[n] == min([i.pitch_classes[n] for i in versions])] return [i for i in versions if i.pitch_classes[n] == min([i.pitch_classes[n] for i in versions])]
@ -281,36 +212,14 @@ class SetClass(list):
n += 1 n += 1
return versions[0] return versions[0]
@cached_property @cache_property
def forte_prime(self) -> SetClass:
"""
Return the Forte prime of the set class, including its inversions. Forte's list of named set
classes included inversions, for instance his "3-11" set class has a normal form of {0,3,7}
which describes the minor chord, and also describes its (set) inversion {0,5,9} which has a
:attr:`prime_form` of {0,4,7}, the major chord.
"""
return min(self, self.inversion)
@cached_property
def forte_name(self) -> str:
"""
Return the Forte name of this set class. Only applies to 12-tonality set classes.
Allen Forte listed and named all possible prime set classes in his book *The Structure
of Atonal Music* (1973) in Appendix 1, p. 179-181.
"""
if self.tonality != 12:
return ''
from setclass.forte import FORTE_PRIMES
d = self.forte_prime.decimal
return FORTE_PRIMES[d] if d in FORTE_PRIMES else ''
@cached_property
def darkest_form(self) -> SetClass: def darkest_form(self) -> SetClass:
""" """
The version of this set class with the smallest :attr:`brightness` value, most packed to the Returns the version with the smallest brightness value, most packed to the left.
left.
""" """
if not self.versions:
return self
B = min(i.brightness for i in self.versions) B = min(i.brightness for i in self.versions)
versions = [i for i in self.versions if i.brightness == B] versions = [i for i in self.versions if i.brightness == B]
if len(versions) == 1: if len(versions) == 1:
@ -325,91 +234,61 @@ class SetClass(list):
n += 1 n += 1
return versions[0] return versions[0]
@cached_property @cache_property
def brightest_form(self) -> SetClass: def brightest_form(self) -> SetClass:
""" """
The version of this set class with the largest :attr:`brightness` value, most packed to the Returns the version with the largest brightness value.
right. TODO: How to break a tie? Or return all matches in a tuple? Sorted by what?
""" """
B = max(i.brightness for i in self.versions) return self.versions[-1] if self.versions else self
versions = [i for i in self.versions if i.brightness == B]
if len(versions) == 1:
return versions[0]
def _most_packed(versions, n): @cache_property
return [i for i in versions if i.pitch_classes[n] == min([i.pitch_classes[n] for i in versions])]
n = 0
while len(versions) > 1:
versions = _most_packed(versions, n)
n += 1
return versions[0]
@cached_property
def inversion(self) -> SetClass: def inversion(self) -> SetClass:
""" """
The inversion of this set class, transposed so the smallest pitch class is 0. Equivalent to Returns the inversion of this set class, equivalent of reflection through the 0 axis on a
a reflection through the 0 axis on a clock diagram. clock diagram.
""" """
return SetClass(*[self.tonality - i for i in self.pitch_classes], tonality=self.tonality) return SetClass(*[self.tonality - i for i in self.pitch_classes], tonality=self.tonality)
@cached_property @cache_property
def is_symmetrical(self) -> bool: def is_symmetrical(self) -> bool:
""" """
Whether this set class is symmetrical upon inversion, for example Forte 5-Z37: Returns whether this set class is symmetrical upon inversion, for example Forte 5-Z17:
{0,1,2,5,9} {0,4,8,9,11}
>>> sc = SetClass(0, 1, 2, 5, 9)
>>> sc.rahn_normal_form
SetClass{0,3,4,5,8}
>>> sc.inversion.rahn_normal_form
SetClass{0,3,4,5,8}
>>> sc.is_symmetrical
True
""" """
return self.darkest_form == self.inversion.darkest_form return self.darkest_form == self.inversion.darkest_form
@cached_property @cache_property
def complement(self) -> SetClass: def complement(self) -> SetClass:
""" """
The set class containing all :attr:`pitch_classes` absent in this one, transposed so the Returns the set class containing all pitch classes absent in this one (rotated so the
smallest pitch class is 0. smallest is 0).
""" """
return SetClass(*[i for i in range(self.tonality) if i not in self.pitch_classes], tonality=self.tonality) return SetClass(*[i for i in range(self.tonality) if i not in self.pitch_classes], tonality=self.tonality)
@cached_property @cache_property
def dozenal_notation(self) -> str: def dozenal_notation(self) -> str:
""" """
A string representation using and for 10 and 11. If tonality is no greater than 12, return a string representation using Dozenal Society
characters '' for 10 and '' for 11 (the Pitman forms from Unicode 8.0 release, 2015).
For a set class with a :attr:`tonality` no greater than 12, this property replaces the 10
and 11 pitch classes with the Dozenal Society characters '' and '' respectively. These are
the Pitman forms from the Unicode 8.0 specification released in 2015.
""" """
return f"{self}" if self.tonality > 12 else f"{self}".replace('10', '').replace('11', '') return f"{self}" if self.tonality > 12 else f"{self}".replace('10', '').replace('11', '')
@cached_property @cache_property
def duodecimal_notation(self) -> str: def duodecimal_notation(self) -> str:
""" """
A string representation using T and E for 10 and 11. If tonality is no greater than 12, replace 10 and 11 with 'T' and 'E'.
For a set class with a :attr:`tonality` no greater than 12, this property replaces the 10
and 11 pitch classes with the letters 'T' and 'E' respectively.
""" """
return f"{self}" if self.tonality > 12 else f"{self}".replace('10', 'T').replace('11', 'E') return f"{self}" if self.tonality > 12 else f"{self}".replace('10', 'T').replace('11', 'E')
@cached_property @cache_property
def leonard_notation(self) -> str: def leonard_notation(self) -> str:
""" """
The string representation proposed by Brian Leonard. Returns a string representation of this set class using subscripts to denote the adjacency
intervals between the pitch classes instead of commas, and the overall brightness (sum of
Returns a string representation of this set class using subscripts to denote the pitch class values) denoted as a superscript. In standard tonality (T=12) the letters T and
:attr:`adjacency_intervals` between the pitch classes instead of commas, and the overall E are used for pitch classes 10 and 11. For example, Forte 7-34, {0,1,3,4,6,8,10} is
:attr:`brightness` (sum of the values of :attr:`pitch_classes`) denoted as a superscript. notated [013468T₂]³².
In standard tonality (T=12) the letters T and E are used for pitch classes 10 and 11. For
example, Forte 7-34 would be written as [013468T₂]³² in this scheme:
>>> SetClass.from_string('{0,1,3,4,6,8,10}').leonard_notation
[013468T₂]³²
""" """
# Return numbers as subscript and superscript strings: # Return numbers as subscript and superscript strings:
def subscript(n: int) -> str: def subscript(n: int) -> str:
@ -430,107 +309,19 @@ class SetClass(list):
# Class methods ----------------------------------------------------------- # Class methods -----------------------------------------------------------
@classmethod @classmethod
def from_string(this, string: str, tonality: int = 12) -> SetClass: def from_string(this, string: str) -> SetClass:
""" """
Create a :class:`SetClass` from a string. Attempt to create a SetClass from any string containing a sequence of zero or more integers.
A useful convenience function, e.g. SetClass.from_string('{0,3,7,9}')
A useful convenience function for converting from various text representations of set
classes that contain a sequence of zero or more integers. Non-integer content is ignored,
and integers must be separated by some non-integer character(s), usually a space, comma, or
similar. For instance:
>>> SetClass.from_string("Prélude à l'après-midi d'un faune uses [0,2,4,6,8,9]")
SetClass{0,2,4,6,8,9}
>>> SetClass.from_string('{0, 3, 7}', tonality=8)
SetClass{0,3,7} (T=8)
Args:
string (str):
Any string representation of a set class, containing some integer content.
tonality (int):
The :attr:`tonality`, or modulus, is the number of uniform divisions of the octave.
If unspecified, the default value of 12 is assumed (Western harmony).
Returns:
A :class:`SetClass` instance with the pitch classes found in the string.
""" """
return SetClass(*re.findall(r'\d+', string), tonality=tonality) return SetClass(*re.findall(r'\d+', string))
@classmethod @classmethod
def from_decimal(this, decimal: int, tonality: int = 12) -> SetClass: def all_of_cardinality(cls, cardinality: int, tonality: int = 12) -> set:
""" """
Create a :class:`SetClass` from its decimal number. 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
A useful convenience function for converting from a decimal representation of set classes.
For instance:
>>> SetClass.from_decimal(145)
SetClass{0,4,7}
>>> SetClass.from_decimal(137, tonality=8)
SetClass{0,3,7} (T=8)
Args:
decmial (int):
The decimal to convert from.
tonality (int):
The :attr:`tonality`, or modulus, is the number of uniform divisions of the octave.
If unspecified, the default value of 12 is assumed (Western harmony).
Returns:
A :class:`SetClass` instance with the pitch classes encoded in the decimal representation.
"""
if decimal.bit_length() > tonality:
raise ValueError("Decimal number too large for tonality's maximum cardinality")
pc = list()
for i in range(decimal.bit_length()):
if decimal & (2 ** i):
pc.append(i)
return SetClass(*pc, tonality=tonality)
@classmethod
def from_forte_name(this, name: str) -> SetClass:
"""
Create a :class:`SetClass` from its Forte prime form designation. Assumes 12-tonality.
Name is case insensitive.
A useful convenience function for creating a set class from its Forte name. For instance:
>>> SetClass.from_forte_name('3-11')
SetClass{0,3,7}
Args:
name (str):
The Forte name (case insensitive).
Returns:
A :class:`SetClass` instance in Forte prime form.
"""
from setclass.forte import FORTE_PRIMES
decimal = next((k for k, v in FORTE_PRIMES.items() if v == str(name).upper()), None)
if decimal:
return SetClass.from_decimal(decimal)
else:
raise ValueError(f"Forte name '{name}' not found.")
@classmethod
def all_of_cardinality(this, cardinality: int, tonality: int = 12) -> set:
"""
Returns a :class:`set` of all possible :class:`SetClass` objects with a given
:attr:`cardinality`.
.. warning:: High values of tonality can take a long time to calculate (T=24 takes about a
minute on an Intel i7-13700H CPU). minute on an Intel i7-13700H CPU).
Args:
cardinality (int):
The :attr:`cardinality` of the set classes to return.
tonality (int):
The :attr:`tonality`, or modulus, is the number of uniform divisions of the octave.
If unspecified, the default value of 12 is assumed (Western harmony).
Returns:
A :class:`set` of :class:`SetClass` objects.
""" """
def _powerset(seq): def _powerset(seq):
"""Set of all possible unique subsets.""" """Set of all possible unique subsets."""
@ -543,72 +334,36 @@ class SetClass(list):
return set(SetClass(*i, tonality=tonality) for i in _powerset(range(tonality)) if len(i) == cardinality and 0 in i) return set(SetClass(*i, tonality=tonality) for i in _powerset(range(tonality)) if len(i) == cardinality and 0 in i)
@classmethod @classmethod
def darkest_of_cardinality(this, cardinality: int, tonality: int = 12) -> set: def darkest_of_cardinality(this, cardinality: int, tonality: int = 12, prime: bool = False) -> set:
""" """
Returns a :class:`set` of all :class:`SetClass` objects with a given :attr:`cardinality` in Returns all set classes of a given cardinality, in their darkest forms (ignore rotations).
their darkest forms. Tonality can be specified, default is 12.
Warning: high values of tonality can take a long time to calculate (T=24 takes about a
This produces a smaller set than :attr:`all_of_cardinality`, since it eliminates set classes
that are "brighter" transpositions of the darkest :class:`SetClass` returned by the
:attr:`darkest_form` property.
.. warning:: High values of tonality can take a long time to calculate (T=24 takes about a
minute on an Intel i7-13700H CPU). minute on an Intel i7-13700H CPU).
Args:
cardinality (int):
The :attr:`cardinality` of the set classes to return.
tonality (int):
The :attr:`tonality`, or modulus, is the number of uniform divisions of the octave.
If unspecified, the default value of 12 is assumed (Western harmony).
Returns:
A :class:`set` of :class:`SetClass` objects in :attr:`darkest_form`.
""" """
return set(i.darkest_form for i in this.all_of_cardinality(cardinality, tonality)) return set(i.darkest_form for i in this.all_of_cardinality(cardinality, tonality))
@classmethod @classmethod
def normal_of_cardinality(this, cardinality: int, tonality: int = 12) -> set: def normal_of_cardinality(this, cardinality: int, tonality: int = 12, prime: bool = False) -> set:
""" """
Returns a :class:`set` of all :class:`SetClass` objects with a given :attr:`cardinality` in Returns all set classes of a given cardinality, in their (darkest) Rahn normal forms (ignore
their Rahn normal form. 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
This produces a smaller set than :attr:`all_of_cardinality`, since it eliminates set classes
that are transpositions of the normalised :class:`SetClass` returned by the
:attr:`rahn_normal_form` property.
.. warning:: High values of tonality can take a long time to calculate (T=24 takes about a
minute on an Intel i7-13700H CPU). minute on an Intel i7-13700H CPU).
Args:
cardinality (int):
The :attr:`cardinality` of the set classes to return.
tonality (int):
The :attr:`tonality`, or modulus, is the number of uniform divisions of the octave.
If unspecified, the default value of 12 is assumed (Western harmony).
Returns:
A :class:`set` of :class:`SetClass` objects in :attr:`rahn_normal_form`.
""" """
return set(i.rahn_normal_form for i in this.all_of_cardinality(cardinality, tonality)) return set(i.rahn_normal_form for i in this.all_of_cardinality(cardinality, tonality))
@classmethod @classmethod
def bright_rahn_normal_forms(this) -> set: def bright_rahn_normal_forms(this) -> set:
""" """
The set of Rahn normal forms that are not the same as the darkest form.
John Rahn's normal form from *Basic Atonal Theory* (1980) is an algorithm to produce a John Rahn's normal form from *Basic Atonal Theory* (1980) is an algorithm to produce a
unique form for each set class (see :attr:`rahn_normal_form`). Most of the time it is also unique form for each set class. Most of the time it is also the "darkest" (smallest
the same as the :attr:`darkest_form` (that with the smallest :attr:`brightness` value), brightness value), except for all the times when that is not the case; this function returns
except for all the times when that is not the case! that list, as a set of (cardinality, darkest, rahn) tuples.
Returns:
A :class:`set` of (:attr:`cardinality`, :attr:`darkest_form`, :attr:`rahn_normal_form`)
tuples.
""" """
cases = set() cases = set()
for C in range(13): # 0 to 12 for C in range(13): # 0 to 12
for sc in this.all_of_cardinality(C): for sc in SetClass.all_of_cardinality(C):
if sc.darkest_form.brightness < sc.rahn_normal_form.brightness: if sc.darkest_form.brightness < sc.rahn_normal_form.brightness:
cases.add((C, sc.darkest_form, sc.rahn_normal_form)) cases.add((C, sc.darkest_form, sc.rahn_normal_form))
return cases return cases
@ -616,21 +371,15 @@ class SetClass(list):
@classmethod @classmethod
def bright_prime_forms(this) -> set: def bright_prime_forms(this) -> set:
""" """
The set of Forte prime forms that are not the same as the darkest form.
Allen Forte describes the algorithm for deriving the "prime" or zeroed normal form, in his Allen Forte describes the algorithm for deriving the "prime" or zeroed normal form, in his
book *The Structure of Atonal Music* (1973) in section 1.2 (pp. 3-5), citing Milton Babbitt book *The Structure of Atonal Music* (1973) in section 1.2 (pp. 3-5), citing Milton Babbitt
(1961). It produces a unique form for each set class (see :attr:`prime_form`), which most of (1961). It produces a unique form for each set class, which most of the time is also the
the time is the same as the :attr:`darkest_form` (that with the smallest :attr:`brightness` "darkest" (smallest brightness value), except for all the times when that is not the case;
value), except for all the times when that is not the case! this function returns that list, as a set of (cardinality, darkest, prime) tuples.
Returns:
A :class:`set` of (:attr:`cardinality`, :attr:`darkest_form`, :attr:`prime_form`) tuples.
""" """
cases = set() cases = set()
for C in range(13): # 0 to 12 for C in range(13): # 0 to 12
for sc in this.all_of_cardinality(C): for sc in SetClass.all_of_cardinality(C):
if sc.darkest_form.brightness < sc.prime_form.brightness: if sc.darkest_form.brightness < sc.prime_form.brightness:
cases.add((C, sc.darkest_form, sc.prime_form)) cases.add((C, sc.darkest_form, sc.prime_form))
return cases return cases

View file

@ -1,4 +1,3 @@
import pytest
from setclass.setclass import SetClass from setclass.setclass import SetClass
@ -6,16 +5,6 @@ from setclass.setclass import SetClass
# Functional tests # Functional tests
def test_no_init_args():
empty = SetClass()
assert empty.pitch_classes == []
def test_non_integer_init_args():
with pytest.raises(TypeError):
SetClass('Z', 'abc')
def test_zero_normalised_pc(): def test_zero_normalised_pc():
"Pitch classes are normalised to start at zero" "Pitch classes are normalised to start at zero"
a = SetClass(0, 1, 3) a = SetClass(0, 1, 3)
@ -77,99 +66,6 @@ def test_interval_vector_invarant():
assert version.interval_vector == iv assert version.interval_vector == iv
def test_z_relations():
# Forte 4-Z15 {0,1,4,6} and Forte 4-Z29 {0,1,3,7} both have iv⟨1,1,1,1,1,1⟩
z1 = SetClass.from_forte_name('4-Z15')
z2 = SetClass.from_forte_name('4-Z29')
assert z1 == SetClass(0, 1, 4, 6)
assert z2 == SetClass(0, 1, 3, 7)
assert z1 in z1.z_relations
assert z1 in z2.z_relations
assert z2 in z1.z_relations
assert z2 in z2.z_relations
def test_forte_name():
tests = [
('1-1', [0]),
('3-11', [0, 3, 7]),
('6-34', [0, 1, 3, 5, 7, 9]),
('6-35', [0, 2, 4, 6, 8, 10]),
('6-Z43', [0, 1, 2, 5, 6, 8]),
('12-1', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
]
for (name, pitch_classes) in tests:
assert SetClass(*pitch_classes).forte_name == name
def test_forte_name_tonality():
assert SetClass(0, 1, 2, tonality=13).forte_name == ''
# ----------------------------------------------------------------------------
# Class methods
def test_from_string():
tests = [
("No integer values in this string", []),
("Here is a 1", [0]),
("{0, 3, 7}", [0, 3, 7]),
(" 0 4 7 ", [0, 4, 7]),
("Prélude à l'après-midi d'un faune uses [0,2,4,6,8,9]", [0, 2, 4, 6, 8, 9]),
("Works from Leonard notation: [0₁1₂3₁4₂6₂8₂9₃]⁽³¹⁾", [0, 1, 3, 4, 6, 8, 9]),
("Repeated numbers: 0,0,1,1,6,8", [0, 1, 6, 8]),
("Modulo numbers: 1, 22, 3, 2, 12, 144", [0, 1, 2, 3, 10]),
]
for (string, pitch_classes) in tests:
assert SetClass.from_string(string).pitch_classes == pitch_classes
def test_from_decimal():
tests = [
(0, []),
(1, [0]),
(137, [0, 3, 7]),
(145, [0, 4, 7]),
(853, [0, 2, 4, 6, 8, 9]),
(1365, [0, 2, 4, 6, 8, 10]),
(4095, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
]
for (decimal, pitch_classes) in tests:
assert SetClass.from_decimal(decimal).pitch_classes == pitch_classes
def test_from_decimal_error():
with pytest.raises(ValueError):
SetClass.from_decimal(4096)
# with pytest.raises(ValueError):
# sc = SetClass.from_decimal(2048, tonality=11)
def test_from_forte_name():
tests = [
('1-1', [0]),
('3-11', [0, 3, 7]),
('6-34', [0, 1, 3, 5, 7, 9]),
('6-35', [0, 2, 4, 6, 8, 10]),
('6-Z43', [0, 1, 2, 5, 6, 8]),
('12-1', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
]
for (name, pitch_classes) in tests:
assert SetClass.from_forte_name(name).pitch_classes == pitch_classes
def test_from_forte_name_errors():
tests = [
'0-1',
'',
'Wibble',
None,
]
for name in tests:
with pytest.raises(ValueError):
SetClass.from_forte_name(name)
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# Example Forte 5-20 set class # Example Forte 5-20 set class
f520 = SetClass(0, 1, 5, 6, 8) f520 = SetClass(0, 1, 5, 6, 8)
@ -190,11 +86,6 @@ def test_brightness():
assert f520.brightness == 20 assert f520.brightness == 20
def test_decimal():
"Set classes have a unique decimal number (sum of 2 raised to each normalised pitch class value)"
assert f520.decimal == 355
def test_pitch_classes(): def test_pitch_classes():
"Set classes have pitch classes" "Set classes have pitch classes"
assert len(f520) == 5 assert len(f520) == 5
@ -211,21 +102,11 @@ def test_versions():
assert len(f520.versions) == 5 assert len(f520.versions) == 5
def test_is_symmetrical():
sc = SetClass(0, 1, 2, 5, 9)
assert sc.rahn_normal_form == sc.inversion.rahn_normal_form
assert sc.is_symmetrical
sc = SetClass(0, 1, 2, 5, 8)
assert sc.rahn_normal_form != sc.inversion.rahn_normal_form
assert not sc.is_symmetrical
def test_brightest_form(): def test_brightest_form():
b = SetClass(0, 1, 2, 3, 4, 6, 7, 8, 10) b = f520.brightest_form
assert b.brightest_form in b.versions assert b in f520.versions
assert b.brightest_form == SetClass(0, 2, 3, 4, 6, 8, 9, 10, 11) assert b == SetClass(0, 4, 5, 9, 10)
assert b.leonard_notation == '[0₁1₁2₁3₁4₂6₁7₁8₂T₂]⁽⁴¹' assert b.leonard_notation == '[0₄4₁5₄9₁T₂]⁽²⁸⁾'
def test_darkest_form(): def test_darkest_form():
@ -241,21 +122,6 @@ def test_rahn_normal_form():
assert r == SetClass(0, 1, 5, 6, 8) assert r == SetClass(0, 1, 5, 6, 8)
def test_rahn_prime_form():
r = f520.rahn_prime_form
assert r == SetClass(0, 2, 3, 7, 8)
assert r not in f520.versions
assert r in f520.inversion.versions
# prime = min(self.rahn_normal_form.decimal, self.inversion.rahn_normal_form.decimal)
# return self.inversion.rahn_normal_form if self.inversion.rahn_normal_form.decimal == prime else self.inversion.rahn_normal_form
def test_packed_left():
r = f520.packed_left
assert r == SetClass(0, 1, 3, 7, 8)
assert r in f520.versions
def test_inversion(): def test_inversion():
i = f520.inversion i = f520.inversion
assert i not in f520.versions assert i not in f520.versions
@ -287,10 +153,6 @@ def test_complement_brightness():
assert f520.complement.brightness == 32 assert f520.complement.brightness == 32
def test_complement_decimal():
assert f520.complement.decimal == 935
def test_complement_pitch_classes(): def test_complement_pitch_classes():
assert len(f520.complement) == 7 assert len(f520.complement) == 7
assert len(f520.complement.pitch_classes) == 7 assert len(f520.complement.pitch_classes) == 7