6 ## so we can call directly as buildscripts/output-distance.py
7 sys.path.insert (0, '../python')
16 OUTPUT_EXPRESSION_PENALTY = 100
17 ORPHAN_GROB_PENALTY = 1000
19 def max_distance (x1, x2):
22 for (p,q) in zip (x1, x2):
23 dist = max (abs (p-q), dist)
28 empty_interval = (INFTY, -INFTY)
29 empty_bbox = (empty_interval, empty_interval)
31 def interval_is_empty (i):
34 def interval_length (i):
35 return max (i[1]-i[0], 0)
37 def interval_union (i1, i2):
38 return (min (i1[0], i2[0]),
41 def interval_intersect (i1, i2):
42 return (max (i1[0], i2[0]),
45 def bbox_is_empty (b):
46 return (interval_is_empty (b[0])
47 or interval_is_empty (b[1]))
49 def bbox_union (b1, b2):
50 return (interval_union (b1[X_AXIS], b2[X_AXIS]),
51 interval_union (b2[Y_AXIS], b2[Y_AXIS]))
53 def bbox_intersection (b1, b2):
54 return (interval_intersect (b1[X_AXIS], b2[X_AXIS]),
55 interval_intersect (b2[Y_AXIS], b2[Y_AXIS]))
58 return interval_length (b[X_AXIS]) * interval_length (b[Y_AXIS])
60 def bbox_diameter (b):
61 return max (interval_length (b[X_AXIS]),
62 interval_length (b[Y_AXIS]))
65 def difference_area (a, b):
66 return bbox_area (a) - bbox_area (bbox_intersection (a,b))
69 def __init__ (self, exp_list):
70 (self.name, self.origin, bbox_x,
71 bbox_y, self.output_expression) = tuple (exp_list)
73 self.bbox = (bbox_x, bbox_y)
74 self.centroid = (bbox_x[0] + bbox_x[1], bbox_y[0] + bbox_y[1])
77 return '%s: (%.2f,%.2f), (%.2f,%.2f)\n' % (self.name,
83 def axis_centroid (self, axis):
84 return apply (sum, self.bbox[axis]) / 2
86 def centroid_distance (self, other, scale):
87 return max_distance (self.centroid, other.centroid) / scale
89 def bbox_distance (self, other):
90 divisor = bbox_area (self.bbox) + bbox_area (other.bbox)
93 return (difference_area (self.bbox, other.bbox) +
94 difference_area (other.bbox, self.bbox)) / divisor
98 def expression_distance (self, other):
99 if self.output_expression == other.output_expression:
102 return OUTPUT_EXPRESSION_PENALTY
104 def distance(self, other, max_distance):
105 return (self.expression_distance (other)
106 + self.centroid_distance (other, max_distance)
107 + self.bbox_distance (other))
109 class SystemSignature:
110 def __init__ (self, grob_sigs):
113 val = d.setdefault (g.name, [])
117 self.set_all_bbox (grob_sigs)
119 def set_all_bbox (self, grobs):
120 self.bbox = empty_bbox
122 self.bbox = bbox_union (g.bbox, self.bbox)
124 def closest (self, grob_name, centroid):
128 grobs = self.grob_dict[grob_name]
131 d = max_distance (g.centroid, centroid)
142 return reduce (lambda x,y: x+y, self.grob_dict.values(), [])
145 def __init__ (self, system1, system2):
146 self.system1 = system1
147 self.system2 = system2
149 self.link_list_dict = {}
150 self.back_link_dict = {}
152 for g in system1.grobs ():
154 ## skip empty bboxes.
155 if bbox_is_empty (g.bbox):
158 closest = system2.closest (g.name, g.centroid)
160 self.link_list_dict.setdefault (closest, [])
161 self.link_list_dict[closest].append (g)
162 self.back_link_dict[g] = closest
167 scale = max (bbox_diameter (self.system1.bbox),
168 bbox_diameter (self.system2.bbox))
170 for (g1,g2) in self.back_link_dict.items ():
172 d += ORPHAN_GROB_PENALTY
174 d += g1.distance (g2, scale)
176 for (g1,g2s) in self.link_list_dict.items ():
178 d += ORPHAN_GROB_PENALTY
182 ################################################################
189 def read_signature_file (name):
190 print 'reading', name
191 exp_str = ("[%s]" % open (name).read ())
192 entries = safeeval.safe_eval (exp_str)
194 grob_sigs = [GrobSignature (e) for e in entries]
195 sig = SystemSignature (grob_sigs)
199 def compare_signature_files (f1, f2):
200 s1 = read_signature_file (f1)
201 s2 = read_signature_file (f2)
203 return SystemLink (s1, s2).distance ()
205 def paired_files (dir1, dir2, pattern):
207 Search DIR1 and DIR2 for PATTERN.
209 Return (PAIRED, MISSING-FROM-2, MISSING-FROM-1)
213 files1 = dict ((os.path.split (f)[1], 1) for f in glob.glob (dir1 + '/' + pattern))
214 files2 = dict ((os.path.split (f)[1], 1) for f in glob.glob (dir2 + '/' + pattern))
218 for f in files1.keys ():
225 return (pairs, files2.keys (), missing)
227 class ComparisonData:
229 self.result_dict = {}
233 def compare_trees (self, dir1, dir2):
234 self.compare_directories (dir1, dir2)
236 (root, dirs, files) = os.walk (dir1).next ()
238 d1 = os.path.join (dir1, d)
239 d2 = os.path.join (dir2, d)
241 if os.path.islink (d1) or os.path.islink (d2):
244 if os.path.isdir (d2):
245 self.compare_trees (d1, d2)
247 def compare_directories (self, dir1, dir2):
249 (paired, m1, m2) = paired_files (dir1, dir2, '*.signature')
251 self.missing += [(dir1, m) for m in m1]
252 self.added += [(dir2, m) for m in m2]
257 distance = compare_signature_files (f1, f2)
258 self.result_dict[f2] = (distance, f1)
260 def create_text_result_page (self, dir1, dir2):
261 self.write_text_result_page (dir2 + '/' + os.path.split (dir1)[1] + '.txt')
263 def write_text_result_page (self, filename):
264 print 'writing "%s"' % filename
269 out = open (filename, 'w')
271 results = [(score, oldfile, file) for (file, (score, oldfile)) in self.result_dict.items ()]
275 for (s, oldfile, f) in results:
276 out.write ('%-30f %-20s\n' % (s, f))
278 for (dir, file) in self.missing:
279 out.write ('%10s%-20s %s\n' % ('', 'missing',os.path.join (dir, file)))
280 for (dir, file) in self.added:
281 out.write ('%20s%-10s %s\n' % ('','added', os.path.join (dir, file)))
283 def print_results (self):
284 self.write_text_result_page ('')
286 def create_html_result_page (self, dir1, dir2):
287 dir1 = dir1.replace ('//', '/')
288 dir2 = dir2.replace ('//', '/')
292 results = [(score, oldfile, file) for (file, (score, oldfile)) in self.result_dict.items ()
293 if score > threshold]
299 old_prefix = os.path.split (dir1)[1]
301 dest_dir = os.path.join (dir2, old_prefix)
302 shutil.rmtree (dest_dir, ignore_errors=True)
304 for (score, oldfile, newfile) in results:
306 old_base = re.sub ("-[0-9]+.signature", '', oldfile)
307 old_name = os.path.split (old_base)[1]
308 new_base = re.sub ("-[0-9]+.signature", '', newfile)
310 for ext in 'png', 'ly':
311 src_file = old_base + '.' + ext
313 if os.path.exists (src_file):
314 shutil.copy2 (src_file, dest_dir)
316 print "warning: can't find", src_file
318 img_1 = os.path.join (old_prefix, old_name + '.png')
319 ly_1 = os.path.join (old_prefix, old_name + '.ly')
321 img_2 = new_base.replace (dir2, '') + '.png'
322 img_2 = re.sub ("^/*", '', img_2)
324 ly_2 = img_2.replace ('.png','.ly')
326 def img_cell (ly, img):
330 <img src="%(img)s" style="border-style: none; max-width: 500px;">
332 <font size="-2">(<a href="%(ly)s">source</a>)
346 ''' % (score, img_cell (ly_1, img_1), img_cell (ly_2, img_2))
360 </html>''' % locals()
362 open (os.path.join (dir2, old_prefix) + '.html', 'w').write (html)
366 def compare_trees (dir1, dir2):
367 data = ComparisonData ()
368 data.compare_trees (dir1, dir2)
369 data.print_results ()
370 data.create_html_result_page (dir1, dir2)
371 # data.create_text_result_page (dir1, dir2)
373 ################################################################
384 def test_paired_files ():
385 print paired_files (os.environ["HOME"] + "/src/lilypond/scripts/",
386 os.environ["HOME"] + "/src/lilypond-stable/buildscripts/", '*.py')
389 def test_compare_trees ():
390 system ('rm -rf dir1 dir2')
391 system ('mkdir dir1 dir2')
392 system ('cp 20{-0.signature,.ly,.png} dir1')
393 system ('cp 20{-0.signature,.ly,.png} dir2')
394 system ('cp 20expr{-0.signature,.ly,.png} dir1')
395 system ('cp 19{-0.signature,.ly,.png} dir2/')
396 system ('cp 19{-0.signature,.ly,.png} dir1/')
397 system ('cp 20grob{-0.signature,.ly,.png} dir2/')
399 ## introduce difference
400 system ('cp 19-0.signature dir2/20-0.signature')
402 compare_trees ('dir1', 'dir2')
405 def test_basic_compare ():
406 ly_template = r"""#(set! toplevel-score-handler print-score-with-defaults)
407 #(set! toplevel-music-handler
409 (if (not (eq? (ly:music-property m 'void) #t))
410 (print-score-with-defaults
411 p (scorify-music m p)))))
415 \new Staff \relative c {
416 c^"%(userstring)s" %(extragrob)s
418 \new Staff \relative c {
419 c^"%(userstring)s" %(extragrob)s
424 dicts = [{ 'papermod' : '',
427 'userstring': 'test' },
428 { 'papermod' : '#(set-global-staff-size 19.5)',
431 'userstring': 'test' },
435 'userstring': 'blabla' },
439 'userstring': 'test' }]
442 open (d['name'] + '.ly','w').write (ly_template % d)
444 names = [d['name'] for d in dicts]
446 system ('lilypond -ddump-signatures --png -b eps ' + ' '.join (names))
448 sigs = dict ((n, read_signature_file ('%s-0.signature' % n)) for n in names)
450 for (n1, s1) in sigs.items():
451 for (n2, s2) in sigs.items():
452 combinations['%s-%s' % (n1, n2)] = SystemLink (s1,s2).distance ()
454 results = combinations.items ()
459 assert combinations['20-20'] == 0.0
460 assert combinations['20-20expr'] > 50.0
461 assert combinations['20-19'] < 10.0
465 sa = read_signature_file (a)
466 sb = read_signature_file (b)
467 link = SystemLink (sa, sb)
468 print link.distance()
473 dir = 'output-distance-test'
475 print 'test results in ', dir
477 system ('rm -rf ' + dir)
478 system ('mkdir ' + dir)
482 test_basic_compare ()
483 test_compare_trees ()
485 ################################################################
489 p = optparse.OptionParser ("output-distance - compare LilyPond formatting runs")
490 p.usage = 'output-distance.py [options] tree1 tree2'
492 p.add_option ('', '--test',
495 help='run test method')
497 (o,a) = p.parse_args ()
507 compare_trees (a[0], a[1])
509 if __name__ == '__main__':