Add Forte prime form, fix darkest form and Rahn errors, use classmethods
- add the packed_left and prime_form property for Allen Forte prime forms - add type annotations - add from_string convenience class factory method - fix darkest_form to always produce the same result using left-packing - amend tests to correct values (eliminated duplicates in bright lists) - produce lists of cases where Rahn and Forte primes are not the "darkest" forms (smallest sum of pitch classes)
This commit is contained in:
parent
d6b4b07d86
commit
0bf0531fd6
3 changed files with 152 additions and 67 deletions
|
|
@ -1,4 +1,7 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
class cache_property:
|
class cache_property:
|
||||||
"""
|
"""
|
||||||
|
|
@ -19,7 +22,7 @@ class SetClass(list):
|
||||||
Musical set class, containing zero or more pitch classes.
|
Musical set class, containing zero or more pitch classes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args: int, tonality: int = 12):
|
def __init__(self, *args: int, tonality: int = 12) -> None:
|
||||||
"""
|
"""
|
||||||
Instantiate a Class Set with a series of integers. Each pitch class is "normalised" by
|
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
|
modulo the tonality; the set is sorted in ascending order; and values "rotated" until the
|
||||||
|
|
@ -57,7 +60,7 @@ class SetClass(list):
|
||||||
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):
|
def pitch_classes(self) -> list:
|
||||||
return list(self)
|
return list(self)
|
||||||
|
|
||||||
@cache_property
|
@cache_property
|
||||||
|
|
@ -79,7 +82,7 @@ class SetClass(list):
|
||||||
return sum(self.pitch_classes)
|
return sum(self.pitch_classes)
|
||||||
|
|
||||||
@cache_property
|
@cache_property
|
||||||
def adjacency_intervals(self):
|
def adjacency_intervals(self) -> list:
|
||||||
"""Adjacency intervals between the pitch classes, used for Leonard notation subscripts."""
|
"""Adjacency intervals between the pitch classes, used for Leonard notation subscripts."""
|
||||||
if not self.pitch_classes:
|
if not self.pitch_classes:
|
||||||
return list()
|
return list()
|
||||||
|
|
@ -93,7 +96,7 @@ class SetClass(list):
|
||||||
return intervals
|
return intervals
|
||||||
|
|
||||||
@cache_property
|
@cache_property
|
||||||
def z_relations(self):
|
def z_relations(self) -> list:
|
||||||
"""
|
"""
|
||||||
Return all distinct set classes with the same interval vector (Allan Forte: "Z-related").
|
Return all distinct set classes with the same interval vector (Allan Forte: "Z-related").
|
||||||
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
|
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
|
||||||
|
|
@ -134,7 +137,7 @@ class SetClass(list):
|
||||||
return min(SetClass.ordered_interval(a, b), SetClass.ordered_interval(b, a))
|
return min(SetClass.ordered_interval(a, b), SetClass.ordered_interval(b, a))
|
||||||
|
|
||||||
@cache_property
|
@cache_property
|
||||||
def versions(self):
|
def versions(self) -> list:
|
||||||
"""
|
"""
|
||||||
Returns all possible zero-normalised versions (clock rotations) of this set class,
|
Returns all possible zero-normalised versions (clock rotations) of this set class,
|
||||||
sorted by brightness. See Rahn (1980) Set types, Tₙ
|
sorted by brightness. See Rahn (1980) Set types, Tₙ
|
||||||
|
|
@ -152,19 +155,12 @@ class SetClass(list):
|
||||||
return versions
|
return versions
|
||||||
|
|
||||||
@cache_property
|
@cache_property
|
||||||
def rahn_normal_form(self):
|
def rahn_normal_form(self) -> SetClass:
|
||||||
"""
|
"""
|
||||||
Return the Rahn normal form of the set class; Leonard describes this as "most dispersed from
|
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
|
the right". Find the smallest outside interval, and proceed inwards from the right until one
|
||||||
result remains. See Rahn (1980), p. 33
|
result remains. See Rahn (1980), 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):
|
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])]
|
||||||
|
|
||||||
|
|
@ -176,15 +172,69 @@ class SetClass(list):
|
||||||
return versions[0]
|
return versions[0]
|
||||||
|
|
||||||
@cache_property
|
@cache_property
|
||||||
def darkest_form(self):
|
def packed_left(self) -> SetClass:
|
||||||
"""
|
"""
|
||||||
Returns the version with the smallest brightness value.
|
Return the form of the set class that is most packed to the left (smallest adjacency
|
||||||
TODO: How to break a tie? Or return all matches in a tuple? Sorted by what?
|
intervals to the left). Find the smallest adjacency interval, and proceed towards the right
|
||||||
|
until one result remains.
|
||||||
"""
|
"""
|
||||||
return self.versions[0] if self.versions else self
|
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])]
|
||||||
|
|
||||||
|
versions = self.versions
|
||||||
|
n = 0
|
||||||
|
while len(versions) > 1:
|
||||||
|
versions = _most_packed(versions, n)
|
||||||
|
n += 1
|
||||||
|
return versions[0]
|
||||||
|
|
||||||
@cache_property
|
@cache_property
|
||||||
def brightest_form(self):
|
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).
|
||||||
|
"""
|
||||||
|
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])]
|
||||||
|
|
||||||
|
# Requirement 1: select forms where the largest pitch class has the smallest value
|
||||||
|
smallest_span = min([i.pitch_classes[-1] for i in self.versions])
|
||||||
|
versions = [i for i in self.versions if i.pitch_classes[-1] == smallest_span]
|
||||||
|
|
||||||
|
# Requirement 2: select forms most packed to the left
|
||||||
|
n = 0
|
||||||
|
while len(versions) > 1:
|
||||||
|
versions = _most_packed(versions, n)
|
||||||
|
n += 1
|
||||||
|
return versions[0]
|
||||||
|
|
||||||
|
@cache_property
|
||||||
|
def darkest_form(self) -> SetClass:
|
||||||
|
"""
|
||||||
|
Returns the version with the smallest brightness value, most packed to the left.
|
||||||
|
"""
|
||||||
|
if not self.versions:
|
||||||
|
return self
|
||||||
|
|
||||||
|
B = min(i.brightness for i in self.versions)
|
||||||
|
versions = [i for i in self.versions if i.brightness == B]
|
||||||
|
if len(versions) == 1:
|
||||||
|
return versions[0]
|
||||||
|
|
||||||
|
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])]
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
while len(versions) > 1:
|
||||||
|
versions = _most_packed(versions, n)
|
||||||
|
n += 1
|
||||||
|
return versions[0]
|
||||||
|
|
||||||
|
@cache_property
|
||||||
|
def brightest_form(self) -> SetClass:
|
||||||
"""
|
"""
|
||||||
Returns the version with the largest brightness value.
|
Returns the version with the largest brightness value.
|
||||||
TODO: How to break a tie? Or return all matches in a tuple? Sorted by what?
|
TODO: How to break a tie? Or return all matches in a tuple? Sorted by what?
|
||||||
|
|
@ -192,7 +242,7 @@ class SetClass(list):
|
||||||
return self.versions[-1] if self.versions else self
|
return self.versions[-1] if self.versions else self
|
||||||
|
|
||||||
@cache_property
|
@cache_property
|
||||||
def inversion(self):
|
def inversion(self) -> SetClass:
|
||||||
"""
|
"""
|
||||||
Returns the inversion of this set class, equivalent of reflection through the 0 axis on a
|
Returns the inversion of this set class, equivalent of reflection through the 0 axis on a
|
||||||
clock diagram.
|
clock diagram.
|
||||||
|
|
@ -200,7 +250,7 @@ class SetClass(list):
|
||||||
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)
|
||||||
|
|
||||||
@cache_property
|
@cache_property
|
||||||
def is_symmetrical(self):
|
def is_symmetrical(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns whether this set class is symmetrical upon inversion, for example Forte 5-Z17:
|
Returns whether this set class is symmetrical upon inversion, for example Forte 5-Z17:
|
||||||
{0,1,2,5,9} → {0,4,8,9,11}
|
{0,1,2,5,9} → {0,4,8,9,11}
|
||||||
|
|
@ -208,7 +258,7 @@ class SetClass(list):
|
||||||
return self.darkest_form == self.inversion.darkest_form
|
return self.darkest_form == self.inversion.darkest_form
|
||||||
|
|
||||||
@cache_property
|
@cache_property
|
||||||
def complement(self):
|
def complement(self) -> SetClass:
|
||||||
"""
|
"""
|
||||||
Returns the set class containing all pitch classes absent in this one (rotated so the
|
Returns the set class containing all pitch classes absent in this one (rotated so the
|
||||||
smallest is 0).
|
smallest is 0).
|
||||||
|
|
@ -257,7 +307,16 @@ class SetClass(list):
|
||||||
|
|
||||||
# Class methods -----------------------------------------------------------
|
# Class methods -----------------------------------------------------------
|
||||||
|
|
||||||
def all_of_cardinality(cardinality: int, tonality: int = 12) -> set:
|
@classmethod
|
||||||
|
def from_string(this, string: str) -> 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}')
|
||||||
|
"""
|
||||||
|
return SetClass(*re.findall(r'\d+', string))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all_of_cardinality(cls, cardinality: int, tonality: int = 12) -> set:
|
||||||
"""
|
"""
|
||||||
Returns all set classes of a given cardinality. Tonality can be specified, default is 12.
|
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
|
Warning: high values of tonality can take a long time to calculate (T=24 takes about a
|
||||||
|
|
@ -273,31 +332,33 @@ 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)
|
||||||
|
|
||||||
def darkest_of_cardinality(cardinality: int, tonality: int = 12, prime: bool = False) -> set:
|
@classmethod
|
||||||
|
def darkest_of_cardinality(this, cardinality: int, tonality: int = 12, prime: bool = False) -> set:
|
||||||
"""
|
"""
|
||||||
Returns all set classes of a given cardinality, in their darkest forms (ignore rotations).
|
Returns all set classes of a given cardinality, in their darkest forms (ignore rotations).
|
||||||
Tonality can be specified, default is 12.
|
Tonality can be specified, default is 12.
|
||||||
Warning: high values of tonality can take a long time to calculate (T=24 takes about a
|
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).
|
||||||
"""
|
"""
|
||||||
return set(i.darkest_form for i in SetClass.all_of_cardinality(cardinality, tonality))
|
return set(i.darkest_form for i in this.all_of_cardinality(cardinality, tonality))
|
||||||
|
|
||||||
def normal_of_cardinality(cardinality: int, tonality: int = 12, prime: bool = False) -> set:
|
@classmethod
|
||||||
|
def normal_of_cardinality(this, cardinality: int, tonality: int = 12, prime: bool = False) -> set:
|
||||||
"""
|
"""
|
||||||
Returns all set classes of a given cardinality, in their (darkest) Rahn normal forms (ignore
|
Returns all set classes of a given cardinality, in their (darkest) Rahn normal forms (ignore
|
||||||
rotations). Tonality can be specified, default is 12.
|
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
|
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).
|
||||||
"""
|
"""
|
||||||
return set(i.rahn_normal_form for i in SetClass.all_of_cardinality(cardinality, tonality))
|
return set(i.rahn_normal_form for i in this.all_of_cardinality(cardinality, tonality))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
def bright_rahn_normal_forms():
|
def bright_rahn_normal_forms(this) -> set:
|
||||||
"""
|
"""
|
||||||
John Rahn's normal form from _Basic Atonal Theory_ (1980) is an algorithm to produce a unique
|
John Rahn's normal form from *Basic Atonal Theory* (1980) is an algorithm to produce a
|
||||||
form for each set class. Most of the time it is also the "darkest" (smallest brightness value),
|
unique form for each set class. Most of the time it is also the "darkest" (smallest
|
||||||
except for all the times when that is not the case; this function returns that list, as a set of
|
brightness value), except for all the times when that is not the case; this function returns
|
||||||
(cardinality, darkest, rahn) tuples.
|
that list, as a set of (cardinality, darkest, rahn) tuples.
|
||||||
"""
|
"""
|
||||||
cases = set()
|
cases = set()
|
||||||
for C in range(13): # 0 to 12
|
for C in range(13): # 0 to 12
|
||||||
|
|
@ -305,3 +366,19 @@ def bright_rahn_normal_forms():
|
||||||
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
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def bright_prime_forms(this) -> set:
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
cases = set()
|
||||||
|
for C in range(13): # 0 to 12
|
||||||
|
for sc in SetClass.all_of_cardinality(C):
|
||||||
|
if sc.darkest_form.brightness < sc.prime_form.brightness:
|
||||||
|
cases.add((C, sc.darkest_form, sc.prime_form))
|
||||||
|
return cases
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ def test_all_of_cardinality_set_lengths():
|
||||||
def test_darkest_of_cardinality_set_lengths():
|
def test_darkest_of_cardinality_set_lengths():
|
||||||
"Confirm expected lengths of the sets of set classes, for cardinality 1 through 12"
|
"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)]
|
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]
|
assert [len(s) for s in sets] == [1, 6, 19, 43, 66, 80, 66, 43, 19, 6, 1, 1]
|
||||||
|
|
||||||
|
|
||||||
def test_normal_of_cardinality_set_lengths():
|
def test_normal_of_cardinality_set_lengths():
|
||||||
|
|
@ -48,7 +48,7 @@ def test_all_of_cardinality():
|
||||||
assert SetClass(0, 1, 2) in s3
|
assert SetClass(0, 1, 2) in s3
|
||||||
assert SetClass(0, 10, 11) in s3
|
assert SetClass(0, 10, 11) in s3
|
||||||
|
|
||||||
assert SetClass(0, 4, 6) in s3
|
assert SetClass(0, 2, 8) in s3
|
||||||
assert SetClass(0, 6, 10) in s3
|
assert SetClass(0, 6, 10) in s3
|
||||||
|
|
||||||
assert SetClass(0, 1, 3, 4, 8, 10) in s6
|
assert SetClass(0, 1, 3, 4, 8, 10) in s6
|
||||||
|
|
@ -58,14 +58,14 @@ def test_all_of_cardinality():
|
||||||
def test_darkest_of_cardinality():
|
def test_darkest_of_cardinality():
|
||||||
s3 = SetClass.darkest_of_cardinality(3)
|
s3 = SetClass.darkest_of_cardinality(3)
|
||||||
s6 = SetClass.darkest_of_cardinality(6)
|
s6 = SetClass.darkest_of_cardinality(6)
|
||||||
assert len(s3) == 22
|
assert len(s3) == 19
|
||||||
assert len(s6) == 127
|
assert len(s6) == 80
|
||||||
|
|
||||||
# only one version (rotation) of each set class is present
|
# only one version (rotation) of each set class is present
|
||||||
assert SetClass(0, 1, 2) in s3
|
assert SetClass(0, 1, 2) in s3
|
||||||
assert SetClass(0, 10, 11) not in s3
|
assert SetClass(0, 10, 11) not in s3
|
||||||
|
|
||||||
assert SetClass(0, 4, 6) in s3
|
assert SetClass(0, 2, 8) in s3
|
||||||
assert SetClass(0, 6, 10) not in s3
|
assert SetClass(0, 6, 10) not in s3
|
||||||
|
|
||||||
assert SetClass(0, 1, 3, 4, 8, 10) in s6
|
assert SetClass(0, 1, 3, 4, 8, 10) in s6
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from setclass.setclass import SetClass, bright_rahn_normal_forms
|
from setclass.setclass import SetClass
|
||||||
|
|
||||||
|
|
||||||
def test_brightness_conjecture():
|
def test_brightness_conjecture():
|
||||||
|
|
@ -23,29 +23,24 @@ def test_brightness_conjecture():
|
||||||
|
|
||||||
def test_bright_rahn():
|
def test_bright_rahn():
|
||||||
"""
|
"""
|
||||||
There are 63 instances where the Rahn normal form is not also the "darkest" (smallest sum of
|
There are 61 instances where the Rahn normal form is not also the "darkest" (smallest sum of
|
||||||
pitch class values).
|
pitch class values).
|
||||||
"""
|
"""
|
||||||
R = bright_rahn_normal_forms()
|
R = SetClass.bright_rahn_normal_forms()
|
||||||
assert len(R) == 63
|
assert len(R) == 61
|
||||||
counts = {
|
sets = [[i for i in R if i[0] == C] for C in range(13)]
|
||||||
0: 0,
|
assert [len(s) for s in sets] == [0, 0, 0, 1, 3, 17, 6, 23, 8, 3, 0, 0, 0]
|
||||||
1: 0,
|
|
||||||
2: 0,
|
|
||||||
3: 1,
|
def test_bright_prime_forms():
|
||||||
4: 3,
|
"""
|
||||||
5: 17,
|
There are 59 instances where the prime form is not also the "darkest" (smallest sum of pitch
|
||||||
6: 7,
|
class values).
|
||||||
7: 23,
|
"""
|
||||||
8: 9,
|
P = SetClass.bright_prime_forms()
|
||||||
9: 3,
|
assert len(P) == 59
|
||||||
10: 0,
|
sets = [[i for i in P if i[0] == C] for C in range(13)]
|
||||||
11: 0,
|
assert [len(s) for s in sets] == [0, 0, 0, 1, 3, 16, 6, 23, 8, 2, 0, 0, 0]
|
||||||
12: 0
|
|
||||||
}
|
|
||||||
for C in range(13): # 0 to 12
|
|
||||||
bright_rahns = set(i for i in R if i[0] == C)
|
|
||||||
assert len(bright_rahns) == counts[C]
|
|
||||||
|
|
||||||
|
|
||||||
def print_bright_rahn():
|
def print_bright_rahn():
|
||||||
|
|
@ -56,6 +51,19 @@ def print_bright_rahn():
|
||||||
values).
|
values).
|
||||||
"""
|
"""
|
||||||
print("Smallest brightness, Rahn normal form")
|
print("Smallest brightness, Rahn normal form")
|
||||||
set_classes = sorted(bright_rahn_normal_forms(), key=lambda x: x[0] * 1000 + x[1].brightness)
|
set_classes = sorted(SetClass.bright_rahn_normal_forms(), key=lambda x: x[0] * 1000 + x[1].brightness)
|
||||||
|
for (C, d, r) in set_classes:
|
||||||
|
print(f"{d.leonard_notation}, {r.leonard_notation}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_bright_prime():
|
||||||
|
"""
|
||||||
|
Convenience function, to print the list of set classes where the Forte prime form is not the
|
||||||
|
"darkest" form (that with the smallest brightness value), as pairs of "darkest" and Forte prime
|
||||||
|
form pairs, using Leonard notation, sorted by cardinality then brightness (sum of pitch class
|
||||||
|
values).
|
||||||
|
"""
|
||||||
|
print("Smallest brightness, Forte prime form")
|
||||||
|
set_classes = sorted(SetClass.bright_prime_forms(), key=lambda x: x[0] * 1000 + x[1].brightness)
|
||||||
for (C, d, r) in set_classes:
|
for (C, d, r) in set_classes:
|
||||||
print(f"{d.leonard_notation}, {r.leonard_notation}")
|
print(f"{d.leonard_notation}, {r.leonard_notation}")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue