Skip to content

Commit 314fd93

Browse files
committed
Merge pull request #541 from mandli/add-new-gauges
Add new gauges
2 parents 40e1f82 + eeeb619 commit 314fd93

File tree

1 file changed

+371
-0
lines changed

1 file changed

+371
-0
lines changed

src/pyclaw/gauges.py

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
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

Comments
 (0)