Constrained tuning: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
m Added internal link for TWE tuning
Rework for constrained tunings in general
Line 1: Line 1:
'''Constrained tunings''' are tuning optimization techniques under the constraints of some purely tuned intervals (i.e. [[eigenmonzo]]s). The '''CTE tuning''' ('''constrained Tenney-Euclidean tuning''') is the most typical instance and will be the focus of this article. Otherwise normed tunings can be defined and computed analogously.  
'''Constrained tunings''' are tuning [[optimization]] techniques using the constraint of some purely tuned intervals (i.e. [[eigenmonzo]]s). '''CTE tuning''' ('''constrained Tenney-Euclidean tuning''') is the most typical instance. It has a more sophisticated variant, '''CTWE tuning''' ('''constrained Tenney-Weil-Euclidean tuning'''), aka '''KE tuning''' ('''Kees-Euclidean tuning'''). These two tunings will be the focus of this article. Otherwise normed tunings can be defined and computed analogously.  


While the TE tuning can be viewed as a [[Wikipedia: Least squares|least squares problem]], the CTE tuning can be viewed as an equality-constrained least squares problem. For a rank-''r'' temperament, specifying ''m'' eigenmonzos will yield ''r'' - ''m'' [[Wikipedia: Degrees of freedom|degrees of freedom]] to be optimized.  
All constrained tunings are standard temperament optimization problems. Specifically, as [[TE tuning]] can be viewed as a [[Wikipedia: Least squares|least squares problem]], CTE tuning can be viewed as an equality-constrained least squares problem.  


The most significant form of CTE tuning is pure-octave constrained, which is assumed unless specified otherwise. For higher-rank temperaments, it may make sense to add multiple constraints, such as the pure-{2, 3} CTE tuning.  
The most common subject of constraint is the octave, which is assumed unless specified otherwise. For higher-rank temperaments, it may make sense to add multiple constraints, such as a pure-2.3 constrained tuning. For a rank-''r'' temperament, specifying ''m'' eigenmonzos will yield ''r'' - ''m'' [[Wikipedia: Degrees of freedom|degrees of freedom]] to be optimized.  


== Definition ==
== Definition ==
Given a temperament [[mapping]] A and the [[JIP]] J<sub>0</sub>, denote the Tenney-weighted temperament mapping by V = AW, and the Tenney-weighted JIP by J = J<sub>0</sub>W. If the tuning is contrained by the eigenmonzo list B, the CTE tuning is equivalent to the following optimization problem:
Given a temperament [[mapping]] A and the [[JIP]] J<sub>0</sub>, denote the weight-skewed mapping V = AWX and the weight-skewed JIP J = J<sub>0</sub>WX. Suppose the tuning is constrained by the eigenmonzo list B<sub>C</sub>. The goal is to find the generator list G by


Minimize
Minimize


<math>\lVert GV - J \rVert</math>
<math>\displaystyle \lVert GV - J \rVert_p </math>


subject to
subject to


<math>(GA - J_0)B = O</math>
<math>\displaystyle (GA - J_0)B_{\rm C} = O </math>


where G is the generator list, and O the zero matrix.  
where O is the zero matrix.  


The problem is feasible if
The problem is feasible if
# rank (B) ≤ rank (A), and
# rank (B<sub>C</sub>) ≤ rank (A), and
# The subspaces of B and N (A) are [[Wikipedia:linear independence|linearly independent]].
# The subspaces of B<sub>C</sub> and N (A) are [[Wikipedia:linear independence|linearly independent]].


== Computation ==
== Computation ==
The tuning can be solved in the [[wikipedia: Lagrange multiplier|method of Lagrange multiplier]]. The solution is given by
As a standard optimization problem, numerous algorithms exist to solve it, such as [[Wikipedia: Sequential quadratic programming|sequential quadratic programming]], to name one. [[Flora Canou]]'s [https://github.com/FloraCanou/temperament_evaluator/blob/55859971e5db037f5353267c328ca4a20f515ecd/te_optimizer_legacy.py tuning optimizer] is such an implementation in [https://www.python.org Python]. Note: it depends on [https://scipy.org/ Scipy].  
 
<math>
\begin{bmatrix}
G^{\mathsf T}  \\
\Lambda^{\mathsf T}
\end{bmatrix}
=
\begin{bmatrix}
VV^{\mathsf T} & AB \\
(AB)^{\mathsf T} & O
\end{bmatrix}^{-1}
 
\begin{bmatrix}
VJ^{\mathsf T}\\
(J_0 B)^{\mathsf T}
\end{bmatrix}
</math>
 
which is almost an analytical solution. 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.
 
