Godtone
Joined 17 December 2020
→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 |
→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 | 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/ | 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 | 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, | 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): | ||
# if the weighting is unspecified, use the default of: | |||
# weighting an interval x proportional to its odd-limit complexity iv_complexity(x); | |||
# this is a compromise between theoretic sensitivity (approx. equal to the square of the odd-limit complexity) | |||
# and not underweighting simpler intervals and accounting for that more complex intervals will tend to be used | |||
# in a harmonic/chordal context which justifies their otherwise-higher effective felt error through templating | |||
# if the weighting is unspecified | if weighting==1 or weighting in ['unweighted','trivial','basic','CE','CEE']: | ||
# weighting an interval x proportional to its odd-limit complexity iv_complexity(x) | |||
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 = | 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 | # deduce if using the default behaviour (mean average of sum of badnesss) and if so note it for later | ||
using_avg = False | |||
global ivs_cache | global ivs_cache | ||
global ivs_sum_weights_cache | global ivs_sum_weights_cache | ||
if | 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) | ||
# 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 | 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) | ||