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