]> git.donarmstrong.com Git - lilypond.git/blob - buildscripts/output-distance.py
*** empty log message ***
[lilypond.git] / buildscripts / output-distance.py
1 #!@TARGET_PYTHON@
2 import sys
3 import optparse
4 import os
5
6 ## so we can call directly as buildscripts/output-distance.py
7 me_path = os.path.abspath (os.path.split (sys.argv[0])[0])
8 sys.path.insert (0, me_path + '/../python/')
9
10
11 import safeeval
12
13
14 X_AXIS = 0
15 Y_AXIS = 1
16 INFTY = 1e6
17
18 OUTPUT_EXPRESSION_PENALTY = 1
19 ORPHAN_GROB_PENALTY = 1
20 THRESHOLD = 1.0
21
22 def max_distance (x1, x2):
23     dist = 0.0
24
25     for (p,q) in zip (x1, x2):
26         dist = max (abs (p-q), dist)
27         
28     return dist
29
30
31 empty_interval = (INFTY, -INFTY)
32 empty_bbox = (empty_interval, empty_interval)
33
34 def interval_is_empty (i):
35     return i[0] > i[1]
36
37 def interval_length (i):
38     return max (i[1]-i[0], 0) 
39     
40 def interval_union (i1, i2):
41     return (min (i1[0], i2[0]),
42             max (i1[1], i2[1]))
43
44 def interval_intersect (i1, i2):
45     return (max (i1[0], i2[0]),
46             min (i1[1], i2[1]))
47
48 def bbox_is_empty (b):
49     return (interval_is_empty (b[0])
50             or interval_is_empty (b[1]))
51
52 def bbox_union (b1, b2):
53     return (interval_union (b1[X_AXIS], b2[X_AXIS]),
54             interval_union (b2[Y_AXIS], b2[Y_AXIS]))
55             
56 def bbox_intersection (b1, b2):
57     return (interval_intersect (b1[X_AXIS], b2[X_AXIS]),
58             interval_intersect (b2[Y_AXIS], b2[Y_AXIS]))
59
60 def bbox_area (b):
61     return interval_length (b[X_AXIS]) * interval_length (b[Y_AXIS])
62
63 def bbox_diameter (b):
64     return max (interval_length (b[X_AXIS]),
65                 interval_length (b[Y_AXIS]))
66                 
67
68 def difference_area (a, b):
69     return bbox_area (a) - bbox_area (bbox_intersection (a,b))
70
71 class GrobSignature:
72     def __init__ (self, exp_list):
73         (self.name, self.origin, bbox_x,
74          bbox_y, self.output_expression) = tuple (exp_list)
75         
76         self.bbox = (bbox_x, bbox_y)
77         self.centroid = (bbox_x[0] + bbox_x[1], bbox_y[0] + bbox_y[1])
78
79     def __repr__ (self):
80         return '%s: (%.2f,%.2f), (%.2f,%.2f)\n' % (self.name,
81                                                  self.bbox[0][0],
82                                                  self.bbox[0][1],
83                                                  self.bbox[1][0],
84                                                  self.bbox[1][1])
85                                                  
86     def axis_centroid (self, axis):
87         return apply (sum, self.bbox[axis])  / 2 
88     
89     def centroid_distance (self, other, scale):
90         return max_distance (self.centroid, other.centroid) / scale 
91         
92     def bbox_distance (self, other):
93         divisor = bbox_area (self.bbox) + bbox_area (other.bbox)
94
95         if divisor:
96             return (difference_area (self.bbox, other.bbox) +
97                     difference_area (other.bbox, self.bbox)) / divisor
98         else:
99             return 0.0
100         
101     def expression_distance (self, other):
102         if self.output_expression == other.output_expression:
103             return 0.0
104         else:
105             return OUTPUT_EXPRESSION_PENALTY
106
107 class SystemSignature:
108     def __init__ (self, grob_sigs):
109         d = {}
110         for g in grob_sigs:
111             val = d.setdefault (g.name, [])
112             val += [g]
113
114         self.grob_dict = d
115         self.set_all_bbox (grob_sigs)
116
117     def set_all_bbox (self, grobs):
118         self.bbox = empty_bbox
119         for g in grobs:
120             self.bbox = bbox_union (g.bbox, self.bbox)
121
122     def closest (self, grob_name, centroid):
123         min_d = INFTY
124         min_g = None
125         try:
126             grobs = self.grob_dict[grob_name]
127
128             for g in grobs:
129                 d = max_distance (g.centroid, centroid)
130                 if d < min_d:
131                     min_d = d
132                     min_g = g
133
134
135             return min_g
136
137         except KeyError:
138             return None
139     def grobs (self):
140         return reduce (lambda x,y: x+y, self.grob_dict.values(), [])
141
142 class SystemLink:
143     def __init__ (self, system1, system2):
144         self.system1 = system1
145         self.system2 = system2
146         
147         self.link_list_dict = {}
148         self.back_link_dict = {}
149
150         for g in system1.grobs ():
151
152             ## skip empty bboxes.
153             if bbox_is_empty (g.bbox):
154                 continue
155             
156             closest = system2.closest (g.name, g.centroid)
157             
158             self.link_list_dict.setdefault (closest, [])
159             self.link_list_dict[closest].append (g)
160             self.back_link_dict[g] = closest
161
162     def geometric_distance (self):
163         d = 0.0
164         for (g1,g2) in self.back_link_dict.items ():
165             if g2:
166                 # , scale
167                 d += g1.bbox_distance (g2)
168
169         return d
170     
171     def orphan_distance (self):
172         d = 0
173         for (g1,g2) in self.back_link_dict.items ():
174             if g2 == None:
175                 d += ORPHAN_GROB_PENALTY
176         return d
177     
178     def output_exp_distance (self):
179         d = 0
180         for (g1,g2) in self.back_link_dict.items ():
181             if g2:
182                 d += g1.expression_distance (g2)
183
184         return d
185
186     def distance (self):
187         return (self.output_exp_distance (),
188                 self.orphan_distance (),
189                 self.geometric_distance ())
190     
191 def read_signature_file (name):
192     print 'reading', name
193     exp_str = ("[%s]" % open (name).read ())
194     entries = safeeval.safe_eval (exp_str)
195
196     grob_sigs = [GrobSignature (e) for e in entries]
197     sig = SystemSignature (grob_sigs)
198     return sig
199
200
201 class FileLink:
202     def __init__ (self):
203         self.original_name = ''
204         self.base_names = ('','')
205         self.system_links = {}
206         self._distance = None
207         
208     def add_system_link (self, link, number):
209         self.system_links[number] = link
210
211     def calc_distance (self):
212         d = 0.0
213         for l in self.system_links.values ():
214             d = max (d, l.geometric_distance ())
215         return d
216
217     def distance (self):
218         if type (self._distance) != type (0.0):
219             return self.calc_distance ()
220         
221         return self._distance
222
223     def text_record_string (self):
224         return '%-30f %-20s\n' % (self.distance (),
225                              self.original_name)
226
227     def source_file (self):
228         for ext in ('.ly', '.ly.txt'):
229             if os.path.exists (self.base_names[1] + ext):
230                 return self.base_names[1] + ext
231         return ''
232     
233     def add_file_compare (self, f1, f2):
234         system_index = [] 
235
236         def note_system_index (m):
237             system_index.append (int (m.group (1)))
238             return ''
239         
240         base1 = re.sub ("-([0-9]+).signature", note_system_index, f1)
241         base2 = re.sub ("-([0-9]+).signature", note_system_index, f2)
242
243         self.base_names = (os.path.normpath (base1),
244                            os.path.normpath (base2))
245
246         def note_original (match):
247             self.original_name = match.group (1)
248             return ''
249         
250         if not self.original_name:
251             self.original_name = os.path.split (base1)[1]
252
253             ## ugh: drop the .ly.txt
254             for ext in ('.ly', '.ly.txt'):
255                 try:
256                     re.sub (r'\\sourcefilename "([^"]+)"',
257                             note_original, open (base1 + ext).read ())
258                 except IOError:
259                     pass
260                 
261         s1 = read_signature_file (f1)
262         s2 = read_signature_file (f2)
263
264         link = SystemLink (s1, s2)
265
266         self.add_system_link (link, system_index[0])
267
268     def link_files_for_html (self, old_dir, new_dir, dest_dir):
269         for ext in ('.png', '.ly'):
270             for oldnew in (0,1):
271                 link_file (self.base_names[oldnew] + ext, 
272                            dest_dir + '/' + self.base_names[oldnew] + ext)
273
274     def html_record_string (self,  old_dir, new_dir):
275         def img_cell (ly, img, name):
276             if not name:
277                 name = 'source'
278             else:
279                 name = '<tt>%s</tt>' % name
280                 
281             return '''
282 <td align="center">
283 <a href="%(img)s">
284 <img src="%(img)s" style="border-style: none; max-width: 500px;">
285 </a><br>
286 <font size="-2">(<a href="%(ly)s">%(name)s</a>)
287 </font>
288 </td>
289 ''' % locals ()
290         
291
292         img_1  = self.base_names[0] + '.png'
293         ly_1  = self.base_names[0] + '.ly'
294         img_2  = self.base_names[1] + '.png'
295         ly_2  = self.base_names[1] + '.ly'
296         html_2  = self.base_names[1] + '.html'
297         name = self.original_name
298         
299         html_entry = '''
300 <tr>
301 <td>
302 %f<br>
303 (<a href="%s">details</a>)
304 </td>
305
306 %s
307 %s
308 </tr>
309 ''' % (self.distance (), html_2,
310        img_cell (ly_1, img_1, name), img_cell (ly_2, img_2, name))
311
312
313         return html_entry
314
315
316     def html_system_details_string (self):
317         systems = self.system_links.items ()
318         systems.sort ()
319
320         html = ""
321         for (c, link) in systems:
322             e = '<td>%d</td>' % c
323             for d in link.distance ():
324                 e += '<td>%f</td>' % d
325             
326             e = '<tr>%s</tr>' % e
327             html += e
328
329         original = self.original_name
330         html = '''<html>
331 <head>
332 <title>comparison details for %(original)s</title>
333 </head>
334 <body>
335 <table border=1>
336 <tr>
337 <th>system</th>
338 <th>output</th>
339 <th>orphan</th>
340 <th>geo</th>
341 </tr>
342
343 %(html)s
344 </table>
345
346 </body>
347 </html>
348 ''' % locals ()
349         return html
350
351     def write_html_system_details (self, dir1, dir2, dest_dir):
352         dest_file =  os.path.join (dest_dir, self.base_names[1] + '.html')
353
354         details = open_write_file (dest_file)
355         details.write (self.html_system_details_string ())
356
357 ################################################################
358 # Files/directories
359
360 import glob
361 import re
362
363
364
365 def compare_signature_files (f1, f2):
366     s1 = read_signature_file (f1)
367     s2 = read_signature_file (f2)
368     
369     return SystemLink (s1, s2).distance ()
370
371 def paired_files (dir1, dir2, pattern):
372     """
373     Search DIR1 and DIR2 for PATTERN.
374
375     Return (PAIRED, MISSING-FROM-2, MISSING-FROM-1)
376
377     """
378     
379     files1 = dict ((os.path.split (f)[1], 1) for f in glob.glob (dir1 + '/' + pattern))
380     files2 = dict ((os.path.split (f)[1], 1) for f in glob.glob (dir2 + '/' + pattern))
381
382     pairs = []
383     missing = []
384     for f in files1.keys ():
385         try:
386             files2.pop (f)
387             pairs.append (f)
388         except KeyError:
389             missing.append (f)
390
391     return (pairs, files2.keys (), missing)
392     
393 class ComparisonData:
394     def __init__ (self):
395         self.result_dict = {}
396         self.missing = []
397         self.added = []
398         self.file_links = {}
399     def compare_trees (self, dir1, dir2):
400         self.compare_directories (dir1, dir2)
401         
402         (root, dirs, files) = os.walk (dir1).next ()
403         for d in dirs:
404             d1 = os.path.join (dir1, d)
405             d2 = os.path.join (dir2, d)
406
407             if os.path.islink (d1) or os.path.islink (d2):
408                 continue
409             
410             if os.path.isdir (d2):
411                 self.compare_trees (d1, d2)
412     
413     def compare_directories (self, dir1, dir2):
414
415         (paired, m1, m2) = paired_files (dir1, dir2, '*.signature')
416
417         self.missing += [(dir1, m) for m in m1] 
418         self.added += [(dir2, m) for m in m2] 
419
420         for p in paired:
421             if len (self.file_links) > 10:
422                 continue
423             
424             f2 = dir2 +  '/' + p
425             f1 = dir1 +  '/' + p
426             self.compare_files (f1, f2)
427
428     def compare_files (self, f1, f2):
429         name = os.path.split (f1)[1]
430         name = re.sub ('-[0-9]+.signature', '', name)
431         
432         file_link = None
433         try:
434             file_link = self.file_links[name]
435         except KeyError:
436             file_link = FileLink ()
437             self.file_links[name] = file_link
438
439         file_link.add_file_compare (f1,f2)
440
441     def write_text_result_page (self, filename):
442         print 'writing "%s"' % filename
443         out = None
444         if filename == '':
445             out = sys.stdout
446         else:
447             out = open_write_file (filename)
448
449         ## todo: support more scores.
450         results = [(link.distance(), link)
451                    for link in self.file_links.values ()]
452         results.sort ()
453         results.reverse ()
454
455         
456         for (score, link) in results:
457             if score > THRESHOLD:
458                 out.write (link.text_record_string ())
459
460         out.write ('\n\n')
461         out.write ('%d below threshold\n' % len ([1 for s,l  in results
462                                                     if THRESHOLD >=  s > 0.0]))
463         out.write ('%d unchanged\n' % len ([1 for (s,l) in results if s == 0.0]))
464         
465     def create_text_result_page (self, dir1, dir2, dest_dir):
466         self.write_text_result_page (dest_dir + '/index.txt')
467         
468     def create_html_result_page (self, dir1, dir2, dest_dir):
469         dir1 = dir1.replace ('//', '/')
470         dir2 = dir2.replace ('//', '/')
471         
472         results = [(link.distance(), link)
473                    for link in self.file_links.values ()]
474         results.sort ()
475         results.reverse ()
476
477         html = ''
478         old_prefix = os.path.split (dir1)[1]
479         for (score, link) in results:
480             if score <= THRESHOLD:
481                 continue
482
483             link.write_html_system_details (dir1, dir2, dest_dir)
484             link.link_files_for_html (dir1, dir2, dest_dir) 
485             html += link.html_record_string (dir1, dir2)
486
487
488         html = '''<html>
489 <table rules="rows" border bordercolor="blue">
490 <tr>
491 <th>distance</th>
492 <th>old</th>
493 <th>new</th>
494 </tr>
495 %(html)s
496 </table>
497 </html>''' % locals()
498
499         html += ('<p>')
500         below_count  =len ([1 for s,l  in results
501                          if THRESHOLD >=  s > 0.0])
502
503         if below_count:
504             html += ('<p>%d below threshold</p>' % below_count)
505
506         html += ('<p>%d unchanged</p>'
507                  % len ([1 for (s,l) in results if s == 0.0]))
508
509
510         dest_file = dest_dir + '/index.html'
511         open_write_file (dest_file).write (html)
512         
513     def print_results (self):
514         self.write_text_result_page ('')
515         
516
517 def compare_trees (dir1, dir2, dest_dir):
518     data = ComparisonData ()
519     data.compare_trees (dir1, dir2)
520     data.print_results ()
521
522     if os.path.isdir (dest_dir):
523         system ('rm -rf %s '% dest_dir)
524
525     data.create_html_result_page (dir1, dir2, dest_dir)
526     data.create_text_result_page (dir1, dir2, dest_dir)
527     
528 ################################################################
529 # TESTING
530
531 def mkdir (x):
532     if not os.path.isdir (x):
533         print 'mkdir', x
534         os.makedirs (x)
535
536 def link_file (x, y):
537     mkdir (os.path.split (y)[0])
538     os.link (x, y)
539     
540 def open_write_file (x):
541     d = os.path.split (x)[0]
542     mkdir (d)
543     return open (x, 'w')
544
545
546 def system (x):
547     
548     print 'invoking', x
549     stat = os.system (x)
550     assert stat == 0
551
552
553 def test_paired_files ():
554     print paired_files (os.environ["HOME"] + "/src/lilypond/scripts/",
555                         os.environ["HOME"] + "/src/lilypond-stable/buildscripts/", '*.py')
556                   
557     
558 def test_compare_trees ():
559     system ('rm -rf dir1 dir2')
560     system ('mkdir dir1 dir2')
561     system ('cp 20{-*.signature,.ly,.png} dir1')
562     system ('cp 20{-*.signature,.ly,.png} dir2')
563     system ('cp 20expr{-*.signature,.ly,.png} dir1')
564     system ('cp 19{-*.signature,.ly,.png} dir2/')
565     system ('cp 19{-*.signature,.ly,.png} dir1/')
566     system ('cp 19-1.signature 19-sub-1.signature')
567     system ('cp 19.ly 19-sub.ly')
568     system ('cp 19.png 19-sub.png')
569     
570     system ('mkdir -p dir1/subdir/ dir2/subdir/')
571     system ('cp 19-sub{-*.signature,.ly,.png} dir1/subdir/')
572     system ('cp 19-sub{-*.signature,.ly,.png} dir2/subdir/')
573     system ('cp 20grob{-*.signature,.ly,.png} dir2/')
574     system ('cp 20grob{-*.signature,.ly,.png} dir1/')
575
576     ## introduce differences
577     system ('cp 19-1.signature dir2/20-1.signature')
578
579     ## radical diffs.
580     system ('cp 19-1.signature dir2/20grob-1.signature')
581     system ('cp 19-1.signature dir2/20grob-2.signature')
582
583     compare_trees ('dir1', 'dir2', 'compare-dir1dir2')
584
585
586 def test_basic_compare ():
587     ly_template = r"""#(set! toplevel-score-handler print-score-with-defaults)
588 #(set! toplevel-music-handler
589  (lambda (p m)
590  (if (not (eq? (ly:music-property m 'void) #t))
591     (print-score-with-defaults
592     p (scorify-music m p)))))
593
594 \sourcefilename "my-source.ly"
595
596 %(papermod)s
597 <<
598 \new Staff \relative c {
599   c4^"%(userstring)s" %(extragrob)s
600   }
601 \new Staff \relative c {
602   c4^"%(userstring)s" %(extragrob)s
603   }
604 >>
605 """
606
607     dicts = [{ 'papermod' : '',
608                'name' : '20',
609                'extragrob': '',
610                'userstring': 'test' },
611              { 'papermod' : '#(set-global-staff-size 19.5)',
612                'name' : '19',
613                'extragrob': '',
614                'userstring': 'test' },
615              { 'papermod' : '',
616                'name' : '20expr',
617                'extragrob': '',
618                'userstring': 'blabla' },
619              { 'papermod' : '',
620                'name' : '20grob',
621                'extragrob': 'r2. \\break c1',
622                'userstring': 'test' }
623
624              ]
625
626     for d in dicts:
627         open (d['name'] + '.ly','w').write (ly_template % d)
628         
629     names = [d['name'] for d in dicts]
630     
631     system ('lilypond -ddump-signatures --png -b eps ' + ' '.join (names))
632     
633     sigs = dict ((n, read_signature_file ('%s-1.signature' % n)) for n in names)
634     combinations = {}
635     for (n1, s1) in sigs.items():
636         for (n2, s2) in sigs.items():
637             combinations['%s-%s' % (n1, n2)] = SystemLink (s1,s2).distance ()
638
639     results = combinations.items ()
640     results.sort ()
641     for k,v in results:
642         print '%-20s' % k, v
643
644     assert combinations['20-20'] == (0.0,0.0,0.0)
645     assert combinations['20-20expr'][0] > 0.0
646     assert combinations['20-19'][2] < 10.0
647     assert combinations['20-19'][2] > 0.0
648
649
650 def run_tests ():
651     do_clean = 0
652     dir = 'output-distance-test'
653
654     print 'test results in ', dir
655     if do_clean:
656         system ('rm -rf ' + dir)
657         system ('mkdir ' + dir)
658         
659     os.chdir (dir)
660     if do_clean:
661         test_basic_compare ()
662         
663     test_compare_trees ()
664     
665 ################################################################
666 #
667
668 def main ():
669     p = optparse.OptionParser ("output-distance - compare LilyPond formatting runs")
670     p.usage = 'output-distance.py [options] tree1 tree2'
671     
672     p.add_option ('', '--test',
673                   dest="run_test",
674                   action="store_true",
675                   help='run test method')
676
677     (o,a) = p.parse_args ()
678
679     if o.run_test:
680         run_tests ()
681         sys.exit (0)
682
683     if len (a) != 2:
684         p.print_usage()
685         sys.exit (2)
686
687     compare_trees (a[0], a[1])
688
689 if __name__ == '__main__':
690     main()
691