Newer
Older
""" A module for fuzzy comparing VTK files.
This module provides methods to compare two VTK files. Applicable
for all VTK style formats like VTK files. Fuzzy compares numbers by
using absolute and/or relative difference comparison.
"""
from __future__ import absolute_import
import xml.etree.ElementTree as ET
from operator import attrgetter, itemgetter
import sys
from six.moves import range
from six.moves import zip
# fuzzy compare VTK tree from VTK strings
def compare_vtk(vtk1, vtk2, absolute=1.5e-7, relative=1e-2, zeroValueThreshold={}, verbose=True):
""" take two vtk files and compare them. Returns an exit key as returnvalue.
Arguments:
----------
vtk1, vtk2 : string
The filenames of the vtk files to compare
Keyword Arguments:
------------------
absolute : float
The epsilon used for comparing numbers with an absolute criterion
relative: float
The epsilon used for comparing numbers with an relative criterion
zeroValueThreshold: dict
A dictionary of parameter value pairs that set the threshold under
which a number is treated as zero for a certain parameter. Use this parameter if
you have to avoid comparisons of very small numbers for a certain parameter.
verbose : bool
If the script should produce informative output. Enabled by default as the details
give the tester a lot more information on why tests fail.
"""
# construct element tree from vtk file
root1 = ET.fromstring(open(vtk1).read())
root2 = ET.fromstring(open(vtk2).read())
# sort the vtk file in case nodes appear in different positions
# e.g. because of minor changes in the output code
sortedroot1 = sort_vtk(root1)
sortedroot2 = sort_vtk(root2)
if verbose:
print("Comparing {} and {}".format(vtk1, vtk2))
print("... with a maximum relative error of {} and a maximum absolute error of {}*max_abs_parameter_value.".format(relative, absolute))
# sort the vtk file so that the comparison is independent of the
# index numbering (coming e.g. from different grid managers)
sortedroot1, sortedroot2 = sort_vtk_by_coordinates(sortedroot1, sortedroot2, verbose)
# do the fuzzy compare
if is_fuzzy_equal_node(sortedroot1, sortedroot2, absolute, relative, zeroValueThreshold, verbose):
return 0
else:
return 1
# fuzzy compare of VTK nodes
def is_fuzzy_equal_node(node1, node2, absolute, relative, zeroValueThreshold, verbose):
is_equal = True
for node1child, node2child in zip(node1.iter(), node2.iter()):
if node1.tag != node2.tag:
if verbose:
print('The name of the node differs in: {} and {}'.format(node1.tag, node2.tag))
is_equal = False
else:
return False
if list(node1.attrib.items()) != list(node2.attrib.items()):
if verbose:
print('Attributes differ in node: {}'.format(node1.tag))
is_equal = False
else:
return False
if len(list(node1.iter())) != len(list(node2.iter())):
if verbose:
print('Number of children differs in node: {}'.format(node1.tag))
is_equal = False
else:
return False
if node1child.text or node2child.text:
if not is_fuzzy_equal_text(node1child.text, node2child.text,
node1child.attrib["Name"],
int(node1child.attrib["NumberOfComponents"]),
absolute, relative, zeroValueThreshold, verbose):
if node1child.attrib["Name"] == node2child.attrib["Name"]:
if verbose:
is_equal = False
else:
return False
else:
if verbose:
print('Comparing different parameters: {} and {}'.format(node1child.attrib["Name"], node2child.attrib["Name"]))
is_equal = False
else:
return False
return is_equal
# fuzzy compare of text (in the xml sense) consisting of whitespace separated numbers
def is_fuzzy_equal_text(text1, text2, parameter, numComp, absolute, relative, zeroValueThreshold, verbose):
list1 = text1.split()
list2 = text2.split()
# difference only in whitespace?
if (list1 == list2):
return True
# compare number by number
# first split the list into compononents
lists1 = []
lists2 = []
parameters = []
for i in range(0, numComp):
lists1.append(list1[i::numComp])
lists2.append(list2[i::numComp])
if numComp > 1:
parameters.append("{}_{}".format(parameter, i))
# if zero threshold was set for all components one component inherits it from the parameter
if parameter in zeroValueThreshold:
zeroValueThreshold["{}_{}".format(parameter, i)] = zeroValueThreshold[parameter]
parameters.append(parameter)
for list1, list2, parameter in zip(lists1, lists2, parameters):
# for verbose output
max_relative_difference = 0.0
message = ''
# see inspiration, explanations in
# https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/
# get the order of magnitude of the parameter by calculating the max
floatList1 = [float(i) for i in list1]
floatList2 = [float(i) for i in list2]
# manipulate the data set for the sake of sensible comparison
# if the parameter is listed in the zeroThreshold dictionary replace all float under threshold with zero.
# only replace them with zero if the parameters in both lists are under the threshold. Otherwise we
# compare a non-zero value with 0 later.
floatList1 = [0.0 if abs(i) < float(zeroValueThreshold[parameter]) and abs(j) < float(zeroValueThreshold[parameter])
else i for i, j in zip(floatList1, floatList2)]
floatList2 = [0.0 if abs(i) < float(zeroValueThreshold[parameter]) and abs(j) < float(zeroValueThreshold[parameter])
else j for i, j in zip(floatList1, floatList2)]
absFloatList1 = [abs(i) for i in floatList1]
absFloatList2 = [abs(i) for i in floatList2]
magnitude = max(max(absFloatList1), max(absFloatList2))
minimal = min(min(absFloatList1), min(absFloatList2))
for number1, number2 in zip(floatList1, floatList2):
diff = abs(number1 - number2)
largernumber = max(abs(number1), abs(number2))
# If the absolute criterion is satisfied we consider the numbers equal...
# scale the absolute tolerance with the magnitude of the parameter
if diff <= absolute * magnitude:
continue
# ...if not check the relative criterion
if diff <= largernumber * relative:
continue
else:
# the numbers are not equal
if largernumber != 0.0:
if diff / largernumber > max_relative_difference:
max_relative_difference = diff / largernumber
message = 'Difference is too large: {:.2%} -> between: {} and {}'.format(max_relative_difference, number1, number2)
if verbose and max_relative_difference != 0.0:
print('\nData differs in parameter: {}'.format(parameter))
print('Info for {}: max_abs_parameter_value={} and min_abs_parameter_value={}.'.format(parameter, magnitude, minimal))
if parameter in zeroValueThreshold:
print('For parameter {} a zero value threshold of {} was given.'.format(parameter, zeroValueThreshold[parameter]))
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
return is_equal
def sort_by_name(elem):
name = elem.get('Name')
if name:
try:
return str(name)
except ValueError:
return ''
return ''
# sorts attributes of an item and returns a sorted item
def sort_attributes(item, sorteditem):
attrkeys = sorted(item.keys())
for key in attrkeys:
sorteditem.set(key, item.get(key))
def sort_elements(items, newroot):
items = sorted(items, key=sort_by_name)
items = sorted(items, key=attrgetter('tag'))
# Once sorted, we sort each of the items
for item in items:
# Create a new item to represent the sorted version
# of the next item, and copy the tag name and contents
newitem = ET.Element(item.tag)
if item.text and item.text.isspace() == False:
newitem.text = item.text
# Copy the attributes (sorted by key) to the new item
sort_attributes(item, newitem)
# Copy the children of item (sorted) to the new item
sort_elements(list(item), newitem)
# Append this sorted item to the sorted root
newroot.append(newitem)
# has to sort all Cell and Point Data after the attribute "Name"!
def sort_vtk(root):
if(root.tag != "VTKFile"):
print('Format is not a VTKFile. Sorting will most likely fail!')
# create a new root for the sorted tree
newroot = ET.Element(root.tag)
# create the sorted copy
# (after the idea of Dale Lane's xmldiff.py)
sort_attributes(root, newroot)
sort_elements(list(root), newroot)
# return the sorted element tree
return newroot
# sorts the data by point coordinates so that it is independent of index numbering
def sort_vtk_by_coordinates(root1, root2, verbose):
if not is_fuzzy_equal_node(root1.find(".//Points/DataArray"), root2.find(".//Points/DataArray"), absolute=1e-2, relative=1.5e-7, zeroValueThreshold=dict(), verbose=False):
if verbose:
print("Sorting vtu by coordinates...")
for root in [root1, root2]:
# parse all DataArrays in a dictionary
pointDataArrays = []
cellDataArrays = []
dataArrays = {}
numberOfComponents = {}
for dataArray in root.findall(".//PointData/DataArray"):
pointDataArrays.append(dataArray.attrib["Name"])
for dataArray in root.findall(".//CellData/DataArray"):
cellDataArrays.append(dataArray.attrib["Name"])
for dataArray in root.findall(".//DataArray"):
dataArrays[dataArray.attrib["Name"]] = dataArray.text
numberOfComponents[dataArray.attrib["Name"]] = dataArray.attrib["NumberOfComponents"]
vertexArray = []
coords = dataArrays["Coordinates"].split()
# group the coordinates into coordinate tuples
dim = int(numberOfComponents["Coordinates"])
for i in range(len(coords) // dim):
vertexArray.append([float(c) for c in coords[i * dim: i * dim + dim]])
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# obtain a vertex index map
vMap = []
for idx, coords in enumerate(vertexArray):
vMap.append((idx, coords))
sortedVMap = sorted(vMap, key=itemgetter(1))
vertexIndexMap = {}
for idxNew, idxOld in enumerate(sortedVMap):
vertexIndexMap[idxOld[0]] = idxNew
# group the cells into vertex index tuples
cellArray = []
offsets = dataArrays["offsets"].split()
connectivity = dataArrays["connectivity"].split()
vertex = 0
for cellIdx, offset in enumerate(offsets):
cellArray.append([])
for v in range(vertex, int(offset)):
cellArray[cellIdx].append(int(connectivity[v]))
vertex += 1
# replace all vertex indices in the cellArray by the new indices
for cell in cellArray:
for idx, vertexIndex in enumerate(cell):
cell[idx] = vertexIndexMap[vertexIndex]
# sort all data arrays
for name, text in list(dataArrays.items()):
# split the text
items = text.split()
# convert if vector
num = int(numberOfComponents[name])
newitems = []
for i in range(len(items) // num):
newitems.append([i for i in items[i * num: i * num + num]])
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
items = newitems
# sort the items: we have either vertex or cell data
if name in pointDataArrays:
sortedItems = [j for (i, j) in sorted(zip(vertexArray, items), key=itemgetter(0))]
elif name in cellDataArrays or name == "types":
sortedItems = [j for (i, j) in sorted(zip(cellArray, items), key=itemgetter(0))]
elif name == "offsets":
sortedItems = []
counter = 0
for cell in sorted(cellArray):
counter += len(cell)
sortedItems.append([str(counter)])
elif name == "Coordinates":
sortedItems = sorted(vertexArray)
elif name == "connectivity":
sortedItems = sorted(cellArray)
# convert the sorted arrays to a xml text
dataArrays[name] = ""
for i in sortedItems:
for j in i:
dataArrays[name] += str(j) + " "
# do the replacement in the actual elements
for dataArray in root.findall(".//DataArray"):
dataArray.text = dataArrays[dataArray.attrib["Name"]]
return (root1, root2)
# main program if called as script return appropriate error codes
if __name__ == "__main__":
# handle arguments and print help message
parser = argparse.ArgumentParser(description='Fuzzy compare of two VTK\
(Visualization Toolkit) files. The files are accepted if for every\
value the difference is below the absolute error or below the\
relative error or below both.')
parser.add_argument('vtk_file_1', type=str, help='first file to compare')
parser.add_argument('vtk_file_2', type=str, help='second file to compare')
parser.add_argument('-r', '--relative', type=float, default=1e-2, help='maximum relative error (default=1e-2)')
parser.add_argument('-a', '--absolute', type=float, default=1.5e-7, help='maximum absolute error (default=1.5e-7)')
parser.add_argument('-v', '--verbose', type=bool, default=True, help='verbosity of the script')
args = vars(parser.parse_args())
sys.exit(compare_vtk(args["vtk_file_1"], args["vtk_file_2"], args["absolute"], args["relative"], args["verbose"]))