Constrained tuning: Difference between revisions

Mike Battaglia (talk | contribs)
Mike Battaglia (talk | contribs)
Simple Fast Algorithm: Better algorithm for general CTWE that doesn't require
Line 187: Line 187:
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.
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.


== Simple Fast Algorithm ==
== Simple Fast Closed-Form Algorithm ==


A much simpler way to compute the CTE tuning is just to note that it's what we get if 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:
A much simpler way to compute the CTE and CWE tunings, and the CTWE tuning in general, is to use the pseudoinverse. This doesn't require doing any intense nonlinear optimization.
 
The basic idea is that the set of all pure-octave tuning maps of some temperament will be the intersection of a linear subspace and a shifted hyperplane, and thus will be a shifted subspace. This means that any pure-octave tuning map can be expressed as the sum of some arbitrary "reference" pure-octave tuning map for the temperament, plus some other one also in the temperament whose octave-coordinate is 0. The set of all such tuning maps of the latter category form a linear subspace.
 
We have the same thing with generator maps, meaning that any pure-octave generator map <math>g</math> can be expressed as:
 
$$
g = hB + x
$$
 
where
 
- <math>x</math> is any random tuning with pure octaves
- <math>B</math> is a matrix whose rows are a basis for the subspace of generator maps with octave coordinate set to 0
- <math>h</math> being a free variable.
 
Given that, and assuming <math>M</math> is our mapping matrix, <math>W</math> our weighting matrix, and <math>j</math> our JIP, we can solve for the best possible <math>g</math> in closed form:
 
$$
gMW ≈ jW
$$
 
which becomes
 
$$
(hB + x)MW ≈ jW \\
h = (j - xM)W \cdot (BMW)^\dagger
$$
 
We note that this also works for any weighting matrix, and so we can use this to compute an arbitrary TWE norm very quickly in closed-form. Here is some Python code:
 
import numpy as np
from numpy.linalg import inv, pinv
from scipy.linalg import null_space, sqrtm
def CTWE(limit, M, k):
    """
    Computes the CTWE tuning of a *full-limit* temperament given
    a limit and mapping matrix M. For k=0, this is CTE, for k=1, this is CWE/KE.
    For subgroup temperaments, first compute the tuning of the full-limit
    temperament with same kernel, then multiply by the subgroup basis matrix.   
    """
    # Basics: get the weighting matrix, JIP, etc
    W_monzo = np.vstack([np.diag(np.log2(limit)), np.log2(limit) * k])
    W_monzo_gram = sqrtm(W_monzo.T @ W_monzo)
    W = inv(W_monzo_gram) # we could call this W_val_gram
    j = 1200*np.log2(limit)
   
    # Simple way to get a random generator map with pure octaves: take the pinv
    # of the octave mapping column. The corresponding tuning map has an octave
    # coordinate of 1 cent, so multiply by the first JIP coord (probably 1200).
    octave_col = M[:,0][:,np.newaxis]
    x = pinv(M[:,0][:,np.newaxis]) * j[0]
   
    # the left nullspace of octave_col has all generators mapping to 0.
    B = null_space(octave_col.T).T
   
    # All pure-octave generator maps are just pure_octave_start + something in
    # the above row space. Now we have to solve
    #  (h@B + x)@M@W ≈ j@W
    # which, solving for h and doing the algebra out gives:
    h = (j - x@M)@W @ pinv(B@M@W)
    g = h@B + x
    t = g@M
    return g, t
   
# %% Compute the CTE of septimal meantone temperament
k = 1
limit = np.array([2, 3, 5, 7])
M = np.array( # mapping matrix for meantone
    [[1, 0, -4, -13],
      [0, 1,  4,  10]]
)
G_CTE, T_CTE = CTWE(limit, M, 0)
print("CTE Generator map: " + str(G_CTE))
print("CTE Tuning map: " + str(T_CTE))
G_CWE, T_CWE = CTWE(limit, M, 1)
print("CWE Generator map: " + str(G_CWE))
print("CWE Tuning map: " + str(T_CWE))
 
=== Interpolating TE/WE ===
 
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:


  import numpy as np
  import numpy as np