Constrained tuning: Difference between revisions
Reduce use of the word "historically" in response to Graham Breed's comment on Facebook where he said the use of "historically" implies this is a settled debate, when it's actually still ongoing |
m Oopsie |
||
| (3 intermediate revisions by 2 users not shown) | |||
| Line 35: | Line 35: | ||
== Computation == | == Computation == | ||
As a standard optimization problem, numerous algorithms exist to solve it, such as {{w|sequential quadratic programming}}, to name one. [[Flora Canou]]'s [https://github.com/FloraCanou/temperament_evaluator | As a standard optimization problem, numerous algorithms exist to solve it, such as {{w|sequential quadratic programming}}, to name one. [[Flora Canou]]'s [https://github.com/FloraCanou/temperament_evaluator Temperament Evaluator] solves constrained tuning problems in [https://www.python.org Python], using [https://scipy.org/ Scipy]'s [https://www.cobyqa.com/stable/ COBYQA] algorithm. Here is an abridged version of it: | ||
{{Todo|rework|comment=Make an absolutely minimal version of it. }} | |||
{{Databox| Code | | {{Databox| Code | | ||
<syntaxhighlight lang="python"> | <syntaxhighlight lang="python"> | ||
# © 2020-2025 Flora Canou | # © 2020-2025 Flora Canou | ||
# This work is licensed under the GNU General Public License version 3. | # This work is licensed under the GNU General Public License version 3. | ||
# Version 0. | # Version 0.30.1 | ||
import warnings | import warnings | ||
| Line 50: | Line 51: | ||
PRIME_LIST = [ | PRIME_LIST = [ | ||
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, | 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, | ||
41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89 | 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89] | ||
] | |||
class SCALAR: | class SCALAR: | ||
| Line 59: | Line 59: | ||
"""Norm profile for the tuning space.""" | """Norm profile for the tuning space.""" | ||
def __init__ (self, wtype = | def __init__ (self, wtype = None, wmode = 1, wstrength = 1, skew = 0, order = 2): | ||
self.wtype = | if wtype: | ||
self. | wmode, wstrength = self.__presets (wtype) | ||
self.wmode = wmode | |||
self.wstrength = wstrength | |||
self.skew = skew | self.skew = skew | ||
self.order = order | self.order = order | ||
def | @staticmethod | ||
match | def __presets (wtype): | ||
match wtype: | |||
case "tenney": | case "tenney": | ||
wmode, wstrength = 1, 1 | |||
case "wilson" | "benedetti": | case "wilson" | "benedetti": | ||
wmode, wstrength = 0, 1 | |||
case "equilateral": | case "equilateral": | ||
wmode, wstrength = 0, 0 | |||
case _: | case _: | ||
warnings.warn ("weighter type not supported, using default (\"tenney\")") | warnings.warn ("weighter type not supported, using default (\"tenney\")") | ||
wmode, wstrength = 1, 1 | |||
return wmode, wstrength | |||
return | |||
def | def __weight_vec (self, primes): | ||
"""Returns the interval weight vector for a list of formal primes. """ | |||
if not isinstance (self.wmode, (int, np.integer)): | |||
raise TypeError ("non-integer modes not supported. ") | |||
def modal_weighter (primes, m): | |||
if m == 0: | |||
return primes | |||
elif m > 0: | |||
return modal_weighter (2*np.log2 (primes), m - 1) | |||
else: | |||
return modal_weighter (np.exp2 (primes/2), m + 1) | |||
return (modal_weighter (np.asarray (primes), self.wmode)/2)**self.wstrength | |||
def val_weight (self, primes): | |||
"""Returns the val weight matrix for a list of formal primes. """ | |||
return np.diag (1/self.__weight_vec (primes)) | |||
def val_skew (self, subgroup): | |||
"""Returns the val skew matrix for a list of formal primes. """ | |||
if self.skew == 0: | if self.skew == 0: | ||
return np.eye (len (subgroup)) | return np.eye (len (subgroup)) | ||
| Line 91: | Line 114: | ||
r*np.ones ((len (subgroup), 1)), axis = 1) | r*np.ones ((len (subgroup), 1)), axis = 1) | ||
def | def val_transform (self, main, subgroup): | ||
return main @ self. | return main @ self.val_weight (subgroup) @ self.val_skew (subgroup) | ||
def __get_subgroup (main, subgroup): | def __get_subgroup (main, subgroup): | ||
| Line 99: | Line 122: | ||
subgroup = PRIME_LIST[:main.shape[1]] | subgroup = PRIME_LIST[:main.shape[1]] | ||
elif main.shape[1] != len (subgroup): | elif main.shape[1] != len (subgroup): | ||
warnings.warn (" | warnings.warn ("dimensionalities do not match. Casting to the smaller dimensionality. ") | ||
dim = min (main.shape[1], len (subgroup)) | dim = min (main.shape[1], len (subgroup)) | ||
main = main[:, :dim] | main = main[:, :dim] | ||
| Line 108: | Line 131: | ||
cons_monzo_list = None, des_monzo = None, show = True): | cons_monzo_list = None, des_monzo = None, show = True): | ||
# NOTE: "map" is a reserved word | # NOTE: "map" is a reserved word | ||
# optimization | # optimization would ideally be performed in the unit of octaves | ||
# unfortunately, that often results in insufficient accuracy | |||
# the cent is a practical choice of unit, and test shows that further scaling | |||
# doesn't improve accuracy for most main-sequence temperaments | |||
breeds, subgroup = __get_subgroup (breeds, subgroup) | breeds, subgroup = __get_subgroup (breeds, subgroup) | ||
just_tuning_map = SCALAR.CENT*np.log2 (subgroup) | just_tuning_map = SCALAR.CENT*np.log2 (subgroup) | ||
breeds_x = norm. | breeds_x = norm.val_transform (breeds, subgroup) | ||
just_tuning_map_x = norm. | just_tuning_map_x = norm.val_transform (just_tuning_map, subgroup) | ||
if norm.order == 2 and cons_monzo_list is None: #simply using lstsq for better performance | if norm.order == 2 and cons_monzo_list is None: #simply using lstsq for better performance | ||
res = linalg.lstsq (breeds_x.T, just_tuning_map_x) | res = linalg.lstsq (breeds_x.T, just_tuning_map_x) | ||
| Line 122: | Line 148: | ||
gen0 = just_tuning_map[:breeds.shape[0]] #initial guess | gen0 = just_tuning_map[:breeds.shape[0]] #initial guess | ||
if cons_monzo_list is None: | if cons_monzo_list is None: | ||
cons_object = () | |||
else: | else: | ||
cons_object = optimize.LinearConstraint ((breeds @ cons_monzo_list).T, | |||
lb = (just_tuning_map @ cons_monzo_list).T, | lb = (just_tuning_map @ cons_monzo_list).T, | ||
ub = (just_tuning_map @ cons_monzo_list).T) | ub = (just_tuning_map @ cons_monzo_list).T) | ||
| Line 140: | Line 166: | ||
if np.asarray (des_monzo).ndim > 1 and np.asarray (des_monzo).shape[1] != 1: | if np.asarray (des_monzo).ndim > 1 and np.asarray (des_monzo).shape[1] != 1: | ||
raise IndexError ("only one destretch target is allowed. ") | raise IndexError ("only one destretch target is allowed. ") | ||
elif ( | elif (des_tempered_size := gen @ breeds @ des_monzo) == 0: | ||
raise ZeroDivisionError ("destretch target is in the nullspace. ") | raise ZeroDivisionError ("destretch target is in the nullspace. ") | ||
else: | else: | ||
gen *= (just_tuning_map @ des_monzo)/ | gen *= (just_tuning_map @ des_monzo)/des_tempered_size | ||
tempered_tuning_map = gen @ breeds | tempered_tuning_map = gen @ breeds | ||
| Line 352: | Line 378: | ||
This is one of the reasons why the tuning of 1-5/4-3/2 is so skewed in CTE blackwood. This problem doesn't happen with the TE tuning: the extra degree of freedom in adjusting the octave, and different weights, tend to even this kind of thing out. TE blackwood has 1-5/4-3/2 tuned to approximately 0-398-717 cents, which does seem to evenly split the error between the 5/4 and 6/5. We can see that something good about the way that TE tunes compact triads has not quite translated to CTE. Another way to look at this situation is that with CTE, 5/4 is prioritized more strongly than 6/5, and also 1-3-5 is tuned as nicely as possible, instead of 1-5/4-3/2. | This is one of the reasons why the tuning of 1-5/4-3/2 is so skewed in CTE blackwood. This problem doesn't happen with the TE tuning: the extra degree of freedom in adjusting the octave, and different weights, tend to even this kind of thing out. TE blackwood has 1-5/4-3/2 tuned to approximately 0-398-717 cents, which does seem to evenly split the error between the 5/4 and 6/5. We can see that something good about the way that TE tunes compact triads has not quite translated to CTE. Another way to look at this situation is that with CTE, 5/4 is prioritized more strongly than 6/5, and also 1-3-5 is tuned as nicely as possible, instead of 1-5/4-3/2. | ||
=== Defense of CTE === | |||
Anyone who performs tuning optimization has [[octave reduction]] to unlearn. It is tempting to optimize for close-voiced chords such as 1–5/4–3/2 without much consideration, since textbooks often present harmony in this way. The close-voiced chord, 1-5/4-3/2, is an octave-reduced version of 1-3-5, with the latter being the simplest voicing possible in the [[chord of nature]] and nontrivially being the simplest such chord containing the fundamental (the 1st harmonic/true root). It is thus important to recognize that all octave-reductions are but simplifications for our cognitive processes. | Anyone who performs tuning optimization has [[octave reduction]] to unlearn. It is tempting to optimize for close-voiced chords such as 1–5/4–3/2 without much consideration, since textbooks often present harmony in this way. The close-voiced chord, 1-5/4-3/2, is an octave-reduced version of 1-3-5, with the latter being the simplest voicing possible in the [[chord of nature]] and nontrivially being the simplest such chord containing the fundamental (the 1st harmonic/true root). It is thus important to recognize that all octave-reductions are but simplifications for our cognitive processes. | ||
| Line 418: | Line 444: | ||
== Special constraint == | == Special constraint == | ||
The special eigenmonzo ''X'''''j''', where '''j''' is the all-ones monzo, has the effect of removing the weighted–skewed tuning bias. This eigenmonzo is actually proportional to the monzo of the extra dimension introduced by the skew. In other words, it forces the extra dimension to be pure, and therefore, the skew will have no effect with this constrained tuning. | The special eigenmonzo ''X''⋅'''j''', where '''j''' is the all-ones monzo, has the effect of removing the weighted–skewed tuning bias. This eigenmonzo is actually proportional to the monzo of the extra dimension introduced by the skew. In other words, it forces the extra dimension to be pure, and therefore, the skew will have no effect with this constrained tuning. | ||
It can be regarded as a distinct optimum. In the case of Tenney weighting, it is the '''TOCTE tuning''' ('''Tenney ones constrained Tenney–Euclidean tuning'''). | It can be regarded as a distinct optimum. In the case of Tenney weighting, it is the '''TOCTE tuning''' ('''Tenney ones constrained Tenney–Euclidean tuning'''). | ||
| Line 444: | Line 470: | ||
$$ | $$ | ||
As a result, the [[ | As a result, the [[relative interval error #Linearity|relative error space]] is also linear with respect to ''V''. | ||
For example, the relative errors of | For example, the relative errors of 12et in 5-limit TOC is | ||
$$ \mathcal{E}_\text {r}(12) = \val{-1.55\% & -4.42\% & +10.08\% } $$ | $$ \mathcal{E}_\text {r}(12) = \val{-1.55\% & -4.42\% & +10.08\% } $$ | ||
That of | That of 19et in this tuning is | ||
$$ \mathcal{E}_\text {r}(19) = \val{+4.08\% & -4.97\% & -2.19\% } $$ | $$ \mathcal{E}_\text {r}(19) = \val{+4.08\% & -4.97\% & -2.19\% } $$ | ||
As 31 = 12 + 19, the relative errors of | As 31 = 12 + 19, the relative errors of 31et in this tuning is | ||
$$ | $$ | ||
| Line 464: | Line 490: | ||
== Systematic name == | == Systematic name == | ||
In [[D&D's guide|D&D's guide to RTT]], the [[Dave Keenan & Douglas Blumeyer's guide to RTT/Alternative complexities#Naming|systematic name]] for the CTE tuning scheme is ''[[Dave Keenan %26 Douglas Blumeyer%27s guide to RTT/All-interval tuning schemes #Held-octave minimax-.28E.29S|held-octave minimax-ES]]'', and the systematic name for the CTWE tuning scheme is ''[[Dave Keenan %26 Douglas Blumeyer%27s guide to RTT/Tuning fundamentals #Held-intervals|held-octave]] [[Dave Keenan %26 Douglas Blumeyer%27s guide to RTT/Alternative complexities #Tunings used in 7|minimax-E-lils-S]]''. | In [[D&D's guide|D&D's guide to RTT]], the [[Dave Keenan & Douglas Blumeyer's guide to RTT/Alternative complexities #Naming|systematic name]] for the CTE tuning scheme is ''[[Dave Keenan %26 Douglas Blumeyer%27s guide to RTT/All-interval tuning schemes #Held-octave minimax-.28E.29S|held-octave minimax-ES]]'', and the systematic name for the CTWE tuning scheme is ''[[Dave Keenan %26 Douglas Blumeyer%27s guide to RTT/Tuning fundamentals #Held-intervals|held-octave]] [[Dave Keenan %26 Douglas Blumeyer%27s guide to RTT/Alternative complexities #Tunings used in 7|minimax-E-lils-S]]''. | ||
== Open problems == | == Open problems == | ||