source: trunk/applications/nxdiff @ 1822

Revision 1584, 19.6 KB checked in by Peter Peterson, 14 months ago (diff)

For scalars just compare them directly. Fixes #265.

  • Property svn:executable set to *
Line 
1#!/usr/bin/env python
2VERSION = "1.4.2"
3
4import nxs
5import numpy
6import math
7import os
8import sys
9
10NAN = float("nan")
11
12class NXSdata:
13    def __init__(self, nxs, path, **kwargs):
14        nxs.openpath(path)
15        (self.dims, self.type) = nxs.getinfo()
16        if len(self.dims) == 1:
17            self.dims=self.dims[0]
18        self.attrs = nxs.getattrs()
19        self.data = nxs.getdata()
20
21def getDiffSym(diffType, format):
22    sumMap = {Diff.SAME:Diff.SAME, Diff.NEWLEFT:'-', Diff.NEWRIGHT:'+',
23              Diff.DIFF:Diff.DIFF, Diff.UNKNOWN:Diff.UNKNOWN}
24    if format == "unified":
25        return sumMap[diffType]
26    else:
27        return diffType
28
29def getPercentDiff(left, right, nandiff=float("nan")):
30    # find the difference (absolute)
31    diffs = left-right
32    combine = numpy.fabs(left) + numpy.fabs(right)
33
34    # do the percent part
35    diffs = 100. * numpy.fabs(diffs/left)
36    try:
37        diffs[0]
38        return diffs
39    except IndexError:
40        return [diffs]
41
42class Diff:
43    SAME     = " "
44    NEWLEFT  = "<"
45    NEWRIGHT = ">"
46    DIFF     = "|"
47    UNKNOWN  = "?"
48    def __init__(self, path, **kwargs):
49        self.path = path
50        try:
51            left = kwargs["left"]
52        except KeyError:
53            left = None
54        try:
55            right = kwargs["right"]
56        except KeyError:
57            right = None
58        temp = self.path.split(" ")
59        if len(temp) == 2:
60            (self.summary, self.path) = temp
61        try:
62            self.summary = kwargs["diff"]
63            if self.summary == "<" or self.summary == "-":
64                self.summary = Diff.NEWLEFT
65            elif self.summary == ">" or self.summary == "+":
66                self.summary = Diff.NEWRIGHT
67            else:
68                raise "Do not understand diff \"%s\"" % self.summary
69        except KeyError:
70            self.summary = Diff.UNKNOWN
71        try:
72            self.setFormat(kwargs["format"])
73        except KeyError:
74            self.setFormat("standard")
75        self.details = []
76        if self.summary != Diff.UNKNOWN:
77            return
78        self.__cmpData(left, right)
79        if len(self.details) > 0:
80            self.summary = Diff.DIFF
81        else:
82            self.summary = Diff.SAME
83
84    def __shortPath(self):
85        mypath= self.path.split("/")
86        mypath = [item.split(":")[0] for item in mypath]
87        return '/'.join(mypath)
88
89    def __str__(self):
90        result = "%s %s" % (getDiffSym(self.summary, self.__format), \
91                            self.__shortPath())
92        if self.summary == Diff.DIFF:
93            result += " " + " ".join(self.details)
94        return result
95
96    def __cmpData(self, left, right):
97        left = left.getData(self.path)
98        right = right.getData(self.path)
99        if left.type != right.type:
100            self.details.append("TYPE MISMATCH: %s != %s" \
101                                % (left.type, right.type))
102        if left.attrs != right.attrs:
103            self.details.append("ATTRIBUTES MISMATCH: %s != %s" % \
104                                (left.attrs, right.attrs))
105        if left.type == "CHAR" or left.type == "char" \
106               or right.type =="CHAR" or right.type == "char":
107            if left.data != right.data:
108                self.details.append("DATA MISMATCH: %s != %s" % \
109                                    (left.data, right.data))
110            return
111        if not numpy.alltrue(left.dims == right.dims):
112            self.details.append("DIMENSION MISMATCH: %s != %s" \
113                                % (left.dims, right.dims))
114            return
115        else:
116            try:
117                if left.data.dtype != right.data.dtype:
118                    self.details.append("DATA MISMATCH: %s != %s" \
119                                        % (left.data.dtype.name, \
120                                           right.data.dtype.name))
121                    return
122            except TypeError:
123                pass
124            except AttributeError:
125                pass
126
127        # special case when the array is only nans
128        if numpy.alltrue(numpy.isnan(left.data)) and \
129           numpy.alltrue(numpy.isnan(right.data)):
130            return
131
132        if numpy.alltrue(left.data == right.data):
133            return
134        else:
135            if left.data.size == 1 and right.data.size == 1:
136                self.details.append("DATA MISMATCH %s != %s" \
137                                    % (str(left.data), str(right.data)))
138                return
139            diffs = getPercentDiff(left.data, right.data)
140            if numpy.nanmax(diffs) <= 0.:
141                return
142            stats = getStats(diffs)
143            self.details.append("MISMATCH [min%s,max%s,med%s,avg%s,dev%s]" \
144                                % (stats[0], stats[1], stats[2], stats[3],
145                                   stats[4]))
146    def setFormat(self, format):
147        if format == "unified":
148            self.__format = "unified"
149        else:
150            self.__format = "standard"
151
152class NXSfile:
153    def __init__(self, name, **kwargs):
154        try:
155            self.__ignorelinks = kwargs["ignorelinks"]
156        except KeyError:
157            self.__ignorelinks = False
158        try:
159            self.__ignorenotes = kwargs["ignorenotes"]
160        except KeyError:
161            self.__ignorenotes = False
162        try:
163            self.__ignorets = kwargs["ignorets"]
164        except KeyError:
165            self.__ignorets = False
166        self.name = os.path.abspath(name)
167        self.__initPaths()
168
169    def __initPaths(self, **kwargs):
170        self.__paths = []
171        self.__nxs = nxs.open(self.name)
172        self.__paths = self.__getPaths()
173        self.__paths.sort()
174        while '/' in self.__paths:
175            del self.__paths[self.__paths.index('/')]
176
177    def __getPaths(self, **kwargs):
178        result = []
179        #result.append(self.__nxs.longpath) # add the groups to the tree
180        parent = self.__nxs.longpath.split('/')[-1]
181        listing = self.__nxs.getentries()
182        for name in listing.keys():
183            nxclass = listing[name]
184            if nxclass == "SDS":
185                if self.__ignorenotes and parent.endswith("NXnote"):
186                    if name == "author" or name == "date":
187                        continue
188                elif self.__ignorets and name == "SNStranslation_service":
189                    continue
190                self.__nxs.opendata(name)
191                attrs = self.__nxs.getattrs()
192                longpath = self.__nxs.longpath
193                shortpath = self.__nxs.path
194                self.__nxs.closedata()
195                if self.__ignorelinks and attrs.has_key("target"):
196                    target = attrs["target"]
197                    if target != shortpath:
198                        continue
199                result.append(longpath)
200            else:
201                self.__nxs.opengroup(name,nxclass)
202                result.extend(self.__getPaths())
203                self.__nxs.closegroup()
204        return result
205
206    def __str__(self):
207        return self.name
208
209    def __eq__(self, other):
210        """This provides only the simplest of cases for comparing equality"""
211        return self.name == other.name
212
213    def __getMissing(self, left, right):
214        result = filter(lambda a, right=right:not a in right, left)
215        return result
216
217    def __removeDuplicate(self, item, array):
218        if item not in array:
219            return
220        index = array.index(item)
221        try:
222            while True:
223                index = array.index(item, index+1)
224                del array[index]
225        except ValueError:
226            pass
227       
228    def cmpPaths(self, other, **kwargs):
229        if self.__paths == other.__paths:
230            diffPaths = self.__paths[:]
231        else:
232            # find what one has that the other is missing
233            missingOther = self.__getMissing(self.__paths, other.__paths)
234            missingSelf = self.__getMissing(other.__paths, self.__paths)
235
236            # merge the results
237            diffPaths = self.__paths[:]
238            diffPaths.extend(other.__paths)
239            diffPaths.sort()
240            for path in diffPaths:
241                self.__removeDuplicate(path, diffPaths)
242            for path in missingSelf:
243                index = diffPaths.index(path)
244                diffPaths[index] = "> %s" % diffPaths[index]
245            for path in missingOther:
246                index = diffPaths.index(path)
247                diffPaths[index] = "< %s" % diffPaths[index]
248
249        result = []
250        for path in diffPaths:
251            if path.startswith("<") or path.startswith(">"):
252                result.append(Diff(path, diff=path[0]))
253            else:
254                result.append(Diff(path, left=self, right=other))
255
256        return result
257
258    def getData(self, path, **kwargs):
259        return NXSdata(self.__nxs, path)
260
261def isNaN(x):
262    if x * 1.0 < x:
263        return True
264    return (x == 1.0) and (x == 2.0)
265
266def removeNaN(array):
267    hasnans = numpy.isnan(array)
268    indices = numpy.where(hasnans == True)[0]
269    if len(indices) <= 0:
270        return array
271    return numpy.delete(array, indices.tolist())
272    try:
273        len(myarray)
274        return myarray
275    except TypeError:
276        return [myarray]
277
278def getStats(array, **kwargs):
279    myarray = numpy.copy(array)
280    myarray = myarray.ravel()
281    origLength = myarray.size
282    myarray = removeNaN(myarray)
283    myarray.sort()
284    length = myarray.size
285    if length ==0 and length < origLength:
286        avg = NAN
287        minimum = NAN
288        maximum  = NAN
289        median = NAN
290        stddev = NAN
291    else:
292        avg = numpy.average(myarray)
293        minimum = numpy.nanmin(myarray)
294        maximum = numpy.nanmax(myarray)
295        median = myarray[(length/2)-1]
296        stddev = myarray.std()
297    result = map(lambda x: "%.2f%%" % x,
298                 (minimum, maximum, median, avg, stddev))
299    result.append(origLength) # add the number of elements
300    result.append(origLength - length) # number of NaNs found
301    return result
302
303def cmpData(left, right, path, **kwargs):
304    left = left.getData(path)
305    right = right.getData(path)
306    if left.type != right.type:
307        return "TYPE MISMATCH: %s != %s" % (left.type, right.type)
308    if left.dims != right.dims:
309        return "DIMENSION MISMATCH: %s != %s" % (left.dims, right.dims)
310    if left.attrs != right.attrs:
311        return "ATTRIBUTES MISMATCH: %s != %s" % (left.attrs, right.attrs)
312    if left.type == "CHAR":
313        if left.data == right.data:
314            return ""
315        else:
316            return "DATA MISMATCH: %s != %s" % (left.data, right.data)
317    if utils.vector_is_equals(left.data, right.data):
318        return ""
319    else:
320        diffs = []
321        import math
322        for i in range(len(left.data)):
323            diffs.append(math.fabs((left.data[i]-right.data[i])/left.data[i]))
324        stats = getStats(diffs)
325        return "MISMATCH [min%s,max%s,med%s,avg%s,dev%s]" \
326               % (stats[0], stats[1], stats[2], stats[3], stats[4])
327
328def delinearIndex(linear, dims):
329    length = len(dims)
330    if length == 1:
331        return linear
332    elif length == 2:
333        index = [0,0]
334        index[1] = linear % dims[1]
335        index[0] = (linear - index[1])/dims[1]
336        return tuple(index)
337    else:
338        raise "Do not know how to deal with dimension " + length
339
340def printDataDiff(left, right, diff, **kwargs):
341    # determine the symbols for right and left
342    try:
343        format = kwargs["format"]
344    except KeyError:
345        format = "standard"
346    leftSym = getDiffSym(Diff.NEWLEFT, format)
347    rightSym = getDiffSym(Diff.NEWRIGHT, format)
348
349    # get the data
350    left = left.getData(diff.path)
351    right = right.getData(diff.path)
352
353    # the dimensions of the data
354    leftLength = 1
355    try:
356        for dim in left.dims:
357            leftLength *= dim
358    except TypeError:
359        leftLength *= left.dims
360    rightLength = 1
361    try:
362        for dim in right.dims:
363            rightLength *= dim
364    except TypeError:
365        rightLength *= right.dims
366
367    # the threshold for printing values
368    try:
369        threshold = kwargs["threshold"]
370    except KeyError:
371        threshold = None
372
373    try:
374        shownandiffs = kwargs["shownandiffs"]
375    except KeyError:
376        shownandiffs = False
377
378    # how many items to show
379    if not threshold:
380        try:
381            numItems = kwargs["numitems"]
382        except KeyError:
383            numItems = 0
384        if numItems < 0:
385            pass
386        elif numItems == 0:
387            leftLength = 10
388            rightLength = 10
389        else:
390            leftLength = numItems
391            rightLength = numItems
392
393
394    # print out type and dims
395    if (left.type == right.type) and (left.dims == right.dims):
396        print left.type, left.dims
397    else:
398        print leftSym, left.type, left.dims
399        print rightSym, right.type, right.dims
400
401    if left.attrs == right.attrs:
402        print left.attrs
403    else:
404        print leftSym, left.attrs
405        print rightSym, right.attrs
406
407    if threshold is None:
408        try:
409            print leftSym, left.data.__str__(last=leftLength)
410        except TypeError:
411            print str(left.data)
412        try:
413            print rightSym, right.data.__str__(last=rightLength)
414        except TypeError:
415            print str(right.data)
416    else:
417        nanIndices = []
418        changeInfo = []
419        length = min(leftLength, rightLength)
420        myDiff = getPercentDiff(left.data, right.data)
421        myDiff.ravel()
422        for i in xrange(myDiff.size):
423            index = delinearIndex(i, left.dims)
424            if isNaN(myDiff):
425                nanIndices.append(index)
426            elif myDiff > threshold:
427                changeInfo.append((index, myDiff, left.data[i], right.data[i]))
428        print "%d values changed between number and NaN" % len(nanIndices)
429        print "%d values changed more than %.2f%%" % (len(changeInfo),
430                                                    threshold)
431        if shownandiffs and len(nanIndices) > 0:
432            print "Indices changed between number and NaN:", nanIndices
433        if len(changeInfo) > 0:
434            print "%10s  %5s  %10s  %10s" % ("index", "%diff", "left", "right")
435            for item in changeInfo:
436                print "%10s  %5.2f  %10f  %10f" % item
437
438if __name__ == "__main__":
439    import optparse
440
441    info = []
442    info.append("This utility compares two files that are readable by the ")
443    info.append("NeXus API.")
444
445    info.append("In the difference the '<' or '-' symbol means that the ")
446    info.append("field exists in the left file but not the right.")
447    info.append("'>' or '+' symbol means that the field exists in the right ")
448    info.append("file but not the left.")
449    info.append("'|' symbol means that the field has changed between the ")
450    info.append("two files.")
451
452    parser = optparse.OptionParser("usage %prog [options] <left> <right>",
453                                   None, optparse.Option, VERSION, 'error',
454                                   " ".join(info))
455    parser.add_option("-v", "--verbose", action="count", dest="verbose",
456                      help="Enable verbose print statements", default=0)
457    parser.add_option("-q", "--brief", action="store_true", dest="quiet",
458                      help="Disable verbose print statements")
459    parser.add_option("-u", "", action="store_true", dest="unifieddiff",
460                      help="Use the unified output format.")
461    parser.add_option("-s", "--report-identical-files", action="store_true",
462                      dest="reportidenticalfiles",
463                      help="Report when two files are the same.")
464    parser.add_option("-S", "--suppress-common-lines", action="store_true",
465                      dest="suppresscommon", help="Do not output common lines")
466    parser.add_option("", "--show-values", action="store_true",
467                      dest="showvalues",
468                      help="Show the values of arrays that do not match")
469    parser.add_option("", "--num-values", dest="numvalues",
470                      help="Set the number of values to show in "
471                      + "\"--show-values\" mode. If not specified the default "
472                      + "is ten (10). To show all values specify minus one "
473                      + "(-1).")
474    parser.add_option("", "--show-percent", dest="threshold",
475                      help="Set a threshold for the minimum percentage "
476                      + "difference shown in values. This turns on "
477                      + "\"--show-values\" mode and overrides "
478                      + "\"--num-values\".")
479    parser.add_option("", "--show-nan-diffs", dest="shownandiffs",
480                      action="store_true",
481                      help="Whether or not to show the indices that changed "
482                      + "to nan when using \"--show-percent\" mode.")
483    parser.add_option("-L", "--ignore-links", dest="ignorelinks",
484                      action="store_true",
485                      help="Only compare the original copy of links")
486    parser.add_option("", "--ignore-note-meta", dest="ignorenotes",
487                      action="store_true",
488                      help="Ignore the author and date fields in notes")
489    parser.add_option("", "--ignore-ts", dest="ignorets", action="store_true",
490                      help="Ignore differences in the version of ts used")
491                     
492    parser.set_defaults(verbose=False)
493    parser.set_defaults(ignorelinks=False)
494    parser.set_defaults(numvalues=None)
495    parser.set_defaults(threshold=None)
496    parser.set_defaults(shownandiffs=False)
497    parser.set_defaults(ignorenotes=False)
498    parser.set_defaults(ignorets=False)
499
500    # parse and fix values
501    (options, args) = parser.parse_args()
502    if options.quiet:
503        options.verbose = -1
504    if options.unifieddiff:
505        format = "unified"
506    else:
507        format = "standard"
508    if options.numvalues is not None:
509        options.showvalues = True
510        options.numvalues = int(options.numvalues)
511    else:
512        options.numvalues = 0
513    if options.threshold is not None:
514        options.showvalues = True
515        options.threshold = float(options.threshold)
516    if len(args) != 2:
517        parser.error("Must compare two files")
518    else:
519        left = NXSfile(args[0], ignorelinks=options.ignorelinks,
520                       ignorenotes=options.ignorenotes,
521                       ignorets=options.ignorets)
522        right = NXSfile(args[1], ignorelinks=options.ignorelinks,
523                        ignorenotes=options.ignorenotes,
524                        ignorets=options.ignorets)
525
526    # direct comparison of the two files
527    if left == right:
528        if options.reportidenticalfiles:
529            print "Files %s and %s are identical" % (left, right)
530        sys.exit()
531
532    # create the full diff
533    diffs = left.cmpPaths(right)
534
535    # determine if there were differences
536    different = False
537    for diff in diffs:
538        if len(diff.summary) > 0:
539            different = True
540
541    # take the easy way out if quiet or identical
542    if not different:
543        if options.reportidenticalfiles:
544            print "Files %s and %s are identical" % (left, right)
545        sys.exit()
546    elif options.verbose == -1:
547        print "Files %s and %s differ" % (left, right)
548        sys.exit()
549
550    # remove common lines if requestsed
551    if options.suppresscommon:
552        indices = range(len(diffs))
553        indices.reverse()
554        for i in indices:
555            if diffs[i].summary == Diff.SAME:
556                del diffs[i]
557
558    # reformat the diff
559    for diff in diffs:
560        diff.setFormat(format)
561
562    # print out the result
563    for diff in diffs:
564        print diff
565        if options.showvalues and diff.summary == Diff.DIFF:
566            printDataDiff(left, right, diff, format=format,
567                          numitems=options.numvalues,
568                          threshold=options.threshold,
569                          shownandiffs=options.shownandiffs)
Note: See TracBrowser for help on using the repository browser.