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