release commit
[lilypond.git] / buildscripts / output-distance.py
bloba19380f1f28b4c9cfbd4aad84749d89e3c733954
1 #!@TARGET_PYTHON@
2 import sys
3 import optparse
5 import safeeval
8 X_AXIS = 0
9 Y_AXIS = 1
10 INFTY = 1e6
12 OUTPUT_EXPRESSION_PENALTY = 100
13 ORPHAN_GROB_PENALTY = 1000
15 def max_distance (x1, x2):
16 dist = 0.0
18 for (p,q) in zip (x1, x2):
19 dist = max (abs (p-q), dist)
21 return dist
24 empty_interval = (INFTY, -INFTY)
25 empty_bbox = (empty_interval, empty_interval)
27 def interval_length (i):
28 return max (i[1]-i[0], 0)
30 def interval_union (i1, i2):
31 return (min (i1[0], i2[0]),
32 max (i1[1], i2[1]))
34 def interval_intersect (i1, i2):
35 return (max (i1[0], i2[0]),
36 min (i1[1], i2[1]))
38 def bbox_union (b1, b2):
39 return (interval_union (b1[X_AXIS], b2[X_AXIS]),
40 interval_union (b2[Y_AXIS], b2[Y_AXIS]))
42 def bbox_intersection (b1, b2):
43 return (interval_intersect (b1[X_AXIS], b2[X_AXIS]),
44 interval_intersect (b2[Y_AXIS], b2[Y_AXIS]))
46 def bbox_area (b):
47 return interval_length (b[X_AXIS]) * interval_length (b[Y_AXIS])
49 def bbox_diameter (b):
50 return max (interval_length (b[X_AXIS]),
51 interval_length (b[Y_AXIS]))
54 def difference_area (a, b):
55 return bbox_area (a) - bbox_area (bbox_intersection (a,b))
57 class GrobSignature:
58 def __init__ (self, exp_list):
59 (self.name, self.origin, bbox_x,
60 bbox_y, self.output_expression) = tuple (exp_list)
62 self.bbox = (bbox_x, bbox_y)
63 self.centroid = (bbox_x[0] + bbox_x[1], bbox_y[0] + bbox_y[1])
65 def __repr__ (self):
66 return '%s: (%.2f,%.2f), (%.2f,%.2f)\n' % (self.name,
67 self.bbox[0][0],
68 self.bbox[0][1],
69 self.bbox[1][0],
70 self.bbox[1][1])
72 def axis_centroid (self, axis):
73 return apply (sum, self.bbox[axis]) / 2
75 def centroid_distance (self, other, scale):
76 return max_distance (self.centroid, other.centroid) / scale
78 def bbox_distance (self, other):
79 divisor = bbox_area (self.bbox) + bbox_area (other.bbox)
81 if divisor:
82 return (difference_area (self.bbox, other.bbox) +
83 difference_area (other.bbox, self.bbox)) / divisor
84 else:
85 return 0.0
87 def expression_distance (self, other):
88 if self.output_expression == other.output_expression:
89 return 0.0
90 else:
91 return OUTPUT_EXPRESSION_PENALTY
93 def distance(self, other, max_distance):
94 return (self.expression_distance (other)
95 + self.centroid_distance (other, max_distance)
96 + self.bbox_distance (other))
98 class SystemSignature:
99 def __init__ (self, grob_sigs):
100 d = {}
101 for g in grob_sigs:
102 val = d.setdefault (g.name, [])
103 val += [g]
105 self.grob_dict = d
106 self.set_all_bbox (grob_sigs)
108 def set_all_bbox (self, grobs):
109 self.bbox = empty_bbox
110 for g in grobs:
111 self.bbox = bbox_union (g.bbox, self.bbox)
113 def closest (self, grob_name, centroid):
114 min_d = INFTY
115 min_g = None
116 try:
117 grobs = self.grob_dict[grob_name]
119 for g in grobs:
120 d = max_distance (g.centroid, centroid)
121 if d < min_d:
122 min_d = d
123 min_g = g
126 return min_g
128 except KeyError:
129 return None
130 def grobs (self):
131 return reduce (lambda x,y: x+y, self.grob_dict.values(), [])
133 class SystemLink:
134 def __init__ (self, system1, system2):
135 self.system1 = system1
136 self.system2 = system2
138 self.link_list_dict = {}
139 self.back_link_dict = {}
141 for g in system1.grobs ():
142 closest = system2.closest (g.name, g.centroid)
144 self.link_list_dict.setdefault (closest, [])
145 self.link_list_dict[closest].append (g)
146 self.back_link_dict[g] = closest
148 def distance (self):
149 d = 0.0
151 scale = max (bbox_diameter (self.system1.bbox),
152 bbox_diameter (self.system2.bbox))
154 for (g1,g2) in self.back_link_dict.items ():
155 if g2 == None:
156 d += ORPHAN_GROB_PENALTY
157 else:
158 d += g1.distance (g2, scale)
160 for (g1,g2s) in self.link_list_dict.items ():
161 if len (g2s) != 1:
162 d += ORPHAN_GROB_PENALTY
164 return d
166 ################################################################
167 # Files/directories
169 import glob
170 import shutil
171 import re
173 def read_signature_file (name):
174 print 'reading', name
175 exp_str = ("[%s]" % open (name).read ())
176 entries = safeeval.safe_eval (exp_str)
178 grob_sigs = [GrobSignature (e) for e in entries]
179 sig = SystemSignature (grob_sigs)
180 return sig
183 def compare_signature_files (f1, f2):
184 s1 = read_signature_file (f1)
185 s2 = read_signature_file (f2)
187 return SystemLink (s1, s2).distance ()
189 def paired_files (dir1, dir2, pattern):
191 Search DIR1 and DIR2 for PATTERN.
193 Return (PAIRED, MISSING-FROM-2, MISSING-FROM-1)
197 files1 = dict ((os.path.split (f)[1], 1) for f in glob.glob (dir1 + '/' + pattern))
198 files2 = dict ((os.path.split (f)[1], 1) for f in glob.glob (dir2 + '/' + pattern))
200 pairs = []
201 missing = []
202 for f in files1.keys ():
203 try:
204 files2.pop (f)
205 pairs.append (f)
206 except KeyError:
207 missing.append (f)
209 return (pairs, files2.keys (), missing)
211 class ComparisonData:
212 def __init__ (self):
213 self.result_dict = {}
214 self.missing = []
215 self.added = []
217 def compare_trees (self, dir1, dir2):
218 self.compare_directories (dir1, dir2)
220 (root, files, dirs) = os.walk (dir1).next ()
221 for d in dirs:
222 d1 = os.path.join (dir1, d)
223 d2 = os.path.join (dir2, d)
225 if os.path.isdir (d2):
226 self.compare_trees (d1, d2)
228 def compare_directories (self, dir1, dir2):
230 (paired, m1, m2) = paired_files (dir1, dir2, '*.signature')
232 self.missing += [(dir1, m) for m in m1]
233 self.added += [(dir2, m) for m in m2]
235 for p in paired:
236 f2 = dir2 + '/' + p
237 f1 = dir1 + '/' + p
238 distance = compare_signature_files (f1, f2)
239 self.result_dict[f2] = (distance, f1)
241 def create_text_result_page (self, dir1, dir2):
242 self.write_text_result_page (dir2 + '/' + os.path.split (dir1)[1] + '.txt')
244 def write_text_result_page (self, filename):
245 print 'writing "%s"' % filename
246 out = None
247 if filename == '':
248 out = sys.stdout
249 else:
250 out = open (filename, 'w')
252 results = [(score, oldfile, file) for (file, (score, oldfile)) in self.result_dict.items ()]
253 results.sort ()
254 results.reverse ()
256 for (s, oldfile, f) in results:
257 out.write ('%-30f %-20s\n' % (s, f))
259 for (dir, file) in self.missing:
260 out.write ('%10s%-20s %s\n' % ('', 'missing',os.path.join (dir, file)))
261 for (dir, file) in self.added:
262 out.write ('%20s%-10s %s\n' % ('','added', os.path.join (dir, file)))
264 def print_results (self):
265 self.write_text_result_page ('')
267 def create_html_result_page (self, dir1, dir2):
268 dir1 = dir1.replace ('//', '/')
269 dir2 = dir2.replace ('//', '/')
271 threshold = 1.0
273 results = [(score, oldfile, file) for (file, (score, oldfile)) in self.result_dict.items ()
274 if score > threshold]
276 results.sort ()
277 results.reverse ()
279 html = ''
280 old_prefix = os.path.split (dir1)[1]
281 os.mkdir (dir2 + '/' + old_prefix)
282 for (score, oldfile, newfile) in results:
283 old_base = re.sub ("-[0-9]+.signature", '', os.path.split (oldfile)[1])
284 new_base = re.sub ("-[0-9]+.signature", '', newfile)
286 for ext in 'png', 'ly':
287 shutil.copy2 (old_base + '.' + ext, dir2 + '/' + old_prefix)
289 img_1 = os.path.join (old_prefix, old_base + '.png')
290 ly_1 = os.path.join (old_prefix, old_base + '.ly')
292 img_2 = new_base.replace (dir2, '') + '.png'
293 img_2 = re.sub ("^/*", '', img_2)
295 ly_2 = img_2.replace ('.png','.ly')
297 def img_cell (ly, img):
298 return '''
299 <td align="center">
300 <a href="%(img)s">
301 <img src="%(img)s" style="border-style: none; max-width: 500px;">
302 </a><br>
303 <font size="-2">(<a href="%(ly)s">source</a>)
304 </font>
305 </td>
306 ''' % locals ()
308 html_entry = '''
309 <tr>
310 <td>
312 </td>
316 </tr>
317 ''' % (score, img_cell (ly_1, img_1), img_cell (ly_2, img_2))
320 html += html_entry
322 html = '''<html>
323 <table>
324 <tr>
325 <th>distance</th>
326 <th>old</th>
327 <th>new</th>
328 </tr>
329 %(html)s
330 </table>
331 </html>''' % locals()
333 open (os.path.join (dir2, old_prefix) + '.html', 'w').write (html)
337 def compare_trees (dir1, dir2):
338 data = ComparisonData ()
339 data.compare_trees (dir1, dir2)
340 data.print_results ()
341 data.create_html_result_page (dir1, dir2)
342 data.create_text_result_page (dir1, dir2)
344 ################################################################
345 # TESTING
347 import os
348 def system (x):
350 print 'invoking', x
351 stat = os.system (x)
352 assert stat == 0
355 def test_paired_files ():
356 print paired_files (os.environ["HOME"] + "/src/lilypond/scripts/",
357 os.environ["HOME"] + "/src/lilypond-stable/buildscripts/", '*.py')
360 def test_compare_trees ():
361 system ('rm -rf dir1 dir2')
362 system ('mkdir dir1 dir2')
363 system ('cp 20{-0.signature,.ly,.png} dir1')
364 system ('cp 20{-0.signature,.ly,.png} dir2')
365 system ('cp 20expr{-0.signature,.ly,.png} dir1')
366 system ('cp 19{-0.signature,.ly,.png} dir2/')
367 system ('cp 19{-0.signature,.ly,.png} dir1/')
368 system ('cp 20grob{-0.signature,.ly,.png} dir2/')
370 ## introduce difference
371 system ('cp 19-0.signature dir2/20-0.signature')
373 compare_trees ('dir1', 'dir2')
376 def test_basic_compare ():
377 ly_template = r"""#(set! toplevel-score-handler print-score-with-defaults)
378 #(set! toplevel-music-handler
379 (lambda (p m)
380 (if (not (eq? (ly:music-property m 'void) #t))
381 (print-score-with-defaults
382 p (scorify-music m p)))))
384 %(papermod)s
386 \relative c {
387 c^"%(userstring)s" %(extragrob)s
391 dicts = [{ 'papermod' : '',
392 'name' : '20',
393 'extragrob': '',
394 'userstring': 'test' },
395 { 'papermod' : '#(set-global-staff-size 19.5)',
396 'name' : '19',
397 'extragrob': '',
398 'userstring': 'test' },
399 { 'papermod' : '',
400 'name' : '20expr',
401 'extragrob': '',
402 'userstring': 'blabla' },
403 { 'papermod' : '',
404 'name' : '20grob',
405 'extragrob': 'c4',
406 'userstring': 'test' }]
408 for d in dicts:
409 open (d['name'] + '.ly','w').write (ly_template % d)
411 names = [d['name'] for d in dicts]
413 system ('lilypond -ddump-signatures --png -b eps ' + ' '.join (names))
415 sigs = dict ((n, read_signature_file ('%s-0.signature' % n)) for n in names)
416 combinations = {}
417 for (n1, s1) in sigs.items():
418 for (n2, s2) in sigs.items():
419 combinations['%s-%s' % (n1, n2)] = SystemLink (s1,s2).distance ()
421 results = combinations.items ()
422 results.sort ()
423 for k,v in results:
424 print '%-20s' % k, v
426 assert combinations['20-20'] == 0.0
427 assert combinations['20-20expr'] > 50.0
428 assert combinations['20-19'] < 10.0
431 def test_sigs (a,b):
432 sa = read_signature_file (a)
433 sb = read_signature_file (b)
434 link = SystemLink (sa, sb)
435 print link.distance()
438 def run_tests ():
439 do_clean = 1
440 dir = 'output-distance-test'
442 print 'test results in ', dir
443 if do_clean:
444 system ('rm -rf ' + dir)
445 system ('mkdir ' + dir)
447 os.chdir (dir)
449 test_basic_compare ()
450 test_compare_trees ()
452 ################################################################
455 def main ():
456 p = optparse.OptionParser ("output-distance - compare LilyPond formatting runs")
457 p.usage = 'output-distance.py [options] tree1 tree2'
459 p.add_option ('', '--test',
460 dest="run_test",
461 action="store_true",
462 help='run test method')
464 (o,a) = p.parse_args ()
466 if o.run_test:
467 run_tests ()
468 sys.exit (0)
470 if len (a) != 2:
471 p.print_usage()
472 sys.exit (2)
474 compare_trees (a[0], a[1])
476 if __name__ == '__main__':
477 main()