Constrained tuning: Difference between revisions
m Oopsie |
|||
| (7 intermediate revisions by 2 users not shown) | |||
| Line 32: | Line 32: | ||
The problem is feasible if | The problem is feasible if | ||
# rank(''M'') ≤ rank(''V''), and | # rank(''M'') ≤ rank(''V''), and | ||
# The subgroups of ''M'' and | # The subgroups of ''M'' and nullspace(''V'') are {{w|linear independence|linearly independent}}. | ||
== 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 200: | Line 226: | ||
Notice we introduced the vector of lagrange multipliers ''Λ'', with length equal to the number of constraints. The lagrange multipliers have no concrete meaning for the resulting tuning, so they can be discarded. | Notice we introduced the vector of lagrange multipliers ''Λ'', with length equal to the number of constraints. The lagrange multipliers have no concrete meaning for the resulting tuning, so they can be discarded. | ||
=== Simple fast closed-form algorithm === | ==== Simple fast closed-form algorithm ==== | ||
Another way to compute the CTE and CWE tunings, and the CTWE tuning in general, is to use the pseudoinverse. | Another way to compute the CTE and CWE tunings, and the CTWE tuning in general, is to use the pseudoinverse. | ||
| Line 285: | Line 311: | ||
}} | }} | ||
=== Interpolating TE/CTE === | ==== Interpolating TE/CTE ==== | ||
We can also interpolate between the TE and CTE tunings, if we want. To do this, we modify the TE tuning so that the weighting of the 2's coefficient is very large. As the weighting goes to infinity, we get the CTE tuning. Thus, we can set it to some sufficiently large number, so that we get whatever numerical precision we want, and compute the result in closed-form using the pseudoinverse. Without comments, docstrings, etc, the calculation is only about five lines of python code: | We can also interpolate between the TE and CTE tunings, if we want. To do this, we modify the TE tuning so that the weighting of the 2's coefficient is very large. As the weighting goes to infinity, we get the CTE tuning. Thus, we can set it to some sufficiently large number, so that we get whatever numerical precision we want, and compute the result in closed-form using the pseudoinverse. Without comments, docstrings, etc, the calculation is only about five lines of python code: | ||
| Line 334: | Line 360: | ||
</pre> | </pre> | ||
== | == Comparison of tunings == | ||
{{Todo|inline=1| rework |comment=More properly and concisely summarize each side's POV. }} | |||
=== Criticism of CTE === | === Criticism of CTE === | ||
People have long noted, since the early days of the tuning | People have long noted, since the early days of the [[Yahoo Groups tuning lists]], that the CTE tuning, despite having very nice qualities on paper, can give surprisingly strange results.{{citation needed}} One good example is blackwood, where the 4:5:6 chord is tuned to 0–386–720 cents, so that the error is not even close to evenly divided between the 5/4, 6/5, and 3/2. The reasons for this are subtle. | ||
This sort of thing was important | This sort of thing was important to early [[regular temperament theory]] pioneers when looking at optimal tunings for meantone, and is ultimately the motivation for advanced tuning methods such as TOP, TE, etc. to begin with. Thus, if our goal is to extend this principle in an elegant way to all intervals (and hopefully, triads and large chords), it would seem to defeat the purpose if we use a tuning optimization that doesn't also have this property, | ||
As a result of this, | As a result of this, many use the POTE tuning, which tunes it to a result that is approximately [[delta-rational]]: 0-400-720 cents. People have also suggested using the Kees-Euclidean or KE tuning, also known as the constrained-Weil-Euclidean or CWE tuning. Here is a summary of the math involved and the reasoning behind this: | ||
The CTE tuning can be thought of as a modified TE tuning in which the weighting (in monzo space) on the 2/1 coordinate has been changed to 0, making it a kind of seminorm rather than a norm. As a result, all elements in the same octave-equivalence class are weighted identically: they are all given complexity equal to the ''representative'' in each equivalence class in which all factors of 2 have been removed. Thus 5/4 is given the same complexity as 5/1, 13/8 as 13/1, and so on. | The CTE tuning can be thought of as a modified TE tuning in which the weighting (in monzo space) on the 2/1 coordinate has been changed to 0, making it a kind of seminorm rather than a norm. As a result, all elements in the same octave-equivalence class are weighted identically: they are all given complexity equal to the ''representative'' in each equivalence class in which all factors of 2 have been removed. Thus 5/4 is given the same complexity as 5/1, 13/8 as 13/1, and so on. | ||
| Line 362: | Line 390: | ||
Another way to think of it is that as POTE destretches the equave, it keeps the angle in the tuning space unchanged, and thus can be thought of as sacrificing multiplicative (typically very large) ratios for divisive (typically very small) ratios, whereas CTE sticks to the original design book of TE-optimality without worrying about that. | Another way to think of it is that as POTE destretches the equave, it keeps the angle in the tuning space unchanged, and thus can be thought of as sacrificing multiplicative (typically very large) ratios for divisive (typically very small) ratios, whereas CTE sticks to the original design book of TE-optimality without worrying about that. | ||
Observe that POTE tuning can be thought of as an approximation to the CWE/KE tuning, which we will talk about below. | |||
=== Using the Weil norm or Kees expressibility === | === Using the Weil norm or Kees expressibility === | ||
| Line 382: | Line 410: | ||
So, one simple solution is to interpolate between the two, giving the '''Tenney–Weil–Euclidean norm''': a weighted average of the TE and WE norms, with free weighting parameter k. This can be thought of as adjusting how much we care about the span: {{nowrap|k {{=}} 0}} is the TE norm, {{nowrap|k {{=}} 1}} is the WE norm, and in between we have intermediate norms. This also gives a '''Constrained Tenney–Weil–Euclidean''' or '''CTWE''' tuning as a result, which interpolates between CTE and CKE. | So, one simple solution is to interpolate between the two, giving the '''Tenney–Weil–Euclidean norm''': a weighted average of the TE and WE norms, with free weighting parameter k. This can be thought of as adjusting how much we care about the span: {{nowrap|k {{=}} 0}} is the TE norm, {{nowrap|k {{=}} 1}} is the WE norm, and in between we have intermediate norms. This also gives a '''Constrained Tenney–Weil–Euclidean''' or '''CTWE''' tuning as a result, which interpolates between CTE and CKE. | ||
=== | === Examples === | ||
These tunings can be very different from each other. | These tunings can be very different from each other. | ||
| Line 416: | 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 442: | 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 462: | 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 == | ||