diff --git a/docs/source/conf.py b/docs/source/conf.py index 5fbe555..b4ff5b5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -28,6 +28,7 @@ release = '0.1' # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ + 'sphinx.ext.napoleon', 'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.viewcode', diff --git a/docs/source/setclass.rst b/docs/source/setclass.rst index ed60012..934bc8b 100644 --- a/docs/source/setclass.rst +++ b/docs/source/setclass.rst @@ -6,6 +6,8 @@ setclass module .. automodule:: setclass.setclass :members: + :special-members: __init__ + :member-order: bysource :undoc-members: :show-inheritance: diff --git a/requirements-dev.txt b/requirements-dev.txt index e60b3ed..7d66da7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ +# Main requirements -r requirements.txt # Code lint @@ -14,5 +15,6 @@ tox # Documentation sphinx -sphinx_rtd_theme +sphinx-rtd-theme +sphinxcontrib-napoleon myst-parser diff --git a/setclass/setclass.py b/setclass/setclass.py index bb58d50..d9b52ce 100644 --- a/setclass/setclass.py +++ b/setclass/setclass.py @@ -6,18 +6,32 @@ import re class SetClass(list): """ - Musical set class, containing zero or more pitch classes. + Musical set class, containing zero or more pitch classes. This implementation can handle set + classes of any arbitrary :attr:`tonality`, or number of divisions of the octave. """ def __init__(self, *args: int, tonality: int = 12) -> None: """ - 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: + Instantiate a :class:`ClassSet` object with a series of integers. - sc = SetClass(0, 7, 9) - sc = SetClass(0, 2, 3, tonality=7) + Each supplied pitch class is "normalised" by modulo the :attr:`tonality`, the set is + sorted in ascending order, and values are transposed (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: + + >>> 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 divisions of the octave. + If unspecified, the default value of 12 is assumed (Western harmony). """ self._tonality = tonality @@ -39,48 +53,60 @@ class SetClass(list): super().append(i) def __repr__(self) -> str: - """Return the Python instance string representation.""" + """Return a string representation of this instance.""" s = f"SetClass{{{','.join(str(i) for i in self.pitch_classes)}}}" return s if self.tonality == 12 else f"{s} (T={self.tonality})" 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]) @property def pitch_classes(self) -> list(int): + """The pitch classes as an ordered :class:`list` of integers.""" return list(self) @cached_property def tonality(self) -> int: """ - Returns the number of (equal) divisions of the octave. The default value is 12, which + 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. """ return self._tonality @cached_property 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) @cached_property 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`. + """ return sum(self.pitch_classes) @cached_property def decimal(self) -> int: """ + 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. For example, {0,1,4,6} has a binary value of - 000001010011, which is the decimal value 83. - — Goyette (2012) p. 25, citing Brinkman (1986). + of 2ⁱ where i is each pitch class value. 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): - """Adjacency intervals between the pitch classes, used for Leonard notation subscripts.""" + """The ordered :class:`list` of adjacency intervals between the pitch classes.""" if not self.pitch_classes: return list() intervals = list() @@ -95,7 +121,14 @@ class SetClass(list): @cached_property def z_relations(self) -> list: """ - Return all distinct set classes with the same interval vector (Allen Forte: "Z-related"). + A :class:`list` of the Z-relations of this set class. + + Allen Forte in his book *The Structure of Atonal Music* (1973) described the relationship + between twins of set classes that share the same :attr:`interval_vector`, but are not + related by :attr:`inversion`, :attr:`complement`, or transposition (rotation), as Z-related + ('Z' for *zygote* from Greek: ζυγωτός, 'joined'). 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 iv⟨1,1,1,1,1,1⟩ but are not inversions, complements, or transpositions (rotations) of each other. """ @@ -104,40 +137,66 @@ class SetClass(list): @cached_property def interval_vector(self) -> list: """ - 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 (1980), p. 100 + An ordered :class:`list` 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⟩. Each element in the vector is the frequency of occurrence of the interval + represented by its ordinal position, i.e. ⟨2,5,4,3,6,1⟩ means two semitones, five major + seconds, four minor thirds, and so on. — Rahn (1980), 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) + ic = self.unordered_interval(a, b) iv[ic - 1] += 1 return iv - def ordered_interval(a: int, b: int) -> int: + def ordered_interval(self, 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 (1980), p. 25 - """ - return (b % 12 - a % 12) % 12 + Return the ordered interval of two pitch classes. - def unordered_interval(a: int, b: int) -> int: + 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: + + i⟨a,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 - 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 (1980), p. 28 + interval") of two :attr:`pitch_classes` is the smaller of the possible values of + :attr:`ordered_interval` (differences in pitch class value): + + i(a,b) = min: i⟨a,b⟩, i⟨b,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(SetClass.ordered_interval(a, b), SetClass.ordered_interval(b, a)) + return min(self.ordered_interval(a, b), self.ordered_interval(b, a)) @cached_property def versions(self) -> list(SetClass): """ - Returns all possible zero-normalised versions (clock rotations) of this set class, - sorted by brightness. See Rahn (1980) Set types, Tₙ + All possible zero-normalised transpositions (rotations) of this set class, sorted by + :attr:`brightness`. See Rahn (1980), Tₙ set types. """ # The empty set class has one version, itself if not self.pitch_classes: @@ -154,9 +213,13 @@ class SetClass(list): @cached_property def rahn_normal_form(self) -> SetClass: """ - 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 (1980), p. 33 + The Rahn normal form of the set class. + + John Rahn's normal form described in his book *Basic Atonal Theory* (1980) is an algorithm + to produce a unique form for each set class (see :attr:`rahn_normal_form`). 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): return [i for i in versions if i.pitch_classes[-n] == min([i.pitch_classes[-n] for i in versions])] @@ -171,8 +234,8 @@ class SetClass(list): @cached_property def packed_left(self) -> SetClass: """ - Return the form of the set class that is most packed to the left (smallest adjacency - intervals to the left). Find the smallest adjacency interval, and proceed towards the right + The form of the set class that is most packed to the left (the smallest adjacency intervals + to the left). Find the smallest first adjacency interval, and proceed towards the right until one result remains. """ def _most_packed(versions, n): @@ -188,11 +251,12 @@ class SetClass(list): @cached_property def prime_form(self) -> SetClass: """ - 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 - working from left to right). - Allen Forte describes the algorithm in his book *The Structure of Atonal Music* (1973) in - section 1.2 (pp. 3-5), citing Milton Babbitt (1961). + Return the prime form of the set class. + + Allen Forte describes the algorithm for finding this normal form in his book *The Structure + of Atonal Music* (1973) in section 1.2 (pp. 3-5), citing Milton Babbitt (1961). Find the + forms with the smallest outside interval, and if necessary chose the form most packed to the + left (the smallest adjacency intervals working from left to right). """ 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])] @@ -211,7 +275,8 @@ class SetClass(list): @cached_property def darkest_form(self) -> SetClass: """ - Returns the version with the smallest brightness value, most packed to the left. + The version of this set class with the smallest :attr:`brightness` value, most packed to the + left. """ if not self.versions: return self @@ -233,7 +298,7 @@ class SetClass(list): @cached_property def brightest_form(self) -> SetClass: """ - Returns the version with the largest brightness value. + The version of this set class with the largest :attr:`brightness` value. TODO: How to break a tie? Or return all matches in a tuple? Sorted by what? """ return self.versions[-1] if self.versions else self @@ -241,15 +306,16 @@ class SetClass(list): @cached_property def inversion(self) -> SetClass: """ - Returns the inversion of this set class, equivalent of reflection through the 0 axis on a - clock diagram. + The inversion of this set class, transposed so the smallest pitch class is 0. Equivalent to + a reflection through the 0 axis on a clock diagram. """ return SetClass(*[self.tonality - i for i in self.pitch_classes], tonality=self.tonality) @cached_property def is_symmetrical(self) -> bool: """ - Returns whether this set class is symmetrical upon inversion, for example Forte 5-Z17: + Whether this set class is symmetrical upon inversion, for example Forte 5-Z17: + {0,1,2,5,9} → {0,4,8,9,11} """ return self.darkest_form == self.inversion.darkest_form @@ -257,34 +323,45 @@ class SetClass(list): @cached_property def complement(self) -> SetClass: """ - Returns the set class containing all pitch classes absent in this one (rotated so the - smallest is 0). + The set class containing all :attr:`pitch_classes` absent in this one, transposed so the + smallest pitch class is 0. """ return SetClass(*[i for i in range(self.tonality) if i not in self.pitch_classes], tonality=self.tonality) @cached_property def dozenal_notation(self) -> str: """ - 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). + A string representation using ↊ and ↋ for 10 and 11. + + 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', '↋') @cached_property def duodecimal_notation(self) -> str: """ - If tonality is no greater than 12, replace 10 and 11 with 'T' and 'E'. + A string representation using T and E for 10 and 11. + + 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') @cached_property def leonard_notation(self) -> str: """ - 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 - pitch class values) denoted as a superscript. In standard tonality (T=12) the letters T and - E are used for pitch classes 10 and 11. For example, Forte 7-34, {0,1,3,4,6,8,10} is - notated [0₁1₂3₁4₂6₂8₂T₂]⁽³²⁾. + The string representation proposed by Brian Leonard. + + Returns a string representation of this set class using subscripts to denote the + :attr:`adjacency_intervals` between the pitch classes instead of commas, and the overall + :attr:`brightness` (sum of the values of :attr:`pitch_classes`) denoted as a superscript. + 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 [0₁1₂3₁4₂6₂8₂T₂]⁽³²⁾ in this scheme: + + >>> SetClass.from_string('{0,1,3,4,6,8,10}').leonard_notation + [0₁1₂3₁4₂6₂8₂T₂]⁽³²⁾ """ # Return numbers as subscript and superscript strings: def subscript(n: int) -> str: @@ -307,17 +384,49 @@ class SetClass(list): @classmethod def from_string(this, string: str, tonality: int = 12) -> SetClass: """ - 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}') + Create a :class:`SetClass` from a string. + + 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,10]") + SetClass{0,2,4,6,8,10} + >>> 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 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) @classmethod - def all_of_cardinality(cls, cardinality: int, tonality: int = 12) -> set: + def all_of_cardinality(this, 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). + 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). + + Args: + cardinality (int): + The :attr:`cardinality` of the set classes to return. + tonality (int): + The :attr:`tonality`, or modulus, is the number of 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): """Set of all possible unique subsets.""" @@ -330,32 +439,68 @@ class SetClass(list): return set(SetClass(*i, tonality=tonality) for i in _powerset(range(tonality)) if len(i) == cardinality and 0 in i) @classmethod - def darkest_of_cardinality(this, cardinality: int, tonality: int = 12, prime: bool = False) -> set: + def darkest_of_cardinality(this, cardinality: int, tonality: int = 12) -> 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). + Returns a :class:`set` of all :class:`SetClass` objects with a given :attr:`cardinality` in + their darkest forms. + + This produces a smaller set than :attr:`all_of_cardinality`, since it eliminates set classes + that are "brighter" transpositions (rotations) 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). + + Args: + cardinality (int): + The :attr:`cardinality` of the set classes to return. + tonality (int): + The :attr:`tonality`, or modulus, is the number of 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)) @classmethod - def normal_of_cardinality(this, cardinality: int, tonality: int = 12, prime: bool = False) -> set: + def normal_of_cardinality(this, cardinality: int, tonality: int = 12) -> 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). + Returns a :class:`set` of all :class:`SetClass` objects with a given :attr:`cardinality` in + their Rahn normal form. + + This produces a smaller set than :attr:`all_of_cardinality`, since it eliminates set classes + that are transpositions (rotations) 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). + + Args: + cardinality (int): + The :attr:`cardinality` of the set classes to return. + tonality (int): + The :attr:`tonality`, or modulus, is the number of 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)) @classmethod 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 - unique form for each set class. Most of the time it is also the "darkest" (smallest - brightness value), except for all the times when that is not the case; this function returns - that list, as a set of (cardinality, darkest, rahn) tuples. + unique form for each set class (see :attr:`rahn_normal_form`). Most of the time it is also + the same as the :attr:`darkest_form` (that with the smallest :attr:`brightness` value), + except for all the times when that is not the case! + + Returns: + A :class:`set` of (:attr:`cardinality`, :attr:`darkest_form`, :attr:`rahn_normal_form`) + tuples. """ cases = set() for C in range(13): # 0 to 12 @@ -367,12 +512,18 @@ class SetClass(list): @classmethod 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 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, which most of the time is also the - "darkest" (smallest brightness 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. + (1961). It produces a unique form for each set class (see :attr:`prime_form`), which most of + the time is the same as the :attr:`darkest_form` (that with the smallest :attr:`brightness` + value), except for all the times when that is not the case! + + Returns: + A :class:`set` of (:attr:`cardinality`, :attr:`darkest_form`, :attr:`prime_form`) tuples. """ + cases = set() for C in range(13): # 0 to 12 for sc in this.all_of_cardinality(C):