Godtone (talk | contribs)
→My Python 3 code: add more functionality to et_badness and explain the code, add more customisability to optimal_edo_sequence and strict_optimal_edo_sequence
Godtone (talk | contribs)
→My Python 3 code: clean code (increase comprehensibility, increase relevance to practical use, improve behaviour, reduce idiosyncracy, add more explanation, rename weird vector average function to smth more obscure, correct error in comment)
Line 988: Line 988:




def avg(vs,weights=None):
def vector_avg_w(vs,weights=None):
if not weights:
if not weights:
weights=[ 1.0 for v in vs ]
weights=[ 1.0 for v in vs ]
Line 994: Line 994:
def length(v):
def length(v):
return sum([ c**2 for c in v ])**(1/2)
return sum([ c**2 for c in v ])**(1/2)
def turns(v): # assumes v is 2D vector; turns expressed as unit up being 0 turns and unit left being -1/2 turns
def turns(v): # assumes v is 2D vector; turns expressed as unit up being 0 turns and unit left being -1/4 turns
return math.atan2(v[0],v[1])
return math.atan2(v[0],v[1]) # assuming 0th/first coordinate (x) is right and 1th/second coordinate (y) is up
# [0,1] (unit up) is exactly in-tune with et2; note the below:
# [0,1] (unit up) is exactly in-tune with et2; note the below:
# mean = turns(result)/math.tau, variance = 1 - length(result), badness = length(result)
# mean = turns(result)/math.tau, variance = 1 - length(result), badness = length(result)
Line 1,002: Line 1,002:
weights = [ 1/math.log(h,2) for h in harmonics ]
weights = [ 1/math.log(h,2) for h in harmonics ]
herr=[ (h,step_err(h,et2)) for h in harmonics ]
herr=[ (h,step_err(h,et2)) for h in harmonics ]
return avg([ [math.sin(math.tau*e[1]),math.cos(math.tau*e[1])] for e in herr ],weights)
return vector_avg_w([ [math.sin(math.tau*e[1]),math.cos(math.tau*e[1])] for e in herr ],weights)
def circular_normalise(e,m): # into range -1/2, +1/2
def circular_normalise(e,m): # into range -1/2, +1/2
return (e-m)%1 if (e-m)%1 < 0.5 else (e-m)%1 - 1
return (e-m)%1 if (e-m)%1 < 0.5 else (e-m)%1 - 1
Line 1,295: Line 1,295:
# the default badness is the sum of the squares of the errors with each interval's contribution weighted proportional to its odd-limit complexity,
# the default badness is the sum of the squares of the errors with each interval's contribution weighted proportional to its odd-limit complexity,
# in terms of absolute (cent/octave) error (corresponding to the multiplication by et2) and divided by the sum of the weightings of the intervals.
# in terms of absolute (cent/octave) error (corresponding to the multiplication by et2) and divided by the sum of the weightings of the intervals.
def et_badness(ivs,v,badness=0,weighting=0,combine=0,et2=0):
def et_badness(ivs,v,badness=lambda rel_err,x,et2: rel_err**2 * et2,weighting=lambda x: iv_complexity(x),combine='avg',et2=0):
# deduce if using default badness and if so note it for later
# if the weighting is unspecified, use the default of:
default_badness = False
# weighting an interval x proportional to its odd-limit complexity iv_complexity(x);
if not badness:
# this is a compromise between theoretic sensitivity (approx. equal to the square of the odd-limit complexity)
badness = lambda rel_err,x,et2: rel_err**2 * et2
# and not underweighting simpler intervals and accounting for that more complex intervals will tend to be used
default_badness = True
# in a harmonic/chordal context which justifies their otherwise-higher effective felt error through templating
# if the weighting is unspecified (=0), use the default of:
if weighting==1 or weighting in ['unweighted','trivial','basic','CE','CEE']:
# weighting an interval x proportional to its odd-limit complexity iv_complexity(x)  
if not weighting:
weighting = lambda x: iv_complexity(x)
elif weighting==1 or weighting in ['unweighted','trivial','basic','CE','CEE']:
weighting = lambda x: 1 # for use with prime harmonic interval sets
weighting = lambda x: 1 # for use with prime harmonic interval sets
elif weighting==2 or weighting in ['natural', 'harmonic', 'TE', 'te', 'tenney']:
elif weighting==2 or weighting in ['natural', 'harmonic', 'TE', 'te', 'tenney']:
weighting = lambda x: 1/math.log2(x[0]) # for use with prime harmonic interval sets
weighting = lambda x: 1/math.log2(x[0] * x[1]) # mainly for use with prime harmonic interval sets
# convert the ivs argument into a valid set of intervals before attempting operations with it
# convert the ivs argument into a valid set of intervals before attempting operations with it
if type(ivs)==int:
if type(ivs)==int:
ivs = [(p,1) for p in lim(ivs)]
if ivs%1: # use odd-limit
elif type(ivs)==list:
ivs = odd_lim(ivs)
else: # use integer-limit
ivs = [x for x in odd_lim(ivs) if x[0] <= ivs]
elif type(ivs)==list or type(ivs)==set:
ivs = odd_lim(1,[],ivs)
ivs = odd_lim(1,[],ivs)
# deduce of using the default behaviour (sum of badnesss) and if so note it for later
# deduce if using the default behaviour (mean average of sum of badnesss) and if so note it for later
using_sum = False
using_avg = False
global ivs_cache
global ivs_cache
global ivs_sum_weights_cache
global ivs_sum_weights_cache
if not combine:
if combine in ['avg','mean','average','sum','net']: # (note the mean average is equal to a sum up to a scalar factor)
using_avg = combine not in ['sum','net'] # if a pure sum is requested, do not divide by the sum of the weights
combine = lambda badnesses: sum(badnesses)
combine = lambda badnesses: sum(badnesses)
using_sum = True
# specifically, cache the sum of weighting(x), as set of intervals likely to remain constant between calls.
# specifically, cache the sum of weighting(x), as set of intervals likely to remain constant between calls.
# this is to allow meaningful comparison of performance between different sets of intervals, as otherwise
# this is to allow meaningful comparison of performance between different sets of intervals, as otherwise
Line 1,329: Line 1,328:
ivs_cache = ivs
ivs_cache = ivs
ivs_sum_weights_cache = sum([ weighting(x) for x in ivs ])
ivs_sum_weights_cache = sum([ weighting(x) for x in ivs ])
elif combine in ['max','maximum'] or combine==1: # average of top 1 highest badnesses
combine = lambda badnesses: max(badnesses)
elif combine in ['min','minimum'] or combine==-1: # average of top 1 lowest badnesses
combine = lambda badnesses: min(badnesses)
elif combine > 0: # (unweighted) average of top <combine> (highest) badnesses
combine = lambda badnesses: sum( sorted(badnesses)[-combine:] )/combine
elif combine < 0: # (unweighted) average of bottom <combine> (lowest) badnesses
combine = lambda badnesses: sum( sorted(badnesses)[:-combine] )/-combine
# use the cached val if it is present; otherwise, create a 251-prime-limit (255-odd-limit) patent val and cache it
# use the cached val if it is present; otherwise, create a 251-prime-limit (255-odd-limit) patent val and cache it
global patent_vals
global patent_vals
Line 1,350: Line 1,357:
et2 = 1/v[0]
et2 = 1/v[0]
# finally, return the result:
# finally, return the result:
return combine([ badness3args(abs( map_iv(v,x) - steps(x,et2) ), x, et2) * weighting(x) for x in ivs ]) / (ivs_sum_weights_cache if using_sum else 1)
return combine([ badness3args(abs( map_iv(v,x) - steps(x,et2) ), x, et2) * weighting(x) for x in ivs ]) / (ivs_sum_weights_cache if using_avg else 1)
# if you just give a list or set of intervals (a,b), the default behaviour is to judge the badness of an edo as:
# if you just give a list or set of intervals (a,b), the default behaviour is to judge the badness of an edo as:
# * sum of squares of errors with each interval's contribution weighted proportional to its odd-limit complexity,
# * sum of squares of errors with each interval's contribution weighted proportional to its odd-limit complexity,
Line 1,356: Line 1,363:
# * for edos in the 2 to 311 range (inclusive)
# * for edos in the 2 to 311 range (inclusive)
def optimal_edo_sequence(edo_badness,up_to=311,weighting=0,combine=0):
def optimal_edo_sequence(edo_badness,up_to=311,weighting=0,combine=0):
if type(edo_badness)==list or type(edo_badness)==set:
if type(ivs)==int or type(edo_badness)==list or type(edo_badness)==set:
ivs = edo_badness
ivs = edo_badness
edo_badness = lambda edo: et_badness(ivs,edo,0,weighting,combine)
edo_badness = lambda edo: et_badness(ivs,edo,0,weighting,combine)