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/')
18 OUTPUT_EXPRESSION_PENALTY = 100
19 ORPHAN_GROB_PENALTY = 1000
21 def max_distance (x1, x2):
24 for (p,q) in zip (x1, x2):
25 dist = max (abs (p-q), dist)
30 empty_interval = (INFTY, -INFTY)
31 empty_bbox = (empty_interval, empty_interval)
33 def interval_is_empty (i):
36 def interval_length (i):
37 return max (i[1]-i[0], 0)
39 def interval_union (i1, i2):
40 return (min (i1[0], i2[0]),
43 def interval_intersect (i1, i2):
44 return (max (i1[0], i2[0]),
47 def bbox_is_empty (b):
48 return (interval_is_empty (b[0])
49 or interval_is_empty (b[1]))
51 def bbox_union (b1, b2):
52 return (interval_union (b1[X_AXIS], b2[X_AXIS]),
53 interval_union (b2[Y_AXIS], b2[Y_AXIS]))
55 def bbox_intersection (b1, b2):
56 return (interval_intersect (b1[X_AXIS], b2[X_AXIS]),
57 interval_intersect (b2[Y_AXIS], b2[Y_AXIS]))
60 return interval_length (b[X_AXIS]) * interval_length (b[Y_AXIS])
62 def bbox_diameter (b):
63 return max (interval_length (b[X_AXIS]),
64 interval_length (b[Y_AXIS]))
67 def difference_area (a, b):
68 return bbox_area (a) - bbox_area (bbox_intersection (a,b))
71 def __init__ (self, exp_list):
72 (self.name, self.origin, bbox_x,
73 bbox_y, self.output_expression) = tuple (exp_list)
75 self.bbox = (bbox_x, bbox_y)
76 self.centroid = (bbox_x[0] + bbox_x[1], bbox_y[0] + bbox_y[1])
79 return '%s: (%.2f,%.2f), (%.2f,%.2f)\n' % (self.name,
85 def axis_centroid (self, axis):
86 return apply (sum, self.bbox[axis]) / 2
88 def centroid_distance (self, other, scale):
89 return max_distance (self.centroid, other.centroid) / scale
91 def bbox_distance (self, other):
92 divisor = bbox_area (self.bbox) + bbox_area (other.bbox)
95 return (difference_area (self.bbox, other.bbox) +
96 difference_area (other.bbox, self.bbox)) / divisor
100 def expression_distance (self, other):
101 if self.output_expression == other.output_expression:
104 return OUTPUT_EXPRESSION_PENALTY
106 class SystemSignature:
107 def __init__ (self, grob_sigs):
110 val = d.setdefault (g.name, [])
114 self.set_all_bbox (grob_sigs)
116 def set_all_bbox (self, grobs):
117 self.bbox = empty_bbox
119 self.bbox = bbox_union (g.bbox, self.bbox)
121 def closest (self, grob_name, centroid):
125 grobs = self.grob_dict[grob_name]
128 d = max_distance (g.centroid, centroid)
139 return reduce (lambda x,y: x+y, self.grob_dict.values(), [])
142 def __init__ (self, system1, system2):
143 self.system1 = system1
144 self.system2 = system2
146 self.link_list_dict = {}
147 self.back_link_dict = {}
149 for g in system1.grobs ():
151 ## skip empty bboxes.
152 if bbox_is_empty (g.bbox):
155 closest = system2.closest (g.name, g.centroid)
157 self.link_list_dict.setdefault (closest, [])
158 self.link_list_dict[closest].append (g)
159 self.back_link_dict[g] = closest
161 def geometric_distance (self):
163 for (g1,g2) in self.back_link_dict.items ():
166 d += g1.bbox_distance (g2)
170 def orphan_distance (self):
172 for (g1,g2) in self.back_link_dict.items ():
174 d += ORPHAN_GROB_PENALTY
177 def output_exp_distance (self):
179 for (g1,g2) in self.back_link_dict.items ():
181 d += g1.expression_distance (g2)
186 return (self.output_exp_distance (),
187 self.orphan_distance (),
188 self.geometric_distance ())
190 ################################################################
197 def read_signature_file (name):
198 print 'reading', name
199 exp_str = ("[%s]" % open (name).read ())
200 entries = safeeval.safe_eval (exp_str)
202 grob_sigs = [GrobSignature (e) for e in entries]
203 sig = SystemSignature (grob_sigs)
207 def compare_signature_files (f1, f2):
208 s1 = read_signature_file (f1)
209 s2 = read_signature_file (f2)
211 return SystemLink (s1, s2).distance ()
213 def paired_files (dir1, dir2, pattern):
215 Search DIR1 and DIR2 for PATTERN.
217 Return (PAIRED, MISSING-FROM-2, MISSING-FROM-1)
221 files1 = dict ((os.path.split (f)[1], 1) for f in glob.glob (dir1 + '/' + pattern))
222 files2 = dict ((os.path.split (f)[1], 1) for f in glob.glob (dir2 + '/' + pattern))
226 for f in files1.keys ():
233 return (pairs, files2.keys (), missing)
235 class ComparisonData:
237 self.result_dict = {}
241 def compare_trees (self, dir1, dir2):
242 self.compare_directories (dir1, dir2)
244 (root, dirs, files) = os.walk (dir1).next ()
246 d1 = os.path.join (dir1, d)
247 d2 = os.path.join (dir2, d)
249 if os.path.islink (d1) or os.path.islink (d2):
252 if os.path.isdir (d2):
253 self.compare_trees (d1, d2)
255 def compare_directories (self, dir1, dir2):
257 (paired, m1, m2) = paired_files (dir1, dir2, '*.signature')
259 self.missing += [(dir1, m) for m in m1]
260 self.added += [(dir2, m) for m in m2]
265 distance = compare_signature_files (f1, f2)
266 self.result_dict[f2] = (distance, f1)
268 def create_text_result_page (self, dir1, dir2):
269 self.write_text_result_page (dir2 + '/' + os.path.split (dir1)[1] + '.txt')
271 def write_text_result_page (self, filename):
272 print 'writing "%s"' % filename
277 out = open (filename, 'w')
279 ## todo: support more scores.
280 results = [(geo_score, oldfile, file)
281 for (file, ((exp_score, orphan_score,
282 geo_score), oldfile))
283 in self.result_dict.items ()]
288 for (s, oldfile, f) in results:
289 out.write ('%-30f %-20s\n' % (s, f))
291 for (dir, file) in self.missing:
292 out.write ('%10s%-20s %s\n' % ('', 'missing',os.path.join (dir, file)))
293 for (dir, file) in self.added:
294 out.write ('%20s%-10s %s\n' % ('','added', os.path.join (dir, file)))
296 def print_results (self):
297 self.write_text_result_page ('')
299 def create_html_result_page (self, dir1, dir2):
300 dir1 = dir1.replace ('//', '/')
301 dir2 = dir2.replace ('//', '/')
305 results = [(geo_score, oldfile, file) for (file, ((exp_score, orphan_score,
306 geo_score), oldfile)) in self.result_dict.items ()
307 if geo_score > threshold]
313 old_prefix = os.path.split (dir1)[1]
315 dest_dir = os.path.join (dir2, old_prefix)
316 shutil.rmtree (dest_dir, ignore_errors=True)
318 for (score, oldfile, newfile) in results:
320 old_base = re.sub ("-[0-9]+.signature", '', oldfile)
321 old_name = os.path.split (old_base)[1]
322 new_base = re.sub ("-[0-9]+.signature", '', newfile)
324 for ext in 'png', 'ly':
325 src_file = old_base + '.' + ext
327 if os.path.exists (src_file):
328 shutil.copy2 (src_file, dest_dir)
330 print "warning: can't find", src_file
332 img_1 = os.path.join (old_prefix, old_name + '.png')
333 ly_1 = os.path.join (old_prefix, old_name + '.ly')
335 img_2 = new_base.replace (dir2, '') + '.png'
336 img_2 = re.sub ("^/*", '', img_2)
338 ly_2 = img_2.replace ('.png','.ly')
340 def img_cell (ly, img):
344 <img src="%(img)s" style="border-style: none; max-width: 500px;">
346 <font size="-2">(<a href="%(ly)s">source</a>)
360 ''' % (score, img_cell (ly_1, img_1), img_cell (ly_2, img_2))
366 <table rules="rows" border bordercolor="blue">
374 </html>''' % locals()
376 open (os.path.join (dir2, old_prefix) + '.html', 'w').write (html)
380 def compare_trees (dir1, dir2):
381 data = ComparisonData ()
382 data.compare_trees (dir1, dir2)
383 data.print_results ()
384 data.create_html_result_page (dir1, dir2)
385 # data.create_text_result_page (dir1, dir2)
387 ################################################################
398 def test_paired_files ():
399 print paired_files (os.environ["HOME"] + "/src/lilypond/scripts/",
400 os.environ["HOME"] + "/src/lilypond-stable/buildscripts/", '*.py')
403 def test_compare_trees ():
404 system ('rm -rf dir1 dir2')
405 system ('mkdir dir1 dir2')
406 system ('cp 20{-0.signature,.ly,.png} dir1')
407 system ('cp 20{-0.signature,.ly,.png} dir2')
408 system ('cp 20expr{-0.signature,.ly,.png} dir1')
409 system ('cp 19{-0.signature,.ly,.png} dir2/')
410 system ('cp 19{-0.signature,.ly,.png} dir1/')
411 system ('cp 20grob{-0.signature,.ly,.png} dir2/')
413 ## introduce difference
414 system ('cp 19-0.signature dir2/20-0.signature')
416 compare_trees ('dir1', 'dir2')
419 def test_basic_compare ():
420 ly_template = r"""#(set! toplevel-score-handler print-score-with-defaults)
421 #(set! toplevel-music-handler
423 (if (not (eq? (ly:music-property m 'void) #t))
424 (print-score-with-defaults
425 p (scorify-music m p)))))
429 \new Staff \relative c {
430 c^"%(userstring)s" %(extragrob)s
432 \new Staff \relative c {
433 c^"%(userstring)s" %(extragrob)s
438 dicts = [{ 'papermod' : '',
441 'userstring': 'test' },
442 { 'papermod' : '#(set-global-staff-size 19.5)',
445 'userstring': 'test' },
449 'userstring': 'blabla' },
453 'userstring': 'test' }]
456 open (d['name'] + '.ly','w').write (ly_template % d)
458 names = [d['name'] for d in dicts]
460 system ('lilypond -ddump-signatures --png -b eps ' + ' '.join (names))
462 sigs = dict ((n, read_signature_file ('%s-0.signature' % n)) for n in names)
464 for (n1, s1) in sigs.items():
465 for (n2, s2) in sigs.items():
466 combinations['%s-%s' % (n1, n2)] = SystemLink (s1,s2).distance ()
468 results = combinations.items ()
473 assert combinations['20-20'] == 0.0
474 assert combinations['20-20expr'] > 50.0
475 assert combinations['20-19'] < 10.0
479 sa = read_signature_file (a)
480 sb = read_signature_file (b)
481 link = SystemLink (sa, sb)
482 print link.distance()
487 dir = 'output-distance-test'
489 print 'test results in ', dir
491 system ('rm -rf ' + dir)
492 system ('mkdir ' + dir)
496 test_basic_compare ()
497 test_compare_trees ()
499 ################################################################
503 p = optparse.OptionParser ("output-distance - compare LilyPond formatting runs")
504 p.usage = 'output-distance.py [options] tree1 tree2'
506 p.add_option ('', '--test',
509 help='run test method')
511 (o,a) = p.parse_args ()
521 compare_trees (a[0], a[1])
523 if __name__ == '__main__':