Otherwise, as a standard optimization problem, numerous algorithms exist to solve it, such as [[Wikipedia: Sequential quadratic programming|sequential quadratic programming]], to name one. [[Flora Canou]]'s [https://github.com/FloraCanou/temperament_evaluator/blob/55859971e5db037f5353267c328ca4a20f515ecd/te_optimizer_legacy.py tuning optimizer] is such an implementation in [https://www.python.org Python]. Note: it depends on [https://scipy.org/ Scipy].  


{{Databox| Code |
{{Databox| Code |
Line 159: Line 138:
</pre>
</pre>


== Versus CTWE and POTE tuning ==
Analytical solutions exist for Euclidean (''L''<sup>2</sup>) tunings, see [[Analytical solution to constrained Euclidean tunings]].
Consider the fact that TE tuning does not treat divisive ratios as more important than multiplicative ratios – 5/3 and 15/1 are taken as equally important, for example. To address that, a skew on the space may be introduced, resulting in [[TWE tuning]]. Constraining the equave to pure on top of TWE gives CTWE aka KE tuning. POTE works as a quick approximation to CTWE. As POTE destretches the equave, it keeps the angle in the tuning space unchanged, and thus sacrifices multiplicative ratios for divisive ratios. On the contrary, CTE sticks to the original design book of TE as its result remains TE optimal.
 
For CTE in particular, it can be solved in the [[wikipedia: Lagrange multiplier|method of Lagrange multiplier]]. The solution is given by
 
<math>
\begin{bmatrix}
G^{\mathsf T}  \\
\Lambda^{\mathsf T}
\end{bmatrix}
=
\begin{bmatrix}
VV^{\mathsf T} & AB \\
(AB)^{\mathsf T} & O
\end{bmatrix}^{-1}
 
\begin{bmatrix}
VJ^{\mathsf T}\\
(J_0 B)^{\mathsf T}
\end{bmatrix}
</math>
 
which is almost an analytical solution. 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.
 
== CTE tuning vs CTWE tuning ==
Consider the fact that TE tuning does not treat divisive ratios as more important than multiplicative ratios – 5/3 and 15/1 are taken as equally important, for example. To address that, a skew on the space may be introduced, resulting in [[TWE tuning]]. Constraining the equave to pure on top of TWE gives CTWE aka KE tuning.  
 
[[POTE tuning]] works as a quick approximation to CTWE. As POTE destretches the equave, it keeps the angle in the tuning space unchanged, and thus sacrifices multiplicative ratios for divisive ratios. On the contrary, CTE sticks to the original design book of TE as its result remains TE optimal.


These tunings can be very different from each other. Take 7-limit meantone as an example. The POTE [[tuning map]] is a little bit flatter than [[quarter-comma meantone]], with all the primes tuned flat:  
These tunings can be very different from each other. Take 7-limit meantone as an example. The POTE [[tuning map]] is a little bit flatter than [[quarter-comma meantone]], with all the primes tuned flat:  
Line 175: Line 179:


== Special constraint ==
== Special constraint ==
The special eigenmonzo Wj removes the weighted tuning bias, where j is the all-ones monzo:
The special eigenmonzo WXj removes the weight-skewed tuning bias, where j is the all-ones monzo.
 
For CTE, it can be easily shown to be


<math>\displaystyle W \vec j = [ \begin{matrix} 1 & 1/\log_2 (3) & \ldots & 1/\log_2 (p) \end{matrix} \rangle </math>
<math>\displaystyle W \vec j = [ \begin{matrix} 1 & 1/\log_2 (3) & \ldots & 1/\log_2 (p) \end{matrix} \rangle </math>

Revision as of 22:32, 1 August 2022

Constrained tunings are tuning optimization techniques using the constraint of some purely tuned intervals (i.e. eigenmonzos). CTE tuning (constrained Tenney-Euclidean tuning) is the most typical instance. It has a more sophisticated variant, CTWE tuning (constrained Tenney-Weil-Euclidean tuning), aka KE tuning (Kees-Euclidean tuning). These two tunings will be the focus of this article. Otherwise normed tunings can be defined and computed analogously.

All constrained tunings are standard temperament optimization problems. Specifically, as TE tuning can be viewed as a least squares problem, CTE tuning can be viewed as an equality-constrained least squares problem.

The most common subject of constraint is the octave, which is assumed unless specified otherwise. For higher-rank temperaments, it may make sense to add multiple constraints, such as a pure-2.3 constrained tuning. For a rank-r temperament, specifying m eigenmonzos will yield r - m degrees of freedom to be optimized.

Definition

Given a temperament mapping A and the JIP J0, denote the weight-skewed mapping V = AWX and the weight-skewed JIP J = J0WX. Suppose the tuning is constrained by the eigenmonzo list BC. The goal is to find the generator list G by

Minimize

[math]\displaystyle{ \displaystyle \lVert GV - J \rVert_p }[/math]

subject to

[math]\displaystyle{ \displaystyle (GA - J_0)B_{\rm C} = O }[/math]

where O is the zero matrix.

The problem is feasible if

  1. rank (BC) ≤ rank (A), and
  2. The subspaces of BC and N (A) are linearly independent.

Computation

As a standard optimization problem, numerous algorithms exist to solve it, such as sequential quadratic programming, to name one. Flora Canou's tuning optimizer is such an implementation in Python. Note: it depends on Scipy.

Code
# © 2020-2022 Flora Canou | Version 0.21.0
# This work is licensed under the GNU General Public License version 3.

import warnings
import numpy as np
from scipy import optimize, linalg
np.set_printoptions (suppress = True, linewidth = 256, precision = 4)

PRIME_LIST = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61]
SCALAR = 1200 #could be in octave, but for precision reason

def get_subgroup (main, subgroup):
    if subgroup is None:
        subgroup = PRIME_LIST[:main.shape[1]]
    elif main.shape[1] != len (subgroup):
        warnings.warn ("dimension does not match. Casting to the smaller dimension. ")
        dim = min (main.shape[1], len (subgroup))
        main = main[:, :dim]
        subgroup = subgroup[:dim]
    return main, subgroup

def get_weight (subgroup, wtype = "tenney"):
    if wtype == "tenney":
        return np.diag (1/np.log2 (subgroup))
    elif wtype == "frobenius":
        return np.eye (len (subgroup))
    elif wtype == "benedetti":
        return np.diag (1/np.array (subgroup))
    else:
        warnings.warn ("weighter type not supported, using default (\"tenney\")")
        return get_weight (subgroup, wtype = "tenney")

def get_skew (subgroup, skew = 0, order = 2):
    if skew == 0:
        return np.eye (len (subgroup))
    elif order == 2:
        r = skew/(len (subgroup)*skew**2 + 1)
        return np.append (np.eye (len (subgroup)) - skew*r*np.ones ((len (subgroup), len (subgroup))),
            r*np.ones ((len (subgroup), 1)), axis = 1)
    else:
        raise NotImplementedError ("Weil skew only works with Euclidean norm as of now.")

def weightskewed (main, subgroup, wtype = "tenney", skew = 0, order = 2):
    return main @ get_weight (subgroup, wtype) @ get_skew (subgroup, skew, order)

weighted = weightskewed

def error (gen, map, jip, order):
    return linalg.norm (gen @ map - jip, ord = order)

def optimizer_main (map, subgroup = None, wtype = "tenney", skew = 0, order = 2,
        cons_monzo_list = None, des_monzo = None, show = True):
    map, subgroup = get_subgroup (np.array (map), subgroup)

    jip = np.log2 (subgroup)*SCALAR
    map_wx = weighted (map, subgroup, wtype, skew, order)
    jip_wx = weighted (jip, subgroup, wtype, skew, order)
    if order == 2 and cons_monzo_list is None: #te with no constraints, simply use lstsq for better performance
        res = linalg.lstsq (map_w.T, jip_wx)
        gen = res[0]
        print ("L2 tuning without constraints, solved using lstsq. ")
    else:
        gen0 = [SCALAR]*map.shape[0] #initial guess
        cons = () if cons_monzo_list is None else {'type': 'eq', 'fun': lambda gen: (gen @ map - jip) @ cons_monzo_list}
        res = optimize.minimize (error, gen0, args = (map_wx, jip_wx, order), method = "SLSQP",
            options = {'ftol': 1e-9}, constraints = cons)
        print (res.message)
        if res.success:
            gen = res.x
        else:
            raise ValueError ("infeasible optimization problem. ")

    if not des_monzo is None:
        if np.array (des_monzo).ndim > 1 and np.array (des_monzo).shape[1] != 1:
            raise IndexError ("only one destretch target is allowed. ")
        elif (tempered_size := gen @ map @ des_monzo) == 0:
            raise ZeroDivisionError ("destretch target is in the nullspace. ")
        else:
            gen *= (jip @ des_monzo)/tempered_size

    tuning_map = gen @ map
    mistuning_map = tuning_map - jip

    if show:
        print (f"Generators: {gen} (¢)",
            f"Tuning map: {tuning_map} (¢)",
            f"Mistuning map: {mistuning_map} (¢)", sep = "\n")

    return gen, tuning_map, mistuning_map

optimiser_main = optimizer_main

Constraints can be added from the parameter cons_monzo_list of the optimizer_main function. For example, to find the CTE tuning for septimal meantone, you type:

map = np.array ([[1, 0, -4, -13], [0, 1, 4, 10]])
optimizer_main (map, cons_monzo_list = np.transpose ([1] + [0]*(map.shape[1] - 1)))

You should get:

Optimization terminated successfully.
Generators: [1200.     1896.9521] (¢)
Tuning map: [1200.     1896.9521 2787.8085 3369.5214] (¢)
Mistuning map: [ 0.     -5.0029  1.4948  0.6955] (¢)

Analytical solutions exist for Euclidean (L2) tunings, see Analytical solution to constrained Euclidean tunings.

For CTE in particular, it can be solved in the method of Lagrange multiplier. The solution is given by

[math]\displaystyle{ \begin{bmatrix} G^{\mathsf T} \\ \Lambda^{\mathsf T} \end{bmatrix} = \begin{bmatrix} VV^{\mathsf T} & AB \\ (AB)^{\mathsf T} & O \end{bmatrix}^{-1} \begin{bmatrix} VJ^{\mathsf T}\\ (J_0 B)^{\mathsf T} \end{bmatrix} }[/math]

which is almost an analytical solution. 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.

CTE tuning vs CTWE tuning

Consider the fact that TE tuning does not treat divisive ratios as more important than multiplicative ratios – 5/3 and 15/1 are taken as equally important, for example. To address that, a skew on the space may be introduced, resulting in TWE tuning. Constraining the equave to pure on top of TWE gives CTWE aka KE tuning.

POTE tuning works as a quick approximation to CTWE. As POTE destretches the equave, it keeps the angle in the tuning space unchanged, and thus sacrifices multiplicative ratios for divisive ratios. On the contrary, CTE sticks to the original design book of TE as its result remains TE optimal.

These tunings can be very different from each other. Take 7-limit meantone as an example. The POTE tuning map is a little bit flatter than quarter-comma meantone, with all the primes tuned flat:

[math]\displaystyle{ \langle \begin{matrix} 1200.000 & 1896.495 & 2785.980 & 3364.949 \end{matrix} ] }[/math]

The CTWE tuning map is a little bit sharper than quarter-comma meantone, with 5 tuned sharp and 3 and 7 flat:

[math]\displaystyle{ \langle \begin{matrix} 1200.000 & 1896.656 & 2786.625 & 3366.562 \end{matrix} ] }[/math]

The CTE tuning map is even sharper, with 3 tuned flat and 5 and 7 sharp.

[math]\displaystyle{ \langle \begin{matrix} 1200.000 & 1896.952 & 2787.809 & 3369.521 \end{matrix} ] }[/math]

Special constraint

The special eigenmonzo WXj removes the weight-skewed tuning bias, where j is the all-ones monzo.

For CTE, it can be easily shown to be

[math]\displaystyle{ \displaystyle W \vec j = [ \begin{matrix} 1 & 1/\log_2 (3) & \ldots & 1/\log_2 (p) \end{matrix} \rangle }[/math]

The monzo introduces weight to the constraint:

[math]\displaystyle{ \displaystyle (GV - J)\vec j = 0 }[/math]

It can be regarded as a distinct optimum, the TOCTE tuning (Tenney ones constrained Tenney-Euclidean tuning).

For equal temperaments

Since the one degree of freedom of equal temperaments is determined by the constraint, optimization is not involved. It is thus reduced to TOC tuning (Tenney ones constrained tuning). This constrained tuning demonstrates interesting properties.

The step size g can be found by

[math]\displaystyle{ \displaystyle g = 1/\operatorname {mean} (V) }[/math]

The edo number n can be found by

[math]\displaystyle{ \displaystyle n = 1/g = \operatorname {mean} (V) }[/math]

Unlike TE or TOP, the optimal edo number space in TOC is linear with respect to A. That is, if A = αA1 + βA2, then

[math]\displaystyle{ \displaystyle \begin{align} n &= \operatorname {mean} (AW) \\ &= \operatorname {mean} ((\alpha A_1 + \beta A_2)W) \\ &= \operatorname {mean} (\alpha A_1 W) + \operatorname {mean} (\beta A_2 W) \\ &= \alpha n_1 + \beta n_2 \end{align} }[/math]

As a result, the relative error space is also linear with respect to A.

For example, the relative errors of 12ettoc5 (12et in 5-limit TOC) is

[math]\displaystyle{ \displaystyle E_\text {r, 12} = \langle \begin{matrix} -1.55\% & -4.42\% & +10.08\% \end{matrix} ] }[/math]

That of 19ettoc5 is

[math]\displaystyle{ \displaystyle E_\text {r, 19} = \langle \begin{matrix} +4.08\% & -4.97\% & -2.19\% \end{matrix} ] }[/math]

As 31 = 12 + 19, the relative errors of 31ettoc5 is

[math]\displaystyle{ \displaystyle \begin{align} E_\text {r, 31} &= E_\text {r, 12} + E_\text {r, 19} \\ &= \langle \begin{matrix} +2.52\% & -9.38\% & +7.88\% \end{matrix} ] \end{align} }[/math]