Constrained tuning: Difference between revisions

BudjarnLambeth (talk | contribs)
m Formatting
Line 166: Line 166:
</pre>
</pre>


Analytical solutions exist for Euclidean (''L''<sup>2</sup>) tunings, see [[Constrained tuning/Analytical solution to constrained Euclidean tunings]]. It can also be solved in the {{w|Lagrange multiplier|method of Lagrange multiplier}}. The solution is given by
=== Analytical solutions ===
Analytical solutions exist for Euclidean (''L''<sup>2</sup>) tunings, see [[Constrained tuning/Analytical solution to constrained Euclidean tunings]].  
 
==== Method of Lagrange multiplier ====
It can also be solved in the {{w|Lagrange multiplier|method of Lagrange multiplier}}. The solution is given by


<math>\displaystyle
<math>\displaystyle
Line 187: Line 191:
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 Closed-Form Algorithm ==
=== Simple fast closed-form algorithm ===


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.
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.
Line 220: Line 224:
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:
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
{{Databox|Code|
from numpy.linalg import inv, pinv
<syntaxhighlight lang="python">
from scipy.linalg import null_space, sqrtm
import numpy as np
from numpy.linalg import inv, pinv
def CTWE(limit, M, k):
from scipy.linalg import null_space, sqrtm
    """
 
    Computes the CTWE tuning of a *full-limit* temperament given a limit and
def CTWE(limit, M, k):
    mapping matrix M. For k=0, this is CTE, for k=1, this is CWE/KE.
    """
    Computes the CTWE tuning of a *full-limit* temperament given a limit and
    For subgroup temperaments, first compute the tuning of the full-limit
    mapping matrix M. For k=0, this is CTE, for k=1, this is CWE/KE.
    temperament with same kernel, then multiply by the subgroup basis matrix.     
 
    """
    For subgroup temperaments, first compute the tuning of the full-limit
    # Basics: get the weighting matrix, JIP, etc
    temperament with same kernel, then multiply by the subgroup basis matrix.     
    W_monzo = np.vstack([np.diag(np.log2(limit)), np.log2(limit) * k])
    """
    W_monzo_gram = sqrtm(W_monzo.T @ W_monzo)
    # Basics: get the weighting matrix, JIP, etc
    W = inv(W_monzo_gram) # we could call this W_val_gram
    W_monzo = np.vstack([np.diag(np.log2(limit)), np.log2(limit) * k])
    j = 1200*np.log2(limit)
    W_monzo_gram = sqrtm(W_monzo.T @ W_monzo)
   
    W = inv(W_monzo_gram) # we could call this W_val_gram
    # Simple way to get a random generator map with pure octaves: take the pinv
    j = 1200*np.log2(limit)
    # 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).
    # Simple way to get a random generator map with pure octaves: take the pinv
    octave_col = M[:,0][:,np.newaxis]
    # of the octave mapping column. The corresponding tuning map has an octave
    x = pinv(M[:,0][:,np.newaxis]) * j[0]
    # coordinate of 1 cent, so multiply by the first JIP coord (probably 1200).
   
    octave_col = M[:,0][:,np.newaxis]
    # the left nullspace of octave_col has all generators mapping to 0.
    x = pinv(M[:,0][:,np.newaxis]) * j[0]
    B = null_space(octave_col.T).T
   
   
    # the left nullspace of octave_col has all generators mapping to 0.
    # All pure-octave generator maps are just pure_octave_start + something in
    B = null_space(octave_col.T).T
    # the above row space. Now we have to solve
   
    #  (h@B + x)@M@W ≈ j@W
    # All pure-octave generator maps are just pure_octave_start + something in
    # which, solving for h and doing the algebra out gives:
    # the above row space. Now we have to solve
    h = (j - x@M)@W @ pinv(B@M@W)
    #  (h@B + x)@M@W ≈ j@W
    g = h@B + x
    # which, solving for h and doing the algebra out gives:
    t = g@M
    h = (j - x@M)@W @ pinv(B@M@W)
    return g, t
    g = h@B + x
   
    t = g@M
# %% Compute the CTE of septimal meantone temperament
    return g, t
k = 1
   
limit = np.array([2, 3, 5, 7])
# %% Compute the CTE of septimal meantone temperament
M = np.array( # mapping matrix for meantone
k = 1
    [[1, 0, -4, -13],
limit = np.array([2, 3, 5, 7])
      [0, 1,  4,  10]]
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_CTE, T_CTE = CTWE(limit, M, 0)
print("CTE Generator map: " + str(G_CTE))
G_CWE, T_CWE = CTWE(limit, M, 1)
print("CTE Tuning map: " + str(T_CTE))
print("CWE Generator map: " + str(G_CWE))
 
print("CWE Tuning map: " + str(T_CWE))
G_CWE, T_CWE = CTWE(limit, M, 1)
print("CWE Generator map: " + str(G_CWE))
print("CWE Tuning map: " + str(T_CWE))
</syntaxhighlight>
}}


=== Interpolating TE/CTE ===
=== Interpolating TE/CTE ===
Line 276: Line 284:
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:


import numpy as np
{{Databox|Code|
<syntaxhighlight lang="python">
def modified_TE(limit, M, H=1000000):
import numpy as np
    """
 
    Computes the interpolated TE/CTE tuning of a *full-limit* temperament given
def modified_TE(limit, M, H=1000000):
    a limit and mapping matrix M. For H = 1 we have TE, and as H -> inf we have
    """
    CTE. To compute CTE to arbitrary precision, make H sufficiently large.
    Computes the interpolated TE/CTE tuning of a *full-limit* temperament given
   
    a limit and mapping matrix M. For H = 1 we have TE, and as H -> inf we have
    For subgroup temperaments, first compute the tuning of the full-limit
    CTE. To compute CTE to arbitrary precision, make H sufficiently large.
    temperament with same kernel, then multiply by the subgroup basis matrix.
   
    """
    For subgroup temperaments, first compute the tuning of the full-limit
    # Compute adjusted TE weighting matrix and JIP
    temperament with same kernel, then multiply by the subgroup basis matrix.
    W = np.diag(1/np.log2(limit))
    """
    W[0,0] = H
    # Compute adjusted TE weighting matrix and JIP
    J = 1200*np.log2(limit)
    W = np.diag(1/np.log2(limit))
    W[0,0] = H
    # Main calculation: get the generator map g such that g@M@W ≈ J@W. Use pinv
    J = 1200*np.log2(limit)
    G = (J@W) @ np.linalg.pinv(M@W)
 
    T = G @ M
    # Main calculation: get the generator map g such that g@M@W ≈ J@W. Use pinv
    G = (J@W) @ np.linalg.pinv(M@W)
    return G, T
    T = G @ M
 
# %% Compute the CTE of septimal meantone temperament
    return G, T
H = 1000000
 
limit = np.array([2, 3, 5, 7])
# %% Compute the CTE of septimal meantone temperament
M = np.array( # mapping matrix for meantone
H = 1000000
    [[1, 0, -4, -13],
limit = np.array([2, 3, 5, 7])
      [0, 1,  4,  10]]
M = np.array( # mapping matrix for meantone
)
    [[1, 0, -4, -13],
    [0, 1,  4,  10]]
G, T = modified_TE(limit, M, H)
)
 
print("Generator map: " + str(G))
G, T = modified_TE(limit, M, H)
print("Tuning map: " + str(T))
 
print("Generator map: " + str(G))
print("Tuning map: " + str(T))
</syntaxhighlight>
}}


The output to the above is:
The output to the above is:


Generator map: [ 1200.0000  1896.9521]
<pre>
Tuning map: [ 1200.0000  1896.9521  2787.8086  3369.5214]
Generator map: [ 1200.0000  1896.9521]
Tuning map: [ 1200.0000  1896.9521  2787.8086  3369.5214]
</pre>


== CTE tuning vs POTE tuning vs CWE tuning vs CTWE tuning ==
== CTE tuning vs POTE tuning vs CWE tuning vs CTWE tuning ==