12 OUTPUT_EXPRESSION_PENALTY
= 100
13 ORPHAN_GROB_PENALTY
= 1000
15 def max_distance (x1
, x2
):
18 for (p
,q
) in zip (x1
, x2
):
19 dist
= max (abs (p
-q
), dist
)
24 empty_interval
= (INFTY
, -INFTY
)
25 empty_bbox
= (empty_interval
, empty_interval
)
27 def interval_length (i
):
28 return max (i
[1]-i
[0], 0)
30 def interval_union (i1
, i2
):
31 return (min (i1
[0], i2
[0]),
34 def interval_intersect (i1
, i2
):
35 return (max (i1
[0], i2
[0]),
38 def bbox_union (b1
, b2
):
39 return (interval_union (b1
[X_AXIS
], b2
[X_AXIS
]),
40 interval_union (b2
[Y_AXIS
], b2
[Y_AXIS
]))
42 def bbox_intersection (b1
, b2
):
43 return (interval_intersect (b1
[X_AXIS
], b2
[X_AXIS
]),
44 interval_intersect (b2
[Y_AXIS
], b2
[Y_AXIS
]))
47 return interval_length (b
[X_AXIS
]) * interval_length (b
[Y_AXIS
])
49 def bbox_diameter (b
):
50 return max (interval_length (b
[X_AXIS
]),
51 interval_length (b
[Y_AXIS
]))
54 def difference_area (a
, b
):
55 return bbox_area (a
) - bbox_area (bbox_intersection (a
,b
))
58 def __init__ (self
, exp_list
):
59 (self
.name
, self
.origin
, bbox_x
,
60 bbox_y
, self
.output_expression
) = tuple (exp_list
)
62 self
.bbox
= (bbox_x
, bbox_y
)
63 self
.centroid
= (bbox_x
[0] + bbox_x
[1], bbox_y
[0] + bbox_y
[1])
66 return '%s: (%.2f,%.2f), (%.2f,%.2f)\n' % (self
.name
,
72 def axis_centroid (self
, axis
):
73 return apply (sum, self
.bbox
[axis
]) / 2
75 def centroid_distance (self
, other
, scale
):
76 return max_distance (self
.centroid
, other
.centroid
) / scale
78 def bbox_distance (self
, other
):
79 divisor
= bbox_area (self
.bbox
) + bbox_area (other
.bbox
)
82 return (difference_area (self
.bbox
, other
.bbox
) +
83 difference_area (other
.bbox
, self
.bbox
)) / divisor
87 def expression_distance (self
, other
):
88 if self
.output_expression
== other
.output_expression
:
91 return OUTPUT_EXPRESSION_PENALTY
93 def distance(self
, other
, max_distance
):
94 return (self
.expression_distance (other
)
95 + self
.centroid_distance (other
, max_distance
)
96 + self
.bbox_distance (other
))
98 class SystemSignature
:
99 def __init__ (self
, grob_sigs
):
102 val
= d
.setdefault (g
.name
, [])
106 self
.set_all_bbox (grob_sigs
)
108 def set_all_bbox (self
, grobs
):
109 self
.bbox
= empty_bbox
111 self
.bbox
= bbox_union (g
.bbox
, self
.bbox
)
113 def closest (self
, grob_name
, centroid
):
117 grobs
= self
.grob_dict
[grob_name
]
120 d
= max_distance (g
.centroid
, centroid
)
131 return reduce (lambda x
,y
: x
+y
, self
.grob_dict
.values(), [])
134 def __init__ (self
, system1
, system2
):
135 self
.system1
= system1
136 self
.system2
= system2
138 self
.link_list_dict
= {}
139 self
.back_link_dict
= {}
141 for g
in system1
.grobs ():
142 closest
= system2
.closest (g
.name
, g
.centroid
)
144 self
.link_list_dict
.setdefault (closest
, [])
145 self
.link_list_dict
[closest
].append (g
)
146 self
.back_link_dict
[g
] = closest
151 scale
= max (bbox_diameter (self
.system1
.bbox
),
152 bbox_diameter (self
.system2
.bbox
))
154 for (g1
,g2
) in self
.back_link_dict
.items ():
156 d
+= ORPHAN_GROB_PENALTY
158 d
+= g1
.distance (g2
, scale
)
160 for (g1
,g2s
) in self
.link_list_dict
.items ():
162 d
+= ORPHAN_GROB_PENALTY
166 ################################################################
173 def read_signature_file (name
):
174 print 'reading', name
175 exp_str
= ("[%s]" % open (name
).read ())
176 entries
= safeeval
.safe_eval (exp_str
)
178 grob_sigs
= [GrobSignature (e
) for e
in entries
]
179 sig
= SystemSignature (grob_sigs
)
183 def compare_signature_files (f1
, f2
):
184 s1
= read_signature_file (f1
)
185 s2
= read_signature_file (f2
)
187 return SystemLink (s1
, s2
).distance ()
189 def paired_files (dir1
, dir2
, pattern
):
191 Search DIR1 and DIR2 for PATTERN.
193 Return (PAIRED, MISSING-FROM-2, MISSING-FROM-1)
197 files1
= dict ((os
.path
.split (f
)[1], 1) for f
in glob
.glob (dir1
+ '/' + pattern
))
198 files2
= dict ((os
.path
.split (f
)[1], 1) for f
in glob
.glob (dir2
+ '/' + pattern
))
202 for f
in files1
.keys ():
209 return (pairs
, files2
.keys (), missing
)
211 class ComparisonData
:
213 self
.result_dict
= {}
217 def compare_trees (self
, dir1
, dir2
):
218 self
.compare_directories (dir1
, dir2
)
220 (root
, files
, dirs
) = os
.walk (dir1
).next ()
222 d1
= os
.path
.join (dir1
, d
)
223 d2
= os
.path
.join (dir2
, d
)
225 if os
.path
.isdir (d2
):
226 self
.compare_trees (d1
, d2
)
228 def compare_directories (self
, dir1
, dir2
):
230 (paired
, m1
, m2
) = paired_files (dir1
, dir2
, '*.signature')
232 self
.missing
+= [(dir1
, m
) for m
in m1
]
233 self
.added
+= [(dir2
, m
) for m
in m2
]
238 distance
= compare_signature_files (f1
, f2
)
239 self
.result_dict
[f2
] = (distance
, f1
)
241 def create_text_result_page (self
, dir1
, dir2
):
242 self
.write_text_result_page (dir2
+ '/' + os
.path
.split (dir1
)[1] + '.txt')
244 def write_text_result_page (self
, filename
):
245 print 'writing "%s"' % filename
250 out
= open (filename
, 'w')
252 results
= [(score
, oldfile
, file) for (file, (score
, oldfile
)) in self
.result_dict
.items ()]
256 for (s
, oldfile
, f
) in results
:
257 out
.write ('%-30f %-20s\n' % (s
, f
))
259 for (dir, file) in self
.missing
:
260 out
.write ('%10s%-20s %s\n' % ('', 'missing',os
.path
.join (dir, file)))
261 for (dir, file) in self
.added
:
262 out
.write ('%20s%-10s %s\n' % ('','added', os
.path
.join (dir, file)))
264 def print_results (self
):
265 self
.write_text_result_page ('')
267 def create_html_result_page (self
, dir1
, dir2
):
268 dir1
= dir1
.replace ('//', '/')
269 dir2
= dir2
.replace ('//', '/')
273 results
= [(score
, oldfile
, file) for (file, (score
, oldfile
)) in self
.result_dict
.items ()
274 if score
> threshold
]
280 old_prefix
= os
.path
.split (dir1
)[1]
281 os
.mkdir (dir2
+ '/' + old_prefix
)
282 for (score
, oldfile
, newfile
) in results
:
283 old_base
= re
.sub ("-[0-9]+.signature", '', os
.path
.split (oldfile
)[1])
284 new_base
= re
.sub ("-[0-9]+.signature", '', newfile
)
286 for ext
in 'png', 'ly':
287 shutil
.copy2 (old_base
+ '.' + ext
, dir2
+ '/' + old_prefix
)
289 img_1
= os
.path
.join (old_prefix
, old_base
+ '.png')
290 ly_1
= os
.path
.join (old_prefix
, old_base
+ '.ly')
292 img_2
= new_base
.replace (dir2
, '') + '.png'
293 img_2
= re
.sub ("^/*", '', img_2
)
295 ly_2
= img_2
.replace ('.png','.ly')
297 def img_cell (ly
, img
):
301 <img src="%(img)s" style="border-style: none; max-width: 500px;">
303 <font size="-2">(<a href="%(ly)s">source</a>)
317 ''' % (score
, img_cell (ly_1
, img_1
), img_cell (ly_2
, img_2
))
331 </html>''' % locals()
333 open (os
.path
.join (dir2
, old_prefix
) + '.html', 'w').write (html
)
337 def compare_trees (dir1
, dir2
):
338 data
= ComparisonData ()
339 data
.compare_trees (dir1
, dir2
)
340 data
.print_results ()
341 data
.create_html_result_page (dir1
, dir2
)
342 data
.create_text_result_page (dir1
, dir2
)
344 ################################################################
355 def test_paired_files ():
356 print paired_files (os
.environ
["HOME"] + "/src/lilypond/scripts/",
357 os
.environ
["HOME"] + "/src/lilypond-stable/buildscripts/", '*.py')
360 def test_compare_trees ():
361 system ('rm -rf dir1 dir2')
362 system ('mkdir dir1 dir2')
363 system ('cp 20{-0.signature,.ly,.png} dir1')
364 system ('cp 20{-0.signature,.ly,.png} dir2')
365 system ('cp 20expr{-0.signature,.ly,.png} dir1')
366 system ('cp 19{-0.signature,.ly,.png} dir2/')
367 system ('cp 19{-0.signature,.ly,.png} dir1/')
368 system ('cp 20grob{-0.signature,.ly,.png} dir2/')
370 ## introduce difference
371 system ('cp 19-0.signature dir2/20-0.signature')
373 compare_trees ('dir1', 'dir2')
376 def test_basic_compare ():
377 ly_template
= r
"""#(set! toplevel-score-handler print-score-with-defaults)
378 #(set! toplevel-music-handler
380 (if (not (eq? (ly:music-property m 'void) #t))
381 (print-score-with-defaults
382 p (scorify-music m p)))))
387 c^"%(userstring)s" %(extragrob)s
391 dicts
= [{ 'papermod' : '',
394 'userstring': 'test' },
395 { 'papermod' : '#(set-global-staff-size 19.5)',
398 'userstring': 'test' },
402 'userstring': 'blabla' },
406 'userstring': 'test' }]
409 open (d
['name'] + '.ly','w').write (ly_template
% d
)
411 names
= [d
['name'] for d
in dicts
]
413 system ('lilypond -ddump-signatures --png -b eps ' + ' '.join (names
))
415 sigs
= dict ((n
, read_signature_file ('%s-0.signature' % n
)) for n
in names
)
417 for (n1
, s1
) in sigs
.items():
418 for (n2
, s2
) in sigs
.items():
419 combinations
['%s-%s' % (n1
, n2
)] = SystemLink (s1
,s2
).distance ()
421 results
= combinations
.items ()
426 assert combinations
['20-20'] == 0.0
427 assert combinations
['20-20expr'] > 50.0
428 assert combinations
['20-19'] < 10.0
432 sa
= read_signature_file (a
)
433 sb
= read_signature_file (b
)
434 link
= SystemLink (sa
, sb
)
435 print link
.distance()
440 dir = 'output-distance-test'
442 print 'test results in ', dir
444 system ('rm -rf ' + dir)
445 system ('mkdir ' + dir)
449 test_basic_compare ()
450 test_compare_trees ()
452 ################################################################
456 p
= optparse
.OptionParser ("output-distance - compare LilyPond formatting runs")
457 p
.usage
= 'output-distance.py [options] tree1 tree2'
459 p
.add_option ('', '--test',
462 help='run test method')
464 (o
,a
) = p
.parse_args ()
474 compare_trees (a
[0], a
[1])
476 if __name__
== '__main__':