forked from FloraCanou/temperament_evaluator
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathte_temperament_measures.py
280 lines (249 loc) · 12.8 KB
/
te_temperament_measures.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# © 2020-2024 Flora Canou | Version 1.5.1
# This work is licensed under the GNU General Public License version 3.
import itertools, re, warnings
import numpy as np
from scipy import linalg
from sympy.matrices import Matrix, BlockMatrix
from sympy import gcd
import te_common as te
np.set_printoptions (suppress = True, linewidth = 256, precision = 4)
class Temperament:
def __init__ (self, breeds, subgroup = None, saturate = True, normalize = True): #NOTE: "map" is a reserved word
breeds, subgroup = te.setup (breeds, subgroup, axis = te.AXIS.ROW)
self.subgroup = subgroup
self.mapping = te.canonicalize (np.rint (breeds).astype (int), saturate, normalize)
def __check_sym (self, order):
"""Checks the availability of the symbolic solver."""
if order == 2:
try:
global te_sym
import te_symbolic as te_sym
except ImportError:
warnings.warn ("Module te_symbolic.py not found. Using main optimizer. ")
return False
return True
else:
warnings.warn ("Condition for symbolic solution not met. Using main optimizer. ")
return False
def __show_header (self, norm = None, mode_text = None, enforce_text = None, ntype = None):
print (f"\nSubgroup: {self.subgroup}",
f"Mapping: \n{self.mapping}", sep = "\n")
if norm:
if norm.wtype == "tenney" and norm.skew == 1:
weightskew_text = "weil"
else:
weight_text = norm.wtype
if norm.wamount != 1:
weight_text += f"[{norm.wamount}]"
if norm.skew != 0:
skew_text = "skewed"
if norm.skew != 1:
skew_text += f"[{norm.skew}]"
weightskew_text = "-".join ((skew_text, weight_text))
else:
weightskew_text = weight_text
order_dict = {1: "chebyshevian", 2: "euclidean", np.inf: "manhattan"}
try:
order_text = order_dict[norm.order]
except KeyError:
order_text = f"L{norm.order}"
print ("Norm: " + "-".join ((weightskew_text, order_text)))
if mode_text:
print ("Mode: " + mode_text)
if enforce_text:
print ("Enforcement: " + enforce_text)
if ntype:
print ("Normalizer: " + ntype)
return
def tune (self, optimizer = "main", norm = te.Norm (), inharmonic = False,
constraint = None, destretch = None):
"""
Gives the tuning.
Calls either wrapper_main or wrapper_symbolic.
"""
# checks optimizer availability
if optimizer == "sym" and not self.__check_sym (norm.order):
return self.tune (optimizer = "main", norm = norm, inharmonic = inharmonic,
constraint = constraint, destretch = destretch)
# gets the enforcement text
cons_text = constraint.__str__ () + "-constrained" if constraint else ""
des_text = destretch.__str__ () + "-destretched" if destretch else ""
enforce_text = " ".join ([cons_text, des_text]) if cons_text or des_text else "none"
if is_trivial := (self.subgroup.is_prime ()
or norm.wtype == "tenney" and self.subgroup.is_prime_power ()):
mode_text = "trivial -- inharmonic and subgroup tunings are identical"
else:
mode_text = "inharmonic tuning" if inharmonic else "subgroup tuning"
# shows the header
self.__show_header (norm = norm, mode_text = mode_text, enforce_text = enforce_text)
# optimization
if optimizer == "main":
import te_optimizer as te_opt
gen, tempered_tuning_map, error_map = te_opt.wrapper_main (
self.mapping, subgroup = self.subgroup, norm = norm,
inharmonic = inharmonic or is_trivial, constraint = constraint, destretch = destretch
)
elif optimizer == "sym":
gen, tempered_tuning_map, error_map = te_sym.wrapper_symbolic (
self.mapping, subgroup = self.subgroup, norm = te_sym.NormSym (norm),
inharmonic = inharmonic or is_trivial, constraint = constraint, destretch = destretch
)
return gen, tempered_tuning_map, error_map
optimise = tune
optimize = tune
analyze = tune
analyse = tune
def wedgie (self, norm = te.Norm (wtype = "equilateral"), show = True):
combinations = itertools.combinations (
range (self.mapping.shape[1] + (1 if norm.skew else 0)), self.mapping.shape[0])
wedgie = np.array (
[linalg.det (norm.tuning_x (self.mapping, self.subgroup)[:, entry]) for entry in combinations])
wedgie *= np.copysign (1, wedgie[0])
# convert to integer type if possible
wedgie_int = wedgie.astype (int)
if all (wedgie == wedgie_int):
wedgie = wedgie_int
if show:
self.__show_header ()
print (f"Wedgie: {wedgie}", sep = "\n")
return wedgie
def complexity (self, ntype = "breed", norm = te.Norm (), inharmonic = False):
"""
Returns the temperament's complexity,
nondegenerate subgroup temperaments supported.
"""
if not norm.order == 2:
raise ValueError ("this measure is only defined on Euclidean norms. ")
do_inharmonic = (inharmonic or self.subgroup.is_prime ()
or norm.wtype == "tenney" and self.subgroup.is_prime_power ())
if not do_inharmonic and self.subgroup.index () == np.inf:
raise ValueError ("this measure is only defined on nondegenerate subgroups. ")
return self.__complexity (ntype, norm, inharmonic = do_inharmonic)
def error (self, ntype = "breed", norm = te.Norm (), inharmonic = False, scalar = te.SCALAR.CENT): #in cents by default
"""
Returns the temperament's inherent inaccuracy regardless of the actual tuning,
all subgroup temperaments supported.
"""
if not norm.order == 2:
raise NotImplementedError ("non-Euclidean norms not supported as of now. ")
do_inharmonic = (inharmonic or self.subgroup.is_prime ()
or norm.wtype == "tenney" and self.subgroup.is_prime_power ())
return self.__error (ntype, norm, inharmonic = do_inharmonic, scalar = scalar)
def __complexity (self, ntype, norm, inharmonic):
if inharmonic:
subgroup = self.subgroup
mapping = self.mapping
index = 1
else:
subgroup = self.subgroup.minimal_prime_subgroup ()
mapping = te.antinullspace (self.subgroup.basis_matrix_to (subgroup) @ te.nullspace (self.mapping))
index = self.subgroup.index ()
r, d = mapping.shape #rank and dimensionality
# standard L2 complexity
complexity = np.sqrt (linalg.det (
norm.tuning_x (mapping, subgroup)
@ norm.tuning_x (mapping, subgroup).T
)) / index
# complexity = linalg.norm (self.wedgie (norm = norm)) #same
if ntype=="breed": #Graham Breed's RMS (default)
complexity *= 1/np.sqrt (d**r)
elif ntype=="smith": #Gene Ward Smith's RMS
complexity *= 1/np.sqrt (len (tuple (itertools.combinations (range (d), r))))
elif ntype=="dirichlet": #Sintel's Dirichlet coefficient
complexity *= 1/linalg.det (norm.tuning_x (np.eye (d), subgroup)[:,:d])**(r/d)
elif ntype=="none":
pass
else:
warnings.warn ("normalizer not supported, using default (\"breed\")")
return self.__complexity (ntype = "breed", norm = norm, inharmonic = inharmonic)
return complexity
def __error (self, ntype, norm, inharmonic, scalar):
if inharmonic:
subgroup = self.subgroup
mapping = self.mapping
else:
subgroup = self.subgroup.minimal_prime_subgroup ()
mapping = te.antinullspace (self.subgroup.basis_matrix_to (subgroup) @ te.nullspace (self.mapping))
r, d = mapping.shape #rank and dimensionality
just_tuning_map = subgroup.just_tuning_map (scalar)
# standard L2 error
error = linalg.norm (
norm.tuning_x (just_tuning_map, subgroup)
@ linalg.pinv (norm.tuning_x (mapping, subgroup))
@ norm.tuning_x (mapping, subgroup)
- norm.tuning_x (just_tuning_map, subgroup)
)
if ntype=="breed": #Graham Breed's RMS (default)
error *= 1/np.sqrt (d)
elif ntype=="smith": #Gene Ward Smith's RMS
try:
error *= np.sqrt ((r + 1)/(d - r))
except ZeroDivisionError:
error = np.nan
elif ntype=="dirichlet": #Sintel's Dirichlet coefficient
error *= 1/(np.sqrt (d)
* linalg.det (norm.tuning_x (np.eye (d), subgroup)[:,:d])**(1/d))
elif ntype=="none":
pass
else:
warnings.warn ("normalizer not supported, using default (\"breed\")")
return self.__error ("breed", norm, inharmonic, scalar)
return error
def badness (self, ntype = "breed", norm = te.Norm (), inharmonic = False,
logflat = False, scalar = te.SCALAR.OCTAVE): #in octaves by default
if not norm.order == 2:
raise ValueError ("this measure is only defined on Euclidean norms. ")
do_inharmonic = (inharmonic or self.subgroup.is_prime ()
or norm.wtype == "tenney" and self.subgroup.is_prime_power ())
if not do_inharmonic and self.subgroup.index () == np.inf:
raise ValueError ("this measure is only defined on nondegenerate subgroups. ")
if logflat:
return self.__badness_logflat (ntype, norm, inharmonic, scalar)
else:
return self.__badness (ntype, norm, inharmonic, scalar)
def __badness (self, ntype, norm, inharmonic, scalar):
return (self.__error (ntype, norm, inharmonic, scalar)
* self.__complexity (ntype, norm, inharmonic))
def __badness_logflat (self, ntype, norm, inharmonic, scalar):
r, d = self.mapping.shape #rank and dimensionality
try:
res = (self.__error (ntype, norm, inharmonic, scalar)
* self.__complexity (ntype, norm, inharmonic)**(d/(d - r)))
except ZeroDivisionError:
res = np.nan
if ntype=="dirichlet":
norm_jtm = 1/linalg.det (norm.tuning_x (np.eye (d), self.subgroup)[:,:d])**(1/d)
res /= norm_jtm
elif ntype in ["breed", "smith", "none"]:
pass
else:
warnings.warn ("normalizer not supported, using default (\"breed\")")
return self.__badness_logflat ("breed", norm, inharmonic, scalar)
return res
def temperament_measures (self, ntype = "breed", norm = te.Norm (), inharmonic = False,
error_scale = te.SCALAR.CENT, badness_scale = te.SCALAR.OCTAVE):
"""Shows the temperament measures."""
if not norm.order == 2:
raise ValueError ("this measure is only defined on Euclidean norms. ")
do_inharmonic = (inharmonic or self.subgroup.is_prime ()
or norm.wtype == "tenney" and self.subgroup.is_prime_power ())
if not do_inharmonic and self.subgroup.index () == np.inf:
raise ValueError ("this measure is only defined on nondegenerate subgroups. ")
return self.__temperament_measures (ntype, norm, do_inharmonic, error_scale, badness_scale)
def __temperament_measures (self, ntype, norm, inharmonic, error_scale, badness_scale):
self.__show_header (norm = norm, ntype = ntype)
complexity = self.__complexity (ntype, norm, inharmonic)
error = self.__error (ntype, norm, inharmonic, error_scale)
badness = self.__badness (ntype, norm, inharmonic, badness_scale)
badness_logflat = self.__badness_logflat (ntype, norm, inharmonic, badness_scale)
print (f"Complexity: {complexity:.6f}",
f"Error: {error:.6f} (oct/{error_scale})",
f"Badness (simple): {badness:.6f} (oct/{badness_scale})",
f"Badness (logflat): {badness_logflat:.6f} (oct/{badness_scale})", sep = "\n")
def comma_basis (self, show = True):
comma_basis = te.nullspace (self.mapping)
if show:
self.__show_header ()
print ("Comma basis: ")
te.show_monzo_list (comma_basis, self.subgroup)
return comma_basis