Constrained tuning: Difference between revisions

BudjarnLambeth (talk | contribs)
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/blob/master/te_optimizer_legacy.py tuning optimizer] solves constrained tuning problems in [https://www.python.org Python], using [https://scipy.org/ Scipy].  
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.28.1
# 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 = "tenney", wamount = 1, skew = 0, order = 2):
     def __init__ (self, wtype = None, wmode = 1, wstrength = 1, skew = 0, order = 2):
         self.wtype = wtype
         if wtype:
         self.wamount = wamount
            wmode, wstrength = self.__presets (wtype)
        self.wmode = wmode
         self.wstrength = wstrength
         self.skew = skew
         self.skew = skew
         self.order = order
         self.order = order


     def __get_tuning_weight (self, subgroup):
    @staticmethod
         match self.wtype:
     def __presets (wtype):
         match wtype:  
             case "tenney":
             case "tenney":
                 weight_vec = np.reciprocal (np.log2 (subgroup, dtype = float))
                 wmode, wstrength = 1, 1
             case "wilson" | "benedetti":
             case "wilson" | "benedetti":
                 weight_vec = np.reciprocal (np.array (subgroup, dtype = float))
                 wmode, wstrength = 0, 1
             case "equilateral":
             case "equilateral":
                 weight_vec = np.ones (len (subgroup))
                 wmode, wstrength = 0, 0
             case _:
             case _:
                 warnings.warn ("weighter type not supported, using default (\"tenney\")")
                 warnings.warn ("weighter type not supported, using default (\"tenney\")")
                 self.wtype = "tenney"
                 wmode, wstrength = 1, 1
                return self.__get_weight (subgroup)
         return wmode, wstrength
         return np.diag (weight_vec**self.wamount)


     def __get_tuning_skew (self, subgroup):
     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 tuning_x (self, main, subgroup):
     def val_transform (self, main, subgroup):
         return main @ self.__get_tuning_weight (subgroup) @ self.__get_tuning_skew (subgroup)
         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 ("dimension does not match. Casting to the smaller dimension. ")
         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 is preferably done in the unit of octaves, but for precision reasons
     # 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.tuning_x (breeds, subgroup)
     breeds_x = norm.val_transform (breeds, subgroup)
     just_tuning_map_x = norm.tuning_x (just_tuning_map, subgroup)
     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 = ()
             cons_object = ()
         else:
         else:
             cons = optimize.LinearConstraint ((breeds @ cons_monzo_list).T,  
             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 (tempered_size := gen @ breeds @ des_monzo) == 0:
         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)/tempered_size
             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 ====
=== 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 [[Relative interval error #Linearity|relative error space]] is also linear with respect to ''V''.  
As a result, the [[relative interval error #Linearity|relative error space]] is also linear with respect to ''V''.  


For example, the relative errors of 12ettoc5 (12et in 5-limit TOC) is
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 19ettoc5 is
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 31ettoc5 is
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 ==