1 | #!/usr/bin/env python |
---|
2 | |
---|
3 | ''' |
---|
4 | A program to compare two SWW files for "equality". |
---|
5 | |
---|
6 | This program makes lots of assumptions about the structure of the SWW files, |
---|
7 | so if that structure changes, this program must change. |
---|
8 | ''' |
---|
9 | |
---|
10 | import sys |
---|
11 | import os |
---|
12 | import os.path |
---|
13 | import getopt |
---|
14 | from Scientific.IO.NetCDF import NetCDFFile |
---|
15 | import numpy as num |
---|
16 | from anuga.config import netcdf_mode_r |
---|
17 | |
---|
18 | |
---|
19 | ##### |
---|
20 | # Various constants. |
---|
21 | ##### |
---|
22 | |
---|
23 | # Global attributes that should exist and be same in both files |
---|
24 | # Don't have to have all of these, and we don't care about others. |
---|
25 | expect_global_attributes = ['smoothing', 'vertices_are_stored_uniquely', |
---|
26 | 'order', 'revision_number', 'starttime', |
---|
27 | 'xllcorner', 'yllcorner', |
---|
28 | 'zone', 'false_easting', 'false_northing', |
---|
29 | 'datum', 'projection', 'units'] |
---|
30 | |
---|
31 | # dimensions expected, with expected values (None means unknown) |
---|
32 | expected_dimensions = {'number_of_volumes': None, |
---|
33 | 'number_of_vertices': 3, |
---|
34 | 'numbers_in_range': 2, |
---|
35 | 'number_of_points': None, |
---|
36 | 'number_of_timesteps': None} |
---|
37 | |
---|
38 | # Variables expected, with expected dimensions. |
---|
39 | # Don't have to have all of these, and we don't care about others. |
---|
40 | expected_variables = {'x': ('number_of_points',), |
---|
41 | 'y': ('number_of_points',), |
---|
42 | 'elevation': ('number_of_points',), |
---|
43 | 'elevation_range': ('numbers_in_range',), |
---|
44 | 'z': ('number_of_points',), |
---|
45 | 'volumes': ('number_of_volumes', 'number of vertices'), |
---|
46 | 'time': ('number_of_timesteps',), |
---|
47 | 'stage': ('numbers_in_range',), |
---|
48 | 'stage_range': ('numbers_in_range',), |
---|
49 | 'xmomentum': ('number_of_timesteps', 'number_of_points'), |
---|
50 | 'xmomentum_range': ('numbers_in_range'), |
---|
51 | 'ymomentum': ('number_of_timesteps', 'number_of_points'), |
---|
52 | 'ymomentum_range': ('numbers_in_range')} |
---|
53 | |
---|
54 | ## |
---|
55 | # @brief An exception to inform user of usage problems. |
---|
56 | class Usage(Exception): |
---|
57 | def __init__(self, msg): |
---|
58 | self.msg = msg |
---|
59 | |
---|
60 | |
---|
61 | ## |
---|
62 | # @brief Compare two SWW files. |
---|
63 | # @param files A tuple of two filenames. |
---|
64 | # @param globals A list of global attribute names to compare. |
---|
65 | # @param timesteps A list of timesteps to compare at. |
---|
66 | # @param variables A list of variable names to compare. |
---|
67 | # @return Returns if files 'equal', else raises RuntimeError. |
---|
68 | def files_are_the_same(files, globals=None, timesteps=None, variables=None): |
---|
69 | # split out the filenames and check they exist |
---|
70 | (file1, file2) = files |
---|
71 | |
---|
72 | error = False |
---|
73 | error_msg = '' |
---|
74 | |
---|
75 | try: |
---|
76 | fid1 = NetCDFFile(file1, netcdf_mode_r) |
---|
77 | except: |
---|
78 | error_msg += "File '%s' can't be opened?\n" % file1 |
---|
79 | error = True |
---|
80 | |
---|
81 | try: |
---|
82 | fid2 = NetCDFFile(file2, netcdf_mode_r) |
---|
83 | except: |
---|
84 | error_msg += "File '%s' can't be opened?\n" % file2 |
---|
85 | error = True |
---|
86 | fid1.close() |
---|
87 | |
---|
88 | if error: |
---|
89 | raise RuntimeError, error_msg |
---|
90 | |
---|
91 | ##### |
---|
92 | # First, check that files have the required structure |
---|
93 | ##### |
---|
94 | |
---|
95 | # dimensions - only check expected dimensions |
---|
96 | for key in expected_dimensions: |
---|
97 | if key not in fid1.dimensions.keys(): |
---|
98 | error_msg += ("File %s doesn't contain dimension '%s'\n" |
---|
99 | % (file1, key)) |
---|
100 | error = True |
---|
101 | if key not in fid2.dimensions.keys(): |
---|
102 | error_msg += ("File %s doesn't contain dimension '%s'\n" |
---|
103 | % (file2, key)) |
---|
104 | error = True |
---|
105 | |
---|
106 | # now check that dimensions are the same length |
---|
107 | # NOTE: DOESN'T CHECK 'UNLIMITED' DIMENSIONS YET! (get None at the moment) |
---|
108 | for dim in expected_dimensions: |
---|
109 | dim1_shape = fid1.dimensions.get(dim, None) |
---|
110 | dim2_shape = fid2.dimensions.get(dim, None) |
---|
111 | if dim1_shape and dim2_shape and dim1_shape != dim2_shape: |
---|
112 | error_msg += ('File %s has %s dimension of size %s, ' |
---|
113 | 'file %s has that dimension of size %s\n' |
---|
114 | % (file1, dim, str(dim1_shape), |
---|
115 | file2, str(dim2_shape))) |
---|
116 | error = True |
---|
117 | |
---|
118 | # check that we have the required globals |
---|
119 | if globals: |
---|
120 | for glob in globals: |
---|
121 | if glob not in dir(fid1): |
---|
122 | error_msg += ("Global attribute '%s' isn't in file %s\n" |
---|
123 | % (glob, file1)) |
---|
124 | error = True |
---|
125 | if glob not in dir(fid2): |
---|
126 | error_msg += ("Global attribute '%s' isn't in file %s\n" |
---|
127 | % (glob, file2)) |
---|
128 | error = True |
---|
129 | else: |
---|
130 | # get list of global attributes |
---|
131 | glob_vars1 = [] |
---|
132 | glob_vars2 = [] |
---|
133 | for glob in expect_global_attributes: |
---|
134 | if glob in dir(fid1): |
---|
135 | glob_vars1.append(glob) |
---|
136 | if glob in dir(fid2): |
---|
137 | glob_vars2.append(glob) |
---|
138 | |
---|
139 | # now check attribute lists are same |
---|
140 | if glob_vars1 != glob_vars2: |
---|
141 | error_msg = ('Files differ in global attributes:\n' |
---|
142 | '%s: %s,\n' |
---|
143 | '%s: %s\n' % (file1, str(glob_vars1), |
---|
144 | file2, str(glob_vars2))) |
---|
145 | error = True |
---|
146 | globals = glob_vars1 |
---|
147 | |
---|
148 | # get variables to test |
---|
149 | if variables: |
---|
150 | for var in variables: |
---|
151 | if var not in fid1.variables.keys(): |
---|
152 | error_msg += ("Variable '%s' isn't in file %s\n" |
---|
153 | % (var, file1)) |
---|
154 | error = True |
---|
155 | if var not in fid2.variables.keys(): |
---|
156 | error_msg += ("Variable '%s' isn't in file %s\n" |
---|
157 | % (var, file2)) |
---|
158 | error = True |
---|
159 | else: |
---|
160 | # check that variables are as expected in both files |
---|
161 | var_names1 = [] |
---|
162 | var_names2 = [] |
---|
163 | for var_name in expected_variables: |
---|
164 | if fid1.variables.has_key(var_name): |
---|
165 | var_names1.append(var_name) |
---|
166 | if fid2.variables.has_key(var_name): |
---|
167 | var_names2.append(var_name) |
---|
168 | |
---|
169 | if var_names1 != var_names2: |
---|
170 | error_msg += ('Variables are not the same between files: ' |
---|
171 | '%s variables= %s, ' |
---|
172 | '%s variables = %s\n' |
---|
173 | % (file1, str(var_names1), file2, str(var_names2))) |
---|
174 | error = True |
---|
175 | variables = var_names1 |
---|
176 | |
---|
177 | if error: |
---|
178 | fid1.close() |
---|
179 | fid2.close() |
---|
180 | raise RuntimeError, error_msg |
---|
181 | |
---|
182 | # get size of time dimension |
---|
183 | num_timesteps1 = fid1.variables['time'].shape |
---|
184 | num_timesteps2 = fid2.variables['time'].shape |
---|
185 | if num_timesteps1 != num_timesteps2: |
---|
186 | error_msg += ('Files have different number of timesteps: %s=%d, %s=%d\n' |
---|
187 | % (file1, num_timesteps1, file2, num_timesteps2)) |
---|
188 | error = True |
---|
189 | |
---|
190 | if error: |
---|
191 | fid1.close() |
---|
192 | fid2.close() |
---|
193 | raise RuntimeError, error_msg |
---|
194 | |
---|
195 | num_timesteps = num_timesteps1[0] |
---|
196 | |
---|
197 | # variable shapes same? |
---|
198 | for var_name in variables: |
---|
199 | var1 = fid1.variables[var_name] |
---|
200 | var2 = fid2.variables[var_name] |
---|
201 | var1_shape = var1.shape |
---|
202 | var2_shape = var2.shape |
---|
203 | if var1_shape != var2_shape: |
---|
204 | error_msg += ('Files differ in variable %s shape:\n' |
---|
205 | '%s: %s,\n' |
---|
206 | '%s: %s\n' |
---|
207 | % (var_name, file1, str(var1_shape), |
---|
208 | file2, str(var2_shape))) |
---|
209 | error = True |
---|
210 | continue |
---|
211 | |
---|
212 | if error: |
---|
213 | fid1.close() |
---|
214 | fid2.close() |
---|
215 | raise RuntimeError, error_msg |
---|
216 | |
---|
217 | ##### |
---|
218 | # Now check that actual data values are the same |
---|
219 | ##### |
---|
220 | |
---|
221 | # check values of global attributes |
---|
222 | for glob_name in globals: |
---|
223 | g1 = getattr(fid1, glob_name) |
---|
224 | g2 = getattr(fid2, glob_name) |
---|
225 | if g1 != g2: |
---|
226 | error_msg += ("Files differ in global '%s': " |
---|
227 | "%s: '%s', " |
---|
228 | "%s: '%s'\n" |
---|
229 | % (glob_name, file1, str(g1), file2, str(g2))) |
---|
230 | error = True |
---|
231 | |
---|
232 | # check data variables, be clever with time series data |
---|
233 | for var_name in variables: |
---|
234 | var_dims = expected_variables[var_name] |
---|
235 | if (len(var_dims) > 1) and (var_dims[0] == 'number_of_timesteps'): |
---|
236 | # time series, check by timestep block |
---|
237 | for i in xrange(num_timesteps): |
---|
238 | var1 = num.array(fid1.variables[var_name][i,:]) |
---|
239 | var2 = num.array(fid2.variables[var_name][i,:]) |
---|
240 | if not num.allclose(var1, var2): |
---|
241 | error_msg += ('Files differ in variable %s data:\n' |
---|
242 | '%s: %s\n' |
---|
243 | '%s: %s\n' |
---|
244 | % (glob_name, file1, str(var1), |
---|
245 | file2, str(var1))) |
---|
246 | error = True |
---|
247 | else: |
---|
248 | # simple data, check whole thing at once |
---|
249 | var1 = num.array(fid1.variables[var_name][:]) |
---|
250 | var2 = num.array(fid2.variables[var_name][:]) |
---|
251 | if not num.allclose(var1, var2): |
---|
252 | error_msg += ('Files differ in variable %s:\n' |
---|
253 | '%s: %s,\n' |
---|
254 | '%s: %s\n' |
---|
255 | % (var_name, file1, str(var1), |
---|
256 | file2, str(var2))) |
---|
257 | error = True |
---|
258 | |
---|
259 | if error: |
---|
260 | fid1.close() |
---|
261 | fid2.close() |
---|
262 | raise RuntimeError, error_msg |
---|
263 | |
---|
264 | ##### |
---|
265 | # All OK, close files and signal EQUAL |
---|
266 | ##### |
---|
267 | |
---|
268 | fid1.close() |
---|
269 | fid2.close() |
---|
270 | |
---|
271 | return |
---|
272 | |
---|
273 | |
---|
274 | ## |
---|
275 | # @brief Return a usage string. |
---|
276 | def usage(): |
---|
277 | result = [] |
---|
278 | a = result.append |
---|
279 | a('Usage: %s <options> <file1> <file2>\n' % ProgName) |
---|
280 | a('where <options> is zero or more of:\n') |
---|
281 | a(' -h print this help\n') |
---|
282 | a(" -a <val> set absolute threshold of 'equivalent'\n") |
---|
283 | a(" -r <val> set relative threshold of 'equivalent'\n") |
---|
284 | a(' -g <arg> check only global attributes specified\n') |
---|
285 | a(' <arg> has the form <globname>[,<globname>[,...]]\n') |
---|
286 | a(' -t <arg> check only timesteps specified\n') |
---|
287 | a(' <arg> has the form <starttime>[,<stoptime>[,<step>]]\n') |
---|
288 | a(' -v <arg> check only the named variables\n') |
---|
289 | a(' <arg> has the form <varname>[,<varname>[,...]]\n') |
---|
290 | a('and <file1> and <file2> are two SWW files to compare.\n') |
---|
291 | a('\n') |
---|
292 | a('The program exit status is one of:\n') |
---|
293 | a(' 0 the two files are equivalent\n') |
---|
294 | a(' else the files are not equivalent.') |
---|
295 | return ''.join(result) |
---|
296 | |
---|
297 | ## |
---|
298 | # @brief Print a message to stderr. |
---|
299 | def warn(msg): |
---|
300 | print >>sys.stderr, msg |
---|
301 | |
---|
302 | |
---|
303 | ## |
---|
304 | # @brief |
---|
305 | # @param argv |
---|
306 | # @return The status code the program will exit with. |
---|
307 | def main(argv=None): |
---|
308 | if argv is None: |
---|
309 | argv = sys.argv |
---|
310 | |
---|
311 | try: |
---|
312 | try: |
---|
313 | opts, args = getopt.getopt(argv[1:], 'hg:t:v:', |
---|
314 | ['help', 'globals', |
---|
315 | 'variables', 'timesteps']) |
---|
316 | except getopt.error, msg: |
---|
317 | raise Usage(msg) |
---|
318 | except Usage, err: |
---|
319 | print >>sys.stderr, err.msg |
---|
320 | print >>sys.stderr, "for help use --help" |
---|
321 | return 2 |
---|
322 | |
---|
323 | # process options |
---|
324 | globals = None |
---|
325 | timesteps = None |
---|
326 | variables = None |
---|
327 | for opt, arg in opts: |
---|
328 | if opt in ('-h', '--help'): |
---|
329 | print usage() |
---|
330 | sys.exit(0) |
---|
331 | elif opt in ('-g', '--globals'): |
---|
332 | globals = arg.split(',') |
---|
333 | elif opt in ('-t', '--timesteps'): |
---|
334 | timesteps = arg.split(',') |
---|
335 | elif opt in ('-v', '--variables'): |
---|
336 | variables = arg.split(',') |
---|
337 | |
---|
338 | # process arguments |
---|
339 | if len(args) != 2: |
---|
340 | msg = usage() |
---|
341 | print 'msg=%s' % msg |
---|
342 | raise Usage(msg) |
---|
343 | |
---|
344 | try: |
---|
345 | files_are_the_same(args, globals=globals, |
---|
346 | timesteps=timesteps, variables=variables) |
---|
347 | except RuntimeError, msg: |
---|
348 | print msg |
---|
349 | return 10 |
---|
350 | |
---|
351 | |
---|
352 | if __name__ == "__main__": |
---|
353 | global ProgName |
---|
354 | |
---|
355 | ProgName = os.path.basename(sys.argv[0]) |
---|
356 | |
---|
357 | sys.exit(main()) |
---|
358 | |
---|