|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +"""Module contains class definitions related to dealing with gauge data""" |
| 4 | + |
| 5 | +from __future__ import print_function |
| 6 | + |
| 7 | + |
| 8 | +import os |
| 9 | +import sys |
| 10 | + |
| 11 | +import numpy |
| 12 | + |
| 13 | +# See if pandas is available |
| 14 | +try: |
| 15 | + import pandas |
| 16 | + pandas_available = True |
| 17 | +except ImportError as e: |
| 18 | + pandas_available = False |
| 19 | + |
| 20 | + |
| 21 | +class GaugeSolution(object): |
| 22 | + r"""Object representing a numerical gauge station |
| 23 | +
|
| 24 | + The `GaugeSolution` class provides a generic means of reading and |
| 25 | + interacting with gauge data output from a run. A header describing the |
| 26 | + contents of the data is read in and |
| 27 | +
|
| 28 | + :Initialization: |
| 29 | +
|
| 30 | + There are a few ways to create a `GaugeSolution`: |
| 31 | + 1. Provide a `gauge_id` and a `path` to find that gauge. This will |
| 32 | + initialize a gauge with the data at `path` for the gauge with gauge |
| 33 | + id `gauge_id`. Note that `path` will default to the current |
| 34 | + working directory provided by `os.getcwd()` if `path` is not |
| 35 | + provided. |
| 36 | + 2. Create an empty gauge by providing neither `gauge_id` or `path`. |
| 37 | +
|
| 38 | + Additionally the parameter `use_pandas` can be provided which determines |
| 39 | + how the gauge's data is stored. By default this is `False` and the |
| 40 | + `GaugeSolution` will use a `numpy` ndarray. |
| 41 | + """ |
| 42 | + |
| 43 | + |
| 44 | + def __init__(self, gauge_id=None, path=None, use_pandas=False): |
| 45 | + r"""GaugeSolution Initiatlization Routine |
| 46 | +
|
| 47 | + See :class:`GaugeSolution` for more info. |
| 48 | + """ |
| 49 | + |
| 50 | + # Gauge descriptors |
| 51 | + self.id = None |
| 52 | + r"""(int) - Gauge id of this gauge. Also known as gauge number""" |
| 53 | + self.location = None |
| 54 | + r"""(tuple) - Location of the gauge recorded in the header""" |
| 55 | + |
| 56 | + # Gauge data |
| 57 | + self.level = None |
| 58 | + r"""(ndarray(:) - int) - The level the given observation used""" |
| 59 | + self.t = None |
| 60 | + r"""(ndarray(:) - float) - The time the given observation was made at""" |
| 61 | + self.q = None |
| 62 | + r"""(ndarray(num_eqn, :) - float) - Observed data""" |
| 63 | + |
| 64 | + # Read in gauge data from file |
| 65 | + if gauge_id is not None: |
| 66 | + if path is None: |
| 67 | + path = os.getcwd() |
| 68 | + self.read(gauge_id, path, use_pandas) |
| 69 | + |
| 70 | + |
| 71 | + def read(self, gauge_id, path=None, use_pandas=False): |
| 72 | + r"""Read the gauge file at path into this object |
| 73 | +
|
| 74 | + Read in the gauge with gauge id `gauge_id` located at `path`. If |
| 75 | + `use_pandas` is `True` then `q` will be a Pandas frame. |
| 76 | +
|
| 77 | + :Input: |
| 78 | + - *gauge_id* - (int) Gauge id to be read |
| 79 | + - *path* - (path) Path to directory containing the gauge data. |
| 80 | + Defaults to `path = os.getcwd()`. |
| 81 | + - *use_pandas* - (bool) If True then `q` will be a Pandas frame, |
| 82 | + otherwise it will be a `numpy.ndarary`. Default to `False`. |
| 83 | +
|
| 84 | + :Output: |
| 85 | + None |
| 86 | + """ |
| 87 | + |
| 88 | + # Construct path to gauge |
| 89 | + if path is None: |
| 90 | + path = os.getcwd() |
| 91 | + gauge_file_name = "gauge%s.txt" % str(gauge_id).zfill(5) |
| 92 | + gauge_path = os.path.join(path, gauge_file_name) |
| 93 | + |
| 94 | + # Read header info |
| 95 | + with open(gauge_path, 'r') as gauge_file: |
| 96 | + # First line |
| 97 | + data = gauge_file.readline().split() |
| 98 | + self.id = int(data[2]) |
| 99 | + if len(data) == 9: |
| 100 | + # 2d |
| 101 | + self.location = (float(data[4]), float(data[5])) |
| 102 | + num_eqn = int(data[8]) |
| 103 | + elif len(data) == 10: |
| 104 | + # 3d |
| 105 | + self.location = (float(data[4]), float(data[5]), float(data[6])) |
| 106 | + num_eqn = int(data[9]) |
| 107 | + |
| 108 | + # Read in one more line to check to make sure there's actually data |
| 109 | + # in here |
| 110 | + gauge_file.readline() |
| 111 | + if len(gauge_file.readline()) == 0: |
| 112 | + import warnings |
| 113 | + warnings.warn("Gauge file %s is empty." % gauge_id) |
| 114 | + return |
| 115 | + |
| 116 | + # Check to see if the gauge file name ID and that inside of the gauge |
| 117 | + # file are the same |
| 118 | + gauge_file_name = os.path.basename(path) |
| 119 | + if self.id != gauge_id: |
| 120 | + raise ValueError("Gauge ID requested does not match ID inside ", |
| 121 | + "file!") |
| 122 | + |
| 123 | + # Read gauge data |
| 124 | + if use_pandas: |
| 125 | + if not pandas_available: |
| 126 | + raise ImportError("Pandas not available.") |
| 127 | + |
| 128 | + raise NotImplementedError("Pandas data backend not implemented yet.") |
| 129 | + self.q = pandas.DataFrame() |
| 130 | + else: |
| 131 | + data = numpy.loadtxt(gauge_path, comments="#") |
| 132 | + self.level = data[:, 0].astype(numpy.int64) |
| 133 | + self.t = data[:, 1] |
| 134 | + self.q = data[:, 2:].transpose() |
| 135 | + |
| 136 | + if num_eqn != self.q.shape[0]: |
| 137 | + raise ValueError("Number of fields in gauge file does not match", |
| 138 | + "recorded number in header.") |
| 139 | + |
| 140 | + |
| 141 | + def write(self, path=None): |
| 142 | + r"""Write the data from this gauge to a file in `path` |
| 143 | +
|
| 144 | + :Input: |
| 145 | + - *path* - (path) Path to write the gauge file to. Defaults to |
| 146 | + `path = os.getcwd()`. |
| 147 | +
|
| 148 | + :Output: |
| 149 | + None |
| 150 | + """ |
| 151 | + |
| 152 | + if path is None: |
| 153 | + path = os.getcwd() |
| 154 | + |
| 155 | + if not self.is_valid(): |
| 156 | + raise ValueError("Gauge is not initialized properly.") |
| 157 | + |
| 158 | + gauge_file_name = "gauge%s.txt" % str(self.id).zfill(5) |
| 159 | + with open(os.path.join(path, gauge_file_name), "w") as gauge_file: |
| 160 | + |
| 161 | + gauge_file.write("# gauge_id= %s location=( %s %s ) num_eqn= %s\n" % |
| 162 | + (self.id, self.location[0], self.location[1], self.q.shape[0])) |
| 163 | + gauge_file.write("# Columns: level time q(1 ... num_eqn)\n") |
| 164 | + |
| 165 | + # print(self.q.shape) |
| 166 | + for i in xrange(self.q.shape[1]): |
| 167 | + gauge_file.write("%02i %+.15e " % (self.level[i], self.t[i])) |
| 168 | + gauge_file.write(" ".join(["%+.15e" % value for value in self.q[:, i]])) |
| 169 | + gauge_file.write("\n") |
| 170 | + # gauge_file.write("{} {}\n".format((self.level[i], self.t[i]))) |
| 171 | + |
| 172 | + |
| 173 | + def is_valid(self): |
| 174 | + r"""Check to see if the gauge data has all been set. |
| 175 | +
|
| 176 | + Simply checks to see if all attributes are not None. |
| 177 | +
|
| 178 | + :Output: |
| 179 | + - (bool) Whether the gauge data has been set. |
| 180 | + """ |
| 181 | + |
| 182 | + if ((self.id is not None) and (self.location is not None) and |
| 183 | + (self.level is not None) and (self.t is not None) and |
| 184 | + (self.q is not None)): |
| 185 | + |
| 186 | + return True |
| 187 | + |
| 188 | + return False |
| 189 | + |
| 190 | + |
| 191 | + def __repr__(self): |
| 192 | + if self.is_valid(): |
| 193 | + output = "%4i" % self.id |
| 194 | + for j in range(len(self.location)): |
| 195 | + output = " ".join((output,"%17.10e" % self.location[j])) |
| 196 | + output = " ".join((output,"%13.6e" % self.t[0])) |
| 197 | + output = " ".join((output,"%13.6e\n" % self.t[-1])) |
| 198 | + else: |
| 199 | + output = None |
| 200 | + |
| 201 | + return output |
| 202 | + |
| 203 | + |
| 204 | + def __str__(self): |
| 205 | + return ("Gauge %s: location = %s, t = [%s, %s]" % |
| 206 | + (self.id, self.location, |
| 207 | + self.t[0], self.t[-1])) |
| 208 | + |
| 209 | + |
| 210 | + |
| 211 | +def convert_gauges(path, output_path=None): |
| 212 | + r"""Convert gauge output data from fort.gauge file to new format |
| 213 | +
|
| 214 | + This is provided for data files that were in the old format and will split |
| 215 | + up the data in the old `fort.gauge` files into separate files for each |
| 216 | + gauge. |
| 217 | +
|
| 218 | + :Input: |
| 219 | + - *path* - (path) Path to old file. |
| 220 | + - *output_path* - (path) Path to directory the new gauge files will be |
| 221 | + placed. Defaults to `output_path = os.getcwd()`. |
| 222 | + """ |
| 223 | + |
| 224 | + if output_path is None: |
| 225 | + output_path = os.getcwd() |
| 226 | + |
| 227 | + # Load old data |
| 228 | + data = numpy.loadtxt(path) |
| 229 | + old_ids = numpy.asarray(data[:, 0], dtype=int) |
| 230 | + unique_ids = numpy.asarray(list(set(old_ids))) |
| 231 | + |
| 232 | + # Create new gauges and compare |
| 233 | + for gauge_id in unique_ids: |
| 234 | + gauge_indices = numpy.nonzero(old_ids == gauge_id)[0] |
| 235 | + new_gauge = GaugeSolution() |
| 236 | + new_gauge.id = gauge_id |
| 237 | + new_gauge.location = (numpy.nan, numpy.nan) |
| 238 | + new_gauge.level = numpy.asarray(data[gauge_indices, 1], dtype=int) |
| 239 | + new_gauge.t = data[gauge_indices, 2] |
| 240 | + new_gauge.q = data[gauge_indices, 3:].transpose() |
| 241 | + new_gauge.write(output_path) |
| 242 | + |
| 243 | + # Test gauge data |
| 244 | + if not compare_gauges(path, output_path, gauge_id): |
| 245 | + print("WARNING: New gauge data does not match old gauge data.") |
| 246 | + print(" gauge id = %s" % gauge_id) |
| 247 | + |
| 248 | + |
| 249 | + |
| 250 | +def compare_gauges(old_path, new_path, gauge_id, plot=False, abs_tol=1e-14, |
| 251 | + rel_tol=0.0, verbose=False): |
| 252 | + r"""Compare old gauge data at `path` to new gauge data at same path |
| 253 | +
|
| 254 | + Provided as a quick check to see if the function `convert_gauges` has |
| 255 | + correctly converted the data located in `fort.gauge`. Really meant as a |
| 256 | + simple check but can be used to compare new test data to old. |
| 257 | +
|
| 258 | + :Input: |
| 259 | + - *old_path* - (path) Path to old file. |
| 260 | + - *new_path* - (path) Path to new gauge files (directory). |
| 261 | + - *gauge_id* - (int) Gauge id to compare. |
| 262 | + - *plot* - (bool) Whether to plot the differences between the gauges. |
| 263 | + - *old_file_name* - (string) Name of the old gauge file, defaults to |
| 264 | + `old_file_name = "fort.gauge"` |
| 265 | + - *abs_tol* - (float) Absolute tolerance used to make the comparison. See |
| 266 | + `numpy.allclose` for more info. |
| 267 | + - *rel_tol* - (float) Relative tolerance used to make the comparison. See |
| 268 | + `numpy.allclose` for more info. |
| 269 | +
|
| 270 | + :Output: |
| 271 | + - (bool) Whether the gauges agreed to double precision. Uses |
| 272 | + `numpy.testing.assert_allequal` to check with the `abs_tol` and `rel_tol` |
| 273 | + specified above. |
| 274 | + """ |
| 275 | + |
| 276 | + # Load old gauge data |
| 277 | + data = numpy.loadtxt(old_path) |
| 278 | + old_ids = numpy.asarray(data[:, 0], dtype=int) |
| 279 | + gauge_indices = numpy.nonzero(old_ids == gauge_id)[0] |
| 280 | + q = data[gauge_indices, 3:] |
| 281 | + |
| 282 | + # Load new data |
| 283 | + gauge = GaugeSolution(gauge_id, new_path) |
| 284 | + |
| 285 | + # Turn these into assertions or logicals |
| 286 | + if verbose: |
| 287 | + print("Comparison of guage %s:" % gauge_id) |
| 288 | + print(" ||\Delta q||_2 = ", |
| 289 | + numpy.linalg.norm(q - gauge.q.transpose(), ord=2)) |
| 290 | + print(" arg(||\Delta q||_\infty = ", |
| 291 | + numpy.argmax(q - gauge.q.transpose())) |
| 292 | + |
| 293 | + if plot: |
| 294 | + import matplotlib.pyplot as plt |
| 295 | + fig = plt.figure() |
| 296 | + for i in xrange(gauge.q.shape[0]): |
| 297 | + axes = fig.add_subplot(1, 3, i + 1) |
| 298 | + axes.plot(q[:, i] - gauge.q[i, :]) |
| 299 | + axes.set_title("q[%s, :] comparison" % i) |
| 300 | + axes.set_xlabel("t (s)") |
| 301 | + axes.set_ylabel("q(%s, :)" % i) |
| 302 | + |
| 303 | + return numpy.allclose(q, gauge.q.transpose(), rtol=rel_tol, atol=abs_tol) |
| 304 | + |
| 305 | + |
| 306 | +def check_old_gauge_data(path, gauge_id, new_gauge_path="./regression_data"): |
| 307 | + """Compare old gauge data to new gauge format |
| 308 | +
|
| 309 | + Function is meant to check discrepencies between versions of the gauge |
| 310 | + files. Note that this function also directly prints out some info. |
| 311 | +
|
| 312 | + :Input: |
| 313 | + - *path* (string) - Path to old gauge data file |
| 314 | + - *gauge_id* (int) - Gauge id to compare |
| 315 | + - *new_gauge_path* (path) - Path to directory containing new gauge files, |
| 316 | + defaults to './regression_data'. |
| 317 | +
|
| 318 | + :Output: |
| 319 | + - (figure) Returns a matplotlib figure object plotting the differences in |
| 320 | + time. |
| 321 | + """ |
| 322 | + |
| 323 | + import matplotlib.pyplot as plt |
| 324 | + |
| 325 | + # Load old gauge data |
| 326 | + data = numpy.loadtxt(path) |
| 327 | + old_ids = numpy.asarray(data[:, 0], dtype=int) |
| 328 | + gauge_indices = numpy.nonzero(old_ids == gauge_id)[0] |
| 329 | + q = data[gauge_indices, 3:] |
| 330 | + |
| 331 | + # Load new data |
| 332 | + gauge = GaugeSolution(gauge_id, new_gauge_path) |
| 333 | + |
| 334 | + print(numpy.linalg.norm(q - gauge.q.transpose(), ord=2)) |
| 335 | + print(numpy.argmax(q - gauge.q.transpose())) |
| 336 | + |
| 337 | + fig = plt.figure() |
| 338 | + for i in xrange(gauge.q.shape[0]): |
| 339 | + axes = fig.add_subplot(1, gauge.q.shape[0], i + 1) |
| 340 | + axes.plot(q[:, i] - gauge.q[i, :]) |
| 341 | + axes.set_title("q[%s, :] comparison" % i) |
| 342 | + |
| 343 | + return fig |
| 344 | + |
| 345 | + |
| 346 | +if __name__ == "__main__": |
| 347 | + |
| 348 | + if len(sys.argv) > 1: |
| 349 | + path = sys.argv[1] |
| 350 | + output_dir = os.getcwd() |
| 351 | + |
| 352 | + if len(sys.argv) == 3: |
| 353 | + output_dir = sys.argv[2] |
| 354 | + |
| 355 | + if len(sys.argv) == 1: |
| 356 | + print( |
| 357 | +""" |
| 358 | +Gauge Module - Module contains definitions for the GaugeSolution class. From |
| 359 | +the command line this module will convert an old formatted gauge file to a |
| 360 | +collection of the new formats. |
| 361 | +
|
| 362 | + gauges.py old_file_location [new_gauge_data_path] |
| 363 | +
|
| 364 | + - old_file_location - Path to the old, monolithic file (e.g. ./fort.gauge) |
| 365 | + - new_gauge_data_location - Path to the directory where the new gauge files |
| 366 | + should be placed. Defaults to the current directory. |
| 367 | +
|
| 368 | +""") |
| 369 | + sys.exit(0) |
| 370 | + |
| 371 | + convert_gauges(path, output_path=output_dir) |
0 commit comments